"use strict";

const keyboard = require("../util/keyboard.js");
const views = require("../util/views.js");
const events = require("../events.js");
const misc = require("../util/misc.js");
const Note = require("../models/note.js");
const Point = require("../models/point.js");

const svgNS = "http://www.w3.org/2000/svg";
const snapThreshold = 10;
const circleSize = 10;

const MOUSE_BUTTON_LEFT = 1;

const KEY_LEFT = 37;
const KEY_UP = 38;
const KEY_RIGHT = 39;
const KEY_DOWN = 40;
const KEY_ESCAPE = 27;
const KEY_RETURN = 13;

function _getDistance(point1, point2) {
    return Math.sqrt(
        Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2)
    );
}

function _setNodeState(node, stateName) {
    if (node === null) {
        return;
    }
    node.setAttribute("data-state", stateName);
}

function _clearEditedNote(hostNode) {
    const node = hostNode.querySelector("[data-state='editing']");
    _setNodeState(node, null);
    return node !== null;
}

function _getNoteCentroid(note) {
    const vertexCount = note.polygon.length;
    const centroid = new Point(0, 0);
    let signedArea = 0.0;
    for (let i of misc.range(vertexCount)) {
        const x0 = note.polygon.at(i).x;
        const y0 = note.polygon.at(i).y;
        const x1 = note.polygon.at((i + 1) % vertexCount).x;
        const y1 = note.polygon.at((i + 1) % vertexCount).y;
        const a = x0 * y1 - x1 * y0;
        signedArea += a;
        centroid.x += (x0 + x1) * a;
        centroid.y += (y0 + y1) * a;
    }
    signedArea *= 0.5;
    centroid.x /= 6 * signedArea;
    centroid.y /= 6 * signedArea;
    return centroid;
}

function _getNoteSize(note) {
    const min = new Point(Infinity, Infinity);
    const max = new Point(-Infinity, -Infinity);
    for (let point of note.polygon) {
        min.x = Math.min(min.x, point.x);
        min.y = Math.min(min.y, point.y);
        max.x = Math.max(max.x, point.x);
        max.y = Math.max(max.y, point.y);
    }
    return new Point(max.x - min.x, max.y - min.y);
}

class State {
    constructor(control, stateName) {
        this._control = control;
        _setNodeState(control._hostNode, stateName);
        _setNodeState(control._textNode, stateName);
    }

    get canShowNoteText() {
        return false;
    }

    evtCanvasKeyDown(e) {}

    evtNoteMouseDown(e, hoveredNote) {}

    evtCanvasMouseDown(e) {}

    evtCanvasMouseMove(e) {}

    evtCanvasMouseUp(e) {}

    _getScreenPoint(point) {
        return new Point(
            point.x * this._control.boundingBox.width,
            point.y * this._control.boundingBox.height
        );
    }

    _snapPoints(targetPoint, referencePoint) {
        const targetScreenPoint = this._getScreenPoint(targetPoint);
        const referenceScreenPoint = this._getScreenPoint(referencePoint);
        if (
            _getDistance(targetScreenPoint, referenceScreenPoint) <
            snapThreshold
        ) {
            targetPoint.x = referencePoint.x;
            targetPoint.y = referencePoint.y;
        }
    }

    _createNote() {
        const note = new Note();
        this._control._createPolygonNode(note);
        return note;
    }

    _getPointFromEvent(e) {
        return new Point(
            (e.clientX - this._control.boundingBox.left) /
                this._control.boundingBox.width,
            (e.clientY - this._control.boundingBox.top) /
                this._control.boundingBox.height
        );
    }
}

class ReadOnlyState extends State {
    constructor(control) {
        super(control, "read-only");
        if (_clearEditedNote(control._hostNode)) {
            this._control.dispatchEvent(new CustomEvent("blur"));
        }
        keyboard.unpause();
    }

    get canShowNoteText() {
        return true;
    }
}

class PassiveState extends State {
    constructor(control) {
        super(control, "passive");
        if (_clearEditedNote(control._hostNode)) {
            this._control.dispatchEvent(new CustomEvent("blur"));
        }
        keyboard.unpause();
    }

    get canShowNoteText() {
        return true;
    }

    evtNoteMouseDown(e, hoveredNote) {
        this._control._state = new SelectedState(this._control, hoveredNote);
    }
}

class ActiveState extends State {
    constructor(control, note, stateName) {
        super(control, stateName);
        if (_clearEditedNote(control._hostNode)) {
            this._control.dispatchEvent(new CustomEvent("blur"));
        }
        keyboard.pause();
        if (note !== null) {
            this._note = note;
            this._control.dispatchEvent(
                new CustomEvent("focus", {
                    detail: { note: note },
                })
            );
            _setNodeState(this._note.groupNode, "editing");
        }
    }
}

class SelectedState extends ActiveState {
    constructor(control, note) {
        super(control, note, "selected");
        this._clickTimeout = null;
        this._control._hideNoteText();
    }

    evtCanvasKeyDown(e) {
        const delta = e.ctrlKey ? 10 : 1;
        const offsetMap = {
            [KEY_LEFT]: [-delta, 0],
            [KEY_UP]: [0, -delta],
            [KEY_DOWN]: [0, delta],
            [KEY_RIGHT]: [delta, 0],
        };
        if (Object.prototype.hasOwnProperty.call(offsetMap, e.witch)) {
            e.stopPropagation();
            e.stopImmediatePropagation();
            e.preventDefault();
            const args = offsetMap[e.which];
            if (e.shiftKey) {
                this._scaleEditedNote(...args);
            } else {
                this._moveEditedNote(...args);
            }
        }
    }

    evtNoteMouseDown(e, hoveredNote) {
        const mousePoint = this._getPointFromEvent(e);
        const mouseScreenPoint = this._getScreenPoint(mousePoint);
        if (e.shiftKey) {
            this._control._state = new ScalingNoteState(
                this._control,
                this._note,
                mousePoint
            );
            return;
        }
        if (this._note !== hoveredNote) {
            this._control._state = new SelectedState(
                this._control,
                hoveredNote
            );
            return;
        }
        this._clickTimeout = window.setTimeout(() => {
            for (let polygonPoint of this._note.polygon) {
                const distance = _getDistance(
                    mouseScreenPoint,
                    this._getScreenPoint(polygonPoint)
                );
                if (distance < circleSize) {
                    this._control._state = new MovingPointState(
                        this._control,
                        this._note,
                        polygonPoint,
                        mousePoint
                    );
                    return;
                }
            }
            this._control._state = new MovingNoteState(
                this._control,
                this._note,
                mousePoint
            );
        }, 100);
    }

    evtCanvasMouseMove(e) {
        const mousePoint = this._getPointFromEvent(e);
        const mouseScreenPoint = this._getScreenPoint(mousePoint);
        for (let polygonPoint of this._note.polygon) {
            const distance = _getDistance(
                mouseScreenPoint,
                this._getScreenPoint(polygonPoint)
            );
            polygonPoint.edgeNode.classList.toggle(
                "nearby",
                distance < circleSize
            );
        }
    }

    evtCanvasMouseDown(e) {
        const mousePoint = this._getPointFromEvent(e);
        const mouseScreenPoint = this._getScreenPoint(mousePoint);
        if (e.shiftKey) {
            this._control._state = new ScalingNoteState(
                this._control,
                this._note,
                mousePoint
            );
            return;
        }
        for (let polygonPoint of this._note.polygon) {
            const distance = _getDistance(
                mouseScreenPoint,
                this._getScreenPoint(polygonPoint)
            );
            if (distance < circleSize) {
                this._control._state = new MovingPointState(
                    this._control,
                    this._note,
                    polygonPoint,
                    mousePoint
                );
                return;
            }
        }
        this._control._state = new PassiveState(this._control);
    }

    evtCanvasMouseUp(e) {
        window.clearTimeout(this._clickTimeout);
    }

    _moveEditedNote(x, y) {
        for (let point of this._note.polygon) {
            point.x += x / this._control.boundingBox.width;
            point.y += y / this._control.boundingBox.height;
        }
    }

    _scaleEditedNote(x, y) {
        const origin = _getNoteCentroid(this._note);
        const originalSize = _getNoteSize(this._note);
        const targetSize = new Point(
            originalSize.x + x / this._control.boundingBox.width,
            originalSize.y + y / this._control.boundingBox.height
        );
        const scale = new Point(
            targetSize.x / originalSize.x,
            targetSize.y / originalSize.y
        );
        for (let point of this._note.polygon) {
            point.x = origin.x + (point.x - origin.x) * scale.x;
            point.y = origin.y + (point.y - origin.y) * scale.y;
        }
    }
}

class MovingPointState extends ActiveState {
    constructor(control, note, notePoint, mousePoint) {
        super(control, note, "moving-point");
        this._notePoint = notePoint;
        this._originalNotePoint = { x: notePoint.x, y: notePoint.y };
        this._originalPosition = mousePoint;
        _setNodeState(this._note.groupNode, "editing");
    }

    evtCanvasKeyDown(e) {
        if (e.which === KEY_ESCAPE) {
            this._notePoint.x = this._originalNotePoint.x;
            this._notePoint.y = this._originalNotePoint.y;
            this._control._state = new SelectedState(
                this._control,
                this._note
            );
        }
    }

    evtCanvasMouseMove(e) {
        const mousePoint = this._getPointFromEvent(e);
        this._notePoint.x += mousePoint.x - this._originalPosition.x;
        this._notePoint.y += mousePoint.y - this._originalPosition.y;
        this._originalPosition = mousePoint;
    }

    evtCanvasMouseUp(e) {
        this._control._state = new SelectedState(this._control, this._note);
    }
}

class MovingNoteState extends ActiveState {
    constructor(control, note, mousePoint) {
        super(control, note, "moving-note");
        this._originalPolygon = [...note.polygon].map((point) => ({
            x: point.x,
            y: point.y,
        }));
        this._originalPosition = mousePoint;
    }

    evtCanvasKeyDown(e) {
        if (e.which === KEY_ESCAPE) {
            for (let i of misc.range(this._note.polygon.length)) {
                this._note.polygon.at(i).x = this._originalPolygon[i].x;
                this._note.polygon.at(i).y = this._originalPolygon[i].y;
            }
            this._control._state = new SelectedState(
                this._control,
                this._note
            );
        }
    }

    evtCanvasMouseMove(e) {
        const mousePoint = this._getPointFromEvent(e);
        for (let point of this._note.polygon) {
            point.x += mousePoint.x - this._originalPosition.x;
            point.y += mousePoint.y - this._originalPosition.y;
        }
        this._originalPosition = mousePoint;
    }

    evtCanvasMouseUp(e) {
        this._control._state = new SelectedState(this._control, this._note);
    }
}

class ScalingNoteState extends ActiveState {
    constructor(control, note, mousePoint) {
        super(control, note, "scaling-note");
        this._originalPolygon = [...note.polygon].map((point) => ({
            x: point.x,
            y: point.y,
        }));
        this._originalMousePoint = mousePoint;
        this._originalSize = _getNoteSize(note);
    }

    evtCanvasKeyDown(e) {
        if (e.which === KEY_ESCAPE) {
            for (let i of misc.range(this._note.polygon.length)) {
                this._note.polygon.at(i).x = this._originalPolygon[i].x;
                this._note.polygon.at(i).y = this._originalPolygon[i].y;
            }
            this._control._state = new SelectedState(
                this._control,
                this._note
            );
        }
    }

    evtCanvasMouseMove(e) {
        const mousePoint = this._getPointFromEvent(e);
        const originalMousePoint = this._originalMousePoint;
        const originalSize = this._originalSize;
        for (let i of misc.range(this._note.polygon.length)) {
            const polygonPoint = this._note.polygon.at(i);
            const originalPolygonPoint = this._originalPolygon[i];
            polygonPoint.x =
                originalMousePoint.x +
                (originalPolygonPoint.x - originalMousePoint.x) *
                    (1 +
                        (mousePoint.x - originalMousePoint.x) /
                            originalSize.x);
            polygonPoint.y =
                originalMousePoint.y +
                (originalPolygonPoint.y - originalMousePoint.y) *
                    (1 +
                        (mousePoint.y - originalMousePoint.y) /
                            originalSize.y);
        }
    }

    evtCanvasMouseUp(e) {
        this._control._state = new SelectedState(this._control, this._note);
    }
}

class ReadyToDrawState extends ActiveState {
    constructor(control) {
        super(control, null, "ready-to-draw");
    }

    evtNoteMouseDown(e, hoveredNote) {
        this._control._state = new SelectedState(this._control, hoveredNote);
    }

    evtCanvasMouseDown(e) {
        const mousePoint = this._getPointFromEvent(e);
        if (e.shiftKey) {
            this._control._state = new DrawingRectangleState(
                this._control,
                mousePoint
            );
        } else {
            this._control._state = new DrawingPolygonState(
                this._control,
                mousePoint
            );
        }
    }
}

class DrawingRectangleState extends ActiveState {
    constructor(control, mousePoint) {
        super(control, null, "drawing-rectangle");
        this._note = this._createNote();
        this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
        this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
        this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
        this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
        _setNodeState(this._note.groupNode, "drawing");
    }

    evtCanvasMouseUp(e) {
        const mousePoint = this._getPointFromEvent(e);
        const x1 = this._note.polygon.at(0).x;
        const y1 = this._note.polygon.at(0).y;
        const x2 = this._note.polygon.at(2).x;
        const y2 = this._note.polygon.at(2).y;
        const width = (x2 - x1) * this._control.boundingBox.width;
        const height = (y2 - y1) * this._control.boundingBox.height;
        this._control._deleteDomNode(this._note);
        if (width < 20 && height < 20) {
            this._control._state = new ReadyToDrawState(this._control);
        } else {
            this._control._post.notes.add(this._note);
            this._control._state = new SelectedState(
                this._control,
                this._note
            );
        }
    }

    evtCanvasMouseMove(e) {
        const mousePoint = this._getPointFromEvent(e);
        this._note.polygon.at(1).x = mousePoint.x;
        this._note.polygon.at(3).y = mousePoint.y;
        this._note.polygon.at(2).x = mousePoint.x;
        this._note.polygon.at(2).y = mousePoint.y;
    }
}

class DrawingPolygonState extends ActiveState {
    constructor(control, mousePoint) {
        super(control, null, "drawing-polygon");
        this._note = this._createNote();
        this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
        this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
        _setNodeState(this._note.groupNode, "drawing");
    }

    evtCanvasKeyDown(e) {
        if (e.which === KEY_ESCAPE) {
            this._note.polygon.remove(this._note.polygon.secondLastPoint);
            if (this._note.polygon.length === 1) {
                this._cancel();
            }
        } else if (e.which === KEY_RETURN) {
            this._finish();
        }
    }

    evtNoteMouseDown(e, hoveredNote) {
        this.evtCanvasMouseDown(e);
    }

    evtCanvasMouseDown(e) {
        const mousePoint = this._getPointFromEvent(e);
        const firstPoint = this._note.polygon.firstPoint;
        const mouseScreenPoint = this._getScreenPoint(mousePoint);
        const firstScreenPoint = this._getScreenPoint(firstPoint);
        if (_getDistance(mouseScreenPoint, firstScreenPoint) < snapThreshold) {
            this._finish();
        } else {
            this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
        }
    }

    evtCanvasMouseMove(e) {
        const mousePoint = this._getPointFromEvent(e);
        const lastPoint = this._note.polygon.lastPoint;
        const secondLastPoint = this._note.polygon.secondLastPoint;
        const firstPoint = this._note.polygon.firstPoint;
        if (!lastPoint) {
            return;
        }

        if (e.shiftKey && secondLastPoint) {
            const direction =
                (Math.round(
                    Math.atan2(
                        secondLastPoint.y - mousePoint.y,
                        secondLastPoint.x - mousePoint.x
                    ) /
                        ((2 * Math.PI) / 4)
                ) +
                    4) %
                4;
            if (direction === 0 || direction === 2) {
                lastPoint.x = mousePoint.x;
                lastPoint.y = secondLastPoint.y;
            } else if (direction === 1 || direction === 3) {
                lastPoint.x = secondLastPoint.x;
                lastPoint.y = mousePoint.y;
            }
        } else {
            lastPoint.x = mousePoint.x;
            lastPoint.y = mousePoint.y;
        }
        this._snapPoints(lastPoint, firstPoint);
    }

    _cancel() {
        this._control._deleteDomNode(this._note);
        this._control._state = new ReadyToDrawState(this._control);
    }

    _finish() {
        this._note.polygon.remove(this._note.polygon.lastPoint);
        if (this._note.polygon.length <= 2) {
            this._cancel();
        } else {
            this._control._deleteDomNode(this._note);
            this._control._post.notes.add(this._note);
            this._control._state = new SelectedState(
                this._control,
                this._note
            );
        }
    }
}

class PostNotesOverlayControl extends events.EventTarget {
    constructor(hostNode, post) {
        super();
        this._post = post;
        this._hostNode = hostNode;

        this._svgNode = document.createElementNS(svgNS, "svg");
        this._svgNode.classList.add("resize-listener");
        this._svgNode.classList.add("notes-overlay");
        this._svgNode.setAttribute("preserveAspectRatio", "none");
        this._svgNode.setAttribute("viewBox", "0 0 1 1");
        for (let note of this._post.notes) {
            this._createPolygonNode(note);
        }
        this._hostNode.appendChild(this._svgNode);
        this._post.addEventListener("change", (e) => this._evtPostChange(e));
        this._post.notes.addEventListener("remove", (e) => {
            this._deleteDomNode(e.detail.note);
        });
        this._post.notes.addEventListener("add", (e) => {
            this._createPolygonNode(e.detail.note);
        });

        const keyHandler = (e) => this._evtCanvasKeyDown(e);
        document.addEventListener("keydown", keyHandler);
        this._svgNode.addEventListener("mousedown", (e) =>
            this._evtCanvasMouseDown(e)
        );
        this._svgNode.addEventListener("mouseup", (e) =>
            this._evtCanvasMouseUp(e)
        );
        this._svgNode.addEventListener("mousemove", (e) =>
            this._evtCanvasMouseMove(e)
        );

        const wrapperNode = document.createElement("div");
        wrapperNode.classList.add("wrapper");
        this._textNode = document.createElement("div");
        this._textNode.classList.add("note-text");
        this._textNode.appendChild(wrapperNode);
        this._textNode.addEventListener("mouseleave", (e) =>
            this._evtNoteMouseLeave(e)
        );
        document.body.appendChild(this._textNode);

        views.monitorNodeRemoval(this._hostNode, () => {
            this._hostNode.removeChild(this._svgNode);
            document.removeEventListener("keydown", keyHandler);
            document.body.removeChild(this._textNode);
            this._state = new ReadOnlyState(this);
        });

        this._state = new ReadOnlyState(this);
    }

    switchToPassiveEdit() {
        this._state = new PassiveState(this);
    }

    switchToDrawing() {
        this._state = new ReadyToDrawState(this);
    }

    get boundingBox() {
        return this._hostNode.getBoundingClientRect();
    }

    _evtPostChange(e) {
        while (this._svgNode.childNodes.length) {
            this._svgNode.removeChild(this._svgNode.firstChild);
        }
        this._post = e.detail.post;
        for (let note of this._post.notes) {
            this._createPolygonNode(note);
        }
    }

    _evtCanvasKeyDown(e) {
        const illegalNodeNames = ["textarea", "input", "select"];
        if (illegalNodeNames.includes(e.target.nodeName.toLowerCase())) {
            return;
        }
        this._state.evtCanvasKeyDown(e);
    }

    _evtCanvasMouseDown(e) {
        e.preventDefault();
        if (e.which !== MOUSE_BUTTON_LEFT) {
            return;
        }
        const hoveredNode = document.elementFromPoint(e.clientX, e.clientY);
        let hoveredNote = null;
        for (let note of this._post.notes) {
            if (note.polygonNode === hoveredNode) {
                hoveredNote = note;
            }
        }
        if (hoveredNote) {
            this._state.evtNoteMouseDown(e, hoveredNote);
        } else {
            this._state.evtCanvasMouseDown(e);
        }
    }

    _evtCanvasMouseUp(e) {
        this._state.evtCanvasMouseUp(e);
    }

    _evtCanvasMouseMove(e) {
        this._state.evtCanvasMouseMove(e);
    }

    _evtNoteMouseEnter(e, note) {
        if (this._state.canShowNoteText) {
            this._showNoteText(note);
        }
    }

    _evtNoteMouseLeave(e) {
        const newElement = e.relatedTarget;
        if (
            newElement === this._svgNode ||
            (!this._svgNode.contains(newElement) &&
                !this._textNode.contains(newElement) &&
                newElement !== this._textNode)
        ) {
            this._hideNoteText();
        }
    }

    _showNoteText(note) {
        this._textNode.querySelector(".wrapper").innerHTML =
            misc.formatMarkdown(note.text);
        this._textNode.style.display = "block";
        const bodyRect = document.body.getBoundingClientRect();
        const noteRect = this._textNode.getBoundingClientRect();
        const svgRect = this.boundingBox;
        const centroid = _getNoteCentroid(note);
        const x =
            -bodyRect.left +
            svgRect.left +
            svgRect.width * centroid.x -
            noteRect.width / 2;
        const y =
            -bodyRect.top +
            svgRect.top +
            svgRect.height * centroid.y -
            noteRect.height / 2;
        this._textNode.style.left = x + "px";
        this._textNode.style.top = y + "px";
    }

    _hideNoteText() {
        this._textNode.style.display = "none";
    }

    _updatePolygonNotePoints(note) {
        note.polygonNode.setAttribute(
            "points",
            [...note.polygon]
                .map((point) => [point.x, point.y].join(","))
                .join(" ")
        );
    }

    _createEdgeNode(point, groupNode) {
        const node = document.createElementNS(svgNS, "ellipse");
        node.setAttribute("cx", point.x);
        node.setAttribute("cy", point.y);
        node.setAttribute("rx", circleSize / 2 / this.boundingBox.width);
        node.setAttribute("ry", circleSize / 2 / this.boundingBox.height);
        point.edgeNode = node;
        groupNode.appendChild(node);
    }

    _deleteEdgeNode(point, note) {
        this._updatePolygonNotePoints(note);
        point.edgeNode.parentNode.removeChild(point.edgeNode);
    }

    _updateEdgeNode(point, note) {
        this._updatePolygonNotePoints(note);
        point.edgeNode.setAttribute("cx", point.x);
        point.edgeNode.setAttribute("cy", point.y);
    }

    _deleteDomNode(note) {
        note.groupNode.parentNode.removeChild(note.groupNode);
    }

    _createPolygonNode(note) {
        const groupNode = document.createElementNS(svgNS, "g");
        note.groupNode = groupNode;
        {
            const node = document.createElementNS(svgNS, "polygon");
            note.polygonNode = node;
            node.setAttribute("vector-effect", "non-scaling-stroke");
            node.setAttribute("stroke-alignment", "inside");
            node.addEventListener("mouseenter", (e) =>
                this._evtNoteMouseEnter(e, note)
            );
            node.addEventListener("mouseleave", (e) =>
                this._evtNoteMouseLeave(e)
            );
            this._updatePolygonNotePoints(note);
            groupNode.appendChild(node);
        }
        for (let point of note.polygon) {
            this._createEdgeNode(point, groupNode);
        }

        note.polygon.addEventListener("change", (e) => {
            this._updateEdgeNode(e.detail.point, note);
            this.dispatchEvent(new CustomEvent("change"));
        });
        note.polygon.addEventListener("remove", (e) => {
            this._deleteEdgeNode(e.detail.point, note);
            this.dispatchEvent(new CustomEvent("change"));
        });
        note.polygon.addEventListener("add", (e) => {
            this._createEdgeNode(e.detail.point, groupNode);
            this.dispatchEvent(new CustomEvent("change"));
        });

        this._svgNode.appendChild(groupNode);
    }
}

module.exports = PostNotesOverlayControl;
