const addButton = document.getElementById('add');
const removeButton = document.getElementById('remove');
const c0Button = document.getElementById('c0');
const c1Button = document.getElementById('c1');
const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');

let adding = false;
/** @type {number|null} */
let selected = null;
let mouseDown = false;
/** @type {number|null} */
let movePointIndex = null;

/** @type {{x: number, y: number}[][]} */
const curves = [];

addButton.addEventListener('click', () => {
    adding = true;
    curves.push([]);
    selected = curves.length - 1;
});

removeButton.addEventListener('click', () => {
    if (selected === null) return;
    curves.splice(selected, 1);
    selected = null;
    draw();
});

c0Button.addEventListener('click', () => {
    if (curves.length <= 1 || selected === null) return;

    let [ci, pi] = getClosestEndPoint(curves[selected][0]);
    if (ci === null || pi === null) return;
    curves[selected][0] = curves[ci][pi];

    [ci, pi] = getClosestEndPoint(curves[selected][3]);
    if (ci === null || pi === null) return;
    curves[selected][3] = curves[ci][pi];

    draw();
});

c1Button.addEventListener('click', () => {
    if (curves.length <= 1 || selected === null) return;

    let [ci, pi] = getClosestEndPoint(curves[selected][0]);
    if (ci === null || pi === null) return;
    // Move first point of selected curve to closest found point
    curves[selected][0] = curves[ci][pi];
    // Get control point of found end point
    const handle1 = curves[ci][pi === 0 ? 1 : 2];
    // Set control point to location mirrored over the point
    curves[selected][1] = {
        x: 2 * curves[selected][0].x - handle1.x,
        y: 2 * curves[selected][0].y - handle1.y,
    };

    [ci, pi] = getClosestEndPoint(curves[selected][3]);
    if (ci === null || pi === null) return;
    curves[selected][3] = curves[ci][pi];
    const handle2 = curves[ci][pi === 0 ? 1 : 2];
    curves[selected][2] = {
        x: 2 * curves[selected][3].x - handle2.x,
        y: 2 * curves[selected][3].y - handle2.y,
    };

    draw();
});

/**
 * @param {number} x
 * @param {number} y
 */
function addPoint(x, y) {
    const curve = curves[curves.length - 1];
    curve.push({ x, y });
    if (curve.length === 4) {
        adding = false;
    }
    draw();
}

/**
 * @param {number} t
 * @param {number} a
 * @param {number} b
 * @returns {number}
 */
function lerp(t, a, b) {
    return (1 - t) * a + t * b;
}

/**
 * interpolates the values
 * @param {number} t
 * @param {number[]} v array of values
 */
function lerpArray(t, v) {
    if (v.length === 1) return v[0];
    return lerp(t, lerpArray(t, v.slice(0, -1)), lerpArray(t, v.slice(1)));
}

function drawCircles() {
    if (selected === null) return;
    for (const { x, y } of curves[selected]) {
        context.beginPath();
        context.ellipse(x, y, 10, 10, 0, 0, Math.PI * 2);
        context.stroke();
    }
}

// Failed attempt at adaptive line length

/**
 * @param {number} tStart
 * @param {number} tEnd
 * @param {{x: number, y: number}[]} points
 * @returns {{x: number, y: number}[]}
 */
function calcPoints(tStart, tEnd, points) {
    const xPoints = points.map((p) => p.x);
    const yPoints = points.map((p) => p.y);

    const x1 = lerpArray(tStart, xPoints);
    const y1 = lerpArray(tStart, yPoints);
    const x2 = lerpArray((tStart + tEnd) / 2, xPoints);
    const y2 = lerpArray((tStart + tEnd) / 2, yPoints);
    const x3 = lerpArray(tEnd, xPoints);
    const y3 = lerpArray(tEnd, yPoints);

    const surface =
        (1 / 2) * Math.abs(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2));

    // const d12 = Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2);
    // const d13 = Math.sqrt((x1 - x3) ** 2 + (y1 - y3) ** 2);
    // const d23 = Math.sqrt((x2 - x3) ** 2 + (y2 - y3) ** 2);
    //
    // const angle = Math.acos((d12 ** 2 + d13 ** 2 - d23 ** 2) / (2 * d12 * d13));

    if (surface < 20) {
        return [{ x: x3, y: y3 }];
    }

    return [
        ...calcPoints(tStart, (tStart + tEnd) / 2, points),
        ...calcPoints((tStart + tEnd) / 2, tEnd, points),
    ];
}

function drawLines() {
    for (let i = 0; i < curves.length; i++) {
        const curve = curves[i];
        for (let j = 0; j < curve.length - 3; j += 3) {
            const points = curve.slice(j, j + 4);

            context.beginPath();

            if (selected === i) {
                context.moveTo(points[0].x, points[0].y);
                context.lineTo(points[1].x, points[1].y);

                context.moveTo(points[2].x, points[2].y);
                context.lineTo(points[3].x, points[3].y);
            }

            context.moveTo(points[0].x, points[0].y);
            //
            // const points = calcPoints(0, 1, curve);
            //
            // for (const { x, y } of points) {
            //     context.lineTo(x, y);
            // }

            for (let t = 0; t <= 100; t++) {
                const tt = t / 100;
                const x = lerpArray(
                    tt,
                    points.map((p) => p.x),
                );
                const y = lerpArray(
                    tt,
                    points.map((p) => p.y),
                );

                context.lineTo(x, y);
            }

            context.stroke();
        }
    }
}

function draw() {
    context.clearRect(0, 0, canvas.width, canvas.height);

    drawCircles();
    drawLines();
}

function resizeCanvas() {
    canvas.height = window.innerHeight;
    canvas.width = window.innerWidth;
    draw();
}

/**
 * Gets the index of the closest curve to the point
 * @param {number} x
 * @param {number} y
 * @returns {number|null}
 */
function getClosestCurveIndex(x, y) {
    /** @type {number|null} */
    let minDist = null;
    /** @type {number|null} */
    let minIndex = null;

    for (let i = 0; i < curves.length; i++) {
        for (const point of curves[i]) {
            const dist = Math.abs(
                Math.sqrt((x - point.x) ** 2 + (y - point.y) ** 2),
            );
            if (!minDist || dist < minDist) {
                minDist = dist;
                minIndex = i;
            }
        }
    }

    return minIndex;
}

/**
 * Gets the selected curve point under the mouse
 * @param {number} x
 * @param {number} y
 * @returns {number|null}
 */
function getSelectedPoint(x, y) {
    if (selected === null) return null;
    for (let i = 0; i < curves[selected].length; i++) {
        const point = curves[selected][i];
        const dist = Math.abs(
            Math.sqrt((x - point.x) ** 2 + (y - point.y) ** 2),
        );
        if (dist <= 10) {
            return i;
        }
    }
    return null;
}

/**
 * Gets the closest end point
 * @param {{x: number, y: number}} point
 * @returns {[number|null, number|null]} curveIndex and pointIndex
 */
function getClosestEndPoint(point) {
    /** @type {number|null} */
    let minDist = null,
        minCurveIndex = null,
        minPointIndex = null;

    for (let i = 0; i < curves.length; i++) {
        if (i === selected) continue;
        let dist = Math.abs(
            Math.sqrt(
                (point.x - curves[i][0].x) ** 2 +
                    (point.y - curves[i][0].y) ** 2,
            ),
        );
        if (minDist === null || dist < minDist) {
            minDist = dist;
            minCurveIndex = i;
            minPointIndex = 0;
        }

        dist = Math.abs(
            Math.sqrt(
                (point.x - curves[i][3].x) ** 2 +
                    (point.y - curves[i][3].y) ** 2,
            ),
        );
        if (minDist === null || dist < minDist) {
            minDist = dist;
            minCurveIndex = i;
            minPointIndex = 3;
        }
    }

    return [minCurveIndex, minPointIndex];
}

canvas.addEventListener('mousedown', (e) => {
    mouseDown = true;

    if (adding) {
        addPoint(e.pageX, e.pageY);
        return;
    }

    const point = getSelectedPoint(e.pageX, e.pageY);
    if (point !== null) {
        movePointIndex = point;
        return;
    }

    const i = getClosestCurveIndex(e.pageX, e.pageY);
    if (i === null) return;
    selected = i;
    draw();
});

canvas.addEventListener('mousemove', (e) => {
    if (!mouseDown) return;
    if (movePointIndex === null) return;
    curves[selected][movePointIndex] = { x: e.pageX, y: e.pageY };
    draw();
});

canvas.addEventListener('mouseup', () => {
    mouseDown = false;
    movePointIndex = null;
});

window.addEventListener('resize', resizeCanvas);
resizeCanvas();