import { mat4 } from "gl-matrix";
import { Color } from "../../../src/core/color/Color.js";
import Signal from "../../../src/core/events/signal/Signal.js";
import { compose_matrix4_array } from "../../../src/core/geom/3d/mat4/compose_matrix4_array.js";
import Quaternion from "../../../src/core/geom/Quaternion.js";
import { v2_distance } from "../../../src/core/geom/vec2/v2_distance.js";
import Vector2 from "../../../src/core/geom/Vector2.js";
import Vector3 from "../../../src/core/geom/Vector3.js";
import { clamp01 } from "../../../src/core/math/clamp01.js";
import { inverseLerp } from "../../../src/core/math/inverseLerp.js";
import { lerp } from "../../../src/core/math/lerp.js";
import {
    quaternion_invert_orientation
} from "../../../src/engine/graphics/ecs/camera/quaternion_invert_orientation.js";
import { readPositionFromMouseEvent } from "../../../src/engine/input/devices/PointerDevice.js";
import { CanvasView } from "../../../src/view/elements/CanvasView.js";

const scratch_color = new Color();
const scratch_quat = new Quaternion();
const scratch_m4 = new Float32Array(16);

class DirectionStyle {
    constructor() {
        this.near = {
            fill: new Color(1, 0, 0),
            stroke_width: 0,
            stroke_color: new Color(0, 0, 0)
        };
        this.far = {
            fill: new Color(1, 0, 0),
            stroke_width: 0,
            stroke_color: new Color(0, 0, 0)
        };
    }

    fromJSON({
                 near_fill = '#FF0000',
                 near_stroke_width = 0,
                 near_stroke_color = '#000000',
                 far_fill = '#FF0000',
                 far_stroke_width = 0,
                 far_stroke_color = '#000000'
             }) {

        this.near.fill.parse(near_fill);
        this.near.stroke_width = near_stroke_width;
        this.near.stroke_color.parse(near_stroke_color);

        this.far.fill.parse(far_fill);
        this.far.stroke_width = far_stroke_width;
        this.far.stroke_color.parse(far_stroke_color);
    }

    static fromJSON(j) {
        const r = new DirectionStyle();

        r.fromJSON(j);

        return r;
    }
}

function apply_hierarchical_options_from_json(source, target) {
    for (const prop_name in source) {
        if (!target.hasOwnProperty(prop_name)) {
            console.warn(`Property '${prop_name}' doesn't exist, valid properties are : ${Object.keys(target)}`);
            continue;
        }

        const existing_target_value = target[prop_name];
        const source_value = source[prop_name];

        if (typeof existing_target_value === "object") {
            if (typeof existing_target_value.fromJSON === "function") {
                existing_target_value.fromJSON(source_value);
            } else {
                apply_hierarchical_options_from_json(source_value, existing_target_value);
            }
        } else {
            target[prop_name] = source_value;
        }


    }
}


/**
 * Modelled on https://github.com/jrj2211/three-orientation-gizmo/blob/master/src/OrientationGizmo.js
 */
export class BlenderCameraOrientationGizmo extends CanvasView {
    /**
     *
     */
    constructor(options = {}) {
        super();

        /**
         *
         * @type {Quaternion}
         */
        this.orientation = null;

        this.options = {
            size: 100,
            padding: 8,
            bubbleSizePrimary: 10,
            bubbleSizeSecondary: 8,
            showSecondary: true,
            lineWidth: 2,
            fontSize: "11px",
            fontFamily: "arial",
            fontWeight: "bold",
            fontColor: "rgba(0,0,0,0.8)",
            fontYAdjust: 1,
            selectionFontColor: "#FFFFFF",
            invertOrientation: true,
            axisStyles: {
                px: DirectionStyle.fromJSON({
                    near_fill: "#FF3653",
                    far_fill: "#9D3B4A"
                }),
                py: DirectionStyle.fromJSON({
                    near_fill: "#8ADB00",
                    far_fill: "#648C23"
                }),
                pz: DirectionStyle.fromJSON({
                    near_fill: "#2C8FFF",
                    far_fill: "#36679D"
                }),
                nx: DirectionStyle.fromJSON({
                    near_stroke_color: "#FF3653",
                    near_stroke_width: 1,
                    near_fill: "#6E3D44",
                    far_stroke_color: "#9D3B4A",
                    far_stroke_width: 1,
                    far_fill: 'rgba(0,0,0,0)'
                }),
                ny: DirectionStyle.fromJSON({
                    near_stroke_color: "#8ADB00",
                    near_stroke_width: 1,
                    near_fill: "#526532",
                    far_stroke_color: "#648C23",
                    far_stroke_width: 1,
                    far_fill: 'rgba(0,0,0,0)'
                }),
                nz: DirectionStyle.fromJSON({
                    near_stroke_color: "#2C8FFF",
                    near_stroke_width: 1,
                    near_fill: "#3B536E",
                    far_stroke_color: "#36679D",
                    far_stroke_width: 1,
                    far_fill: 'rgba(0,0,0,0)'
                })
            }
        };

        // read options
        apply_hierarchical_options_from_json(options, this.options);

        // Generate list of axes
        this.bubbles = [
            {
                axis: "x",
                direction: new Vector3(1, 0, 0),
                size: this.options.bubbleSizePrimary,
                style: this.options.axisStyles.px,
                line: this.options.lineWidth,
                label: "X",
                primary: true
            },
            {
                axis: "y",
                direction: new Vector3(0, 1, 0),
                size: this.options.bubbleSizePrimary,
                style: this.options.axisStyles.py,
                line: this.options.lineWidth,
                label: "Y",
                primary: true
            },
            {
                axis: "z",
                direction: new Vector3(0, 0, 1),
                size: this.options.bubbleSizePrimary,
                style: this.options.axisStyles.pz,
                line: this.options.lineWidth,
                label: "Z",
                primary: true
            },
            {
                axis: "-x",
                direction: new Vector3(-1, 0, 0),
                size: this.options.bubbleSizeSecondary,
                style: this.options.axisStyles.nx,
                label: "-X",
                labelHideUnselected: true,
                primary: false
            },
            {
                axis: "-y",
                direction: new Vector3(0, -1, 0),
                size: this.options.bubbleSizeSecondary,
                style: this.options.axisStyles.ny,
                label: "-Y",
                labelHideUnselected: true,
                primary: false
            },
            {
                axis: "-z",
                direction: new Vector3(0, 0, -1),
                size: this.options.bubbleSizeSecondary,
                style: this.options.axisStyles.nz,
                label: "-Z",
                labelHideUnselected: true,
                primary: false
            },
        ];

        /**
         *
         * @type {Vector3|null}
         */
        this.input_pointer_position = null;

        this.center = new Vector3(this.options.size / 2, this.options.size / 2, 0);
        this.selectedAxis = null;

        this.size.setScalar(this.options.size);

        this.onMouseMove = this.onMouseMove.bind(this);
        this.onMouseOut = this.onMouseOut.bind(this);
        this.onMouseClick = this.onMouseClick.bind(this);

        /**
         *
         * Invoked when axis is clicked
         * @type {Signal<string,Vector3>}
         */
        this.on.axisSelected = new Signal();

        /**
         *
         * @type {boolean}
         * @private
         */
        this.__needs_update = true;
    }

    link() {
        super.link();

        const el = this.el;

        el.addEventListener('mousemove', this.onMouseMove);
        el.addEventListener('mouseout', this.onMouseOut);
        el.addEventListener('click', this.onMouseClick);
    }

    unlink() {
        const el = this.el;

        el.removeEventListener('mousemove', this.onMouseMove);
        el.removeEventListener('mouseout', this.onMouseOut);
        el.removeEventListener('click', this.onMouseClick);

        super.unlink();
    }

    /**
     *
     * @param {MouseEvent} evt
     */
    onMouseMove(evt) {
        this._setPointerPositionFromEvent(evt);
        this.__try_update();
    }

    /**
     *
     * @param {MouseEvent} evt
     * @private
     */
    _setPointerPositionFromEvent(evt) {
        const v2 = new Vector2();

        readPositionFromMouseEvent(v2, evt);

        const position_changed = (this.input_pointer_position === null || this.input_pointer_position.x !== v2.x || this.input_pointer_position.y !== v2.y);

        this.input_pointer_position = new Vector3(v2.x, v2.y, 0);


        if (position_changed) {
            this.__needs_update = true;
        }

        return position_changed;
    }

    /**
     *
     * @param {MouseEvent} evt
     */
    onMouseOut(evt) {
        this.input_pointer_position = null;
        this.__needs_update = true;
        this.__try_update();
    }

    /**
     *
     * @param {MouseEvent} evt
     */
    onMouseClick(evt) {

        this._setPointerPositionFromEvent(evt);

        this.__try_update();

        if (this.selectedAxis !== null) {

            if (this.on.axisSelected.hasHandlers()) {
                // axis selection will be handled, stop propagation
                evt.stopPropagation();
            }

            this.on.axisSelected.send2(
                this.selectedAxis.axis,
                this.selectedAxis.direction.clone()
            );
        }
    }

    drawCircle(p, radius = 10, fill_color = "#FF0000", stroke_width = 0, stroke_color = "#FFFFFF") {
        const ctx = this.context2d;

        ctx.beginPath();
        ctx.arc(p.x, p.y, radius, 0, 2 * Math.PI, false);
        ctx.fillStyle = fill_color;
        ctx.fill();

        if (stroke_width > 0) {
            ctx.lineWidth = stroke_width;
            ctx.strokeStyle = stroke_color;
            ctx.stroke();
        }

        ctx.closePath();
    }

    drawLine(p1, p2, width = 1, color = "#FF0000") {
        const ctx = this.context2d;

        ctx.beginPath();
        ctx.moveTo(p1.x, p1.y);
        ctx.lineTo(p2.x, p2.y);
        ctx.lineWidth = width;
        ctx.strokeStyle = color;
        ctx.stroke();
        ctx.closePath();
    }

    __try_update() {
        if (!this.__needs_update) {
            return;
        }
        this.update();
    }

    update() {
        // reset flag
        this.__needs_update = false;

        this.clear();

        // Calculate the rotation matrix from the camera
        const rotation_matrix = scratch_m4;
        let rotation = this.orientation;

        if (rotation === null) {
            // not connected
            return;
        }

        if (this.options.invertOrientation) {
            const q = scratch_quat;

            quaternion_invert_orientation(q, rotation);

            rotation = q;
        }

        compose_matrix4_array(rotation_matrix, Vector3.zero, rotation, Vector3.one);
        mat4.invert(rotation_matrix, rotation_matrix);

        for (const bubble of this.bubbles) {
            const direction = bubble.direction.clone();
            direction.applyMatrix4(rotation_matrix);

            bubble.position = this.getBubblePosition(direction);
        }

        // Generate a list of layers to draw
        const layers = [];

        for (const bubble of this.bubbles) {
            // Check if the name starts with a negative and dont add it to the layer list if secondary axis is turned off
            const is_secondary = bubble.axis[0] !== "-";
            if (this.options.showSecondary === true || is_secondary) {
                layers.push(bubble);
            }
        }

        // Sort the layers where the +Z position is last so its drawn on top of anything below it
        layers.sort((a, b) => (a.position.z > b.position.z) ? 1 : -1);

        // If the mouse is over the gizmo, find the closest axis and highlight it
        this.selectedAxis = null;

        if (this.input_pointer_position) {
            let closestDist = Infinity;
            let closestZ = -Infinity;

            // Loop through each layer
            for (let bubble of layers) {
                const distance = v2_distance(this.input_pointer_position.x, this.input_pointer_position.y, bubble.position.x, bubble.position.y);

                // Only select the axis if its closer to the mouse than the previous or if its within its bubble circle
                if (
                    distance < closestDist
                    && closestZ < bubble.position.z
                    && distance <= bubble.size
                ) {

                    closestDist = distance;
                    closestZ = bubble.position.z;
                    this.selectedAxis = bubble;

                }
            }
        }

        // Draw the layers
        this.drawLayers(layers);
    }

    /**
     *
     * @param {{style:DirectionStyle, position:Vector3, size:number,line:boolean, label?:string,labelHideUnselected?:boolean}[]} layers
     */
    drawLayers(layers) {
        // For each layer, draw the bubble
        for (let bubble of layers) {

            // Find the color
            const is_selected = this.selectedAxis === bubble;

            const closeness = clamp01(inverseLerp(-1, 0.8, bubble.position.z));

            const style = bubble.style;

            scratch_color.lerpColors(style.far.fill, style.near.fill, closeness);
            const fill_color = scratch_color.toCssRGBAString();

            scratch_color.lerpColors(style.far.stroke_color, style.near.stroke_color, closeness);
            const stroke_color = scratch_color.toCssRGBAString();

            const stroke_width = lerp(style.far.stroke_width, style.near.stroke_width, closeness);

            // Draw the circle for the bubbble
            this.drawCircle(bubble.position, bubble.size, fill_color, stroke_width, stroke_color);

            // Draw the line that connects it to the center if enabled
            if (bubble.line) {
                this.drawLine(this.center, bubble.position, bubble.line, fill_color);
            }

            // Write the axis label (X,Y,Z) if provided
            if (bubble.label && (is_selected || (bubble.labelHideUnselected !== true))) {
                const ctx = this.context2d;

                ctx.font = [this.options.fontWeight, this.options.fontSize, this.options.fontFamily].join(" ");
                ctx.fillStyle = is_selected ? this.options.selectionFontColor : this.options.fontColor;
                ctx.textBaseline = 'middle';
                ctx.textAlign = 'center';
                ctx.fillText(bubble.label, bubble.position.x, bubble.position.y + this.options.fontYAdjust);
            }
        }
    }

    /**
     *
     * @param {Vector3} position
     * @returns {Vector3}
     */
    getBubblePosition(position) {
        return new Vector3(
            (position.x * (this.center.x - (this.options.bubbleSizePrimary / 2) - this.options.padding)) + this.center.x,
            this.center.y - (position.y * (this.center.y - (this.options.bubbleSizePrimary / 2) - this.options.padding)),
            position.z
        );
    }
}
