import { createCanvas } from "@common/display/canvas";
import { gameLoop } from "@common/game";
import { bresenhamCircleGen } from "@common/navigation/bresenham";
import Physics from "@common/physics";
import { mapNumber } from "@common/utils";

interface Ball {
    body: number;
    damage: number;
    health: number;
}

interface Peg {
    body: number;
    health: number;
}

interface State extends Record<string, unknown> {
    canvas: HTMLCanvasElement;
    ctx: CanvasRenderingContext2D;
    bottom: number;
    balls: Set<Ball>;
    pegs: Set<Peg>;

    pegHealth: number;
    ballHealth: number;
    ballDamage: number;
}

let lastTick = performance.now();
function update() {
    const dt = (performance.now() - lastTick) / 1000;
    if (dt < 1) {
        Physics.update(dt);
    }
    lastTick = performance.now();
    setTimeout(update, 5);
}

const killBody = (state: State, body: Peg | Ball) => {
    state.balls.delete(body as Ball);
    state.pegs.delete(body as Peg);
    Physics.deleteBody(body.body);
}

const onCollision = (state: State, b1: number, b2: number) => {
    const ball = state.balls.values().find(b => b.body === b1 || b.body === b2);
    const peg = state.pegs.values().find(b => b.body === b1 || b.body === b2);

    if (ball && (b1 === state.bottom || b2 === state.bottom)) {
        killBody(state, ball);
    } else if (peg && ball) {
        peg.health -= ball.damage;
        ball.health -= 1;
        if (peg.health <= 0) {
            killBody(state, peg);
        }
        if (ball.health <= 0) {
            killBody(state, ball);
        }
    }
}

const generatePegs = (state: State) => {
    const { canvas } = state;
    state.pegs.clear();

    const gap = 45;
    const vgap = gap * Math.cos(Math.PI / 6);
    let offset = 0;
    for (let y = canvas.height / 3; y <= canvas.height - gap; y += vgap) {
        for (let i = offset; i <= canvas.width / 2; i += gap) {
            const x1 = canvas.width / 2 + i;
            state.pegs.add({
                body: Physics.newCircle(x1, y, 3, Number.POSITIVE_INFINITY),
                health: state.pegHealth,
            });

            const x2 = canvas.width / 2 - i;
            if (Math.abs(x1 - x2) < gap) continue;
            state.pegs.add({
                body: Physics.newCircle(x2, y, 3, Number.POSITIVE_INFINITY),
                health: state.pegHealth,
            });
        }
        offset = gap / 2 - offset;
    }
}

const setup = (): State => {
    const canvas = createCanvas(640, 480);
    const ctx = canvas.getContext('2d');
    if (!ctx) {
        throw new Error('Failed to get canvas context');
    }

    const state: State = {
        canvas,
        ctx,
        bottom: 0,
        pegs: new Set(),
        balls: new Set(),
        pegHealth: 3,
        ballHealth: 10,
        ballDamage: 1,
    };

    ctx.imageSmoothingEnabled = false;
    canvas.addEventListener('contextmenu', e => {
        e.stopPropagation();
        e.preventDefault();
    });
    canvas.addEventListener('mouseup', () => {
        const body = Physics.newCircle(canvas.width / 2 + Math.random() - 0.5, 50, 10, 1);
        const health = state.ballHealth;
        const damage = state.ballDamage;
        const ball: Ball = {
            body,
            health,
            damage,
        }
        state.balls.add(ball);
        return false;
    });

    generatePegs(state);

    state.bottom = Physics.newPlane(0, canvas.height, 0, -1);
    Physics.newPlane(0, 0, 1, 0);
    Physics.newPlane(0, 0, 0, 1);
    Physics.newPlane(canvas.width, 0, -1, 0);

    Physics.setCollisionCallback((a, b) => onCollision(state, a, b));

    update();

    return state;
}

const frame = (dt: number, state: State) => {
    Physics.addGlobalForce(0, 100);
    Physics.update(dt);

    const { ctx, canvas } = state;
    ctx.fillStyle = `#111`;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    for (const peg of state.pegs) {
        const { x, y, radius } = Physics.getBody(peg.body);
        if (!radius) continue;

        ctx.fillStyle = `hsl(0, 0%, ${mapNumber(peg.health, 0, state.pegHealth, 0, 100)}%)`;
        fillCircle(ctx, x, y, radius);
    }
    for (const ball of state.balls) {
        const { id, x, y, radius } = Physics.getBody(ball.body);
        if (!radius) continue;

        ctx.fillStyle = `hsl(${id % 360}, 100%, 50%)`;
        fillCircle(ctx, x, y, radius);
    }
}

function fillCircle(ctx: CanvasRenderingContext2D, xc: number, yc: number, r: number) {
    // because default circle fill is antialiased
    for (const { x, y } of bresenhamCircleGen(xc, yc, r, { fill: true })) {
        ctx.fillRect(x, y, 1, 1);
    }
}

export default gameLoop(setup, frame);