/*
Copyright (c) 2023 Rupert Carmichael

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
*/

#include <fstream>
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <stdint.h>

#include <SDL.h>
#include <speex/speex_resampler.h>

#include <gambatte.h>

#define SCREEN_WIDTH 160
#define SCREEN_HEIGHT 144
#define SAMPLERATE_IN 2097152
#define SAMPLERATE_OUT 48000
#define FRAMERATE 60 // Approximately 60Hz
#define CHANNELS 2

// Speex Resampler
static SpeexResamplerState *resampler = NULL;
static int err;

// SDL Audio
static SDL_AudioSpec spec, obtained;
static SDL_AudioDeviceID dev;

// SDL Video
static SDL_Window *window;
static SDL_Renderer *renderer;
static SDL_Texture *texture;

// Gambatte
static gambatte::GB gb;

// Video rendering
static uint32_t *vbuf = NULL;
static unsigned scale = 4;

// Audio buffers
static uint32_t inbuf[6400];
static int16_t outbuf[256];

// Button state
unsigned buttons[8] = {0};

// Keep track of whether the emulator should be running or not
static int running = 1;

class GetInput : public gambatte::InputGetter {
public:
    unsigned operator()() {
        unsigned bstate = 0x00;
        for (unsigned i = 0; i < 8; ++i) {
            if (buttons[i]) bstate |= (1 << i);
        }
        return bstate;
    }
} static GetInput;

// Resample raw audio and queue it for output
static void gbex_audio_out(size_t frames) {
    if (!frames)
        return;

    spx_uint32_t insamps = frames;
    spx_uint32_t outsamps = SAMPLERATE_OUT / FRAMERATE;

    /* Gambatte stores stereo audio sample pairs in a single uint32_t, so
       reinterpret the input buffer as int16_t for resampling.
    */
    err = speex_resampler_process_interleaved_int(resampler,
        reinterpret_cast<spx_int16_t*>(inbuf), &insamps,
        (spx_int16_t*)outbuf, &outsamps);

    /* Multiply outsamps by 4 because SDL_QueueAudio wants the number in bytes,
     * and there are two channels: 2 channels * sizeof(int16_t) = 4
    */
    SDL_QueueAudio(dev, outbuf, outsamps << 2);
}

// Handle SDL input events
static void gbex_input_handler(SDL_Event& event) {
    if (event.type == SDL_KEYUP || event.type == SDL_KEYDOWN) {
        switch (event.key.keysym.scancode) {
            case SDL_SCANCODE_ESCAPE:
                running = 0;
                break;

            case SDL_SCANCODE_UP:
                buttons[6] = event.type == SDL_KEYDOWN;
                break;
            case SDL_SCANCODE_DOWN:
                buttons[7] = event.type == SDL_KEYDOWN;
                break;
            case SDL_SCANCODE_LEFT:
                buttons[5] = event.type == SDL_KEYDOWN;
                break;
            case SDL_SCANCODE_RIGHT:
                buttons[4] = event.type == SDL_KEYDOWN;
                break;

            case SDL_SCANCODE_RSHIFT:
                buttons[2] = event.type == SDL_KEYDOWN;
                break;
            case SDL_SCANCODE_RETURN:
                buttons[3] = event.type == SDL_KEYDOWN;
                break;

            case SDL_SCANCODE_Z:
                buttons[0] = event.type == SDL_KEYDOWN;
                break;
            case SDL_SCANCODE_A:
                buttons[1] = event.type == SDL_KEYDOWN;
                break;
            default: break;
        }
    }
}

int main (int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: ./%s [FILE]\n", argv[0]);
        exit(1);
    }

    // Load the uncompressed game data into memory
    std::ifstream stream(argv[1], std::ios::in | std::ios::binary);
    std::vector<uint8_t> game((std::istreambuf_iterator<char>(stream)),
        std::istreambuf_iterator<char>());
    stream.close();

    // Attempt to load the ROM into the emulator
    int ret = !gb.load(game.data(), game.size(), argv[1], 0);
    if (!ret) {
        fprintf(stderr, "Failed to load ROM\n");
        exit(1);
    }

    // Set up Gambatte
    gb.setInputGetter(&GetInput);
    gb.reset();

    // Allow joystick input when the window is not focused
    SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");

    // Keep window fullscreen if the window manager tries to iconify it
    SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");

    // Initialize SDL
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_JOYSTICK) < 0) {
        fprintf(stderr, "Failed to initialize SDL: %s\n", SDL_GetError());
        return EXIT_FAILURE;
    }

    // Set up the window
    Uint32 windowflags = SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE |
        SDL_WINDOW_ALLOW_HIGHDPI;

    int windowwidth = SCREEN_WIDTH * scale;
    int windowheight = SCREEN_HEIGHT * scale;

    window = SDL_CreateWindow("libgambatte-example",
        SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
        windowwidth, windowheight, windowflags);

    renderer = SDL_CreateRenderer(window, -1,
        SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
    SDL_SetRenderDrawColor(renderer, 0x0, 0x0, 0x0, 0xff);
    SDL_RenderSetLogicalSize(renderer, windowwidth, windowheight);

    texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888,
        SDL_TEXTUREACCESS_STREAMING, SCREEN_WIDTH, SCREEN_HEIGHT);

    SDL_ShowCursor(false);

    // Get current display mode
    SDL_DisplayMode dm;
    SDL_GetCurrentDisplayMode(SDL_GetWindowDisplayIndex(window), &dm);

    // Allocate video buffer
    vbuf =
        (uint32_t*)calloc(SCREEN_WIDTH * SCREEN_HEIGHT * sizeof(uint32_t), 1);

    // Set up SDL Audio
    spec.channels = CHANNELS;
    spec.freq = SAMPLERATE_OUT;
    spec.silence = 0;
    spec.samples = 512;
    spec.userdata = 0;
    #if SDL_BYTEORDER == SDL_BIG_ENDIAN
    spec.format = AUDIO_S16MSB;
    #else
    spec.format = AUDIO_S16LSB;
    #endif

    // Open the Audio Device
    dev = SDL_OpenAudioDevice(NULL, 0, &spec, &obtained,
        SDL_AUDIO_ALLOW_ANY_CHANGE);

    // Set up resampling
    resampler = speex_resampler_init(CHANNELS, SAMPLERATE_IN, SAMPLERATE_OUT,
        0, &err);

    // Allow audio playback
    SDL_PauseAudioDevice(dev, 0);

    int runframes = 0;
    int collector = 0;

    SDL_Event event;

    while (running) {
        // Divide core framerate by screen framerate and collect remainders
        runframes = FRAMERATE / dm.refresh_rate;
        collector += FRAMERATE % dm.refresh_rate;

        if (collector >= dm.refresh_rate) {
            ++runframes;
            collector -= dm.refresh_rate;
        }

        for (int i = 0; i < runframes; ++i) {
            // Run emulation until a frame is ready
            size_t samples = 2064;
            while (gb.runFor(vbuf, SCREEN_WIDTH, inbuf, samples) == -1)
                gbex_audio_out(samples);
            gbex_audio_out(samples); // Clear out samples left after while loop

            // Manage audio output queue size by resampling
            uint32_t qsamps = SDL_GetQueuedAudioSize(dev);
            if (qsamps > SAMPLERATE_OUT) {
                SDL_ClearQueuedAudio(dev);
            }
            else if (qsamps > 12800) {
                unsigned offset = (qsamps / 6400) * FRAMERATE;
                err = speex_resampler_set_rate_frac(resampler,
                    SAMPLERATE_IN, SAMPLERATE_OUT - offset,
                    SAMPLERATE_IN, SAMPLERATE_OUT - offset);
            }
            else {
                err = speex_resampler_set_rate_frac(resampler,
                    SAMPLERATE_IN, SAMPLERATE_OUT,
                    SAMPLERATE_IN, SAMPLERATE_OUT);
            }
        }

        // Render the scene
        SDL_RenderClear(renderer);
        SDL_UpdateTexture(texture, NULL, vbuf,
            SCREEN_WIDTH * sizeof(uint32_t));
        SDL_RenderCopy(renderer, texture, NULL, NULL);
        SDL_RenderPresent(renderer);

        // Poll for events
        while (SDL_PollEvent(&event)) {
            switch (event.type) {
                case SDL_QUIT: {
                    running = 0;
                    break;
                }
                default: {
                    gbex_input_handler(event);
                    break;
                }
            }
        }

    }

    // Bring down audio and video
    speex_resampler_destroy(resampler);

    if (vbuf)
        free(vbuf);

    SDL_DestroyTexture(texture);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);

    return 0;
}
