import { assert } from "../../../../core/assert.js";
import AABB2 from "../../../../core/geom/2d/aabb/AABB2.js";
import { m3_cm_compose_transform } from "../../../../core/geom/mat3/m3_cm_compose_transform.js";
import { m3_cm_invert } from "../../../../core/geom/mat3/m3_cm_invert.js";
import { v2_length } from "../../../../core/geom/vec2/v2_length.js";
import { v2_matrix3_cm_multiply } from "../../../../core/geom/vec2/v2_matrix3_cm_multiply.js";
import { CanvasView } from "../../../../view/elements/CanvasView.js";
import EmptyView from "../../../../view/elements/EmptyView.js";
import { canvas2d_draw_grid } from "../../../graphics/canvas/canvas2d_draw_grid.js";
import { animation_curve_compute_aabb } from "../animation_curve_compute_aabb.js";
import { AnimationCurve } from "../AnimationCurve.js";

const scratch_v2_a = new Float32Array(2);
const scratch_v2_b = new Float32Array(2);

export class AnimationCurveView extends EmptyView {

    #curve = AnimationCurve.constant();
    #canvas = new CanvasView();
    #frame = new AABB2();
    #curve_bounds = new AABB2();

    /**
     * 3x3 transform matrix
     * Transform from curve space to canvas space
     * @type {Float32Array}
     */
    #transform = new Float32Array(9);
    /**
     * 3x3 inverse of transform matrix
     * @type {Float32Array}
     */
    #transform_inverse = new Float32Array(9);

    /**
     *
     * @type {number}
     */
    #margin = 36

    /**
     *
     * @param {AnimationCurve} v
     */
    set curve(v) {
        this.#curve = v;

        this.update();
    }


    constructor() {
        super();

        this.addChild(this.#canvas);

        this.size.onChanged.add((x, y) => {

            this.#canvas.size.set(x, y);
            this.update();

        });
    }

    auto_set_frame() {
        animation_curve_compute_aabb(this.#curve_bounds, this.#curve);

        this.#frame.copy(this.#curve_bounds);

        if (this.#frame.height === 0) {
            this.#frame.growHeight(0.5);
        }

        if (this.#frame.width === 0) {
            this.#frame.growWidth(0.5);
        }

        const margin_top = this.#margin;
        const margin_bottom = this.#margin;
        const margin_left = this.#margin;
        const margin_right = this.#margin;

        const normalized_margin_left = (margin_left / this.size.x) * this.#frame.width;
        const normalized_margin_right = (margin_right / this.size.x) * this.#frame.width;
        const normalized_margin_top = (margin_top / this.size.y) * this.#frame.height;
        const normalized_margin_bottom = (margin_bottom / this.size.y) * this.#frame.height;

        this.#frame.x0 -= normalized_margin_left;
        this.#frame.x1 += normalized_margin_right;
        this.#frame.y0 -= normalized_margin_top;
        this.#frame.y1 += normalized_margin_bottom;

        this.#update_frame();
    }

    #update_frame() {

        const frame = this.#frame;
        const size = this.size;

        const scale_x = size.x / frame.width;
        const scale_y = -size.y / frame.height;

        m3_cm_compose_transform(
            this.#transform,
            -frame.x0, -frame.y1,
            scale_x, scale_y,
            frame.x0, frame.y1, 0
        );

        m3_cm_invert(this.#transform_inverse, this.#transform);
    }

    /**
     *
     * @param {Float32Array|number[]} out
     * @param {number} x
     * @param {number} y
     */
    point_curve_to_canvas(out, x, y) {

        assert.notNaN(x, 'x');
        assert.notNaN(y, 'y');

        v2_matrix3_cm_multiply(out, 0,
            x, y, this.#transform
        );

    }

    /**
     *
     * @param {Float32Array|number[]} out
     * @param {number} x
     * @param {number} y
     */
    point_canvas_to_curve(out, x, y) {

        v2_matrix3_cm_multiply(out, 0,
            x, y, this.#transform_inverse
        );

    }

    update() {
        if (this.size.x <= 0 || this.size.y <= 0) {
            // size too small
            return;
        }

        this.auto_set_frame();

        this.draw();
    }

    draw() {
        const ctx = this.#canvas.context2d;

        ctx.fillStyle = '#222222';
        ctx.fillRect(0, 0, this.size.x, this.size.y);

        this.draw_grid();


        ctx.fillStyle = 'none';
        ctx.strokeStyle = '#00ff00';
        ctx.lineWidth = 1;

        this.draw_curve();

        ctx.fillStyle = 'none';
        ctx.strokeStyle = '#0080ff';
        ctx.lineWidth = 1;

        this.draw_tangents();

        this.draw_key_knots();
    }

    draw_grid() {
        const ctx = this.#canvas.context2d;

        const width = this.size.x;
        const height = this.size.y;

        canvas2d_draw_grid({
            ctx,
            width,
            height,
            color: '#262626',
            spacing: 32,
            offset_x: 0
        });

        canvas2d_draw_grid({
            ctx,
            width,
            height,
            color: '#303030',
            spacing: 32,
            offset_x: 16,
            offset_y: 16
        });
    }

    draw_key_knots() {
        const keys = this.#curve.keys;
        const key_count = keys.length;

        for (let i = 0; i < key_count; i++) {
            this.draw_key_knot(keys[i]);
        }

    }

    draw_key_knot(key) {

        const ctx = this.#canvas.context2d;

        this.point_curve_to_canvas(scratch_v2_a, key.time, key.value);

        const stroke_width = 1;
        const radius = 2;

        ctx.beginPath();
        ctx.arc(scratch_v2_a[0], scratch_v2_a[1], radius + stroke_width * 0.5, 0, 2 * Math.PI, false);
        ctx.fillStyle = 'green';
        ctx.fill();
        ctx.lineWidth = stroke_width;
        ctx.strokeStyle = 'rgba(0,133,0,0.2)';
        ctx.stroke();

    }


    /**
     *
     * @param {Keyframe} keyframe
     */
    draw_key_tangents(keyframe) {
        const handle_length = 36;

        const ctx = this.#canvas.context2d;

        // incoming
        const in_angle = -Math.atan2(keyframe.inTangent, -1);
        this.draw_tangent(ctx, keyframe, in_angle, handle_length);

        // outgoing
        const ut_angle = Math.atan2(keyframe.outTangent, 1);

        this.draw_tangent(ctx, keyframe, ut_angle, handle_length);
    }

    /**
     *
     * @param {CanvasRenderingContext2D} ctx
     * @param {Keyframe} keyframe
     * @param {number} angle
     * @param {number} handle_length in pixels
     */
    draw_tangent(ctx, keyframe, angle, handle_length) {
        ctx.beginPath();

        this.point_curve_to_canvas(scratch_v2_a, keyframe.time, keyframe.value);

        ctx.moveTo(scratch_v2_a[0], scratch_v2_a[1]);

        this.point_curve_to_canvas(scratch_v2_b,
            keyframe.time + Math.cos(angle),
            keyframe.value + Math.sin(angle)
        );

        // a - b
        scratch_v2_b[0] -= scratch_v2_a[0];
        scratch_v2_b[1] -= scratch_v2_a[1];

        // normalize to desired length
        const norm = handle_length / v2_length(scratch_v2_b[0], scratch_v2_b[1]);

        scratch_v2_b[0] *= norm;
        scratch_v2_b[1] *= norm;

        // restore offset
        scratch_v2_b[0] += scratch_v2_a[0];
        scratch_v2_b[1] += scratch_v2_a[1];

        ctx.lineTo(scratch_v2_b[0], scratch_v2_b[1]);

        ctx.stroke();
    }

    draw_tangents() {
        const curve = this.#curve;
        const keys = curve.keys;

        for (let i = 0; i < keys.length; i++) {
            const keyframe = keys[i];

            this.draw_key_tangents(keyframe);
        }
    }

    draw_curve() {

        const ctx = this.#canvas.context2d;

        ctx.beginPath();

        const curve = this.#curve;

        const keys = curve.keys;

        if (keys.length < 2) {
            return;
        }

        const duration = curve.duration;
        const first_key = curve.keys[0];
        const time_start = first_key.time;

        const segments = Math.ceil(this.size.x / 4);

        this.point_curve_to_canvas(scratch_v2_a, first_key.time, first_key.value);

        ctx.moveTo(scratch_v2_a[0], scratch_v2_a[1]);

        for (let i = 1; i < segments; i++) {
            const t = i / (segments - 1);

            const time = time_start + duration * t;

            const value = curve.evaluate(time);

            this.point_curve_to_canvas(scratch_v2_a, time, value);

            ctx.lineTo(scratch_v2_a[0], scratch_v2_a[1]);
        }

        ctx.stroke();
    }

}