A rust port of vgm2mid by Paul Jensen and Valley Bell
use crate::ay8910::MIDI_CHANNEL_PSG_BASE;
use crate::config::Config;
use crate::midi_shim::{
	db_to_midi_vol, lin_to_db, MIDIShim, MIDI_PAN, MIDI_PAN_CENTER, MIDI_PAN_LEFT,
	MIDI_PAN_RIGHT, MIDI_VOLUME,
};
use crate::strict;
use crate::utils::{hz_to_note, shift, FactoredState};
use anyhow::Result;
use midly::num::u4;

#[allow(dead_code)]
const NR10: u8 = 0x0;
const NR11: u8 = 0x1;
const NR12: u8 = 0x2;
const NR13: u8 = 0x3;
const NR14: u8 = 0x4;
const NR21: u8 = 0x6;
const NR22: u8 = 0x7;
const NR23: u8 = 0x8;
const NR24: u8 = 0x9;
const NR30: u8 = 0xA;
const NR31: u8 = 0xB;
const NR32: u8 = 0xC;
const NR33: u8 = 0xD;
const NR34: u8 = 0xE;
const NR41: u8 = 0x10;
const NR42: u8 = 0x11;
const NR43: u8 = 0x12;
const NR44: u8 = 0x13;
#[allow(dead_code)]
const NR50: u8 = 0x14;
const NR51: u8 = 0x15;
#[allow(dead_code)]
const NR52: u8 = 0x16;

fn hz_game_boy(fnum: u32) -> f64 {
	131072.0 / f64::from(2048 - fnum)
}

fn hz_game_boy_noise(poly_cntr: u8) -> f64 {
	let mut freq_div = f64::from(poly_cntr & 0x7); // Division Ratio of Freq
	 // Shift Clock Freq (poly cntr)

	if freq_div == 0.0 {
		freq_div = 0.5
	}
	let shft_frq: u16 = ((poly_cntr & 0xF0) / 0x10).into();
	524288.0 / freq_div / (2 ^ (shft_frq + 0x1)) as f64
}

pub(crate) struct GameBoyState {
	envelope_1: [u8; 4],
	envelope_2: [u8; 4],
	duty_1: [u8; 4],
	duty_2: [u8; 4],
	gb_pan: [u8; 5],
	factored: FactoredState,
}

impl Default for GameBoyState {
	fn default() -> Self {
		GameBoyState {
			envelope_1: [0; 4],
			envelope_2: [0xFF; 4],
			duty_1: [0; 4],
			duty_2: [0xFF; 4],
			gb_pan: [0, 0, 0, 0, 0xFF],
			factored: Default::default(),
		}
	}
}

pub(crate) struct GameBoy<'config> {
	state: GameBoyState,
	config: &'config Config,
}

impl<'config> GameBoy<'config> {
	pub(crate) fn new<'c: 'config>(
		config: &'c Config,
		opt_state: Option<GameBoyState>,
	) -> Self {
		Self {
			state: opt_state.unwrap_or_default(),
			config,
		}
	}

	pub(crate) fn init(&mut self, midi: &mut MIDIShim) {
		midi.program_change_write(MIDI_CHANNEL_PSG_BASE + 0x02.into(), 0x50.into());
		midi.program_change_write(MIDI_CHANNEL_PSG_BASE + 0x03.into(), 0x7F.into());
	}

	pub(crate) fn command_handle(
		&mut self,
		register: u8,
		data: u8,
		midi: &mut MIDIShim,
	) -> Result<()> {
		let temp_byte: u8;

		/*if Variables_Clear_YM2612 = 1 {
			Erase FNum_1: Erase FNum_2: Erase Hz_1: Erase Hz_2: Erase Note_1: Erase Note_2
			Erase Envelope_1: Erase Envelope_2: Erase state.Duty1: Erase Duty_2: Erase NoteOn_1: Erase NoteOn_2
			Erase MIDINote: Erase MIDIWheel: Erase NoteOn_1: Erase NoteOn_2: MIDIVolume = 0
			for CH in 0x0..=0x3 {
				Envelope_2[channel as usize] = 0xFF
				Duty_2[channel as usize] = 0xFF
				state.factored.note_1[channel as usize] = 0xFF
				state.factored.note_2[channel as usize] = 0xFF
				state.factored.note_on_2[channel as usize] = 0x0
				state.factored.midi_note[channel as usize] = 0xFF
				state.factored.midi_wheel[channel as usize] = 0x8000
			}
			for CH in 0x0..=0x3 {
				GB_Pan[channel as usize] = 0x0
			}
			GB_Pan(0x4) = 0xFF
			Variables_Clear_YM2612 = 0

			midi.event_write(MIDI_PROGRAM_CHANGE, MIDI_CHANNEL_PSG_BASE + 0x2, 0x50);
			midi.event_write(MIDI_PROGRAM_CHANGE, MIDI_CHANNEL_PSG_BASE + 0x3, 0x7F);
		}*/

		// Wave RAM
		if register >= 0x20 {
			return Ok(()); //FIXME: Should this be a strict?
		}

		let channel = register / 0x5;
		let channel_ptr = channel as usize;

		if (channel < 0x3 && self.config.sn76489_ch_disabled[channel_ptr])
			|| (channel == 0x3 && self.config.sn76489_noise_disabled)
		{
			return Ok(());
		}

		let midi_channel = MIDI_CHANNEL_PSG_BASE + channel.into();
		match register {
			NR30 => {
				// Wave Channel - Note On
				self.state.factored.note_on_1[channel_ptr] = shift(
					&mut self.state.factored.note_on_2[channel_ptr],
					(data & 0x80) != 0,
				);

				if self.state.factored.note_on_1[channel_ptr]
					!= self.state.factored.note_on_2[channel_ptr]
				{
					if self.state.factored.note_on_2[channel_ptr] {
						midi.do_note_on(
							self.state.factored.note_2[channel_ptr],
							self.state.factored.note_2[channel_ptr],
							midi_channel,
							&mut self.state.factored.midi_note
								[channel_ptr],
							&mut self.state.factored.midi_wheel
								[channel_ptr],
							Some(255),
							None,
						)?;
					} else if self.state.factored.midi_note[channel_ptr] != 0xFF
					{
						midi.note_off_write(
							midi_channel,
							self.state.factored.midi_note[channel_ptr],
							0x00.into(),
						);
						self.state.factored.midi_note[channel_ptr] =
							0xFF.into()
					}
				}
			},
			NR11 | NR21 | NR31 | NR41 => {
				// Sound Length, Wave pattern duty
				// how do I do this?
				if register == NR11 || register == NR21 {
					// Wave duties are: 12.5%, 25%, 50%, 75%
					self.state.duty_1[channel_ptr] =
						self.state.duty_2[channel_ptr];
					self.state.duty_2[channel_ptr] = (data & 0xC0) / 0x40;

					if self.state.duty_1[channel_ptr]
						!= self.state.duty_2[channel_ptr]
					{
						temp_byte = 0x4F
							+ (!self.state.duty_2[channel_ptr] & 0x3);
						midi.program_change_write(
							midi_channel,
							temp_byte.into(),
						);
					}
				}
			},
			NR12 | NR22 | NR32 | NR42 => {
				envelope(
					&mut self.state,
					channel,
					register,
					data,
					midi,
					midi_channel,
				);
			},
			NR13 | NR23 | NR33 | NR43 | NR14 | NR24 | NR34 | NR44 => {
				noise(
					register,
					&mut self.state,
					channel,
					data,
					midi_channel,
					midi,
				)?;
			},
			NR51 => gameboy_stereo(data, &mut self.state, midi)?,
			_ => strict!("Invalid register"),
		}
		Ok(())
	}
}

fn envelope(
	state: &mut GameBoyState,
	channel: u8,
	register: u8,
	data: u8,
	midi: &mut MIDIShim,
	midi_channel: u4,
) {
	let channel_ptr = channel as usize;
	// Envelope
	state.envelope_1[channel_ptr] = state.envelope_2[channel_ptr];

	if register != NR32 {
		// output is 1 * envelope
		state.envelope_2[channel_ptr] = (data & 0xF0) / 0x10;
		state.factored.midi_volume[0] = db_to_midi_vol(lin_to_db(
			f64::from(state.envelope_2[channel_ptr]) / 0x0F as f64,
		));
	} else {
		// output is 0xF >> (envelope - 1)
		// >> 1 = 6 db
		state.envelope_2[channel_ptr] = (data & 0x60) / 0x20;
		state.factored.midi_volume[0] =
			db_to_midi_vol(-f64::from(state.envelope_2[channel_ptr]) * 6.0);
	}

	if state.envelope_1[channel_ptr] != state.envelope_2[channel_ptr] {
		midi.controller_write(
			midi_channel,
			MIDI_VOLUME,
			state.factored.midi_volume[0],
		);
	}
}

fn noise(
	register: u8,
	state: &mut GameBoyState,
	channel: u8,
	data: u8,
	midi_channel: u4,
	midi: &mut MIDIShim,
) -> Result<()> {
	// Frequency Low, High
	if (register % 0x5) == 0x3 {
		state.factored.fnum_lsb[channel as usize] = data;
	//Exit Sub
	} else if (register % 0x5) == 0x4 {
		state.factored.fnum_msb[channel as usize] = data;
	}
	state.factored.fnum_1[channel as usize] = state.factored.fnum_2[channel as usize];
	state.factored.fnum_2[channel as usize] = ((state.factored.fnum_msb[channel as usize]
		as u32 & 0x7) << 8)
		| state.factored.fnum_lsb[channel as usize] as u32;

	state.factored.hz_1[channel as usize] = state.factored.hz_2[channel as usize];
	if channel <= 0x1 {
		state.factored.hz_2[channel as usize] =
			hz_game_boy(state.factored.fnum_2[channel as usize]);
	} else if channel == 0x2 {
		state.factored.hz_2[channel as usize] =
			hz_game_boy(state.factored.fnum_2[channel as usize]) / 2.0;
	} else if channel == 0x3 {
		state.factored.hz_2[channel as usize] =
			hz_game_boy_noise(state.factored.fnum_lsb[channel as usize]) / 32.0;
	}

	state.factored.note_1[channel as usize] = state.factored.note_2[channel as usize];
	let mut temp_note = hz_to_note(state.factored.hz_2[channel as usize]);
	if temp_note >= 0x80.into() {
		//TempNote = 0x7F - state.factored.fnum_2[channel as usize]
		temp_note = 0x7F.into();
	}
	state.factored.note_2[channel as usize] = temp_note;

	if (register % 0x5) == 0x4 {
		if channel == 0x2 {
			state.factored.note_on_1[channel as usize] =
				state.factored.note_on_2[channel as usize]
		} else {
			// force NoteOn for all channels but the Wave channel
			state.factored.note_on_1[channel as usize] = false
		}
		state.factored.note_on_2[channel as usize] =
			state.factored.note_on_1[channel as usize] || (data & 0x80) != 0;

		if state.factored.note_on_1[channel as usize]
			!= state.factored.note_on_2[channel as usize]
			&& state.factored.note_on_2[channel as usize]
		{
			midi.do_note_on(
				state.factored.note_1[channel as usize],
				state.factored.note_2[channel as usize],
				midi_channel,
				&mut state.factored.midi_note[channel as usize],
				&mut state.factored.midi_wheel[channel as usize],
				Some(255),
				None,
			)?;
		} else if state.factored.note_on_2[channel as usize]
			&& state.factored.note_1[channel as usize]
				!= state.factored.note_2[channel as usize]
		{
			midi.do_note_on(
				state.factored.note_1[channel as usize],
				state.factored.note_2[channel as usize],
				midi_channel,
				&mut state.factored.midi_note[channel as usize],
				&mut state.factored.midi_wheel[channel as usize],
				None,
				None,
			)?;
		}
	} else if state.factored.note_1[channel as usize] != state.factored.note_2[channel as usize]
	{
		midi.do_note_on(
			state.factored.note_1[channel as usize],
			state.factored.note_2[channel as usize],
			midi_channel,
			&mut state.factored.midi_note[channel as usize],
			&mut state.factored.midi_wheel[channel as usize],
			None,
			None,
		)?;
	}
	Ok(())
}

fn gameboy_stereo(data: u8, state: &mut GameBoyState, midi: &mut MIDIShim) -> Result<()> {
	let mut pan_value = 0x00;

	if state.gb_pan[0x4] == data {
		return Ok(());
	}

	for current_bit in 0x0..=0x3 {
		let channel_mask = 2 ^ current_bit; // replaces "1 << CurBit"

		if data & (channel_mask * 0x10) != 0 {
			pan_value |= 0x1 // Left Channel On
		}
		if data & channel_mask != 0 {
			pan_value |= 0x2 // Right Channel On
		}

		if state.gb_pan[current_bit as usize] != pan_value || state.gb_pan[0x4] == data {
			let channel = MIDI_CHANNEL_PSG_BASE + current_bit.into();
			match pan_value {
				0x1 => midi.controller_write(
					channel,
					MIDI_PAN,
					MIDI_PAN_LEFT,
				),
				0x2 => midi.controller_write(
					channel,
					MIDI_PAN,
					MIDI_PAN_RIGHT,
				),
				0x3 => midi.controller_write(
					channel,
					MIDI_PAN,
					MIDI_PAN_CENTER,
				),
				_ => strict!(),
			};
		}
		state.gb_pan[current_bit as usize] = pan_value;
	}

	state.gb_pan[0x4] = data;
	Ok(())
}