/*
* midi_in.cc
* DIN Is Noise MIDI Input is copyright (c) 2006-2022 Jagannathan Sampath
* DIN Is Noise is released under GNU Public License 2.0
* For more information, please visit https://dinisnoise.org/
*/

#include "midi_in.h"
#include "console.h"
#include "tcl_interp.h"
#include "keyboard_keyboard.h"
#include "chrono.h"
#include "log.h"
#include <fstream>
#include <string>
using namespace std;

extern double MIDI_BPM;
extern char BUFFER [];

#define DIN_IS_NOISE_MIDI_INPUT "DIN Is Noise MIDI Input"

#if defined __WINDOWS_MM__
midi_in::midi_in () : rt (RtMidi::WINDOWS_MM, DIN_IS_NOISE_MIDI_INPUT)
#elif defined __MACOSX_CORE__
midi_in::midi_in () : rt (RtMidi::MACOSX_CORE, DIN_IS_NOISE_MIDI_INPUT)
#elif defined __UNIX_JACK__
midi_in::midi_in () : rt (RtMidi::UNIX_JACK, DIN_IS_NOISE_MIDI_INPUT)
#elif defined __LINUX_ALSA__
midi_in::midi_in () : rt (RtMidi::LINUX_ALSA, DIN_IS_NOISE_MIDI_INPUT)
#endif 

{
	available = 0;
	input_port = 0;
	channel = -1;
	probe ();	
}

void midi_in::probe () {
	num_ports = rt.getPortCount ();
	int api = rt.getCurrentApi ();
	if (api == RtMidi::WINDOWS_MM) dlog << "*** current MIDI API = Windows Multimedia ***" << endl;
	else if (api == RtMidi::MACOSX_CORE) dlog << "*** current MIDI API = Mac OS X Core ***" << endl;
	else if (api == RtMidi::UNIX_JACK) dlog << "*** current MIDI API = JACK ***" << endl;
  else if (api == RtMidi::LINUX_ALSA) dlog << "*** current MIDI API = ALSA ***" << endl;
	else dlog << "!!! MIDI API not found. No MIDI input is possible !!!" << endl;
	if (num_ports == 0) {
		dlog << "!!! found no MIDI input ports !!!" << endl;
	}
	else {
		dlog  << "+++ found " << num_ports << " MIDI input ports +++" << endl;
		names.clear ();
		for (int i = 0; i < num_ports; ++i) {
			string name = rt.getPortName (i);
			names.push_back (name);
		}
		input_port = 0;
	}
}

void midi_in::open () {
	if (input_port >= num_ports) {
		dlog << "!!! couldnt open MIDI port " << input_port << " !!!" << endl;
	} else {
		if (available) rt.closePort ();
		rt.openPort (input_port);
		rt.ignoreTypes (0, 0, 0); // dont ignore sysex, timing or active sensing messages
		available = 1;
		dlog  << "+++ opened MIDI input port " << input_port << " ["<< rt.getPortName (input_port) << "] +++" << endl;
	}
}

void midi_in::open (int port) {
	input_port = port;
	open ();
}

void run_midi_cmd (const string& c, std::vector<unsigned char>& args) {
	string cmd (c);
	for (size_t i = 0, j = args.size (); i < j; ++i) {
		sprintf (BUFFER, " %u", args[i]);
		cmd += BUFFER;
	}
	cons (cmd);
}

void midi_in::handle_input () {

	if (available == 0) return;

	std::vector<unsigned char> args;
	rt.getMessage (&args); 
	int args_size = args.size ();

	static int num_clocks = 0;
	static double start_time = ui_clk();

	channel = cc = val = 0xff;

	if (args_size) {

		unsigned char type = args[0];
		unsigned char type_f0 = type & 0xf0;
		channel = type & 0x0f;

    args.push_back (channel);
		if (type_f0 == 0xb0) { // control change
			cc = args[1];
			val = args[2];
			run_midi_cmd ("midi-cc", args);
		} else if (type_f0 == 0x90) { // note on
			run_midi_cmd ("midi-note-on", args);
			keybd2.note_on (args[1], args[2]);
		} else if (type_f0 == 0x80) { // note off
			run_midi_cmd ("midi-note-off", args);
			keybd2.note_off (args[1]);
		} else if (type_f0 == 0xc0) { // program change
			run_midi_cmd ("midi-program-change", args);
		} else if (type_f0 == 0xe0) { // pitch bend

			char status = args[0];
			char lsb = args[1];
			char msb = args[2];

			// normalise pitchbend range to -1.0...1.0
			short value = (lsb | (msb << 7)) - 0x2000;
			float fvalue = (float)value / ((value < 0) ? 0x2000 : 0x1FFF);

			keybd2.pitch_bend (fvalue);

			ostringstream msg;
			msg << "midi-pitch-bend " << status << " " << value << " " << fvalue;
			cons (msg.str());

		} else if (type == 0xf8) { // midi clock tick
			++num_clocks;
			double time_now = ui_clk();
			double elapsed_time = time_now - start_time;
			if (elapsed_time >= 1) {
				// from MIDI specification - see https://en.wikipedia.org/wiki/MIDI_beat_clock
				float quarter_notes_per_sec = num_clocks * 1.0f / (elapsed_time * 24); 
				MIDI_BPM = quarter_notes_per_sec * 60; // a beat is a quarter note
				Tcl_UpdateLinkedVar (interpreter.interp, "midibpm"); // accessible as Tcl variable on DIN's command line
				cons ("midi-clock");
				num_clocks = 0;
				start_time = time_now;
			}

		} else if (type == 0xfa) { // midi start
			cons ("midi-start");
			num_clocks = 0;
			start_time = ui_clk();
		}
	}
}
