import { formatError, formatErrorMessage } from "./errors";
import Input from "./input";
import { nextFrame } from "./utils";

interface FrameMeta {
    fps: number;
    now: number;
}
type Awaitable<T> = PromiseLike<T> | T;

type Setup<T> = () => Awaitable<T>;
type Frame<T> = (dt: number, state: T, meta: FrameMeta) => Awaitable<T | void>;

export function gameLoop<T>(frame: Frame<T>): RunGame;
export function gameLoop<T>(setup: Setup<T>, frame: Frame<T>): RunGame;
export function gameLoop<T>(setupOrFrame: Setup<T> | Frame<T>, frame?: Frame<T>): RunGame {
    return async () => {
        let state: T;
        try {
            if (frame) {
                state = await (setupOrFrame as Setup<T>)();
            } else {
                frame = setupOrFrame as Frame<T>;
                state = {} as T;
            }
        } catch (e) {
            console.error(formatError(e, 'Error in game setup'));
            alert(formatErrorMessage(e));
            return;
        }

        try {
            let prevFrame = performance.now();
            let fpsCounter = 0;
            let fpsTimer = 0;
            const meta: FrameMeta = { fps: 0, now: 0 };
            while (true) {
                await nextFrame();
                Input.updateKeys();

                const now = performance.now();
                const dt = (now - prevFrame) / 1000;

                if (dt < 1) { // skip long pause to avoid blowing up values
                    meta.now += dt;
                    const newState = await frame(dt, state, meta);
                    if (newState) {
                        state = newState;
                    }
                }

                fpsCounter += 1;
                if (now - fpsTimer > 500) {
                    meta.fps = fpsCounter * 2;
                    fpsCounter = 0;
                    fpsTimer = now;
                }

                prevFrame = performance.now();
            }
        } catch (e) {
            console.error(formatError(e, 'Error in game loop'));
            alert(formatErrorMessage(e));
            return;
        }
    }
};
