// based on  Mindi by Spencer Jackson
//poly_midi_pc.c
#include "poly_midi_pc.h"
#include<string.h>
#include<stdlib.h>
#include<stdio.h>
#include<stdbool.h>
#include<math.h>
#include<lv2.h>
#include<lv2/lv2plug.in/ns/ext/urid/urid.h>
#include<lv2/lv2plug.in/ns/ext/midi/midi.h>
#include<lv2/lv2plug.in/ns/ext/atom/util.h>
#include "lv2/lv2plug.in/ns/ext/time/time.h"
#include "lv2/lv2plug.in/ns/ext/atom/forge.h"

//midi stuff
#define MIDI_NOTE_OFF        0x80
#define MIDI_NOTE_ON         0x90
#define MIDI_KEYPRESSURE     0xA0
#define MIDI_CONTROL_CHANGE  0xB0
#define MIDI_PROGRAMCHANGE   0xC0
#define MIDI_CHANNELPRESSURE 0xD0
#define MIDI_PITCHBEND       0xE0
#define MIDI_SYSTEMMSG       0xF0

#define MIDI_TYPE_MASK       0xF0
#define MIDI_CHANNEL_MASK    0x0F
#define MIDI_DATA_MASK       0x7F
#define MIDI_PITCH_CENTER    0x2000
#define MIDI_SUSTAIN_PEDAL   0x40
#define MIDI_ALL_NOTES_OFF   123
#define MIDI_STOP            252

typedef struct _POLY_MIDI_PC
{
    //midi
    LV2_URID_Map* urid_map;
    LV2_URID midi_ev_urid;
    LV2_Atom_Forge forge;
    LV2_Atom_Forge_Frame frame;

    //ports
    LV2_Atom_Sequence* midi_out_p;
    const float* set_cv_p;
    const float* up_cv_p;
    const float* down_cv_p;
    const float* chan_p; 
    const float* pc_num_p;

	float* pc_out;

	// 
	bool prev_set;
	bool prev_up;
	bool prev_down;
	uint32_t capacity; 
	uint8_t current_pc;
	uint8_t prev_pc;
} POLY_MIDI_PC;

//main functions
LV2_Handle init_poly_midi_pc(const LV2_Descriptor *descriptor,double sample_rate, const char *bundle_path,const LV2_Feature * const* host_features)
{
    POLY_MIDI_PC* plug = (POLY_MIDI_PC*)calloc(1, sizeof(POLY_MIDI_PC));

    //get urid map value for midi events
    for (int i = 0; host_features[i]; i++)
    {
        if (strcmp(host_features[i]->URI, LV2_URID__map) == 0)
        {
            plug->urid_map = (LV2_URID_Map *) host_features[i]->data;
            if (plug->urid_map)
            {
                plug->midi_ev_urid = plug->urid_map->map(plug->urid_map->handle, LV2_MIDI__MidiEvent);
                break;
            }
        }
    }

    lv2_atom_forge_init(&plug->forge,plug->urid_map);

    return plug;
}

static void
activate(LV2_Handle instance)
{
    POLY_MIDI_PC* plug = (POLY_MIDI_PC*)instance;

	plug->capacity = 0;
	plug->prev_set    = false;
	plug->prev_up    = false;
	plug->prev_down    = false;
	plug->current_pc = 0;
	plug->prev_pc = 127; // so we set on first run
}

void connect_poly_midi_pc_ports(LV2_Handle handle, uint32_t port, void *data)
{
    POLY_MIDI_PC* plug = (POLY_MIDI_PC*)handle;
    if(port == MIDI_OUT)    plug->midi_out_p = (LV2_Atom_Sequence*)data;
    else if(port == SET_CV)plug->set_cv_p = (const float*)data;
    else if(port == UP_CV)plug->up_cv_p = (const float*)data;
    else if(port == DOWN_CV)plug->down_cv_p = (const float*)data;
    else if(port == CHAN)   plug->chan_p = (const float*)data;
    else if(port == PC_NUM)  plug->pc_num_p = (const float*)data;
    else if(port == PC_OUT)  plug->pc_out = (float*)data;
}

static void midi_tx(POLY_MIDI_PC *plug, int64_t tme, uint8_t raw_midi[2])
{
	LV2_Atom midiatom;
	midiatom.type = plug->midi_ev_urid;
	midiatom.size = 2;
	lv2_atom_forge_frame_time(&plug->forge, tme);
	lv2_atom_forge_raw(&plug->forge, &midiatom, sizeof(LV2_Atom));
	lv2_atom_forge_raw(&plug->forge, raw_midi, 2);
	lv2_atom_forge_pad(&plug->forge, sizeof(LV2_Atom) + midiatom.size);
}

void run_poly_midi_pc( LV2_Handle handle, uint32_t n_samples)
{
    POLY_MIDI_PC* plug = (POLY_MIDI_PC*)handle;
    /* uint8_t msg[3]; */
	//
	// Struct for a 2 byte MIDI event, used for writing PC
	uint8_t        msg[2];

	const float* set_trigger = plug->set_cv_p;
	const float* up_trigger = plug->up_cv_p;
	const float* down_trigger = plug->down_cv_p;

	bool prev_set    = plug->prev_set;
	bool prev_up    = plug->prev_up;
	bool prev_down = plug->prev_down;

	if (plug->capacity == 0){
		plug->capacity = plug->midi_out_p->atom.size;
	}
	uint8_t pc_num_p = (uint8_t) *(plug->pc_num_p);
	uint8_t chan_p = (uint8_t) *(plug->chan_p);

	uint8_t prev_pc = plug->prev_pc;

	// if user has changed the pc num
	uint8_t current_pc;

	if (prev_pc != pc_num_p){
		current_pc = pc_num_p;
		prev_pc = pc_num_p;
	} else {
		current_pc = plug->current_pc;
	}


	lv2_atom_forge_set_buffer(&plug->forge, (uint8_t*)plug->midi_out_p, plug->capacity);
	lv2_atom_forge_sequence_head(&plug->forge, &plug->frame, 0);

	const uint8_t msg_type = (uint8_t)MIDI_PROGRAMCHANGE + ((chan_p - 1) % 16);

	bool pc_changed = false; 

	for (uint32_t s = 0; (s < n_samples); s = (s + 1)) {
		pc_changed = false;
		if (set_trigger[s] > 0.4f && !(prev_set)) {
			prev_set = true;
			pc_changed = true;
			current_pc = pc_num_p;
		} else if (set_trigger[s] <= 0.08){ // 0.25 volts in Hector
			prev_set = false;
		}

		if (up_trigger[s] > 0.4f && !(prev_up)) {
			prev_up = true;
			pc_changed = true;
			current_pc = (current_pc + 1) % 127;
		} else if (up_trigger[s] <= 0.08){ // 0.25 volts in Hector
			prev_up = false;
		}

		if (down_trigger[s] > 0.4f && !(prev_down)) {
			prev_down = true;
			pc_changed = true;
			if (current_pc == 0){
				current_pc = 127;
			} else {
				current_pc = current_pc - 1;
			}
		} else if (down_trigger[s] <= 0.08){ // 0.25 volts in Hector
			prev_down = false;
		}

		if(pc_changed)
		{
			//make event
			msg[0] = msg_type;
			msg[1] = MIDI_DATA_MASK & (uint8_t)current_pc;

			midi_tx(plug, s, msg);
		}
	}
	plug->prev_set = prev_set;
	plug->prev_up = prev_up;
	plug->prev_down = prev_down;
	plug->pc_out[0] = (float) current_pc;
	plug->current_pc = current_pc;
	plug->prev_pc = prev_pc;
}

void cleanup_poly_midi_pc(LV2_Handle handle)
{
    POLY_MIDI_PC* plug = (POLY_MIDI_PC*) handle;
    free(plug);
}


//lv2 stuff
static const LV2_Descriptor poly_midi_pc_descriptor=
{
    POLY_MIDI_PC_URI,
    init_poly_midi_pc,
    connect_poly_midi_pc_ports,
    activate,
    run_poly_midi_pc,
    NULL,//deactivate
    cleanup_poly_midi_pc,
    NULL//extension
};

LV2_SYMBOL_EXPORT
const LV2_Descriptor* lv2_descriptor(uint32_t index)
{
    switch (index)
    {
    case 0:
        return &poly_midi_pc_descriptor;
    default:
        return NULL;
    }
}
