/*
 * Copyright © 2015-2016 Canonical Ltd.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authored by: Alan Griffiths <alan@octopull.co.uk>
 */

#include "basic_window_manager.h"
#include "miral/window_manager_tools.h"

#include <mir/scene/session.h>
#include <mir/scene/surface.h>
#include <mir/scene/surface_creation_parameters.h>
#include <mir/shell/display_layout.h>
#include <mir/shell/persistent_surface_store.h>
#include <mir/shell/surface_ready_observer.h>
#include <mir/version.h>

#include <boost/throw_exception.hpp>

#include <algorithm>

using namespace mir;
using namespace mir::geometry;

namespace
{
int const title_bar_height = 12;

struct Locker
{
    Locker(std::mutex& mutex, std::unique_ptr<miral::WindowManagementPolicy> const& policy) :
        lock{mutex},
        policy{policy.get()}
    {
        policy->advise_begin();
    }

    ~Locker()
    {
        policy->advise_end();
    }

    std::lock_guard<std::mutex> const lock;
    miral::WindowManagementPolicy* const policy;
};
}

miral::BasicWindowManager::BasicWindowManager(
    shell::FocusController* focus_controller,
    std::shared_ptr<shell::DisplayLayout> const& display_layout,
    std::shared_ptr<mir::shell::PersistentSurfaceStore> const& persistent_surface_store,
    WindowManagementPolicyBuilder const& build) :
    focus_controller(focus_controller),
    display_layout(display_layout),
    persistent_surface_store{persistent_surface_store},
    policy(build(WindowManagerTools{this}))
{
}

void miral::BasicWindowManager::add_session(std::shared_ptr<scene::Session> const& session)
{
    Locker lock{mutex, policy};
    policy->advise_new_app(app_info[session] = ApplicationInfo(session));
}

void miral::BasicWindowManager::remove_session(std::shared_ptr<scene::Session> const& session)
{
    Locker lock{mutex, policy};
    policy->advise_delete_app(app_info[session]);
    app_info.erase(session);
}

auto miral::BasicWindowManager::add_surface(
    std::shared_ptr<scene::Session> const& session,
    scene::SurfaceCreationParameters const& params,
    std::function<frontend::SurfaceId(std::shared_ptr<scene::Session> const& session, scene::SurfaceCreationParameters const& params)> const& build)
-> frontend::SurfaceId
{
    Locker lock{mutex, policy};

    auto& session_info = info_for(session);

    WindowSpecification const& spec = policy->place_new_surface(session_info, place_new_surface(session_info, params));
    scene::SurfaceCreationParameters parameters;
    spec.update(parameters);
    auto const surface_id = build(session, parameters);
    Window const window{session, session->surface(surface_id)};
    auto& window_info = this->window_info.emplace(window, WindowInfo{window, spec}).first->second;

    if (spec.parent().is_set() && spec.parent().value().lock())
        window_info.parent(info_for(spec.parent().value()).window());

    if (spec.userdata().is_set())
        window_info.userdata() = spec.userdata().value();

    session_info.add_window(window);

    auto const parent = window_info.parent();

    if (parent)
        info_for(parent).add_child(window);

    if (window_info.state() == mir_surface_state_fullscreen)
        fullscreen_surfaces.insert(window_info.window());

    policy->advise_new_window(window_info);

    if (window_info.can_be_active())
    {
        std::shared_ptr<scene::Surface> const scene_surface = window_info.window();
        scene_surface->add_observer(std::make_shared<shell::SurfaceReadyObserver>(
            [this, &window_info](std::shared_ptr<scene::Session> const&, std::shared_ptr<scene::Surface> const&)
                { Locker lock{mutex, policy}; policy->handle_window_ready(window_info); },
            session,
            scene_surface));
    }

#if MIR_SERVER_VERSION >= MIR_VERSION_NUMBER(0, 25, 0)
    if (parent && spec.aux_rect().is_set() && spec.placement_hints().is_set())
    {
        Rectangle relative_placement{window.top_left() - (parent.top_left()-Point{}), window.size()};
        auto const mir_surface = std::shared_ptr<scene::Surface>(window);
        mir_surface->placed_relative(relative_placement);
    }
#endif

    return surface_id;
}

void miral::BasicWindowManager::modify_surface(
    std::shared_ptr<scene::Session> const& /*application*/,
    std::shared_ptr<scene::Surface> const& surface,
    shell::SurfaceSpecification const& modifications)
{
    Locker lock{mutex, policy};
    auto& info = info_for(surface);
    WindowSpecification mods{modifications};
    validate_modification_request(mods, info);
    place_and_size_for_state(mods, info);
    policy->handle_modify_window(info, mods);
}

void miral::BasicWindowManager::remove_surface(
    std::shared_ptr<scene::Session> const& session,
    std::weak_ptr<scene::Surface> const& surface)
{
    Locker lock{mutex, policy};
    remove_window(session, info_for(surface));
}

void miral::BasicWindowManager::remove_window(Application const& application, miral::WindowInfo const& info)
{
    policy->advise_delete_window(info);

    bool const is_active_window{mru_active_windows.top() == info.window()};

    info_for(application).remove_window(info.window());
    mru_active_windows.erase(info.window());
    fullscreen_surfaces.erase(info.window());

    application->destroy_surface(info.window());

    // NB erase() invalidates info, but we want to keep access to "parent".
    auto const parent = info.parent();
    erase(info);

    if (is_active_window)
    {
        // Try to make the parent active
        if (parent && select_active_window(parent))
            return;

        if (can_activate_window_for_session(application))
            return;

        // Try to activate to recently active window of any application
        {
            miral::Window new_focus;

            mru_active_windows.enumerate([&](miral::Window& window)
                {
                    // select_active_window() calls set_focus_to() which updates mru_active_windows and changes window
                    auto const w = window;
                    return !(new_focus = select_active_window(w));
                });

            if (new_focus) return;
        }

        // Fallback to cycling through applications
        focus_next_application();
    }
}

void miral::BasicWindowManager::erase(miral::WindowInfo const& info)
{
    if (auto const parent = info.parent())
        info_for(parent).remove_child(info.window());

    for (auto& child : info.children())
        info_for(child).parent({});

    window_info.erase(info.window());
}

void miral::BasicWindowManager::add_display(geometry::Rectangle const& area)
{
    Locker lock{mutex, policy};
    displays.add(area);

    for (auto window : fullscreen_surfaces)
    {
        if (window)
        {
            auto& info = info_for(window);
            auto const rect = fullscreen_rect_for(info);
            place_and_size(info, rect.top_left, rect.size);
        }
    }
}

void miral::BasicWindowManager::remove_display(geometry::Rectangle const& area)
{
    Locker lock{mutex, policy};
    displays.remove(area);
    for (auto window : fullscreen_surfaces)
    {
        if (window)
        {
            auto& info = info_for(window);
            auto const rect = fullscreen_rect_for(info);
            place_and_size(info, rect.top_left, rect.size);
        }
    }
}

bool miral::BasicWindowManager::handle_keyboard_event(MirKeyboardEvent const* event)
{
    Locker lock{mutex, policy};
    update_event_timestamp(event);
    return policy->handle_keyboard_event(event);
}

bool miral::BasicWindowManager::handle_touch_event(MirTouchEvent const* event)
{
    Locker lock{mutex, policy};
    update_event_timestamp(event);
    return policy->handle_touch_event(event);
}

bool miral::BasicWindowManager::handle_pointer_event(MirPointerEvent const* event)
{
    Locker lock{mutex, policy};
    update_event_timestamp(event);

    cursor = {
        mir_pointer_event_axis_value(event, mir_pointer_axis_x),
        mir_pointer_event_axis_value(event, mir_pointer_axis_y)};

    return policy->handle_pointer_event(event);
}

void miral::BasicWindowManager::handle_raise_surface(
    std::shared_ptr<scene::Session> const& /*application*/,
    std::shared_ptr<scene::Surface> const& surface,
    uint64_t timestamp)
{
    Locker lock{mutex, policy};
    if (timestamp >= last_input_event_timestamp)
        policy->handle_raise_window(info_for(surface));
}

int miral::BasicWindowManager::set_surface_attribute(
    std::shared_ptr<scene::Session> const& /*application*/,
    std::shared_ptr<scene::Surface> const& surface,
    MirSurfaceAttrib attrib,
    int value)
{
    WindowSpecification modification;
    switch (attrib)
    {
    case mir_surface_attrib_type:
        modification.type() = MirSurfaceType(value);
        break;
    case mir_surface_attrib_state:
        modification.state() = MirSurfaceState(value);
        break;

    case mir_surface_attrib_preferred_orientation:
        modification.preferred_orientation() = MirOrientationMode(value);
        break;

    case mir_surface_attrib_visibility:
        // The client really shouldn't be trying to set this.
        // But, as the legacy API exists, we treat it as a query
        return surface->query(mir_surface_attrib_visibility);

    case mir_surface_attrib_focus:
        // The client really shouldn't be trying to set this.
        // But, as the legacy API exists, we treat it as a query
        return surface->query(mir_surface_attrib_focus);

    case mir_surface_attrib_swapinterval:
    case mir_surface_attrib_dpi:
    default:
        return surface->configure(attrib, value);
    }

    Locker lock{mutex, policy};
    auto& info = info_for(surface);

    validate_modification_request(modification, info);
    place_and_size_for_state(modification, info);
    policy->handle_modify_window(info, modification);

    switch (attrib)
    {
    case mir_surface_attrib_type:
        return info.type();

    case mir_surface_attrib_state:
        return info.state();

    case mir_surface_attrib_preferred_orientation:
        return info.preferred_orientation();
        break;

    default:
        return 0; // Can't get here anyway
    }
}

auto miral::BasicWindowManager::count_applications() const
-> unsigned int
{
    return app_info.size();
}


void miral::BasicWindowManager::for_each_application(std::function<void(ApplicationInfo& info)> const& functor)
{
    for(auto& info : app_info)
    {
        functor(info.second);
    }
}

auto miral::BasicWindowManager::find_application(std::function<bool(ApplicationInfo const& info)> const& predicate)
-> Application
{
    for(auto& info : app_info)
    {
        if (predicate(info.second))
        {
            return Application{info.first};
        }
    }

    return Application{};
}

auto miral::BasicWindowManager::info_for(std::weak_ptr<scene::Session> const& session) const
-> ApplicationInfo&
{
    return const_cast<ApplicationInfo&>(app_info.at(session));
}

auto miral::BasicWindowManager::info_for(std::weak_ptr<scene::Surface> const& surface) const
-> WindowInfo&
{
    return const_cast<WindowInfo&>(window_info.at(surface));
}

auto miral::BasicWindowManager::info_for(Window const& window) const
-> WindowInfo&
{
    return info_for(std::weak_ptr<mir::scene::Surface>(window));
}

void miral::BasicWindowManager::ask_client_to_close(Window const& window)
{
    if (auto const mir_surface = std::shared_ptr<scene::Surface>(window))
        mir_surface->request_client_surface_close();
}

void miral::BasicWindowManager::force_close(Window const& window)
{
    auto application = window.application();

    if (application && window)
        remove_window(application, info_for(window));
}

auto miral::BasicWindowManager::active_window() const -> Window
{
    return mru_active_windows.top();
}

void miral::BasicWindowManager::focus_next_application()
{
    focus_controller->focus_next_session();

    if (can_activate_window_for_session(focus_controller->focused_session()))
        return;

    // Last resort: accept wherever focus_controller placed focus
    auto const focussed_surface = focus_controller->focused_surface();
    select_active_window(focussed_surface ? info_for(focussed_surface).window() : Window{});
}

void miral::BasicWindowManager::focus_next_within_application()
{
    if (auto const prev = active_window())
    {
        auto const& siblings = info_for(prev.application()).windows();
        auto current = find(begin(siblings), end(siblings), prev);

        if (current != end(siblings))
        {
            while (++current != end(siblings) && prev == select_active_window(*current))
                ;
        }

        if (current == end(siblings))
        {
            current = begin(siblings);
            while (prev != *current && prev == select_active_window(*current))
                ++current;
        }
    }
}

auto miral::BasicWindowManager::window_at(geometry::Point cursor) const
-> Window
{
    auto surface_at = focus_controller->surface_at(cursor);
    return surface_at ? info_for(surface_at).window() : Window{};
}

auto miral::BasicWindowManager::active_display()
-> geometry::Rectangle const
{
    geometry::Rectangle result;

    // 1. If a window has input focus, whichever display contains the largest
    //    proportion of the area of that window.
    if (auto const surface = focus_controller->focused_surface())
    {
        auto const surface_rect = surface->input_bounds();
        int max_overlap_area = -1;

        for (auto const& display : displays)
        {
            auto const intersection = surface_rect.intersection_with(display).size;
            if (intersection.width.as_int()*intersection.height.as_int() > max_overlap_area)
            {
                max_overlap_area = intersection.width.as_int()*intersection.height.as_int();
                result = display;
            }
        }
        return result;
    }

    // 2. Otherwise, if any window previously had input focus, for the window that had
    //    it most recently, the display that contained the largest proportion of the
    //    area of that window at the moment it closed, as long as that display is still
    //    available.

    // 3. Otherwise, the display that contains the pointer, if there is one.
    for (auto const& display : displays)
    {
        if (display.contains(cursor))
        {
            // Ignore the (unspecified) possiblity of overlapping displays
            return display;
        }
    }

    // 4. Otherwise, the primary display, if there is one (for example, the laptop display).

    // 5. Otherwise, the first display.
    if (displays.size())
        result = *displays.begin();

    return result;
}

void miral::BasicWindowManager::raise_tree(Window const& root)
{
    std::vector<Window> windows;

    std::function<void(WindowInfo const& info)> const add_children =
        [&,this](WindowInfo const& info)
            {
                for (auto const& child : info.children())
                {
                    windows.push_back(child);
                    add_children(info_for(child));
                }
            };

    windows.push_back(root);
    add_children(info_for(root));

    policy->advise_raise(windows);
    focus_controller->raise({begin(windows), end(windows)});
}

void miral::BasicWindowManager::move_tree(miral::WindowInfo& root, mir::geometry::Displacement movement)
{
    if (movement == mir::geometry::Displacement{})
        return;

    auto const top_left = root.window().top_left() + movement;

    policy->advise_move_to(root, top_left);
    root.window().move_to(top_left);

    for (auto const& child: root.children())
    {
        auto const& pos = policy->confirm_inherited_move(info_for(child), movement);
        place_and_size(info_for(child), pos.top_left, pos.size);
    }
}

void miral::BasicWindowManager::modify_window(WindowInfo& window_info, WindowSpecification const& modifications)
{
    WindowInfo window_info_tmp{window_info};

#define COPY_IF_SET(field)\
    if (modifications.field().is_set())\
        window_info_tmp.field(modifications.field().value())

    COPY_IF_SET(name);
    COPY_IF_SET(type);
    // state is handled "differently" and only updated by set_state() at the end
    COPY_IF_SET(min_width);
    COPY_IF_SET(min_height);
    COPY_IF_SET(max_width);
    COPY_IF_SET(max_height);
    COPY_IF_SET(width_inc);
    COPY_IF_SET(height_inc);
    COPY_IF_SET(min_aspect);
    COPY_IF_SET(max_aspect);
    COPY_IF_SET(output_id);
    COPY_IF_SET(preferred_orientation);
    COPY_IF_SET(confine_pointer);
    COPY_IF_SET(userdata);

#undef COPY_IF_SET

    if (modifications.parent().is_set())
        window_info_tmp.parent(info_for(modifications.parent().value()).window());

    if (window_info.type() != window_info_tmp.type())
    {
        auto const new_type = window_info_tmp.type();

        if (!window_info.can_morph_to(new_type))
        {
            throw std::runtime_error("Unsupported window type change");
        }

        if (window_info_tmp.must_not_have_parent())
        {
            if (modifications.parent().is_set())
                throw std::runtime_error("Target window type does not support parent");

            window_info_tmp.parent({});
        }
        else if (window_info_tmp.must_have_parent())
        {
            if (!window_info_tmp.parent())
                throw std::runtime_error("Target window type requires parent");
        }
    }

    std::swap(window_info_tmp, window_info);

    auto& window = window_info.window();

    if (window_info.type() != window_info_tmp.type())
        std::shared_ptr<scene::Surface>(window)->configure(mir_surface_attrib_type, window_info.type());

    if (window_info.parent() != window_info_tmp.parent())
    {
        if (window_info_tmp.parent())
        {
            auto& parent_info = info_for(window_info_tmp.parent());
            parent_info.remove_child(window);
        }

        if (window_info.parent())
        {
            auto& parent_info = info_for(window_info.parent());
            parent_info.add_child(window);
        }
    }

    if (modifications.name().is_set())
        std::shared_ptr<scene::Surface>(window)->rename(modifications.name().value());

    if (modifications.input_shape().is_set())
        std::shared_ptr<scene::Surface>(window)->set_input_region(modifications.input_shape().value());

    if (modifications.state().is_set() && window_info.state() != modifications.state().value())
    {
        switch (window_info.state())
        {
        case mir_surface_state_restored:
        case mir_surface_state_hidden:
            window_info.restore_rect({window.top_left(), window.size()});
            break;

        case mir_surface_state_vertmaximized:
        {
            auto restore_rect = window_info.restore_rect();
            restore_rect.top_left.x = window.top_left().x;
            restore_rect.size.width = window.size().width;
            window_info.restore_rect(restore_rect);
            break;
        }

        case mir_surface_state_horizmaximized:
        {
            auto restore_rect = window_info.restore_rect();
            restore_rect.top_left.y = window.top_left().y;
            restore_rect.size.height= window.size().height;
            window_info.restore_rect(restore_rect);
            break;
        }

        default:
            break;
        }
    }

    Point new_pos = modifications.top_left().is_set() ? modifications.top_left().value() : window.top_left();

    if (modifications.size().is_set())
    {
        place_and_size(window_info, new_pos, modifications.size().value());
    }
    else if (modifications.min_width().is_set() || modifications.min_height().is_set() ||
             modifications.max_width().is_set() || modifications.max_height().is_set() ||
             modifications.width_inc().is_set() || modifications.height_inc().is_set())
    {
        Size new_size = window.size();

        window_info.constrain_resize(new_pos, new_size);
        place_and_size(window_info, new_pos, new_size);
    }
    else if (modifications.top_left().is_set())
    {
        place_and_size(window_info, new_pos, window.size());
    }

    if (modifications.placement_hints().is_set())
    {
        if (auto parent = window_info.parent())
        {
            auto new_pos = place_relative({parent.top_left(), parent.size()}, modifications, window.size());

            if (new_pos.is_set())
                place_and_size(window_info, new_pos.value().top_left, new_pos.value().size);

#if MIR_SERVER_VERSION >= MIR_VERSION_NUMBER(0, 25, 0)
            Rectangle relative_placement{window.top_left() - (parent.top_left()-Point{}), window.size()};
            auto const mir_surface = std::shared_ptr<scene::Surface>(window);
            mir_surface->placed_relative(relative_placement);
#endif
        }
    }

    if (modifications.state().is_set())
    {
        set_state(window_info, modifications.state().value());
    }

#if MIR_SERVER_VERSION >= MIR_VERSION_NUMBER(0, 24, 0)
    if (modifications.confine_pointer().is_set())
        std::shared_ptr<scene::Surface>(window)->set_confine_pointer_state(modifications.confine_pointer().value());
#endif
}

auto miral::BasicWindowManager::info_for_window_id(std::string const& id) const -> WindowInfo&
{
#if MIR_SERVER_VERSION >= MIR_VERSION_NUMBER(0, 24, 0)
    auto surface = persistent_surface_store->surface_for_id(mir::shell::PersistentSurfaceStore::Id{id});

    if (!surface)
        BOOST_THROW_EXCEPTION(std::runtime_error{"No surface matching ID"});

    return info_for(surface);
#else
    (void)id;
    BOOST_THROW_EXCEPTION(std::runtime_error{"No surface matching ID"});
#endif
}

auto miral::BasicWindowManager::id_for_window(Window const& window) const -> std::string
{
    if (!window)
        BOOST_THROW_EXCEPTION(std::runtime_error{"Null Window has no Persistent ID"});

#if MIR_SERVER_VERSION >= MIR_VERSION_NUMBER(0, 24, 0)
    return persistent_surface_store->id_for_surface(window).serialize_to_string();
#else
    BOOST_THROW_EXCEPTION(std::runtime_error{"Persistent IDs unavailable with this Mir server version"});
#endif
}

void miral::BasicWindowManager::place_and_size(WindowInfo& root, Point const& new_pos, Size const& new_size)
{
    if (root.window().size() != new_size)
    {
        policy->advise_resize(root, new_size);
        root.window().resize(new_size);
    }

    move_tree(root, new_pos - root.window().top_left());
}

void miral::BasicWindowManager::place_and_size_for_state(
    WindowSpecification& modifications, WindowInfo const& window_info) const
{
    if (!modifications.state().is_set())
        return;

    auto const new_state = modifications.state().value();

    switch (new_state)
    {
    case mir_surface_state_fullscreen:
        if (modifications.output_id().is_set() &&
           (!window_info.has_output_id() || modifications.output_id().value() != window_info.output_id()))
                break;

    default:
        if (new_state == window_info.state())
            return;
    }

    auto const display_area = displays.bounding_rectangle();
    auto const window = window_info.window();

    auto restore_rect = window_info.restore_rect();

    // window_info.restore_rect() was cached on last state change, update to reflect current window position
    switch (window_info.state())
    {
    case mir_surface_state_restored:
    case mir_surface_state_hidden:
        restore_rect = {window.top_left(), window.size()};
        break;

    case mir_surface_state_vertmaximized:
    {
        restore_rect.top_left.x = window.top_left().x;
        restore_rect.size.width = window.size().width;
        break;
    }

    case mir_surface_state_horizmaximized:
    {
        restore_rect.top_left.y = window.top_left().y;
        restore_rect.size.height= window.size().height;
        break;
    }

    default:
        break;
    }

    // If the shell has also set position, that overrides restore_rect default
    if (modifications.top_left().is_set())
        restore_rect.top_left = modifications.top_left().value();

    // If the client or shell has also set size, that overrides restore_rect default
    if (modifications.size().is_set())
        restore_rect.size = modifications.size().value();

    Rectangle rect;

    switch (new_state)
    {
    case mir_surface_state_restored:
        rect = restore_rect;
        break;

    case mir_surface_state_maximized:
        rect = display_area;
        break;

    case mir_surface_state_horizmaximized:
        rect.top_left = {display_area.top_left.x, restore_rect.top_left.y};
        rect.size = {display_area.size.width, restore_rect.size.height};
        break;

    case mir_surface_state_vertmaximized:
        rect.top_left = {restore_rect.top_left.x, display_area.top_left.y};
        rect.size = {restore_rect.size.width, display_area.size.height};
        break;

    case mir_surface_state_fullscreen:
    {
        rect = fullscreen_rect_for(window_info);

        break;
    }

    case mir_surface_state_hidden:
    case mir_surface_state_minimized:
    default:
        return;
    }

    modifications.top_left() = rect.top_left;
    modifications.size() = rect.size;
}

auto miral::BasicWindowManager::fullscreen_rect_for(miral::WindowInfo const& window_info) const -> Rectangle
{
    auto const w = window_info.window();
    Rectangle r = {(w.top_left()), w.size()};

    if (window_info.has_output_id())
    {
        graphics::DisplayConfigurationOutputId id{window_info.output_id()};
        if (display_layout->place_in_output(id, r))
            return r;
    }

    display_layout->size_to_output(r);

    return r;
}

void miral::BasicWindowManager::set_state(miral::WindowInfo& window_info, MirSurfaceState value)
{
    auto const window = window_info.window();
    auto const mir_surface = std::shared_ptr<scene::Surface>(window);

    if (value != mir_surface_state_fullscreen)
    {
        fullscreen_surfaces.erase(window);
    }
    else
    {
        fullscreen_surfaces.insert(window);
    }

    if (window_info.state() == value)
    {
        return;
    }

    bool const was_hidden = window_info.state() == mir_surface_state_hidden ||
                            window_info.state() == mir_surface_state_minimized;

    policy->advise_state_change(window_info, value);
    window_info.state(value);

    mir_surface->configure(mir_surface_attrib_state, value);

    switch (value)
    {
    case mir_surface_state_hidden:
    case mir_surface_state_minimized:
        mir_surface->hide();

        if (window == active_window())
        {
            // Try to activate to recently active window of any application
            mru_active_windows.enumerate([&](Window& candidate)
                {
                    if (candidate == window)
                        return true;
                    if (!std::shared_ptr<scene::Surface>(candidate)->visible())
                        return true;
                    auto const w = candidate;
                    return !(select_active_window(w));
                });

            if (window == active_window())
                select_active_window({});

            mru_active_windows.erase(window);
        }
        break;

    default:
        mir_surface->show();
        if (was_hidden && !active_window())
            select_active_window(window);
    }
}

void miral::BasicWindowManager::update_event_timestamp(MirKeyboardEvent const* kev)
{
    auto iev = mir_keyboard_event_input_event(kev);
    last_input_event_timestamp = mir_input_event_get_event_time(iev);
}

void miral::BasicWindowManager::update_event_timestamp(MirPointerEvent const* pev)
{
    auto iev = mir_pointer_event_input_event(pev);
    auto pointer_action = mir_pointer_event_action(pev);

    if (pointer_action == mir_pointer_action_button_up ||
        pointer_action == mir_pointer_action_button_down)
    {
        last_input_event_timestamp = mir_input_event_get_event_time(iev);
    }
}

void miral::BasicWindowManager::update_event_timestamp(MirTouchEvent const* tev)
{
    auto iev = mir_touch_event_input_event(tev);
    auto touch_count = mir_touch_event_point_count(tev);
    for (unsigned i = 0; i < touch_count; i++)
    {
        auto touch_action = mir_touch_event_action(tev, i);
        if (touch_action == mir_touch_action_up ||
            touch_action == mir_touch_action_down)
        {
            last_input_event_timestamp = mir_input_event_get_event_time(iev);
            break;
        }
    }
}

void miral::BasicWindowManager::invoke_under_lock(std::function<void()> const& callback)
{
    Locker lock{mutex, policy};
    callback();
}

auto miral::BasicWindowManager::select_active_window(Window const& hint) -> miral::Window
{
    auto const prev_window = active_window();

    if (!hint)
    {
        if (prev_window)
        {
            focus_controller->set_focus_to(hint.application(), hint);
            policy->advise_focus_lost(info_for(prev_window));
        }

        return hint;
    }

    auto const& info_for_hint = info_for(hint);

    for (auto const& child : info_for_hint.children())
    {
        if (std::shared_ptr<mir::scene::Surface> surface = child)
        {
            if (surface->type() == mir_surface_type_dialog && surface->visible())
                return select_active_window(child);
        }
    }

    if (info_for_hint.can_be_active() && info_for_hint.is_visible())
    {
        mru_active_windows.push(hint);
        focus_controller->set_focus_to(hint.application(), hint);

        if (prev_window && prev_window != hint)
            policy->advise_focus_lost(info_for(prev_window));

        policy->advise_focus_gained(info_for_hint);
        return hint;
    }
    else
    {
        // Cannot have input focus - try the parent
        if (auto const parent = info_for_hint.parent())
            return select_active_window(parent);
    }

    return {};
}

void miral::BasicWindowManager::drag_active_window(mir::geometry::Displacement movement)
{
    auto const window = active_window();

    if (!window)
        return;

    auto& window_info = info_for(window);

    // When a surface is moved interactively
    // -------------------------------------
    // Regular, floating regular, dialog, and satellite surfaces should be user-movable.
    // Popups, glosses, and tips should not be.
    // Freestyle surfaces may or may not be, as specified by the app.
    //                              Mir and Unity: Surfaces, input, and displays (v0.3)
    switch (window_info.type())
    {
    case mir_surface_type_normal:   // regular
    case mir_surface_type_utility:  // floating regular
    case mir_surface_type_dialog:   // dialog
    case mir_surface_type_satellite:// satellite
    case mir_surface_type_freestyle:// freestyle
        break;

    case mir_surface_type_gloss:
    case mir_surface_type_menu:
    case mir_surface_type_inputmethod:
    case mir_surface_type_tip:
    case mir_surface_types:
        return;
    }

    switch (window_info.state())
    {
    case mir_surface_state_restored:
        break;

        // "A vertically maximised window is anchored to the top and bottom of
        // the available workspace and can have any width."
    case mir_surface_state_vertmaximized:
        movement.dy = DeltaY(0);
        break;

        // "A horizontally maximised window is anchored to the left and right of
        // the available workspace and can have any height"
    case mir_surface_state_horizmaximized:
        movement.dx = DeltaX(0);
        break;

        // "A maximised window is anchored to the top, bottom, left and right of the
        // available workspace. For example, if the launcher is always-visible then
        // the left-edge of the window is anchored to the right-edge of the launcher."
    case mir_surface_state_maximized:
    case mir_surface_state_fullscreen:
    default:
        return;
    }

    move_tree(window_info, movement);
}

auto miral::BasicWindowManager::can_activate_window_for_session(miral::Application const& session) -> bool
{
    miral::Window new_focus;

    mru_active_windows.enumerate([&](miral::Window& window)
        {
            // select_active_window() calls set_focus_to() which updates mru_active_windows and changes window
            auto const w = window;
            return w.application() != session || !(new_focus = select_active_window(w));
        });

    return new_focus;
}

auto miral::BasicWindowManager::place_new_surface(ApplicationInfo const& app_info, WindowSpecification parameters)
-> WindowSpecification
{
    if (!parameters.type().is_set())
        parameters.type() = mir_surface_type_normal;

    if (!parameters.state().is_set())
        parameters.state() = mir_surface_state_restored;

    auto const active_display_area = active_display();
    auto const height = parameters.size().value().height.as_int();

    bool positioned = false;

    bool const has_parent{parameters.parent().is_set() && parameters.parent().value().lock()};

    if (parameters.output_id().is_set() && parameters.output_id().value() != 0)
    {
        Rectangle rect{parameters.top_left().value(), parameters.size().value()};
        graphics::DisplayConfigurationOutputId id{parameters.output_id().value()};
        display_layout->place_in_output(id, rect);
        parameters.top_left() = rect.top_left;
        parameters.size() = rect.size;
        parameters.state() = mir_surface_state_fullscreen;
        positioned = true;
    }
    else if (!has_parent) // No parent => client can't suggest positioning
    {
        if (app_info.windows().size() > 0)
        {
            if (auto const default_window = app_info.windows()[0])
            {
                static Displacement const offset{title_bar_height, title_bar_height};

                parameters.top_left() = default_window.top_left() + offset;

                Rectangle display_for_app{default_window.top_left(), default_window.size()};

                display_layout->size_to_output(display_for_app);

                positioned = display_for_app.overlaps(Rectangle{parameters.top_left().value(), parameters.size().value()});
            }
        }
    }

    if (has_parent)
    {
        auto const parent = info_for(parameters.parent().value()).window();

        if (parameters.aux_rect().is_set() && parameters.placement_hints().is_set())
        {
            auto const position = place_relative(
                {parent.top_left(), parent.size()},
                parameters,
                parameters.size().value());

            if (position.is_set())
            {
                parameters.top_left() = position.value().top_left;
                parameters.size() = position.value().size;
                positioned = true;
            }
        }
        else
        {
            //  o Otherwise, if the dialog is not the same as any previous dialog for the
            //    same parent window, and/or it does not have user-customized position:
            //      o It should be optically centered relative to its parent, unless this
            //        would overlap or cover the title bar of the parent.
            //      o Otherwise, it should be cascaded vertically (but not horizontally)
            //        relative to its parent, unless, this would cause at least part of
            //        it to extend into shell space.
            auto const parent_top_left = parent.top_left();
            auto const centred = parent_top_left
                                 + 0.5*(as_displacement(parent.size()) - as_displacement(parameters.size().value()))
                                 - DeltaY{(parent.size().height.as_int()-height)/6};

            parameters.top_left() = centred;
            positioned = true;
        }
    }

    if (!positioned)
    {
        auto centred = active_display_area.top_left
                       + 0.5*(as_displacement(active_display_area.size) - as_displacement(parameters.size().value()))
                       - DeltaY{(active_display_area.size.height.as_int()-height)/6};

        switch (parameters.state().value())
        {
        case mir_surface_state_fullscreen:
        case mir_surface_state_maximized:
            parameters.top_left() = active_display_area.top_left;
            parameters.size() = active_display_area.size;
            break;

        case mir_surface_state_vertmaximized:
            centred.y = active_display_area.top_left.y;
            parameters.top_left() = centred;
            parameters.size() = Size{parameters.size().value().width, active_display_area.size.height};
            break;

        case mir_surface_state_horizmaximized:
            centred.x = active_display_area.top_left.x;
            parameters.top_left() = centred;
            parameters.size() = Size{active_display_area.size.width, parameters.size().value().height};
            break;

        default:
            parameters.top_left() = centred;
        }

        auto const display_area = displays.bounding_rectangle();

        if (parameters.top_left().value().y < display_area.top_left.y)
            parameters.top_left() = Point{parameters.top_left().value().x, display_area.top_left.y};
    }

    return parameters;
}

namespace
{
auto flip_x(MirPlacementGravity rect_gravity) -> MirPlacementGravity
{
    switch (rect_gravity)
    {
    case mir_placement_gravity_northwest:
        return mir_placement_gravity_northeast;

    case mir_placement_gravity_northeast:
        return mir_placement_gravity_northwest;

    case mir_placement_gravity_west:
        return mir_placement_gravity_east;

    case mir_placement_gravity_east:
        return mir_placement_gravity_west;

    case mir_placement_gravity_southwest:
        return mir_placement_gravity_southeast;

    case mir_placement_gravity_southeast:
        return mir_placement_gravity_southwest;

    default:
        return rect_gravity;
    }
}

auto flip_y(MirPlacementGravity rect_gravity) -> MirPlacementGravity
{
    switch (rect_gravity)
    {
    case mir_placement_gravity_northwest:
        return mir_placement_gravity_southwest;

    case mir_placement_gravity_north:
        return mir_placement_gravity_south;

    case mir_placement_gravity_northeast:
        return mir_placement_gravity_southeast;

    case mir_placement_gravity_southwest:
        return mir_placement_gravity_northwest;

    case mir_placement_gravity_south:
        return mir_placement_gravity_north;

    case mir_placement_gravity_southeast:
        return mir_placement_gravity_northeast;

    default:
        return rect_gravity;
    }
}

auto flip_x(Displacement const& d) -> Displacement { return {-1*d.dx, d.dy}; }
auto flip_y(Displacement const& d) -> Displacement { return {d.dx, -1*d.dy}; }

auto antipodes(MirPlacementGravity rect_gravity) -> MirPlacementGravity
{
    switch (rect_gravity)
    {
    case mir_placement_gravity_northwest:
        return mir_placement_gravity_southeast;

    case mir_placement_gravity_north:
        return mir_placement_gravity_south;

    case mir_placement_gravity_northeast:
        return mir_placement_gravity_southwest;

    case mir_placement_gravity_west:
        return mir_placement_gravity_east;

    case mir_placement_gravity_east:
        return mir_placement_gravity_west;

    case mir_placement_gravity_southwest:
        return mir_placement_gravity_northeast;

    case mir_placement_gravity_south:
        return mir_placement_gravity_north;

    case mir_placement_gravity_southeast:
        return mir_placement_gravity_northwest;

    default:
        return rect_gravity;
    }
}

auto constrain_to(mir::geometry::Rectangle const& rect, Point point) -> Point
{
    if (point.x < rect.top_left.x)
        point.x = rect.top_left.x;

    if (point.y < rect.top_left.y)
        point.y = rect.top_left.y;

    if (point.x > rect.bottom_right().x)
        point.x = rect.bottom_right().x;

    if (point.y > rect.bottom_right().y)
        point.y = rect.bottom_right().y;

    return point;
}

auto anchor_for(Rectangle const& aux_rect, MirPlacementGravity rect_gravity) -> Point
{
    switch (rect_gravity)
    {
    case mir_placement_gravity_northwest:
        return aux_rect.top_left;

    case mir_placement_gravity_north:
        return aux_rect.top_left + 0.5*as_displacement(aux_rect.size).dx;

    case mir_placement_gravity_northeast:
        return aux_rect.top_right();

    case mir_placement_gravity_west:
        return aux_rect.top_left + 0.5*as_displacement(aux_rect.size).dy;

    case mir_placement_gravity_center:
        return aux_rect.top_left + 0.5*as_displacement(aux_rect.size);

    case mir_placement_gravity_east:
        return aux_rect.top_right() + 0.5*as_displacement(aux_rect.size).dy;

    case mir_placement_gravity_southwest:
        return aux_rect.bottom_left();

    case mir_placement_gravity_south:
        return aux_rect.bottom_left() + 0.5*as_displacement(aux_rect.size).dx;

    case mir_placement_gravity_southeast:
        return aux_rect.bottom_right();

    default:
        BOOST_THROW_EXCEPTION(std::runtime_error("bad placement gravity"));
    }
}

auto offset_for(Size const& size, MirPlacementGravity rect_gravity) -> Displacement
{
    auto const displacement = as_displacement(size);

    switch (rect_gravity)
    {
    case mir_placement_gravity_northwest:
        return {0, 0};

    case mir_placement_gravity_north:
        return {-0.5 * displacement.dx, 0};

    case mir_placement_gravity_northeast:
        return {-1 * displacement.dx, 0};

    case mir_placement_gravity_west:
        return {0, -0.5 * displacement.dy};

    case mir_placement_gravity_center:
        return {-0.5 * displacement.dx, -0.5 * displacement.dy};

    case mir_placement_gravity_east:
        return {-1 * displacement.dx, -0.5 * displacement.dy};

    case mir_placement_gravity_southwest:
        return {0, -1 * displacement.dy};

    case mir_placement_gravity_south:
        return {-0.5 * displacement.dx, -1 * displacement.dy};

    case mir_placement_gravity_southeast:
        return {-1 * displacement.dx, -1 * displacement.dy};

    default:
        BOOST_THROW_EXCEPTION(std::runtime_error("bad placement gravity"));
    }
}
}

auto miral::BasicWindowManager::place_relative(mir::geometry::Rectangle const& parent, WindowSpecification const& parameters, Size size)
-> mir::optional_value<Rectangle>
{
    auto const hints = parameters.placement_hints().value();
    auto const active_display_area = active_display();
    auto const win_gravity = parameters.window_placement_gravity().value();

    if (parameters.size().is_set())
        size = parameters.size().value();

    auto offset = parameters.aux_rect_placement_offset().is_set() ?
                  parameters.aux_rect_placement_offset().value() : Displacement{};

    Rectangle aux_rect = parameters.aux_rect().value();
    aux_rect.top_left = aux_rect.top_left + (parent.top_left-Point{});

    std::vector<MirPlacementGravity> rect_gravities{parameters.aux_rect_placement_gravity().value()};

    if (hints & mir_placement_hints_antipodes)
        rect_gravities.push_back(antipodes(parameters.aux_rect_placement_gravity().value()));

    mir::optional_value<Rectangle> default_result;

    for (auto const& rect_gravity : rect_gravities)
    {
        {
            auto result = constrain_to(parent, anchor_for(aux_rect, rect_gravity) + offset) +
                offset_for(size, win_gravity);

            if (active_display_area.contains(Rectangle{result, size}))
                return Rectangle{result, size};

            if (!default_result.is_set())
                default_result = Rectangle{result, size};
        }

        if (hints & mir_placement_hints_flip_x)
        {
            auto result = constrain_to(parent, anchor_for(aux_rect, flip_x(rect_gravity)) + flip_x(offset)) +
                offset_for(size, flip_x(win_gravity));

            if (active_display_area.contains(Rectangle{result, size}))
                return Rectangle{result, size};
        }

        if (hints & mir_placement_hints_flip_y)
        {
            auto result = constrain_to(parent, anchor_for(aux_rect, flip_y(rect_gravity)) + flip_y(offset)) +
                offset_for(size, flip_y(win_gravity));

            if (active_display_area.contains(Rectangle{result, size}))
                return Rectangle{result, size};
        }

        if (hints & mir_placement_hints_flip_x && hints & mir_placement_hints_flip_y)
        {
            auto result = constrain_to(parent, anchor_for(aux_rect, flip_x(flip_y(rect_gravity))) + flip_x(flip_y(offset))) +
                offset_for(size, flip_x(flip_y(win_gravity)));

            if (active_display_area.contains(Rectangle{result, size}))
                return Rectangle{result, size};
        }
    }

    for (auto const& rect_gravity : rect_gravities)
    {
        auto result = constrain_to(parent, anchor_for(aux_rect, rect_gravity) + offset) +
            offset_for(size, win_gravity);

        if (hints & mir_placement_hints_slide_x)
        {
            auto const left_overhang  = result.x - active_display_area.top_left.x;
            auto const right_overhang = (result + as_displacement(size)).x - active_display_area.top_right().x;

            if (left_overhang < DeltaX{0})
                result -= left_overhang;
            else if (right_overhang > DeltaX{0})
                result -= right_overhang;
        }

        if (hints & mir_placement_hints_slide_y)
        {
            auto const top_overhang  = result.y - active_display_area.top_left.y;
            auto const bot_overhang = (result + as_displacement(size)).y - active_display_area.bottom_left().y;

            if (top_overhang < DeltaY{0})
                result -= top_overhang;
            else if (bot_overhang > DeltaY{0})
                result -= bot_overhang;
        }

        if (active_display_area.contains(Rectangle{result, size}))
            return Rectangle{result, size};
    }

    for (auto const& rect_gravity : rect_gravities)
    {
        auto result = constrain_to(parent, anchor_for(aux_rect, rect_gravity) + offset) +
            offset_for(size, win_gravity);

        if (hints & mir_placement_hints_resize_x)
        {
            auto const left_overhang  = result.x - active_display_area.top_left.x;
            auto const right_overhang = (result + as_displacement(size)).x - active_display_area.top_right().x;

            if (left_overhang < DeltaX{0})
            {
                result -= left_overhang;
                size = Size{size.width + left_overhang, size.height};
            }

            if (right_overhang > DeltaX{0})
            {
                size = Size{size.width - right_overhang, size.height};
            }
        }

        if (hints & mir_placement_hints_resize_y)
        {
            auto const top_overhang  = result.y - active_display_area.top_left.y;
            auto const bot_overhang = (result + as_displacement(size)).y - active_display_area.bottom_left().y;

            if (top_overhang < DeltaY{0})
            {
                result -= top_overhang;
                size = Size{size.width, size.height + top_overhang};
            }

            if (bot_overhang > DeltaY{0})
            {
                size = Size{size.width, size.height - bot_overhang};
            }
        }

        if (active_display_area.contains(Rectangle{result, size}))
            return Rectangle{result, size};
    }

    return default_result;
}

void miral::BasicWindowManager::validate_modification_request(WindowSpecification const& modifications, WindowInfo const& window_info) const
{
    auto target_type = window_info.type();

    if (modifications.type().is_set())
    {
        auto const original_type = target_type;

        target_type = modifications.type().value();

        switch (original_type)
        {
        case mir_surface_type_normal:
        case mir_surface_type_utility:
        case mir_surface_type_dialog:
        case mir_surface_type_satellite:
            switch (target_type)
            {
            case mir_surface_type_normal:
            case mir_surface_type_utility:
            case mir_surface_type_dialog:
            case mir_surface_type_satellite:
                break;

            default:
                BOOST_THROW_EXCEPTION(std::runtime_error("Invalid surface type change"));
            }

        case mir_surface_type_menu:
            switch (target_type)
            {
            case mir_surface_type_menu:
            case mir_surface_type_satellite:
                break;

            default:
                BOOST_THROW_EXCEPTION(std::runtime_error("Invalid surface type change"));
            }

        case mir_surface_type_gloss:
        case mir_surface_type_freestyle:
        case mir_surface_type_inputmethod:
        case mir_surface_type_tip:
            if (target_type != original_type)
                BOOST_THROW_EXCEPTION(std::runtime_error("Invalid surface type change"));
            break;

        default:
            break;
        }
    }

    switch (target_type)
    {
    case mir_surface_type_normal:
    case mir_surface_type_utility:
        if (modifications.parent().is_set() ? modifications.parent().value().lock() : window_info.parent())
            BOOST_THROW_EXCEPTION(std::runtime_error("Surface type must not have a parent"));
        break;

    case mir_surface_type_satellite:
    case mir_surface_type_gloss:
    case mir_surface_type_tip:
        if (modifications.parent().is_set() ? !modifications.parent().value().lock() : !window_info.parent())
            BOOST_THROW_EXCEPTION(std::runtime_error("Surface type must have a parent"));
        break;

    case mir_surface_type_inputmethod:
    case mir_surface_type_dialog:
    case mir_surface_type_menu:
    case mir_surface_type_freestyle:
        break;

    default:
        BOOST_THROW_EXCEPTION(std::runtime_error("Invalid surface type"));
    }

    if (modifications.size().is_set())
    {
        auto const size = modifications.size().value();

        if (size.width <= Width{0})
            BOOST_THROW_EXCEPTION(std::runtime_error("width must be positive"));

        if (size.height <= Height{0})
            BOOST_THROW_EXCEPTION(std::runtime_error("height must be positive"));
    }
}
