#include <math.h>
#include <string.h>
#include <stdio.h>
#include <SDL.h>
#include <SDL_audio.h>

#include "Constants.h"
#include "Timing.h"
#include "Log.h"
#include "Sound.h"
#include "SoundBuffer.h"
#include "TapLoader.h"
#include "Notification.h"
#include "Video.h"

// this many samples are averaged and stored as a single sample
#define OVERSAMPLE_FACTOR 11
#define OVERSAMPLE_FACTOR_F (OVERSAMPLE_FACTOR.0f)
// collects oversamples, before they're averaged out and stored in the sound buffer proper
static Uint8* _oversampleBuffer = NULL;
static Uint64 _oversampleBufferIndex = 0;

static Uint32 _sampleRatePerSecond;

const Uint8 NUMBER_OF_CHANNELS = 2; // stereo

static Uint8 _sound_volume_down_scheduled;
static Uint8 _sound_volume_up_scheduled;

const float AUDIO_SAMPLE_MAX = 1.0f;
static float _audio_sample_HIGH;

static Uint8 _sound_volume;

static Uint64 _framesWithStaticSound = 0;
static Uint64 _framesWithDynamicSound = 0;

static Uint8 _soundInitialized = 0;
enum soundSamplingMode _samplingMode;
enum soundMode _userRequestedSoundMode;

//    ++-----------------------sample is signed if set
//    ||
//    ||       ++-----------sample is bigendian if set
//    ||       ||
//    ||       ||          ++---sample is float if set
//    ||       ||          ||
//    ||       ||          ||     +---sample bit size---+
//    ||       ||          ||     |                     |
//    15 14 13 12 11 10 09 08     07 06 05 04 03 02 01 00
SDL_AudioFormat _format;
Uint8 _formatBitsPerSample;
Uint8 _formatIsFloat;

// used to decide when to sample the speaker, writing into the sound buffer
static double _spectrumSpeakerSamplingIntervalTstates_d;
// CPU counts total tstates via an Uint64, while we rely on a double
// there is no loss of precision converting an Uint64 to a double up to 2^53,
// a value unlikely to be reached during normal run times of zxian
static double _tstatesAtLastSample_d;

static Uint64 _frame = 0;

#define _DEBUG_BUFFER_SIZE 256
static char _debugBuffer[_DEBUG_BUFFER_SIZE];

// NOTE: sample is on-or-off
void _sound_oversample_write(Uint8 value8) {
    if (!_soundInitialized) {
        return;
    }

    // collect oversample
    _oversampleBuffer[_oversampleBufferIndex++] = value8;
    if (_oversampleBufferIndex != OVERSAMPLE_FACTOR) {
        // we have not yet collected enough oversamples to write a real sample
        return;
    }

    // we have now collected enough oversamples, so we will write an averaged, normalized sample
    Uint32 sampleSum = 0;
    for (Uint64 i = 0; i < OVERSAMPLE_FACTOR; i++) {
        sampleSum += _oversampleBuffer[i];
    }

    // write to buffer
    float averagedNormalizedSample = ((float)sampleSum) / OVERSAMPLE_FACTOR_F;
    soundbuffer_write(averagedNormalizedSample);

    // reset oversample buffer index
    _oversampleBufferIndex = 0;
    //if(averagedNormalizedSample > 0.0f)log_write_string_int("%f", averagedNormalizedSample);
    //if (sampleSum > 0)log_write_string_int("%d", sampleSum);
    return;
}

float _sound_spectrum_buffer_read(Uint8 preferLongResyncs) {
    if (!_soundInitialized) {
        return 0xF3;
    }

    return soundbuffer_read(preferLongResyncs);

    /*float valueF = soundbuffer_read();
    log_write_string_int("%f", valueF);
    return valueF;*/
}

void sound_play() {
    if (!_soundInitialized) {
        return;
    }

    SDL_PauseAudio(0);      // start playing sound
}

void sound_set_user_mode(enum soundMode mode) {
    _userRequestedSoundMode = mode;
    
    switch (_userRequestedSoundMode)
    {
    case Auto:
        break;
    case Static:
        // force it immediately
        sound_set_sampling_mode(SoundStaticSampling);
        break;
    case Dynamic:
        // force it immediately
        sound_set_sampling_mode(SoundDynamicSampling);
        break;
    default:
        break;
    }
}

void sound_set_sampling_mode(enum soundSamplingMode mode) {
    if (_userRequestedSoundMode == Static && mode == SoundDynamicSampling) {
        // mode forced by user and requested mode do not match
        return;
    }

    if (_userRequestedSoundMode == Dynamic && mode == SoundStaticSampling) {
        // mode forced by user and requested mode do not match
        return;
    }

    _samplingMode = mode;
}

// determines sampling interval
void _sound_compute_write_timing() {
    double soundFrameDurationMs;

    // this is valuable only when frames are allowed to go over their target run time by
    // a few percent
    if (_samplingMode == SoundStaticSampling ) {
        // when frames are at their target run time, or very slightly above or below, 
        // static sampling means that the target is used to compute speaker sampling interval
        // 
        // NOTE: interestingly, even when actual video frame time is extremely close to target,
        // using dynamic performs worse than static
        soundFrameDurationMs = timing_get_target_milliseconds_per_video_frame();
    }
    else {
        // when frames are a few percent slower than their target run time, the sound quality
        // is improved by computing a speaker sampling interval based on an actual frame
        // run time average
        //
        // read the NOTE above for the reason why we simply don't just use dynamic all the time
        soundFrameDurationMs = timing_get_actual_milliseconds_per_video_frame();
    }

    Uint32 _samplesPerFrame = (Uint32)((double)_sampleRatePerSecond * (soundFrameDurationMs / 1000.0f));
    // calculate sampling interval in tstates
    Uint64 tstatesPerVideoFrame = timing_get_states_per_video_frame();

    // we will sample the speaker this many tstates
    double tstateInterval = (double)tstatesPerVideoFrame / ((double)_samplesPerFrame * (double)OVERSAMPLE_FACTOR_F);
    _spectrumSpeakerSamplingIntervalTstates_d = tstateInterval;
}

void _sound_buffer_filler_callback(void* consumerData, Uint8* sdlBuffer, int bytesToFill) {
    int lengthInSamples = bytesToFill / (_formatBitsPerSample/8);
    lengthInSamples = lengthInSamples / NUMBER_OF_CHANNELS;

    // we prefer long resyncs when the tape is playing some noise
    // this is because short resyncs cause clicking noises during tape data bytes
    struct tapRunState tapeState = taploader_get_state();
    Uint8 preferLongResyncs = tapeState.type != Stopped && tapeState.type != Silence;

    // assumptions:
    //   sample format is float
    //   bits per sample is 32 (i.e. C's float type width)

    float* buffer = (float*)sdlBuffer;
    for (int i = 0; i < lengthInSamples * NUMBER_OF_CHANNELS; i += NUMBER_OF_CHANNELS) {
        float value = _sound_spectrum_buffer_read(preferLongResyncs) * _audio_sample_HIGH;
        for (int j = 0; j < NUMBER_OF_CHANNELS; j++) {
            buffer[i + j] = value;
        }
    }
}

// called after each CPU instruction, it is in charge of deciding
// if it's time to write to the sound buffer
void sound_add_sample(Uint8 isSpeakerActive, Uint64 totalTStates) {
    if (!_soundInitialized) {
        return;
    }

    while ((double)totalTStates - _tstatesAtLastSample_d >= _spectrumSpeakerSamplingIntervalTstates_d) {
        // enough time has passed since we collected last sample, so it's time
        // to collect a new sample

        // this allows for a catchup on consecutive CPU instructions, if, for some reason,
        // we fall behind for more than one sampling interval
        _tstatesAtLastSample_d += _spectrumSpeakerSamplingIntervalTstates_d;

        if (timing_is_faster()) {
            // silence during faster CPU - such as when loading
            _sound_oversample_write(0);
        }
        else {
            _sound_oversample_write(isSpeakerActive);
        }
    }
}

void sound_notify_start_of_frame() {
    soundbuffer_notify_start_of_frame();
}

void sound_notify_end_of_frame() {
    if (!_soundInitialized) {
        return;
    }

    _frame++;
    _sound_compute_write_timing();
    if (_samplingMode == SoundStaticSampling) {
        _framesWithStaticSound++;
    }
    else {
        _framesWithDynamicSound++;
    }

    soundbuffer_notify_end_of_frame();
}

void sound_destroy() {
    if (!_soundInitialized) {
        return;
    }

    _soundInitialized = 0;

    SDL_PauseAudio(1); // stop playing sound
    SDL_CloseAudio();

    log_write("SOUND");

    switch (_userRequestedSoundMode)
    {
    case Auto:
        log_write("Sound mode: Auto");
        break;
    case Static:
        log_write("Sound mode: Static");
        break;
    case Dynamic:
        log_write("Sound mode: Dynamic");
        break;
    default:
        break;
    }

    char* message = (char*)malloc(500 * sizeof(char));
    if (message) {
        sprintf_s(message, 490, "Frame counts by sound timing type: %I64d static, %I64d dynamic",
            _framesWithStaticSound,
            _framesWithDynamicSound);
        log_write(message);
        free(message);
    }

    if (_oversampleBuffer != NULL) {
        free(_oversampleBuffer);
    }

    soundbuffer_destroy();
}

void _sound_recompute_sample_amplitude() {
    _audio_sample_HIGH = (((float)_sound_volume) / 255.0f) * AUDIO_SAMPLE_MAX;
}

void sound_start(Uint32 samplesPerSecond, Uint8 volume) {
    sound_destroy();

    _sound_volume = volume;
    _sampleRatePerSecond = samplesPerSecond;

    _oversampleBuffer = (Uint8*)malloc(OVERSAMPLE_FACTOR * sizeof(Uint8));
    if (_oversampleBuffer == NULL) {
        log_write("Error: could not initialize oversample buffer");
        return;
    }

    if (SDL_Init(SDL_INIT_AUDIO) != 0) {
        log_write("Error: could not initialize SDL audio");
        log_write((char*)SDL_GetError());
        return;
    }

    sound_set_sampling_mode(SoundStaticSampling);

    SDL_AudioSpec want;
    want.freq = _sampleRatePerSecond;
    want.format = AUDIO_F32SYS;
    want.channels = NUMBER_OF_CHANNELS;
    //want.samples = 0x1000;  // buffer size
    want.samples = _sampleRatePerSecond / 2;  // buffer size
    want.callback = _sound_buffer_filler_callback;
    want.userdata = NULL;   // unused
    want.silence = 0;

    char* message = (char*)malloc(500 * sizeof(char));
    if (message) {
        sprintf_s(message, 490, "Requesting audio spec %04x, with %d samples/s", want.format, want.freq);
        log_write(message);
        free(message);
    }

    SDL_AudioSpec got;
    if (SDL_OpenAudio(&want, &got) != 0) {
        log_write("Error: could not open audio");
        log_write((char*)SDL_GetError());
        return;
    }
    _format = got.format;
    _formatBitsPerSample = SDL_AUDIO_BITSIZE(got.format);
    _formatIsFloat = SDL_AUDIO_ISFLOAT(got.format) ? 1 : 0;

    message = (char*)malloc(500 * sizeof(char));
    if (message) {
        sprintf_s(message, 490, "Got audio spec %04x, with %d samples/s", got.format, got.freq);
        log_write(message);
        free(message);
    }

    if (SDL_AUDIO_ISBIGENDIAN(got.format)) {
        log_write("Warning: got audio spec that is big endian, which is unsupported; audio will likely be silence or garbage");
    }

    if (want.format != got.format || 
        want.channels != got.channels ||
        want.freq != got.freq) {

        log_write("Warning: got an audio spec different than the one requested");
    }

    _sound_recompute_sample_amplitude();

    _sound_compute_write_timing();
    _tstatesAtLastSample_d = 0.0f;

    soundbuffer_start();
    _soundInitialized = 1;
}

void sound_keydown(SDL_Keysym key) {
    if (!_soundInitialized) {
        return;
    }

    switch (key.sym) {
    case SDLK_LEFTBRACKET:
        _sound_volume_down_scheduled = 1;
        break;
    case SDLK_RIGHTBRACKET:
        _sound_volume_up_scheduled = 1;
        break;
    }
}

void _sound_print_volume() {
    sprintf_s(_debugBuffer, _DEBUG_BUFFER_SIZE - 10, "Sound volume: %u", _sound_volume);
    notification_show(_debugBuffer, 1500, video_force_next_frame_full_render, NULL);
}

void _sound_perform_volume_down() {
    if (_sound_volume > 0) {
        _sound_volume--;
    }

    _sound_recompute_sample_amplitude();
    _sound_volume_down_scheduled = 0;
    _sound_print_volume();
}

void _sound_perform_volume_up() {
    if (_sound_volume < 255) {
        _sound_volume++;
    }

    _sound_recompute_sample_amplitude();
    _sound_volume_up_scheduled = 0;
    _sound_print_volume();
}

void sound_handle_state_change() {
    if (_sound_volume_down_scheduled) {
        _sound_perform_volume_down();
    }

    if (_sound_volume_up_scheduled) {
        _sound_perform_volume_up();
    }
}