import kit.sys.utils;

struct RenderThreadData {
    var renderMutex: Ptr[SDL_mutex];
    var renderCondMutex: Ptr[SDL_mutex];
    var renderCond: Ptr[SDL_cond];
    var context: Ptr[RenderContext];
    var killed: Bool = false;
}

struct App {
    static inline var MAX_FRAMES: Int = 5;
    var title: CString;
    var engine: Engine;
    var window: Window;
    var renderer: Renderer;

    var running: Bool;

    function init(allocator: Box[Allocator], options: Ptr[KritOptions]) using implicit allocator {
        this.window.init(options.title, options.width, options.height);
        this.engine = struct Engine {fps: options.fps};
        this.renderer = struct Renderer;
        this.renderer.init();
        this.running = false;
    }

    function run() {
        this.running = true;
        var frameDelta1: Double = 1_f64 / (this.engine.fps - 1);
        var frameDelta2: Double = 1_f64 / (this.engine.fps + 4);
        var update = struct UpdateContext {
            engine: this.engine,
            window: this.window,
            asset: this.engine.asset,
            camera: this.engine.camera,
            input: this.engine.input,
        };
        var render = struct RenderContext {
            engine: this.engine,
            window: this.window,
            renderer: this.renderer,
            renderSystem: this.renderer.renderSystem,
        };
        var accumulator: Double = 0;
        var elapsed: Double = 0;
        var frameStart = time();
        var frameFinish;

        var renderThreadData: RenderThreadData;
        static if defined(SINGLE_THREAD) {} else {
            const renderMutex = SDL_CreateMutex();
            const renderCondMutex = SDL_CreateMutex();
            const renderCond = SDL_CreateCond();
            renderThreadData = struct RenderThreadData {
                renderMutex,
                renderCondMutex,
                renderCond,
                context: &render,
            };
            const renderThread = SDL_CreateThread(Self.renderLoop, "render", &renderThreadData);
            SDL_DetachThread(renderThread);
        }

        while this.running {
            do {
                frameFinish = time();
                elapsed = frameFinish - frameStart;
            } while elapsed < frameDelta2;
            accumulator += elapsed;
            update.elapsed = update.frameCount = 0;
            while accumulator >= frameDelta2 && update.frameCount < Self.MAX_FRAMES {
                ++update.frameCount;
                update.elapsed += frameDelta1;
                accumulator -= frameDelta1;
                if accumulator < 0 {
                    accumulator = 0;
                }
            }
            if accumulator > frameDelta2 {
                accumulator = accumulator % frameDelta2;
            }
            frameStart = frameFinish;
            this.handleEvents(update);
            this.engine.update(update);

            if update.frameCount > 0 {
                static if defined(SINGLE_THREAD) {} else {
                    SDL_LockMutex(renderThreadData.renderMutex);
                }
                this.engine.render(render);
                static if defined(SINGLE_THREAD) {
                    Self.flushRender(render);
                } else {
                    SDL_UnlockMutex(renderThreadData.renderMutex);
                    SDL_LockMutex(renderThreadData.renderCondMutex);
                    SDL_CondSignal(renderThreadData.renderCond);
                    SDL_UnlockMutex(renderThreadData.renderCondMutex);
                    SDL_GL_MakeCurrent(this.window.windowData.window, this.window.windowData.glContext);
                }
            }

            static if defined(FPS) {
                printf("%.4f FPS\n", 1_f64 / update.elapsed);
            }
        }

        if defined(SINGLE_THREAD) {} else {
            SDL_LockMutex(renderThreadData.renderMutex);
            renderThreadData.killed = true;
            SDL_LockMutex(renderThreadData.renderCondMutex);
            SDL_CondSignal(renderThreadData.renderCond);
            SDL_UnlockMutex(renderThreadData.renderCondMutex);
            SDL_UnlockMutex(renderThreadData.renderMutex);
        }
    }

    function destroy() {
        this.window.destroy();
    }

    function handleEvents(context: Ptr[UpdateContext]) {
        var event: SDL_Event;
        while SDL_PollEvent(event) != 0 {
            if event.type == ${SDL_QUIT: Uint} {
                this.running = false;
            } else if event.type == ${SDL_WINDOWEVENT: Uint} {
                if event.window.event == ${SDL_WINDOWEVENT_SIZE_CHANGED: Uint} {
                    // resize
                    var w = event.window.data1;
                    var h = event.window.data2;
                    this.window.onResize(w, h);
                }
            } else if event.type == SDL_KEYDOWN {
                context.input.key.registerDown(event.key.keysym.scancode as Uint8);
            } else if event.type == SDL_KEYUP {
                context.input.key.registerUp(event.key.keysym.scancode as Uint8);
            } else if event.type == SDL_MOUSEBUTTONDOWN {
                context.input.mouse.registerDown(Self.sdlMouseButton(event.button.button));
            } else if event.type == SDL_MOUSEBUTTONUP {
                context.input.mouse.registerUp(Self.sdlMouseButton(event.button.button));
            } else if event.type == SDL_MOUSEMOTION {
                context.input.mouse.registerPos(event.motion.x, event.motion.y);
            } else if event.type == SDL_WINDOWEVENT_ENTER {
                context.input.mouse.registerOver(true);
            } else if event.type == SDL_WINDOWEVENT_LEAVE {
                context.input.mouse.registerOver(false);
            }
        }
    }

    static function sdlMouseButton(b: Uint8): Mouse {
        match b {
            SDL_BUTTON_MIDDLE => return Mouse.MouseMiddleButton;
            SDL_BUTTON_RIGHT => return Mouse.MouseRightButton;
            default => return Mouse.MouseLeftButton;
        }
    }

    static function renderLoop(data: Ptr[RenderThreadData]): Int {
        static if defined(SINGLE_THREAD) {} else {
            while true {
                SDL_LockMutex(data.renderCondMutex);
                SDL_CondWait(data.renderCond, data.renderCondMutex);
                SDL_UnlockMutex(data.renderCondMutex);

                if data.killed {
                    SDL_DestroyCond(data.renderCond);
                    SDL_DestroyMutex(data.renderMutex);
                    SDL_DestroyMutex(data.renderCondMutex);
                    return 0;
                }

                SDL_LockMutex(data.renderMutex);
                SDL_GL_MakeCurrent(data.context.window.windowData.window, data.context.window.windowData.glContext);
                Self.flushRender(data.context);
                SDL_UnlockMutex(data.renderMutex);
            }
        }
    }

    static function flushRender(render: Ptr[RenderContext]) {
        const renderer = render.renderer;
        renderer.flushBatch(render);
        renderer.flushFrame(render);
    }
}
