import EmptyView from "../../../../view/elements/EmptyView.js";
import {DraggableAspect} from "../../../ui/DraggableAspect.js";
import ObservedValue from "../../../../core/model/ObservedValue.js";
import {Keyframe} from "../Keyframe.js";
import Vector1 from "../../../../core/geom/Vector1.js";
import {position_curve_to_canvas} from "./position_curve_to_canvas.js";
import {normalize_angle_rad} from "../../../../core/geom/normalize_angle_rad.js";
import {TangentChangeAction} from "../actionProcessorOperations/curveActions.js";
import ObservedBoolean from "../../../../core/model/ObservedBoolean.js";
import {clamp} from "../../../../core/math/clamp.js";
import {EPSILON} from "../../../../core/math/EPSILON.js";
import {createKeyframeMarker} from "../editor/createKeyframeMarker.js";

const tangentHandlerDir = {
    incoming:1,
    outgoing:2
};

function updateTangentValues(keyframePos, cursorPos, keyframe, isAligned, activeTangent) {

    function calculateNewAngle(){
        const passAngle = normalize_angle_rad(Math.atan2(cursorPos.y - keyframePos.y, cursorPos.x - keyframePos.x));

        const maxBound = Math.PI / 2 - EPSILON;
        const minBound = -Math.PI / 2 + EPSILON;
        let tempAngle = 0;
        if (activeTangent === tangentHandlerDir.outgoing) {
            tempAngle = clamp(passAngle, minBound, maxBound);
        }
        else if (activeTangent === tangentHandlerDir.incoming) {
            const flipAngle = clamp(normalize_angle_rad((passAngle + Math.PI)), minBound, maxBound);
            tempAngle = normalize_angle_rad((flipAngle-Math.PI));
        }

        return tempAngle;
    }

    const newAngle =  calculateNewAngle();
    const newTangentValue = Math.tan(newAngle);

    if (activeTangent === tangentHandlerDir.outgoing){
            keyframe.outTangent = -newTangentValue;
    }
    else if (activeTangent === tangentHandlerDir.incoming){
            keyframe.inTangent = -newTangentValue;
    }
    else{
        throw new Error(`Incorrect activeTangent value ${activeTangent}`);
    }

    if (isAligned.getValue()){
        if (activeTangent === tangentHandlerDir.outgoing){
            const flipAngle = normalize_angle_rad((newAngle - Math.PI))
            keyframe.inTangent = -Math.tan(flipAngle);
        }
        else if (activeTangent === tangentHandlerDir.incoming){
            const flipAngle = normalize_angle_rad((newAngle + Math.PI))
            keyframe.outTangent = -Math.tan(flipAngle);
        }
    }
}

/**
 *
 * @param {CanvasRenderingContext2D} ctx
 * @param {Keyframe} keyframe
 * @param {Vector2} size
 * @param {AABB2} frame
 * @param {Vector2} margin
 * @param {ObservedBoolean} enableTangentAlignment
 * @param cbFnCurveUpdate
 * @param {Signal} funcTrigger
 * @param {ActionProcessor} actionProcessor
 * @param {keyframesContext} actionProcessorCTX
 */
export function build_tangent_editor({
                                  ctx,
                                  keyframe,
                                  size,
                                  frame,
                                  margin,
                                  enableTangentAlignment = new ObservedBoolean(),
                                  cbFnCurveUpdate,
                                  funcTrigger,
                                         actionProcessor = null,
                                  actionProcessorCTX = null
                              }) {

    if (actionProcessor === null){
        throw new Error("Empty action processor argument")
    }
    if (actionProcessorCTX === null){
        throw new Error("Empty action processor ctx argument")
    }

    const scale = 36;

    const noActiveHandler = null;
    /**
     *
     * @type {ObservedValue<Vector1>}
     */
    const activeHandler = new ObservedValue(noActiveHandler);

    function buildHandle(handlerDirection) {
        const handleID = new Vector1();

        const marker = createKeyframeMarker();
        marker.css({
            pointerEvents: "auto",
            borderRadius: "0px"
        })

        const markerElement = marker.el;
        const keyframeValueStart = new Keyframe(),
              keyframeValueEnd = new Keyframe();
        const rotatable = new DraggableAspect({
            el: markerElement,
            drag(cursorCanvasPosition){
                const keyframeCanvasPosition = position_curve_to_canvas(size, frame, margin, keyframe.time, keyframe.value);

                updateTangentValues(keyframeCanvasPosition, cursorCanvasPosition, keyframe, enableTangentAlignment, activeHandler.get().x);

                handleTangentUpdate()
                cbFnCurveUpdate();
            },
            dragStart(){
                handleID.x = handlerDirection.x;
                activeHandler.set(handleID);

                keyframeValueStart.copy(keyframe);
            },
            dragEnd() {
                keyframeValueEnd.copy(keyframe)

                keyframe.copy(keyframeValueStart);

                actionProcessor.mark('move tangent');
                actionProcessor.do(new TangentChangeAction(keyframe, keyframeValueStart, keyframeValueEnd, handleTangentUpdate, actionProcessorCTX));
            }
        });

        rotatable.getPointer().on.tap.add(
            () => {
                handleID.x = handlerDirection.x;
                activeHandler.set(handleID);
            });

        function updateActiveState() {
            if (activeHandler.get() === handleID) {
                marker.css({
                    background: '#FFFFFF'
                });
            } else {
                marker.css({
                    background: '#00ff00'
                });
            }
        }

        marker.on.linked.add(rotatable.start, rotatable);
        marker.on.unlinked.add(rotatable.stop, rotatable);
        marker.on.unlinked.add(() => {
            activeHandler.set(noActiveHandler)
        });

        marker.bindSignal(activeHandler.onChanged, updateActiveState);
        marker.bindSignal(funcTrigger, handleTangentUpdate);

        return marker;
    }

    function buildTangent() {
        const view = new EmptyView({
            css: {
                position: "absolute",
                left: "0",
                top: "0",
                background: 'red',
                width: `10px`,
            }
        });
        view.position.set(-0.5, -0.5);
        view.transformOrigin.set(0, 0.5);

        function updateAlignmentState(){
            if (!enableTangentAlignment.getValue()) {
                view.css({
                    background: 'red'
                });
            } else {
                view.css({
                    background: 'white'
                });
            }
        }

        view.bindSignal(enableTangentAlignment.onChanged, updateAlignmentState);

        view.on.unlinked.add(() => {
            //Maybe redundant, but used as a safeguard
            enableTangentAlignment.setFalse();
        })

        return view;
    }

    const handleIn = buildHandle(new Vector1(tangentHandlerDir.incoming));
    const handleOut = buildHandle(new Vector1(tangentHandlerDir.outgoing));

    const tangentIn = buildTangent();
    const tangentOut = buildTangent();

    function handleTangentUpdate() {
        tangentIn.size.set(scale, 1);
        const angleIn = Math.PI - Math.atan2(keyframe.inTangent, 1);
        tangentIn.rotation.set(angleIn);

        tangentOut.size.set(scale, 1);
        const angleOut = -Math.atan2(keyframe.outTangent, 1);
        tangentOut.rotation.set(angleOut);

        handleIn.position.set(
            Math.cos(angleIn) * scale, Math.sin(angleIn) * scale
        );

        handleOut.position.set(
            Math.cos(angleOut) * scale, Math.sin(angleOut) * scale
        );
    }

    const keyframeValueStart = new Keyframe(),
          keyframeValueEnd = new Keyframe();
    function modifyTangentAlignmentPosition(){

        function isNotAlreadyAligned(){
            return !(keyframe.inTangent === -keyframe.outTangent)
        }

        function validateCalculatedAvgTangent(){
            let newTangent = (keyframe.outTangent + keyframe.inTangent) * 0.5;
            if(isNaN(newTangent) || Math.abs(newTangent) > Infinity){
                newTangent = 0;
            }
            return newTangent;
        }

        if (enableTangentAlignment.getValue() === true && isNotAlreadyAligned){

            keyframeValueStart.copy(keyframe);

            const avgTangent = validateCalculatedAvgTangent();
            keyframe.outTangent = avgTangent;
            keyframe.inTangent = avgTangent;

            keyframeValueEnd.copy(keyframe);

            keyframe.copy(keyframeValueStart);

            actionProcessor.mark('auto move tangents');
            actionProcessor.do(new TangentChangeAction(keyframe, keyframeValueStart, keyframeValueEnd, handleTangentUpdate, actionProcessorCTX));
        }
    }

    const vContainer = new EmptyView({
        css: {
            position: 'absolute',
            left: 0,
            top: 0
        }
    });

    vContainer.addChild(handleIn);
    vContainer.addChild(handleOut);

    vContainer.addChild(tangentIn);
    vContainer.addChild(tangentOut);

    vContainer.bindSignal(enableTangentAlignment.onChanged, modifyTangentAlignmentPosition)

    vContainer.on.linked.add(handleTangentUpdate);

    return vContainer;
}