const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

const dropZone = document.getElementById("drop-zone");
dropZone.addEventListener("dragenter", (e) => e.preventDefault());
dropZone.addEventListener("dragover", (e) => e.preventDefault());
dropZone.addEventListener("drop", (e) => {
    const reader = new FileReader();
    reader.addEventListener("loadend", async () => {
        const data = JSON.parse(reader.result);
        await run(data);
    });
    reader.readAsText(e.dataTransfer.files[0]);
    e.preventDefault();
});

async function run({ models, scene }) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    for (const { model, transforms } of scene) {
        let m = Matrix.identity();

        for (const transform of transforms) {
            switch (transform.type) {
                case "scale":
                    m = m.scale(transform.factor[0], transform.factor[1]);
                    break;
                case "rotate":
                    m = m.rotate(transform.angle);
                    break;
                case "translate":
                    m = m.translate(transform.vector[0], transform.vector[1]);
                    break;
            }
        }

        m = m.screen(0, 0, canvas.width, canvas.height);

        for (const line of models[model]) {
            const p1 = m.multiply(Matrix.vector(...line[0], 1));
            ctx.moveTo(p1.vector[0], p1.vector[1]);

            const p2 = m.multiply(Matrix.vector(...line[1], 1));
            ctx.lineTo(p2.vector[0], p2.vector[1]);
        }
        ctx.stroke();
    }
}

class Matrix extends Array {
    /**
     * @param {number[][]} matrix
     */
    constructor(matrix) {
        super();
        this.push(...matrix);
    }

    static identity() {
        return new Matrix([
            [1, 0, 0],
            [0, 1, 0],
            [0, 0, 1],
        ]);
    }

    /**
     * @param {number} vector
     */
    static vector(...vector) {
        const matrix = Array(vector.length);
        for (let i = 0; i < vector.length; i++) {
            matrix[i] = [vector[i]];
        }
        return new Matrix(matrix);
    }

    get width() {
        if (this.length === 0) {
            return 0;
        }
        return this[0].length;
    }

    get height() {
        return this.length;
    }

    get vector() {
        const v = [];
        for (const l of this) {
            v.push(l[0]);
        }
        return v;
    }

    toString() {
        let str = "";
        for (const line of this) {
            str += line.join(", ") + "\n";
        }
        return str;
    }

    /**
     * @param {Matrix} matrix
     */
    multiply(matrix) {
        if (this.width !== matrix.height) {
            throw Error("Can not multiply");
        }

        const m = Array.from({ length: this.height },
            () => Array.from(
                { length: matrix.width }, () => 0,
            ),
        );

        for (let i = 0; i < m.length; i++) {
            for (let j = 0; j < m[0].length; j++) {
                let sum = 0;
                for (let k = 0; k < this.width; k++) {
                    sum += this[i][k] * matrix[k][j];
                }
                m[i][j] = sum;
            }
        }

        return new Matrix(m);
    }

    /**
     * @param {number} x
     * @param {number} y
     */
    scale(x, y) {
        return new Matrix([
            [x, 0, 0],
            [0, y, 0],
            [0, 0, 1],
        ]).multiply(this);
    }

    /**
     * @param {number} angle
     */
    rotate(angle) {
        return new Matrix([
            [Math.cos(angle), -Math.sin(angle), 0],
            [Math.sin(angle), Math.cos(angle), 0],
            [0, 0, 1],
        ]).multiply(this);
    }

    translate(x, y) {
        return new Matrix([
            [1, 0, x],
            [0, 1, y],
            [0, 0, 1],
        ]).multiply(this);
    }

    /**
     * @param {number} x0
     * @param {number} y0
     * @param {number} x1
     * @param {number} y1
     */
    screen(x0, y0, x1, y1) {
        return new Matrix([
            [(x1 - x0) / 2, 0, (x0 + x1) / 2],
            [0, -(y1 - y0) / 2, (y0 + y1) / 2],
            [0, 0, 1],
        ]).multiply(this);
    }
}