A rust port of vgm2mid by Paul Jensen and Valley Bell
// OPL Module for YM3812, YM3526, Y8950 and YMF262, based on YM2413 (OPLL) Module

use crate::config::Config;
use crate::midi_shim::{
	db_to_midi_vol, MIDIShim, MIDI_PAN, MIDI_PAN_CENTER, MIDI_PAN_LEFT, MIDI_PAN_RIGHT,
	MIDI_VOLUME,
};
use crate::strict;
use crate::utils::{hz_to_note, FactoredState};
use crate::vgm2mid::{CHN_DAC, OPL_TYPE_YMF262};
use anyhow::Result;
use midly::num::{u4, u7};

// YM3812/YMF262 Register Constants
const YM3812_REG_TEST_WAVESEL_EN: u16 = 0x1;
const YM3812_REG_CSW_NOTESEL: u16 = 0x8;
const YM3812_REG_MOD_VIB_EG_KS_MULT: u16 = 0x20;
const YM3812_REG_MOD_VIB_EG_KS_MULT_END: u16 = YM3812_REG_MOD_VIB_EG_KS_MULT + 0x15;
const YM3812_REG_KSL_TL: u16 = 0x40;
const YM3812_REG_KSL_TL_END: u16 = YM3812_REG_KSL_TL + 0x17;
#[allow(dead_code)]
const YM3812_REG_AR_DR: u16 = 0x60;
#[allow(dead_code)]
const YM3812_REG_SL_RR: u16 = 0x80;
const YM3812_REG_FNUM_LSB: u16 = 0xA0;
const YM3812_REG_FNUM_LSB_END: u16 = YM3812_REG_FNUM_LSB + 0x8;
const YM3812_REG_KEY_BLOCK_FNUM_MSB: u16 = 0xB0;
const YM3812_REG_KEY_BLOCK_FNUM_MSB_END: u16 = YM3812_REG_KEY_BLOCK_FNUM_MSB + 0x8;
const YM3812_REG_RHYTHM: u16 = 0xBD;
const YM3812_FB_CONNECTION_PAN: u16 = 0xC0;
const YM3812_FB_CONNECTION_PAN_END: u16 = YM3812_FB_CONNECTION_PAN + 0x8;
#[allow(dead_code)]
const YM3812_WAVESEL: u16 = 0xE0;
const YMF262_4OP_EN: u16 = 0x104;
const YMF262_OPL3_EN: u16 = 0x105;

const YM3812_REG_KEY: u8 = 0x20;
const YM3812_REG_BLOCK: u8 = 0x1C;
const YM3812_REG_FNUM_MSB: u8 = 0x3;

#[allow(dead_code)]
const YM3812_REG_RHYTHM_MODE: u16 = 0x20;
const YM3812_REG_BD: u8 = 0x10;
const YM3812_REG_SD: u8 = 0x8;
const YM3812_REG_TOM: u8 = 0x4;
const YM3812_REG_TCT: u8 = 0x2;
const YM3812_REG_HH: u8 = 0x1;

pub(crate) fn hz_ym3812(fnum: f64, opt_block: Option<u8>, fsam_3812: f64) -> f64 {
	let block = opt_block.unwrap_or(0);

	if block == 0 && fnum == 0.0 {
		0.0
	} else {
		fnum * fsam_3812 * f64::powi(2.0, block as i32 - 20)
	}
}

pub(crate) struct YM3812State {
	key_on: [bool; 8],
	block: [u8; 8],
	volume: [u8; 8],
	bd_vol: u7,
	hh_vol: u7,
	sd_vol: u7,
	tom_vol: u7,
	tct_vol: u7,
	percussion_on: [bool; 5],
	opl3_mode: u8,
	pub opl_type: u8,
	factored: FactoredState,
}

pub(crate) struct YM3812<'config> {
	pub state: YM3812State,
	config: &'config Config, //FIXME: This should be able to be a reference. It's basically static anyway though...
}

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

	pub(crate) fn command_handle(
		&mut self,
		register: u16,
		data: u8,
		fsam_3812: f64,
		midi: &mut MIDIShim,
	) -> Result<()> {
		match register {
			YM3812_REG_TEST_WAVESEL_EN => (),
			//WaveselEnable = (Data & 0x20) / 0x20
			YMF262_4OP_EN => (),
			YMF262_OPL3_EN => self.state.opl3_mode = data & 0x1,
			YM3812_REG_CSW_NOTESEL => (),
			YM3812_REG_MOD_VIB_EG_KS_MULT..=YM3812_REG_MOD_VIB_EG_KS_MULT_END => {
				process_mod_vib_eg_ks_mult(register, data, self.config, midi)
			},
			YM3812_REG_KSL_TL..=YM3812_REG_KSL_TL_END => {
				ksl_tl(self, register, data, midi)
			},
			YM3812_REG_FNUM_LSB..=YM3812_REG_FNUM_LSB_END
			| YM3812_REG_KEY_BLOCK_FNUM_MSB..=YM3812_REG_KEY_BLOCK_FNUM_MSB_END => {
				let channel = (register & 0xF) as u8;

				if channel > 0x8 {
					return Ok(());
				}
				if self.config.ym2413_ch_disabled[channel as usize] {
					return Ok(());
				}

				if (register & 0xF0) == YM3812_REG_FNUM_LSB {
					self.state.factored.fnum_lsb[channel as usize] = data;
				//Exit Sub
				} else if (register & 0xF0) == YM3812_REG_KEY_BLOCK_FNUM_MSB {
					self.state.key_on[channel as usize] =
						(data & YM3812_REG_KEY) / YM3812_REG_KEY != 0;
					self.state.block[channel as usize] =
						(data & YM3812_REG_BLOCK) / 0x4;
					self.state.factored.fnum_msb[channel as usize] =
						data & YM3812_REG_FNUM_MSB;
				}

				self.state.factored.fnum_1[channel as usize] =
					self.state.factored.fnum_2[channel as usize];
				self.state.factored.fnum_2[channel as usize] =
					(self.state.factored.fnum_msb[channel as usize] as u32
						* 256) + self.state.factored.fnum_lsb[channel as usize]
						as u32;

				self.state.factored.hz_1[channel as usize] =
					self.state.factored.hz_2[channel as usize];
				self.state.factored.hz_2[channel as usize] = hz_ym3812(
					self.state.factored.fnum_2[channel as usize] as f64,
					Some(self.state.block[channel as usize]),
					fsam_3812,
				);

				self.state.factored.note_1[channel as usize] =
					self.state.factored.note_2[channel as usize];
				self.state.factored.note_2[channel as usize] =
					hz_to_note(self.state.factored.hz_2[channel as usize]);

				self.state.factored.note_on_1[channel as usize] =
					self.state.factored.note_on_2[channel as usize];
				if (register & 0xF0) == YM3812_REG_KEY_BLOCK_FNUM_MSB {
					self.state.factored.note_on_2[channel as usize] =
						self.state.key_on[channel as usize]
				}

				if !self.state.key_on[channel as usize] {
					if self.state.factored.note_on_1[channel as usize]
						&& self.state.factored.note_1[channel as usize]
							!= 0.0
					{
						midi.note_off_write(
							channel.into(),
							self.state.factored.midi_note
								[channel as usize],
							0x00.into(),
						);
						self.state.factored.midi_note[channel as usize] =
							0xFF.into();
					}
				} else if self.state.key_on[channel as usize] {
					if self.state.factored.note_on_2[channel as usize]
						!= self.state.factored.note_on_1[channel as usize]
					{
						midi.do_note_on(
							self.state.factored.note_1
								[channel as usize],
							self.state.factored.note_2
								[channel as usize],
							channel.into(),
							&mut self.state.factored.midi_note
								[channel as usize],
							&mut self.state.factored.midi_wheel
								[channel as usize],
							Some(255),
							None,
						)?;
					} else {
						//if (Register & 0xF0) = YM3812_REG_KEY_BLOCK_FNum_MSB & _
						//self.state.factored.note_1[channel as usize] != self.state.factored.note_2[channel as usize] {
						midi.do_note_on(
							self.state.factored.note_1
								[channel as usize],
							self.state.factored.note_2
								[channel as usize],
							channel.into(),
							&mut self.state.factored.midi_note
								[channel as usize],
							&mut self.state.factored.midi_wheel
								[channel as usize],
							None,
							None,
						)?;
					}
				}
			},
			YM3812_REG_RHYTHM => {
				process_reg_rhythm(data, &mut self.state, self.config, midi)
			},
			YM3812_FB_CONNECTION_PAN..=YM3812_FB_CONNECTION_PAN_END => {
				process_fb_connection_pan(
					register,
					data,
					&mut self.state,
					self.config,
					midi,
				);
			},
			_ => {
				strict!("Error when processing ym3812: {}, {}", register, data);
			},
		}
		Ok(())
	}
}

impl Default for YM3812State {
	fn default() -> Self {
		YM3812State {
			key_on: [false; 8],
			block: [0; 8],
			volume: [0; 8],
			bd_vol: 0.into(),
			hh_vol: 0.into(),
			sd_vol: 0.into(),
			tom_vol: 0.into(),
			tct_vol: 0.into(),
			percussion_on: [false; 5],
			opl3_mode: 0,
			opl_type: 0,
			factored: Default::default(),
		}
	}
}

fn ksl_tl(me: &mut YM3812, register: u16, data: u8, midi: &mut MIDIShim) {
	//if (register & 0x7) > 0x5 {}
	let operation = (register & 0x7) / 0x3;
	let channel = (((register & 0x18) / 0x8 * 0x3) + ((register & 0x7) % 0x3)) as u8;
	if me.config.ym2413_ch_disabled[channel as usize] {
		return;
	}
	match (channel, operation) {
		(0x6, 0x1) =>
		//me.state.bd_vol = OPL_TL2Vol(Data & 0x3F)
		{
			me.state.bd_vol = db_to_midi_vol(ym3812_vol_to_db(data & 0x3F))
		},
		(0x7, 0x0) =>
		//me.state.hh_vol = OPL_TL2Vol(Data & 0x3F)
		{
			me.state.hh_vol = db_to_midi_vol(ym3812_vol_to_db(data & 0x3F))
		},
		(0x7, 0x1) =>
		//me.state.sd_vol = OPL_TL2Vol(Data & 0x3F)
		{
			me.state.sd_vol = db_to_midi_vol(ym3812_vol_to_db(data & 0x3F))
		},
		(0x8, 0x0) =>
		//me.state.tom_vol = OPL_TL2Vol(Data & 0x3F)
		{
			me.state.tom_vol = db_to_midi_vol(ym3812_vol_to_db(data & 0x3F))
		},
		(0x8, 0x1) =>
		//me.state.tct_vol = OPL_TL2Vol(Data & 0x3F)
		{
			me.state.tct_vol = db_to_midi_vol(ym3812_vol_to_db(data & 0x3F))
		},
		(_, _) => (),
	}
	if me.config.ym2413_vol_disabled[channel as usize] || operation == 0x0 {
		return;
	}
	let temp_byte = db_to_midi_vol(ym3812_vol_to_db(data & 0x3F));

	//TempByt = OPL_TL2Vol(Data & 0x3F)

	if me.state.volume[channel as usize] != temp_byte {
		me.state.volume[channel as usize] = temp_byte.into();
		midi.controller_write(
			channel.into(),
			MIDI_VOLUME,
			me.state.volume[channel as usize].into(),
		);
	}
}

fn process_fb_connection_pan(
	register: u16,
	data: u8,
	state: &mut YM3812State,
	config: &Config,
	midi: &mut MIDIShim,
) {
	let channel = register as u8 & 0xF;
	if config.ym2413_ch_disabled[channel as usize] {
		return;
	}
	if config.ym2413_vol_disabled[channel as usize] {
		return;
	}
	if state.opl_type != OPL_TYPE_YMF262 {
		return;
	}
	let temp_pan = match (state.opl3_mode, (data & 0x30) / 0x10) {
		(_, 0x0) => MIDI_PAN_CENTER, // should actually be complete silence
		(_, 0x1) => MIDI_PAN_LEFT,
		(_, 0x2) => MIDI_PAN_CENTER,
		(_, 0x3) => MIDI_PAN_RIGHT,
		(0x0, _) => MIDI_PAN_CENTER,
		(_, _) => MIDI_PAN_CENTER, //FIXME: this is likely incorrect
	};
	state.factored.midi_pan[channel as usize] = temp_pan;
	midi.controller_write(
		channel.into(),
		MIDI_PAN,
		state.factored.midi_pan[channel as usize],
	);
}

fn process_reg_rhythm(data: u8, state: &mut YM3812State, config: &Config, midi: &mut MIDIShim) {
	if config.ym2413_percussion_disabled {
		return;
	}
	let channel = CHN_DAC;

	toggle_drum(
		data,
		YM3812_REG_BD,
		&mut state.percussion_on[0],
		midi,
		channel,
		0x23.into(), //FIXME: Magic numbers
		state.bd_vol,
	);
	toggle_drum(
		data,
		YM3812_REG_SD,
		&mut state.percussion_on[1],
		midi,
		channel,
		0x26.into(),
		state.sd_vol,
	);
	toggle_drum(
		data,
		YM3812_REG_TOM,
		&mut state.percussion_on[2],
		midi,
		channel,
		0x2D.into(),
		state.tom_vol,
	);
	toggle_drum(
		data,
		YM3812_REG_TCT,
		&mut state.percussion_on[3],
		midi,
		channel,
		0x33.into(),
		state.tct_vol,
	);
	toggle_drum(
		data,
		YM3812_REG_HH,
		&mut state.percussion_on[4],
		midi,
		channel,
		0x2A.into(),
		state.hh_vol,
	);
}

pub(crate) fn toggle_drum(
	data: u8,
	reg: u8,
	percussion_on: &mut bool,
	midi: &mut MIDIShim,
	channel: u4,
	key: u7,
	drum_vol: u7,
) {
	if (data & reg) == 0 {
		if *percussion_on {
			midi.note_off_write(channel, key, 0x00.into());
			*percussion_on = false;
		}
	} else if !*percussion_on {
 			//midi.event_write(Midi { channel: CH, message: NoteOff { key: 35, vel: 0x0 }});
 			midi.note_on_write(channel, key, drum_vol);
 			*percussion_on = true;
 		}
}

fn process_mod_vib_eg_ks_mult(register: u16, data: u8, config: &Config, midi: &mut MIDIShim) {
	if (register & 0x7) > 0x5 {
		return;
	}

	let operation = (register & 0x7) / 0x3;
	let channel = ((register & 0x18) / 0x8 * 0x3 + (register & 0x7) % 0x3) as u8;
	let temp_byte = (data & 0xFE) / 0x2;

	if config.ym2413_ch_disabled[channel as usize]
		|| config.ym2413_vol_disabled[channel as usize]
		|| operation == 0x00
	{
		return;
	}

	midi.program_change_write(channel.into(), temp_byte.into());
}

pub(crate) fn ym3812_vol_to_db(tl: u8) -> f64 {
	-f64::from(tl) * 4.0 / 3.0
}