#include "mousedraghandler.h"

#include "client.h"
#include "decoration.h"
#include "framedata.h"
#include "layout.h"
#include "monitormanager.h"
#include "mouse.h"
#include "tag.h"
#include "tagmanager.h"
#include "x11-utils.h"

using std::make_shared;
using std::shared_ptr;
using std::weak_ptr;

MouseDragHandlerFloating::MouseDragHandlerFloating(MonitorManager* monitors, TagManager* tags, Client* dragClient, DragFunction function)
  : monitors_(monitors)
  , tags_(tags)
  , winDragClient_(dragClient)
  , dragFunction_(function)
{
    winDragStart_ = winDragClient_->float_size_;
    buttonDragStart_ = get_cursor_position();
    dragMonitor_ = monitors_->byTag(winDragClient_->tag());
    if (dragMonitor_) {
        dragMonitorIndex_ = dragMonitor_->index();
        // always start with the effective floating geometry
        winDragStart_ = dragMonitor_->clampRelativeGeometry(winDragStart_);
    }
    assertDraggingStillSafe();
}

void MouseDragHandlerFloating::handle_motion_event(Point2D newCursorPos)
{
    assertDraggingStillSafe();
    (this ->* dragFunction_)(newCursorPos);
}

MouseDragHandler::Constructor MouseDragHandlerFloating::construct(DragFunction dragFunction)
{
    return [dragFunction](MonitorManager* monitors, TagManager* tags, Client* client)
            -> shared_ptr<MouseDragHandler> {
        return make_shared<MouseDragHandlerFloating>(monitors, tags, client, dragFunction);
    };
}

void MouseDragHandlerFloating::assertDraggingStillSafe() {
    if (!dragMonitor_
            || monitors_->byIdx(dragMonitorIndex_) != dragMonitor_
            || !dragMonitor_
            || !winDragClient_
            || winDragClient_->is_client_floated() == false)
    {
        throw DragNotPossible("Monitor or floating client disappeared");
    }
}

/** finalize what was started by the drag, do more than what is
 * usually done in the destructor.
 * This relayouts the monitor on which the drag happend
 */
void MouseDragHandlerFloating::finalize() {
    assertDraggingStillSafe();
    dragMonitor_->applyLayout();
}

void MouseDragHandlerFloating::mouse_function_move(Point2D newCursorPos) {
    bool unlockMonitorAgain = false;
    if (!dragMonitor_->rect->contains(newCursorPos)) {
        // if the cursor leaves the monitor, then consider moving
        // the client to another monitor.
        Monitor* newMonitor = monitors_->byCoordinate(newCursorPos);
        if (newMonitor) {
            // transfer everything to newMonitor:
            buttonDragStart_ =
                    buttonDragStart_
                    - dragMonitor_->getFloatingArea().tl()
                    + newMonitor->getFloatingArea().tl();
            if (!newMonitor->tag->floating()) {
                // if the tag on the new monitor is in tiling mode
                // then ensure that the dragged client is floating
                winDragClient_->floating_ = true;
            }
            bool focus = monitors_->focus()->tag->focusedClient() == winDragClient_;
            monitors_->lock();
            unlockMonitorAgain = true;
            tags_->moveClient(winDragClient_, newMonitor->tag, {}, focus);
            dragMonitor_ = newMonitor;
            dragMonitorIndex_ = newMonitor->index();
            if (focus) {
                monitor_focus_by_index(dragMonitorIndex_);
            }
        }
    }
    int x_diff = newCursorPos.x - buttonDragStart_.x;
    int y_diff = newCursorPos.y - buttonDragStart_.y;
    // we need to assign it such that the border snapping works
    winDragClient_->float_size_ = winDragStart_.shifted({x_diff, y_diff});
    // snap it to other windows
    int dx, dy;
    client_snap_vector(winDragClient_, dragMonitor_,
                       SNAP_EDGE_ALL, &dx, &dy);
    winDragClient_->float_size_ = winDragClient_->float_size_->shifted({dx, dy});
    winDragClient_->resize_floating(dragMonitor_, get_current_client() == winDragClient_);
    if (unlockMonitorAgain) {
        monitors_->unlock();
    }
}

void MouseDragHandlerFloating::mouse_function_resize(Point2D newCursorPos) {
    int x_diff = newCursorPos.x - buttonDragStart_.x;
    int y_diff = newCursorPos.y - buttonDragStart_.y;
    winDragClient_->float_size_ = winDragStart_;
    // relative x/y coords in drag window
    Monitor* m = dragMonitor_;
    int rel_x = m->relativeX(buttonDragStart_.x) - winDragStart_.x;
    int rel_y = m->relativeY(buttonDragStart_.y) - winDragStart_.y;
    bool top = false;
    bool left = false;
    if (rel_y < winDragStart_.height/2) {
        top = true;
        y_diff *= -1;
    }
    if (rel_x < winDragStart_.width/2) {
        left = true;
        x_diff *= -1;
    }
    if (lockWidth) {
        x_diff = 0;
        if (winDragClient_->mina_ > 0 || winDragClient_->maxa_ > 0) {
            // if the client requires an aspect ratio, (e.g. mpv)
            // then just keep current aspect ratio of the window
            x_diff = (y_diff * winDragStart_.width) / winDragStart_.height;
        }
    }
    if (lockHeight) {
        y_diff = 0;
        if (winDragClient_->mina_ > 0 || winDragClient_->maxa_ > 0) {
            // if the client requires an aspect ratio, (e.g. mpv)
            // then just keep current aspect ratio of the window
            y_diff = (x_diff * winDragStart_.height) / winDragStart_.width;
        }
    }
    // avoid an overflow
    int new_width  = winDragClient_->float_size_->width + x_diff;
    int new_height = winDragClient_->float_size_->height + y_diff;
    Client* client = winDragClient_;
    Rectangle new_geometry = winDragClient_->float_size_;
    if (left) {
        new_geometry.x -= x_diff;
    }
    if (top) {
        new_geometry.y -= y_diff;
    }
    new_geometry.width = new_width;
    new_geometry.height = new_height;
    winDragClient_->float_size_ = new_geometry;
    // snap it to other windows
    int dx, dy;
    int snap_flags = 0;
    if (left) {
        snap_flags |= SNAP_EDGE_LEFT;
    } else {
        snap_flags |= SNAP_EDGE_RIGHT;
    }
    if (top) {
        snap_flags |= SNAP_EDGE_TOP;
    } else {
        snap_flags |= SNAP_EDGE_BOTTOM;
    }
    client_snap_vector(winDragClient_, dragMonitor_,
                       (SnapFlags)snap_flags, &dx, &dy);
    if (left) {
        new_geometry.x += dx;
        dx *= -1;
    }
    if (top) {
        new_geometry.y += dy;
        dy *= -1;
    }
    new_geometry.width += dx;
    new_geometry.height += dy;
    client->applysizehints(&new_geometry.width,
                           &new_geometry.height);
    if (left) {
        new_geometry.x =
            winDragStart_.x + winDragStart_.width
            - new_geometry.width;
    }
    if (top) {
        new_geometry.y =
            winDragStart_.y + winDragStart_.height
            - new_geometry.height;
    }
    winDragClient_->float_size_ = new_geometry;
    winDragClient_->resize_floating(dragMonitor_, get_current_client() == winDragClient_);
}

void MouseDragHandlerFloating::mouse_function_zoom(Point2D newCursorPos) {
    // stretch, where center stays at the same position
    int x_diff = newCursorPos.x - buttonDragStart_.x;
    int y_diff = newCursorPos.y - buttonDragStart_.y;
    // relative x/y coords in drag window
    Monitor* m = dragMonitor_;
    int rel_x = m->relativeX(buttonDragStart_.x) - winDragStart_.x;
    int rel_y = m->relativeY(buttonDragStart_.y) - winDragStart_.y;
    int cent_x = winDragStart_.x + winDragStart_.width  / 2;
    int cent_y = winDragStart_.y + winDragStart_.height / 2;
    if (rel_x < winDragStart_.width/2) {
        x_diff *= -1;
    }
    if (rel_y < winDragStart_.height/2) {
        y_diff *= -1;
    }
    Client* client = winDragClient_;

    // avoid an overflow
    int new_width  = winDragStart_.width  + 2 * x_diff;
    int new_height = winDragStart_.height + 2 * y_diff;
    // apply new rect
    client->float_size_ = Rectangle(cent_x - new_width / 2,
                                    cent_y - new_height / 2,
                                    new_width,
                                    new_height);
    // snap it to other windows
    int right_dx, bottom_dy;
    int left_dx, top_dy;
    // we have to distinguish the direction in which we zoom
    client_snap_vector(winDragClient_, m,
                     (SnapFlags)(SNAP_EDGE_BOTTOM | SNAP_EDGE_RIGHT), &right_dx, &bottom_dy);
    client_snap_vector(winDragClient_, m,
                       (SnapFlags)(SNAP_EDGE_TOP | SNAP_EDGE_LEFT), &left_dx, &top_dy);
    // e.g. if window snaps by vector (3,3) at topleft, window has to be shrinked
    // but if the window snaps by vector (3,3) at bottomright, window has to grow
    if (abs(right_dx) < abs(left_dx)) {
        right_dx = -left_dx;
    }
    if (abs(bottom_dy) < abs(top_dy)) {
        bottom_dy = -top_dy;
    }
    new_width += 2 * right_dx;
    new_height += 2 * bottom_dy;
    client->applysizehints(&new_width, &new_height);
    // center window again
    client->float_size_ = Rectangle(cent_x - new_width / 2,
                                    cent_y - new_height / 2,
                                    new_width,
                                    new_height);
    winDragClient_->resize_floating(dragMonitor_, get_current_client() == winDragClient_);
}

MouseResizeFrame::MouseResizeFrame(MonitorManager *monitors, shared_ptr<FrameLeaf> frame,
                     weak_ptr<FrameSplit> splitX,
                     weak_ptr<FrameSplit> splitY)
    : monitors_(monitors)
    , dragFrameX_(splitX)
    , dragFrameY_(splitY)
{
    dragMonitor_ = monitors_->byFrame(frame);
    dragMonitorIndex_ = dragMonitor_->index();
    dragTag_ = dragMonitor_->tag;
    if (!dragMonitor_) {
        throw DragNotPossible("Frame not on any monitor");
    }
    buttonDragStart_ = get_cursor_position();
    auto dfX = dragFrameX_.lock();
    if (dfX != nullptr) {
        dragDistanceUnitX_ = dfX->lastRect().width;
        dragStartFractionX_ = dfX->getFraction();
    }
    auto dfY = dragFrameY_.lock();
    if (dfY != nullptr) {
        dragDistanceUnitY_ = dfY->lastRect().height;
        dragStartFractionY_ = dfY->getFraction();
    }
    if (!dfX && !dfY) {
        throw DragNotPossible("No neighbour frame");
    }
}

void MouseResizeFrame::finalize()
{
    assertDraggingStillSafe();
    dragMonitor_->applyLayout();
}

void MouseResizeFrame::handle_motion_event(Point2D newCursorPos)
{
    assertDraggingStillSafe();
    auto dfX = dragFrameX_.lock();
    auto dfY = dragFrameY_.lock();

    auto deltaVec = newCursorPos - buttonDragStart_;
    // translate deltaVec from 'pixels' to 'FRACTION_UNIT'
    if (dfX) {
        int delta = (deltaVec.x * dragStartFractionX_.unit_) / dragDistanceUnitX_;
        dfX->setFraction(dragStartFractionX_ + FixPrecDec::raw(delta));
    }
    if (dfY) {
        int delta = (deltaVec.y * dragStartFractionY_.unit_) / dragDistanceUnitY_;
        dfY->setFraction(dragStartFractionY_ + FixPrecDec::raw(delta));
    }
    dragMonitor_->applyLayout();
}

MouseDragHandler::Constructor MouseResizeFrame::construct(shared_ptr<FrameLeaf> frame, const ResizeAction& resize)
{
    // a helper function to find a split in a certain direction.
    // for simpler compilation, frame is passed as an argument
    // and not as a closure / variable capture.
    auto splitInDirection =
    [](shared_ptr<FrameLeaf>& frameStart, Direction dir) -> shared_ptr<FrameSplit> {
        shared_ptr<Frame> neighbour = frameStart->neighbour(dir);
        if (neighbour) {
            return neighbour->getParent();
        }
        return {};
    };
    shared_ptr<FrameSplit> horizontalSplit = {};
    shared_ptr<FrameSplit> verticalSplit = {};
    if (resize.left) {
        horizontalSplit = splitInDirection(frame, Direction::Left);
    }
    if (resize.right) {
        horizontalSplit = splitInDirection(frame, Direction::Right);
    }
    if (resize.top) {
        verticalSplit = splitInDirection(frame, Direction::Up);
    }
    if (resize.bottom) {
        verticalSplit = splitInDirection(frame, Direction::Down);
    }
    return [frame, horizontalSplit, verticalSplit](MonitorManager* monitors, TagManager*, Client*) {
        return make_shared<MouseResizeFrame>(monitors, frame, horizontalSplit, verticalSplit);
    };
}

void MouseResizeFrame::assertDraggingStillSafe()
{
    bool allFine =
            dragMonitor_
            && dragMonitorIndex_ < monitors_->size()
            && monitors_->byIdx(dragMonitorIndex_) == dragMonitor_
            && dragTag_ == dragMonitor_->tag
            && (!dragFrameX_.expired() || !dragFrameY_.expired());
    if (!allFine) {
        throw DragNotPossible("Monitor, tag or frame disappeared");
    }
}
