#include <stdio.h>
#include <stdint.h>
#include <errno.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <SDL2/SDL.h>

#include "emu76489.h"
#include "player.h"

#define SAMPLE_RATE    48000
#define MUSIC_HZ       50

typedef struct {
    SNG *psg;
    uint32_t samples_until_tick;
} AudioState;

SNG * g_psg = NULL;

typedef struct {
    SDL_atomic_t running;
    int old_stdin_flags;
    int terminal_is_raw;
    struct termios old_terminal;
} InputState;

void psg_write(uint8_t value){
    SNG_writeIO(g_psg, value);
}

static int input_thread(void *userdata){
    InputState *state = userdata;

    while(SDL_AtomicGet(&state->running)){
        uint8_t input;
        ssize_t bytes_read = read(STDIN_FILENO, &input, sizeof(input));

        if(bytes_read == 1){
            input_interrupt(input);
        }else if(bytes_read == 0 || errno == EAGAIN || errno == EWOULDBLOCK){
            SDL_Delay(10);
        }else if(errno != EINTR){
            break;
        }
    }

    return 0;
}

static int start_terminal_input(InputState *state){
    SDL_zero(*state);
    SDL_AtomicSet(&state->running, 1);

    state->old_stdin_flags = fcntl(STDIN_FILENO, F_GETFL);
    if(state->old_stdin_flags == -1 ||
       fcntl(STDIN_FILENO, F_SETFL, state->old_stdin_flags | O_NONBLOCK) == -1){
        return -1;
    }

    if(isatty(STDIN_FILENO)){
        if(tcgetattr(STDIN_FILENO, &state->old_terminal) == -1){
            fcntl(STDIN_FILENO, F_SETFL, state->old_stdin_flags);
            return -1;
        }

        struct termios raw_terminal = state->old_terminal;
        raw_terminal.c_lflag &= (tcflag_t) ~(ICANON | ECHO);
        raw_terminal.c_cc[VMIN] = 0;
        raw_terminal.c_cc[VTIME] = 0;

        if(tcsetattr(STDIN_FILENO, TCSANOW, &raw_terminal) == -1){
            fcntl(STDIN_FILENO, F_SETFL, state->old_stdin_flags);
            return -1;
        }
        state->terminal_is_raw = 1;
    }

    return 0;
}

static void stop_terminal_input(InputState *state, SDL_Thread *thread){
    SDL_AtomicSet(&state->running, 0);
    SDL_WaitThread(thread, NULL);

    if(state->terminal_is_raw){
        tcsetattr(STDIN_FILENO, TCSANOW, &state->old_terminal);
    }
    fcntl(STDIN_FILENO, F_SETFL, state->old_stdin_flags);
}

void serial_write(char c){
    write(STDOUT_FILENO, &c, 1);
}

void disk_write(void * data, size_t len){
    FILE * f = fopen("disk.bin", "wb");
    fwrite(data, 1, len, f);
    fclose(f);
}

void disk_read(void * data, size_t len){
    FILE * f = fopen("disk.bin", "rb");
    fread(data, 1, len, f);
    fclose(f);
}

static void audio_callback(void *userdata, Uint8 *stream, int len){
    AudioState *state = userdata;
    int16_t *output = (int16_t *)stream;
    int sample_count = len / sizeof(int16_t);

    for (int i = 0; i < sample_count; i++){
        /*
         * SAMPLE_RATE = 48000
         * MUSIC_HZ    = 50
         *
         * 48000 / 50 = 960
         *
         * Therefore music_tick() happens once every
         * 960 generated audio samples.
         */
        if(state->samples_until_tick == 0){
            music_tick_interrupt();

            state->samples_until_tick = SAMPLE_RATE / MUSIC_HZ;
        }

        /*
         * Ask emu76489 to generate one PCM sample.
         */
        output[i] = SNG_calc(state->psg);

        state->samples_until_tick--;
    }
}

int main(int argc, char ** argv){
    (void) argc;
    (void) argv;

    if(SDL_Init(SDL_INIT_AUDIO) != 0){
        fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
        return 1;
    }

    g_psg = SNG_new(PSG_CLOCK, SAMPLE_RATE);
    if(!g_psg){
        fprintf(stderr, "Could not create SN76489 emulator\n");
        SDL_Quit();
        return 1;
    }
    SNG_reset(g_psg);

    AudioState state = {
        .psg = g_psg,
        .samples_until_tick = 0
    };

    SDL_AudioSpec wanted;
    SDL_zero(wanted);

    wanted.freq     = SAMPLE_RATE;
    wanted.format   = AUDIO_S16SYS;
    wanted.channels = 1;       /* mono */
    wanted.samples  = 1024;

    wanted.callback = audio_callback;
    wanted.userdata = &state;

    SDL_AudioDeviceID device = SDL_OpenAudioDevice(
        NULL,
        0,
        &wanted,
        NULL,
        0
    );

    if(device == 0){
        fprintf(stderr, "SDL_OpenAudioDevice failed: %s\n", SDL_GetError());
        SNG_delete(g_psg);
        SDL_Quit();
        return 1;
    }
    SDL_PauseAudioDevice(device, 0);

    InputState input_state;
    if(start_terminal_input(&input_state) != 0){
        fprintf(stderr, "Could not configure terminal input\n");
        SDL_CloseAudioDevice(device);
        SNG_delete(g_psg);
        SDL_Quit();
        return 1;
    }

    SDL_Thread *keyboard_thread = SDL_CreateThread(
        input_thread,
        "terminal input",
        &input_state
    );
    if(!keyboard_thread){
        fprintf(stderr, "Could not create keyboard input thread: %s\n", SDL_GetError());
        if(input_state.terminal_is_raw){
            tcsetattr(STDIN_FILENO, TCSANOW, &input_state.old_terminal);
        }
        fcntl(STDIN_FILENO, F_SETFL, input_state.old_stdin_flags);
        SDL_CloseAudioDevice(device);
        SNG_delete(g_psg);
        SDL_Quit();
        return 1;
    }

    SDL_Delay(1000);
    player_main();

    stop_terminal_input(&input_state, keyboard_thread);
    SDL_PauseAudioDevice(device, 1);
    SDL_CloseAudioDevice(device);
    SNG_delete(g_psg);
    SDL_Quit();
    return 0;
}
