// TechnicalEntropy browser adapter for doomgeneric.
// This file is GPL-2.0-only, matching the engine it links with.

#include <ctype.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

#include <SDL.h>
#include <SDL_mixer.h>
#include <emscripten.h>

#include "d_event.h"
#include "d_main.h"
#include "d_player.h"
#include "doomdef.h"
#include "doomgeneric.h"
#include "doomkeys.h"
#include "doomstat.h"
#include "g_game.h"
#include "m_argv.h"
#include "p_saveg.h"

#define KEYQUEUE_SIZE 128
#define AUTOSAVE_SLOT 7
#define DOOM_MIN_FRAME_MS (1000.0 / 35.0)
#define DOOM_MAX_FRAME_MS (1000.0 / 18.0)
#define DOOM_TARGET_DUTY_CYCLE 0.24
#define DOOM_PREGAME_IDLE_MS 1000.0

extern boolean sendsave;
extern boolean usergame;
void G_DoSaveGame(void);

static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static SDL_Texture *texture = NULL;
static unsigned short key_queue[KEYQUEUE_SIZE];
static unsigned int key_write = 0;
static unsigned int key_read = 0;
static unsigned char held_keys[256];
static int mouse_buttons = 0;
static int te_paused = 0;
static int te_destroyed = 0;
static int te_controls_captured = 0;
static int te_audio_muted = 1;
static int te_input_seen = 0;
static int te_main_loop_paused = 0;
static double te_next_frame = 0.0;
static double te_frame_ms = DOOM_MIN_FRAME_MS;
static double te_tick_cost_ms = 0.0;
static double te_last_input_ms = 0.0;

static void pause_browser_loop(void)
{
    if (!te_main_loop_paused && !te_destroyed)
    {
        te_main_loop_paused = 1;
        emscripten_pause_main_loop();
    }
}

static void resume_browser_loop(void)
{
    if (te_main_loop_paused && !te_destroyed)
    {
        te_main_loop_paused = 0;
        emscripten_resume_main_loop();
    }
}

void TE_ForceInitialMute(void)
{
    // SDL_mixer divides an all-channel volume result by its channel count.
    // DG_Init runs before channels exist, so only issue that call once the
    // mixer has allocated at least one channel.
    if (Mix_AllocateChannels(-1) > 0)
    {
        Mix_Volume(-1, 0);
    }
    Mix_VolumeMusic(0);
}

static void browser_tick(void)
{
    double now;
    double tick_started;
    double tick_cost;
    double desired_frame_ms;

    if (te_paused || te_destroyed)
    {
        return;
    }

    now = emscripten_get_now();
    // A captured session with no input yet does not need to burn a core while
    // the player is away. The first control wakes it and permanently disables
    // this pre-game governor, so active user levels are never idle-throttled.
    if (te_controls_captured && !te_input_seen && te_last_input_ms > 0.0
        && now - te_last_input_ms >= DOOM_PREGAME_IDLE_MS)
    {
        pause_browser_loop();
        return;
    }
    if (te_next_frame == 0.0)
    {
        te_next_frame = now;
    }
    if (now + 0.5 >= te_next_frame)
    {
        tick_started = now;
        doomgeneric_Tick();
        // A zero-volume MIDI stream still consumes substantial main-thread CPU
        // in SDL_mixer. Keep synthesis paused until the player explicitly
        // enables audio, including after a level starts a new music track.
        if (te_audio_muted)
        {
            Mix_PauseMusic();
        }

        tick_cost = emscripten_get_now() - tick_started;
        te_tick_cost_ms = te_tick_cost_ms == 0.0
            ? tick_cost
            : te_tick_cost_ms * 0.85 + tick_cost * 0.15;
        desired_frame_ms = te_tick_cost_ms / DOOM_TARGET_DUTY_CYCLE;
        if (desired_frame_ms < DOOM_MIN_FRAME_MS)
        {
            desired_frame_ms = DOOM_MIN_FRAME_MS;
        }
        else if (desired_frame_ms > DOOM_MAX_FRAME_MS)
        {
            desired_frame_ms = DOOM_MAX_FRAME_MS;
        }
        // Smooth cadence changes so a transient long frame cannot cause a
        // visible oscillation. TryRunTics still advances simulation at 35 Hz.
        te_frame_ms = te_frame_ms * 0.8 + desired_frame_ms * 0.2;
        te_next_frame += te_frame_ms;
        if (now - te_next_frame > te_frame_ms * 2.0)
        {
            te_next_frame = now + te_frame_ms;
        }
    }
}

static void queue_key(int pressed, unsigned char key)
{
    unsigned int next = (key_write + 1) % KEYQUEUE_SIZE;

    if (next == key_read)
    {
        key_read = (key_read + 1) % KEYQUEUE_SIZE;
    }

    key_queue[key_write] = (pressed << 8) | key;
    key_write = next;
    held_keys[key] = pressed ? 1 : 0;
}

void DG_Init(void)
{
    if (DG_ScreenBuffer == NULL || SDL_Init(SDL_INIT_VIDEO) != 0)
    {
        fprintf(stderr, "SDL initialization failed\n");
        return;
    }

    window = SDL_CreateWindow("Freedoom",
                              SDL_WINDOWPOS_UNDEFINED,
                              SDL_WINDOWPOS_UNDEFINED,
                              DOOMGENERIC_RESX,
                              DOOMGENERIC_RESY,
                              SDL_WINDOW_SHOWN);
    if (window == NULL)
    {
        fprintf(stderr, "SDL window creation failed\n");
        te_destroyed = 1;
        return;
    }
    renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
    if (renderer == NULL)
    {
        renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
    }
    if (renderer == NULL)
    {
        fprintf(stderr, "SDL renderer creation failed\n");
        te_destroyed = 1;
        return;
    }
    SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0");
    texture = SDL_CreateTexture(renderer,
                                SDL_PIXELFORMAT_RGB888,
                                SDL_TEXTUREACCESS_STREAMING,
                                DOOMGENERIC_RESX,
                                DOOMGENERIC_RESY);
    if (texture == NULL)
    {
        fprintf(stderr, "SDL texture creation failed\n");
        te_destroyed = 1;
        return;
    }
    // The engine must be silent before JavaScript receives its first chance to
    // apply the persisted preference.
    TE_ForceInitialMute();
}

void DG_DrawFrame(void)
{
    SDL_Event event;

    if (te_destroyed || texture == NULL || renderer == NULL || DG_ScreenBuffer == NULL)
    {
        return;
    }

    SDL_UpdateTexture(texture, NULL, DG_ScreenBuffer,
                      DOOMGENERIC_RESX * sizeof(uint32_t));
    SDL_RenderClear(renderer);
    SDL_RenderCopy(renderer, texture, NULL, NULL);
    SDL_RenderPresent(renderer);

    while (SDL_PollEvent(&event))
    {
        if (event.type == SDL_QUIT)
        {
            te_paused = 1;
        }
    }
}

void DG_SleepMs(uint32_t ms)
{
    SDL_Delay(ms);
}

uint32_t DG_GetTicksMs(void)
{
    return SDL_GetTicks();
}

int DG_GetKey(int *pressed, unsigned char *doom_key)
{
    unsigned short data;

    if (key_read == key_write)
    {
        return 0;
    }

    data = key_queue[key_read];
    key_read = (key_read + 1) % KEYQUEUE_SIZE;
    *pressed = data >> 8;
    *doom_key = data & 0xff;
    return 1;
}

void DG_SetWindowTitle(const char *title)
{
    if (window != NULL)
    {
        SDL_SetWindowTitle(window, title);
    }
}

EMSCRIPTEN_KEEPALIVE int TE_Healthy(void)
{
    return !te_destroyed && DG_ScreenBuffer != NULL && window != NULL
        && renderer != NULL && texture != NULL;
}

EMSCRIPTEN_KEEPALIVE void TE_KeyEvent(int key, int pressed)
{
    if (!te_destroyed && te_controls_captured && key >= 0 && key <= 255)
    {
        te_last_input_ms = emscripten_get_now();
        te_next_frame = te_last_input_ms;
        te_input_seen = 1;
        resume_browser_loop();
        queue_key(pressed != 0, (unsigned char) key);
    }
}

EMSCRIPTEN_KEEPALIVE void TE_MouseEvent(int buttons, int dx, int dy)
{
    event_t event;

    if (te_destroyed || !te_controls_captured)
    {
        return;
    }

    if (buttons != 0 || dx != 0 || dy != 0)
    {
        te_last_input_ms = emscripten_get_now();
        te_next_frame = te_last_input_ms;
        te_input_seen = 1;
        resume_browser_loop();
    }

    mouse_buttons = buttons & 7;
    event.type = ev_mouse;
    event.data1 = mouse_buttons;
    event.data2 = dx;
    event.data3 = dy;
    event.data4 = 0;
    D_PostEvent(&event);
}

EMSCRIPTEN_KEEPALIVE void TE_ClearInput(void)
{
    int key;

    for (key = 0; key < 256; key++)
    {
        if (held_keys[key])
        {
            queue_key(0, (unsigned char) key);
        }
    }

    if (te_controls_captured)
    {
        TE_MouseEvent(0, 0, 0);
    }
    else
    {
        mouse_buttons = 0;
    }
}

EMSCRIPTEN_KEEPALIVE void TE_Pause(void)
{
    TE_ClearInput();
    te_controls_captured = 0;
    te_paused = 1;
    pause_browser_loop();
}

EMSCRIPTEN_KEEPALIVE void TE_Resume(void)
{
    if (!te_destroyed)
    {
        te_controls_captured = 1;
        te_next_frame = emscripten_get_now();
        if (!te_input_seen)
        {
            te_last_input_ms = te_next_frame;
        }
        te_paused = 0;
        resume_browser_loop();
    }
}

EM_JS(void, TE_ResumeAudioContext, (), {
    if (typeof SDL2 !== 'undefined' && SDL2.audioContext
        && SDL2.audioContext.state === 'suspended') {
        SDL2.audioContext.resume();
    }
});

EMSCRIPTEN_KEEPALIVE void TE_ResumeAudio(void)
{
    if (!te_destroyed) TE_ResumeAudioContext();
}

EMSCRIPTEN_KEEPALIVE int TE_Autosave(void)
{
    if (!usergame || gamestate != GS_LEVEL || gameaction != ga_nothing
        || players[consoleplayer].playerstate != PST_LIVE)
    {
        return 0;
    }

    // JavaScript pauses immediately after this call. Complete the write in the
    // current engine turn so persistence never depends on a timing delay.
    G_SaveGame(AUTOSAVE_SLOT, "TECHNICALENTROPY AUTO");
    sendsave = false;
    G_DoSaveGame();
    return 1;
}

EMSCRIPTEN_KEEPALIVE int TE_LoadAutosave(void)
{
    char *path = P_SaveGameFile(AUTOSAVE_SLOT);
    FILE *file = fopen(path, "rb");

    if (file == NULL)
    {
        return 0;
    }

    fclose(file);
    G_LoadGame(path);
    return 1;
}

EMSCRIPTEN_KEEPALIVE void TE_SetVolume(int volume)
{
    int bounded = volume;

    if (bounded < 0) bounded = 0;
    if (bounded > MIX_MAX_VOLUME) bounded = MIX_MAX_VOLUME;
    if (!te_destroyed)
    {
        te_audio_muted = bounded == 0;
        Mix_Volume(-1, bounded);
        Mix_VolumeMusic(bounded);
        if (te_audio_muted)
        {
            Mix_PauseMusic();
        }
        else
        {
            Mix_ResumeMusic();
        }
    }
}

EMSCRIPTEN_KEEPALIVE void TE_Destroy(void)
{
    if (te_destroyed)
    {
        return;
    }

    te_destroyed = 1;
    te_paused = 1;
    te_controls_captured = 0;
    TE_ClearInput();
    emscripten_cancel_main_loop();
    Mix_HaltMusic();
    Mix_HaltChannel(-1);
    Mix_CloseAudio();

    if (texture != NULL) SDL_DestroyTexture(texture);
    if (renderer != NULL) SDL_DestroyRenderer(renderer);
    if (window != NULL) SDL_DestroyWindow(window);
    texture = NULL;
    renderer = NULL;
    window = NULL;
    SDL_Quit();
}

int main(int argc, char **argv)
{
    doomgeneric_Create(argc, argv);
    // requestAnimationFrame is browser-native. The adapter caps presentation
    // between 18 and 35 Hz while TryRunTics advances game time at Doom's 35 Hz.
    emscripten_set_main_loop(browser_tick, 0, 1);
    return 0;
}
