#include "libraries/cosmic_unicorn/cosmic_unicorn.hpp"
#include "libraries/pico_graphics/pico_graphics.hpp"
#include "micropython/modules/util.hpp"
#include <cstdio>
#include <cfloat>


using namespace pimoroni;

extern "C" {
#include "cosmic_unicorn.h"
#include "micropython/modules/pimoroni_i2c/pimoroni_i2c.h"
#include "py/builtin.h"


/********** Channel **********/

/***** Variables Struct *****/
typedef struct _Channel_obj_t {
    mp_obj_base_t base;
    AudioChannel* channel;
} _Channel_obj_t;


/***** Print *****/
void Channel_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
    (void)kind; //Unused input parameter
    //_Channel_obj_t *self = MP_OBJ_TO_PTR2(self_in, _Channel_obj_t);
    //AudioChannel* channel = self->channel;
    mp_print_str(print, "Channel(");
    mp_print_str(print, ")");
}


/***** Constructor *****/
mp_obj_t Channel_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
    mp_raise_msg(&mp_type_RuntimeError, "Cannot create Channel objects. They can only be accessed from CosmicUnicorn.synth_channel()");
    return mp_const_none;
}


/***** Destructor ******/
mp_obj_t Channel___del__(mp_obj_t self_in) {
    return mp_const_none;
}


/***** Helper Functions *****/
void set_channel_waveforms(AudioChannel& channel, mp_obj_t in) {
    int waveforms = mp_obj_get_int(in);
    const int mask = (NOISE | SQUARE | SAW | TRIANGLE | SINE | WAVE);
    if(waveforms < 0 || (waveforms & mask) == 0) {
        mp_raise_ValueError("waveforms invalid. Expected a combination of NOISE, SQUARE, SAW, TRIANGLE, SINE, or WAVE");
    }
    channel.waveforms = (uint8_t)waveforms;
}

void set_channel_frequency(AudioChannel& channel, mp_obj_t in) {
    int freq = mp_obj_get_int(in);
    if(freq <= 0 || freq > UINT16_MAX) {
        mp_raise_ValueError("frequency out of range. Expected greater than 0Hz to 65535Hz");
    }
    channel.frequency = (uint16_t)freq;
}

void set_channel_volume(AudioChannel& channel, mp_obj_t in) {
    float volume = mp_obj_get_float(in);
    if(volume < 0.0f || volume > 1.0f) {
        mp_raise_ValueError("volume out of range. Expected 0.0 to 1.0");
    }
    channel.volume = (uint16_t)(volume * UINT16_MAX);
}

void set_channel_attack(AudioChannel& channel, mp_obj_t in) {
    int attack_ms = (int)(mp_obj_get_float(in) * 1000.0f);
    if(attack_ms < 0 || attack_ms > UINT16_MAX) {
        mp_raise_ValueError("attack out of range. Expected 0.0s to 65.5s");
    }
    channel.attack_ms = MAX(attack_ms, 1);
}

void set_channel_decay(AudioChannel& channel, mp_obj_t in) {
    int decay_ms = (int)(mp_obj_get_float(in) * 1000.0f);
    if(decay_ms < 0 || decay_ms > UINT16_MAX) {
        mp_raise_ValueError("decay out of range. Expected 0.0s to 65.5s");
    }
    channel.decay_ms = MAX(decay_ms, 1);
}

void set_channel_sustain(AudioChannel& channel, mp_obj_t in) {
    float sustain = mp_obj_get_float(in);
    if(sustain < 0.0f || sustain > 1.0f) {
        mp_raise_ValueError("sustain out of range. Expected 0.0 to 1.0");
    }
    channel.sustain = (uint16_t)(sustain * UINT16_MAX);
}

void set_channel_release(AudioChannel& channel, mp_obj_t in) {
    int release_ms = (int)(mp_obj_get_float(in) * 1000.0f);
    if(release_ms < 0 || release_ms > UINT16_MAX) {
        mp_raise_ValueError("release out of range. Expected 0.0s to 65.5s");
    }
    channel.release_ms = MAX(release_ms, 1);
}

void set_channel_pulse_width(AudioChannel& channel, mp_obj_t in) {
    float pulse_width = mp_obj_get_float(in);
    if(pulse_width < 0.0f || pulse_width > 1.0f) {
        mp_raise_ValueError("pulse_width out of range. Expected 0.0 to 1.0");
    }
    channel.pulse_width = (uint16_t)(pulse_width * UINT16_MAX);
}


/***** Methods *****/
mp_obj_t Channel_configure(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
    enum { ARG_self, ARG_waveforms, ARG_frequency, ARG_volume, ARG_attack, ARG_decay, ARG_sustain, ARG_release, ARG_pulse_width };
    static const mp_arg_t allowed_args[] = {
        { MP_QSTR_, MP_ARG_REQUIRED | MP_ARG_OBJ },
        { MP_QSTR_waveforms, MP_ARG_OBJ, {.u_obj = mp_const_none} },
        { MP_QSTR_frequency, MP_ARG_OBJ, {.u_obj = mp_const_none} },
        { MP_QSTR_volume, MP_ARG_OBJ, {.u_obj = mp_const_none} },
        { MP_QSTR_attack, MP_ARG_OBJ, {.u_obj = mp_const_none} },
        { MP_QSTR_decay, MP_ARG_OBJ, {.u_obj = mp_const_none} },
        { MP_QSTR_sustain, MP_ARG_OBJ, {.u_obj = mp_const_none} },
        { MP_QSTR_release, MP_ARG_OBJ, {.u_obj = mp_const_none} },
        { MP_QSTR_pulse_width, MP_ARG_OBJ, {.u_obj = mp_const_none} }
    };

    // Parse args.
    mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
    mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);

    _Channel_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self].u_obj, _Channel_obj_t);

    mp_obj_t waveforms = args[ARG_waveforms].u_obj;
    if(waveforms != mp_const_none) {
        set_channel_waveforms(*self->channel, waveforms);
    }

    mp_obj_t frequency = args[ARG_frequency].u_obj;
    if(frequency != mp_const_none) {
        set_channel_frequency(*self->channel, frequency);
    }

    mp_obj_t volume = args[ARG_volume].u_obj;
    if(volume != mp_const_none) {
        set_channel_volume(*self->channel, volume);
    }

    mp_obj_t attack = args[ARG_attack].u_obj;
    if(attack != mp_const_none) {
        set_channel_attack(*self->channel, attack);
    }

    mp_obj_t decay = args[ARG_decay].u_obj;
    if(decay != mp_const_none) {
        set_channel_decay(*self->channel, decay);
    }

    mp_obj_t sustain = args[ARG_sustain].u_obj;
    if(sustain != mp_const_none) {
        set_channel_sustain(*self->channel, sustain);
    }

    mp_obj_t release = args[ARG_release].u_obj;
    if(release != mp_const_none) {
        set_channel_release(*self->channel, release);
    }

    mp_obj_t pulse_width = args[ARG_pulse_width].u_obj;
    if(pulse_width != mp_const_none) {
        set_channel_pulse_width(*self->channel, pulse_width);
    }

    return mp_const_none;
}

mp_obj_t Channel_restore(mp_obj_t self_in) {
    _Channel_obj_t *self = MP_OBJ_TO_PTR2(self_in, _Channel_obj_t);
    self->channel->restore();
    return mp_const_none;
}

mp_obj_t Channel_waveforms(size_t n_args, const mp_obj_t *args) {
    _Channel_obj_t *self = MP_OBJ_TO_PTR2(args[0], _Channel_obj_t);

    if(n_args == 1) {
        return mp_obj_new_int(self->channel->waveforms);
    }

    set_channel_waveforms(*self->channel, args[1]);
    return mp_const_none;
}

mp_obj_t Channel_frequency(size_t n_args, const mp_obj_t *args) {
    _Channel_obj_t *self = MP_OBJ_TO_PTR2(args[0], _Channel_obj_t);

    if(n_args == 1) {
        return mp_obj_new_int(self->channel->frequency);
    }

    set_channel_frequency(*self->channel, args[1]);
    return mp_const_none;
}

mp_obj_t Channel_volume(size_t n_args, const mp_obj_t *args) {
    _Channel_obj_t *self = MP_OBJ_TO_PTR2(args[0], _Channel_obj_t);

    if(n_args == 1) {
        return mp_obj_new_float((float)self->channel->volume / UINT16_MAX);
    }

    set_channel_volume(*self->channel, args[1]);
    return mp_const_none;
}

mp_obj_t Channel_attack_duration(size_t n_args, const mp_obj_t *args) {
    _Channel_obj_t *self = MP_OBJ_TO_PTR2(args[0], _Channel_obj_t);

    if(n_args == 1) {
        return mp_obj_new_float((float)self->channel->attack_ms / 1000.0f);
    }

    set_channel_attack(*self->channel, args[1]);
    return mp_const_none;
}

mp_obj_t Channel_decay_duration(size_t n_args, const mp_obj_t *args) {
    _Channel_obj_t *self = MP_OBJ_TO_PTR2(args[0], _Channel_obj_t);

    if(n_args == 1) {
        return mp_obj_new_float((float)self->channel->decay_ms / 1000.0f);
    }

    set_channel_decay(*self->channel, args[1]);
    return mp_const_none;
}

mp_obj_t Channel_sustain_level(size_t n_args, const mp_obj_t *args) {
    _Channel_obj_t *self = MP_OBJ_TO_PTR2(args[0], _Channel_obj_t);

    if(n_args == 1) {
        return mp_obj_new_float((float)self->channel->sustain / UINT16_MAX);
    }

    set_channel_sustain(*self->channel, args[1]);
    return mp_const_none;
}

mp_obj_t Channel_release_duration(size_t n_args, const mp_obj_t *args) {
    _Channel_obj_t *self = MP_OBJ_TO_PTR2(args[0], _Channel_obj_t);

    if(n_args == 1) {
        return mp_obj_new_float((float)self->channel->release_ms / 1000.0f);
    }

    set_channel_release(*self->channel, args[1]);
    return mp_const_none;
}

mp_obj_t Channel_pulse_width(size_t n_args, const mp_obj_t *args) {
    _Channel_obj_t *self = MP_OBJ_TO_PTR2(args[0], _Channel_obj_t);

    if(n_args == 1) {
        return mp_obj_new_float((float)self->channel->pulse_width / 0xffff);
    }

    set_channel_pulse_width(*self->channel, args[1]);
    return mp_const_none;
}

mp_obj_t Channel_trigger_attack(mp_obj_t self_in) {
    _Channel_obj_t *self = MP_OBJ_TO_PTR2(self_in, _Channel_obj_t);
    self->channel->trigger_attack();

    return mp_const_none;
}

mp_obj_t Channel_trigger_release(mp_obj_t self_in) {
    _Channel_obj_t *self = MP_OBJ_TO_PTR2(self_in, _Channel_obj_t);
    self->channel->trigger_release();

    return mp_const_none;
}

mp_obj_t Channel_play_tone(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
    enum { ARG_self, ARG_freq, ARG_volume, ARG_fade_in, ARG_fade_out };
    static const mp_arg_t allowed_args[] = {
        { MP_QSTR_, MP_ARG_REQUIRED | MP_ARG_OBJ },
        { MP_QSTR_frequency, MP_ARG_REQUIRED | MP_ARG_OBJ },
        { MP_QSTR_volume, MP_ARG_OBJ, {.u_obj = mp_const_none} },
        { MP_QSTR_attack, MP_ARG_OBJ, {.u_obj = mp_const_none} },
        { MP_QSTR_release, MP_ARG_OBJ, {.u_obj = mp_const_none} },
    };

    // Parse args.
    mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
    mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);

    _Channel_obj_t *self = MP_OBJ_TO_PTR2(args[ARG_self].u_obj, _Channel_obj_t);

    set_channel_frequency(*self->channel, args[ARG_freq].u_obj);

    mp_obj_t volume = args[ARG_volume].u_obj;
    if(volume != mp_const_none) {
        set_channel_volume(*self->channel, volume);
    }
    else {
        self->channel->volume = UINT16_MAX;
    }

    mp_obj_t attack_ms = args[ARG_fade_in].u_obj;
    if(attack_ms != mp_const_none) {
        set_channel_attack(*self->channel, attack_ms);
    }
    else {
        self->channel->attack_ms = 1;
    }

    mp_obj_t release_ms = args[ARG_fade_out].u_obj;
    if(release_ms != mp_const_none) {
        set_channel_release(*self->channel, release_ms);
    }
    else {
        self->channel->release_ms = 1;
    }

    self->channel->waveforms = Waveform::SINE;
    self->channel->decay_ms = 1;
    self->channel->sustain = UINT16_MAX;

    self->channel->trigger_attack();

    return mp_const_none;
}


/********** CosmicUnicorn **********/

/***** Variables Struct *****/
typedef struct _CosmicUnicorn_obj_t {
    mp_obj_base_t base;
    CosmicUnicorn* Cosmic;
} _CosmicUnicorn_obj_t;

typedef struct _ModPicoGraphics_obj_t {
    mp_obj_base_t base;
    PicoGraphics *graphics;
    DisplayDriver *display;
    void *spritedata;
    void *buffer;
    _PimoroniI2C_obj_t *i2c;
    //mp_obj_t scanline_callback; // Not really feasible in MicroPython
} ModPicoGraphics_obj_t;

/***** Print *****/
void CosmicUnicorn_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
    (void)kind; //Unused input parameter
    //_CosmicUnicorn_obj_t *self = MP_OBJ_TO_PTR2(self_in, _CosmicUnicorn_obj_t);
    mp_print_str(print, "CosmicUnicorn()");
}


/***** Constructor *****/
mp_obj_t CosmicUnicorn_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
    _CosmicUnicorn_obj_t *self = nullptr;

    enum { ARG_pio, ARG_sm, ARG_pins, ARG_common_pin, ARG_direction, ARG_counts_per_rev, ARG_count_microsteps, ARG_freq_divider };
    static const mp_arg_t allowed_args[] = {
        { MP_QSTR_pio, MP_ARG_INT },
        { MP_QSTR_sm, MP_ARG_INT }
    };

    // Parse args.
    mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
    mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);

    int pio_int = args[ARG_pio].u_int;
    if(pio_int < 0 || pio_int > (int)NUM_PIOS) {
        mp_raise_ValueError("pio out of range. Expected 0 to 1");
    }
    //PIO pio = pio_int == 0 ? pio0 : pio1;

    int sm = args[ARG_sm].u_int;
    if(sm < 0 || sm > (int)NUM_PIO_STATE_MACHINES) {
        mp_raise_ValueError("sm out of range. Expected 0 to 3");
    }


    CosmicUnicorn *Cosmic = m_new_class(CosmicUnicorn);
    Cosmic->init();

    self = m_new_obj_with_finaliser(_CosmicUnicorn_obj_t);
    self->base.type = &CosmicUnicorn_type;
    self->Cosmic = Cosmic;

    return MP_OBJ_FROM_PTR(self);
}


/***** Destructor ******/
mp_obj_t CosmicUnicorn___del__(mp_obj_t self_in) {
    _CosmicUnicorn_obj_t *self = MP_OBJ_TO_PTR2(self_in, _CosmicUnicorn_obj_t);
    m_del_class(CosmicUnicorn, self->Cosmic);
    return mp_const_none;
}


/***** Methods *****/
extern mp_obj_t CosmicUnicorn_clear(mp_obj_t self_in) {
    _CosmicUnicorn_obj_t *self = MP_OBJ_TO_PTR2(self_in, _CosmicUnicorn_obj_t);
    self->Cosmic->clear();
    return mp_const_none;
}

extern mp_obj_t CosmicUnicorn_update(mp_obj_t self_in, mp_obj_t graphics_in) {
    _CosmicUnicorn_obj_t *self = MP_OBJ_TO_PTR2(self_in, _CosmicUnicorn_obj_t);
    ModPicoGraphics_obj_t *picographics = MP_OBJ_TO_PTR2(graphics_in, ModPicoGraphics_obj_t);

    self->Cosmic->update(picographics->graphics);

    return mp_const_none;
}

extern mp_obj_t CosmicUnicorn_set_brightness(mp_obj_t self_in, mp_obj_t value) {
    _CosmicUnicorn_obj_t *self = MP_OBJ_TO_PTR2(self_in, _CosmicUnicorn_obj_t);
    self->Cosmic->set_brightness(mp_obj_get_float(value));
    return mp_const_none;
}

extern mp_obj_t CosmicUnicorn_get_brightness(mp_obj_t self_in) {
    _CosmicUnicorn_obj_t *self = MP_OBJ_TO_PTR2(self_in, _CosmicUnicorn_obj_t);
    return mp_obj_new_float(self->Cosmic->get_brightness());
}

extern mp_obj_t CosmicUnicorn_adjust_brightness(mp_obj_t self_in, mp_obj_t delta) {
    _CosmicUnicorn_obj_t *self = MP_OBJ_TO_PTR2(self_in, _CosmicUnicorn_obj_t);
    self->Cosmic->adjust_brightness(mp_obj_get_float(delta));
    return mp_const_none;
}

extern mp_obj_t CosmicUnicorn_set_volume(mp_obj_t self_in, mp_obj_t value) {
    _CosmicUnicorn_obj_t *self = MP_OBJ_TO_PTR2(self_in, _CosmicUnicorn_obj_t);
    self->Cosmic->set_volume(mp_obj_get_float(value));
    return mp_const_none;
}

extern mp_obj_t CosmicUnicorn_get_volume(mp_obj_t self_in) {
    _CosmicUnicorn_obj_t *self = MP_OBJ_TO_PTR2(self_in, _CosmicUnicorn_obj_t);
    return mp_obj_new_float(self->Cosmic->get_volume());
}

extern mp_obj_t CosmicUnicorn_adjust_volume(mp_obj_t self_in, mp_obj_t delta) {
    _CosmicUnicorn_obj_t *self = MP_OBJ_TO_PTR2(self_in, _CosmicUnicorn_obj_t);
    self->Cosmic->adjust_volume(mp_obj_get_float(delta));
    return mp_const_none;
}


extern mp_obj_t CosmicUnicorn_light(mp_obj_t self_in) {
    _CosmicUnicorn_obj_t *self = MP_OBJ_TO_PTR2(self_in, _CosmicUnicorn_obj_t);
    return mp_obj_new_float(self->Cosmic->light());
}

extern mp_obj_t CosmicUnicorn_is_pressed(mp_obj_t self_in, mp_obj_t button) {
    _CosmicUnicorn_obj_t *self = MP_OBJ_TO_PTR2(self_in, _CosmicUnicorn_obj_t);
    return mp_obj_new_bool(self->Cosmic->is_pressed((uint8_t)mp_obj_get_int(button)));
}

extern mp_obj_t CosmicUnicorn_play_sample(mp_obj_t self_in, mp_obj_t data) {
    _CosmicUnicorn_obj_t *self = MP_OBJ_TO_PTR2(self_in, _CosmicUnicorn_obj_t);

    mp_buffer_info_t bufinfo;
    mp_get_buffer_raise(data, &bufinfo, MP_BUFFER_RW);
    if(bufinfo.len < 1) {
        mp_raise_ValueError("Supplied buffer is too small!");
    }

    self->Cosmic->play_sample((uint8_t *)bufinfo.buf, bufinfo.len);

    return mp_const_none;
}

extern mp_obj_t CosmicUnicorn_play_synth(mp_obj_t self_in) {
    _CosmicUnicorn_obj_t *self = MP_OBJ_TO_PTR2(self_in, _CosmicUnicorn_obj_t);
    self->Cosmic->play_synth();

    return mp_const_none;
}

extern mp_obj_t CosmicUnicorn_stop_playing(mp_obj_t self_in) {
    _CosmicUnicorn_obj_t *self = MP_OBJ_TO_PTR2(self_in, _CosmicUnicorn_obj_t);
    self->Cosmic->stop_playing();

    return mp_const_none;
}

extern mp_obj_t CosmicUnicorn_synth_channel(mp_obj_t self_in, mp_obj_t channel_in) {
    _CosmicUnicorn_obj_t *self = MP_OBJ_TO_PTR2(self_in, _CosmicUnicorn_obj_t);

    // Check that the channel is valid
    int channel = mp_obj_get_int(channel_in);
    if(channel < 0 || channel >= (int)PicoSynth::CHANNEL_COUNT) {
        mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("channel out of range. Expected 0 to %d"), PicoSynth::CHANNEL_COUNT - 1);
    }

    // NOTE This seems to work, in that it give MP access to the calibration object
    // Could very easily mess up in weird ways once object deletion is considered?
    _Channel_obj_t *channel_obj = m_new_obj_with_finaliser(_Channel_obj_t);
    channel_obj->base.type = &Channel_type;
    channel_obj->channel = &self->Cosmic->synth_channel(channel);

    return MP_OBJ_FROM_PTR(channel_obj);
}
}
