import Vector2 from "../../../../core/geom/Vector2.js";
import AABB2 from "../../../../core/geom/2d/aabb/AABB2.js";
import {ActionProcessor} from "../../../../core/process/undo/ActionProcessor.js";
import EmptyView from "../../../../view/elements/EmptyView.js";
import {CanvasView} from "../../../../view/elements/CanvasView.js";
import {createKeyCoordinateLabelView} from "./createKeyCoordinateLabelView.js";
import Signal from "../../../../core/events/signal/Signal.js";
import KeyboardDevice from "../../../input/devices/KeyboardDevice.js";
import {KeyframeStateManager} from "./KeyframeStateManager.js";
import {keyframesContext} from "../actionProcessorOperations/curveActions.js";
import {createKeyframeMarker} from "./createKeyframeMarker.js";
import {updateMarkerPosition} from "./updateMarkerPosition.js";
import {updateMarkerVisual} from "./updateMarkerVisual.js";
import {createKeyframeDraggableAspect} from "./createKeyframeDraggableAspect.js";
import {
    handleKeyDownEvents,
    handleKeyframeSelectedByDevice,
    handleMouseDoubleClickEvents,
    handleMouseDownEvents
} from "./inputEventHandlers.js";
import {build_tangent_editor} from "../draw/build_tangent_editor.js";
import {buildReadOnlyDisplay} from "../draw/buildReadOnlyDisplay.js";
import {KeyboardEvents} from "../../../input/devices/events/KeyboardEvents.js";
import {MouseEvents} from "../../../input/devices/events/MouseEvents.js";
import {canvas2dPlotCurveLine} from "./canvas2dPlotCurveLine.js";
import {updateAxisRange} from "./updateAxisRange.js";
import {determineUnitSpacing} from "./determineUnitSpacing.js";
import {animation_curve_compute_aabb} from "../animation_curve_compute_aabb.js";
import {displayMousePos} from "../draw/displayMousePos.js";
import {PointerDevice} from "../../../input/devices/PointerDevice.js";
import {createSelectionBoxTool} from "./createSelectionBoxTool.js";
import {createPanTool} from "./createPanTool.js";
import {OperationRouter} from "./OperationRouter.js";
import {decodeMouseEventButtons} from "../../../input/devices/mouse/decodeMouseEventButtons.js";
import {createCurveUploader} from "./createCurveUploader.js";


const graphToolType = ({
    SELECT: "select",
    PAN: "pan",
});

export class CurveEditorView extends EmptyView{
    /**
     *
     * @param {AnimationCurve} curve
     * @param {Vector2} [size]
     * @param {Vector2} [margin] How much space to leave empty around the plotted bounds
     * @param {boolean} [enableEdit]
     * @param {AABB2} [validEditableBounds]
     * @param {ActionProcessor} [actionProcessor]
     */
    constructor({
                    curve,
                    size = new Vector2(400, 400),
                    margin = new Vector2(46, 46),
                    enableEdit= true,
                    validEditableBounds = new AABB2(-Infinity,-Infinity,Infinity,Infinity),
                    actionProcessor = new ActionProcessor({})
                }) {
        super({
            css: {
                pointerEvents: "auto",
                outline: 'none',
                overflow: 'hidden',
                position: 'relative'
            }
        });
        this.curve = curve;
        this.size.copy(size);
        this.margin = margin;
        this.enableEdit = enableEdit;
        this.validEditableBounds = validEditableBounds;
        this.actionProcessor = actionProcessor;

        this.gridSpacing = 32;
        this.zoomLevel = 1;

        this.build();
    }

    build() {
        this.el.setAttribute('tabindex', '-1'); //allow programmatic focus only

        // ---- Interface Setup
        const graph = new CanvasView();
        graph.size.copy(this.size);

        const frameRegion = new AABB2();
        const offset = 0.5;
        //Initialize the frameRegion size
        if (this.curve.length > 1) {
            animation_curve_compute_aabb(frameRegion, this.curve);
            if (frameRegion.y0 === frameRegion.y1) {
                frameRegion.y0 -= offset
                frameRegion.y1 += offset
            }
        }
        else {
            const key = this.curve.keys?.[0]
            frameRegion.set(
                key ? key.time - offset : 0,
                key ? key.value - offset : 0,
                key ? key.time + offset : 1,
                key ? key.value + offset : 1
            );
        }

        const keyCoordinateDisplay = createKeyCoordinateLabelView();

        // ---- Triggers
        const frameUpdated = new Signal();
        const keyframeChange = new Signal();
        const curveUploadChange = createCurveUploader(this.el);

        // ---- Keyboard Startup
        const keyboardElement = new KeyboardDevice(this.el);
        keyboardElement.start();

        // Todo maybe have const uiStateManager = new UIContext(curve, size, margin, frameRegion, validEditableBounds)
        const keyframeStateManager = new KeyframeStateManager();

        // ----
        const actionProcessorCTX = new keyframesContext({
            curve: this.curve,
            removeKeyframe: removeKeyframe,
            addKeyframe: addKeyframe,
            keyframeViews: keyframeStateManager.keyframeViews,
            activeKeyframe: keyframeStateManager.observedActiveKeyframe,
            selectedKeyframes: keyframeStateManager.selectedKeyframes,
            handleCurveUpdate: handleCurveUpdate,
            updateGraph: updateGraph
        });

        const self = this;

        function reinitCurve(newCurve) {
            self.curve.keys.forEach(removeKeyframe);
            self.curve.copy(newCurve)
            self.curve.keys.forEach(addKeyframe);

            handleCurveUpdate();
        }

        function handleCurveUpdate() {
            frameUpdated.send0();
            updateGraph();
        }

        function updateGraph() {

            canvas2dPlotCurveLine({
                curve: self.curve,
                ctx: graph.context2d,
                width: graph.size.x,
                height: graph.size.y,
                margin: self.margin,
                range_y: [frameRegion.y0, frameRegion.y1],
                range_x: [frameRegion.x0, frameRegion.x1],
                spacing: self.gridSpacing
            });
        }

        handleCurveUpdate()
        /**
         *
         * @param {Keyframe} keyframe
         */
        function addKeyframe(keyframe) {

            const vContainerMarker = new EmptyView({
                css: {
                    position: "absolute",
                    top: 0,
                    left: 0,
                    pointerEvents: "none"
                }
            });

            const marker = createKeyframeMarker();

            const fnUpdateMarkerPosition = () => updateMarkerPosition(graph, frameRegion, self.margin, keyframe, vContainerMarker);

            const fnUpdateMarkerVisual = () => updateMarkerVisual(keyframeStateManager, keyframe, vTangentEditor, marker);

            if (self.enableEdit) {
                const markerElement = marker.el;

                const draggable = createKeyframeDraggableAspect({
                    markerElement: markerElement,
                    graph: graph,
                    frame: frameRegion,
                    margin: self.margin,
                    validEditableBounds: self.validEditableBounds,
                    curve: self.curve,
                    fnUpdateGraph: updateGraph,
                    keyframe: keyframe,
                    selectedKeyframes: keyframeStateManager.selectedKeyframes,
                    activeKeyframe: keyframeStateManager.observedActiveKeyframe,
                    keyCoordinateDisplay: keyCoordinateDisplay,
                    fnUpdateMarkerPosition: fnUpdateMarkerPosition,
                    frameUpdated: frameUpdated,
                    keyframeChange: keyframeChange,
                    vContainer: self,
                    actionProcessor: self.actionProcessor,
                    actionProcessorCTX: actionProcessorCTX
                });

                draggable.getPointer().on.down.add(() => {
                    handleKeyframeSelectedByDevice(keyboardElement, keyframe, self.actionProcessor, actionProcessorCTX, keyframeStateManager);
                });

                marker.on.linked.add(draggable.start, draggable);
                marker.on.unlinked.add(draggable.stop, draggable);
            }

            marker.bindSignal(frameUpdated, fnUpdateMarkerPosition);
            marker.bindSignal(keyframeStateManager.observedActiveKeyframe.onChanged, fnUpdateMarkerVisual);
            marker.bindSignal(keyframeStateManager.selectedKeyframes.on.added, fnUpdateMarkerVisual);
            marker.bindSignal(keyframeStateManager.selectedKeyframes.on.removed, fnUpdateMarkerVisual);

            const vTangentEditor = build_tangent_editor({
                keyframe: keyframe,
                size: graph.size,
                ctx: graph.context2d,
                frame: frameRegion,
                margin: self.margin,
                enableTangentAlignment: keyframeStateManager.tangentAlignmentEnabled,
                cbFnCurveUpdate: updateGraph,
                funcTrigger: keyframeChange,
                actionProcessor: self.actionProcessor,
                actionProcessorCTX: actionProcessorCTX
            });

            keyframeStateManager.keyframeViews.set(keyframe, vContainerMarker);

            vContainerMarker.addChild(marker);
            self.addChild(vContainerMarker);
        }

        function removeKeyframe(keyframe) {
            const markerView = keyframeStateManager.keyframeViews.get(keyframe);
            self.removeChild(markerView);

            keyframeStateManager.keyframeViews.delete(keyframe)
        }

        graph.on.linked.add(() => {
            this.curve.keys.forEach(addKeyframe);
        });

        graph.on.unlinked.add(() => {
            this.curve.keys.forEach(removeKeyframe);
        });

        if (!this.enableEdit){
            const labelReadOnly = buildReadOnlyDisplay();
            this.addChild(labelReadOnly);
        }

        this.el.addEventListener(KeyboardEvents.KeyDown,  (event) => {
            handleKeyDownEvents(
                event,
                keyboardElement,
                this.curve,
                keyframeStateManager,
                this.actionProcessor,
                actionProcessorCTX
            );
        });

        const graphPointer = new PointerDevice(this.el);
            this.on.linked.add(graphPointer.start, graphPointer);
            this.on.unlinked.add(graphPointer.stop, graphPointer);

        const selectionBoxTool = new createSelectionBoxTool({
            keyframeStateManager: keyframeStateManager,
            graph: graph,
            frame: frameRegion,
            margin: this.margin,
            fnUpdate: updateGraph,
            actionProcessor: this.actionProcessor,
            actionCTX: actionProcessorCTX
        })
        const panTool = new createPanTool({
            graph: graph,
            frame: frameRegion,
            margin: this.margin,
            fnUpdate: handleCurveUpdate,
        })
        const graphToolList = {
            [graphToolType.SELECT]: selectionBoxTool,
            [graphToolType.PAN]: panTool,
        }

        const operationRouter = new OperationRouter(graphPointer, (event) => {
            if (decodeMouseEventButtons(event.buttons)[0] && this.enableEdit) return graphToolList[graphToolType.SELECT];
            if (decodeMouseEventButtons(event.buttons)[1]) return graphToolList[graphToolType.PAN];
            return null;
        });

        this.el.addEventListener(MouseEvents.Down, (e) => {
            handleMouseDownEvents(e, graph, keyboardElement, this.actionProcessor, actionProcessorCTX, keyframeStateManager);
        });

        if (this.enableEdit) {
            //create new keyframe at location
            this.el.addEventListener(MouseEvents.DoubleClick, (e) => {
                handleMouseDoubleClickEvents(e, this, graph, frameRegion, this.margin, this.curve, this.validEditableBounds, this.actionProcessor, actionProcessorCTX);
            });
        }

        this.el.addEventListener(MouseEvents.Wheel, (e) => {
            const zoomFactor = e.deltaY < 0 ? 1.2 : 1/1.2;
            this.zoomLevel *= zoomFactor;

            //Reminder: apply vertical flip to Y axis for graph coordinates
            const cursorToGraphPos = new Vector2(e.x - this.margin.x, (graph.size.y - this.margin.y) - e.y);
            const newX = updateAxisRange(graph.size.x - this.margin.x * 2, cursorToGraphPos.x, frameRegion.x0, frameRegion.x1, zoomFactor);
            const newY = updateAxisRange(graph.size.y - this.margin.y * 2, cursorToGraphPos.y, frameRegion.y0, frameRegion.y1, zoomFactor);

            frameRegion.set(newX[0], newY[0], newX[1], newY[1]);

            this.gridSpacing = determineUnitSpacing(graph.size.x - this.margin.x * 2, this.zoomLevel);
            handleCurveUpdate();
            },
            {passive: true});

        graph.on.linked.add(handleCurveUpdate);
        // graph.bindSignal(curveUploadChange, handleCurveUpdate);
        graph.bindSignal(curveUploadChange, (data) => reinitCurve(data));

        displayMousePos.call(this, graph, frameRegion);

        this.addChild(graph);
    }
}