use midly::num::u7;
use crate::config::Config;
use crate::midi_shim::{
	MIDIShim, MIDI_DATA_ENTRY_MSB, MIDI_MODULATOR_WHEEL, MIDI_NRPN_LSB, MIDI_NRPN_MSB,
	MIDI_PAN, MIDI_VOLUME, NRPN_DRUM_PAN, NRPN_DRUM_PITCH_COARSE,
};
use crate::strict;
use crate::utils::FactoredState;
use crate::vgm2mid::CHN_DAC;
use crate::ym3812::YM3812;
use anyhow::{anyhow, Result};
use midly::num::u4;
#[allow(dead_code)]
const YM278_REG_LSI_TEST: u8 = 0x00;
#[allow(dead_code)]
const YM278_REG_MEMORY_ACCESS_MODE: u8 = 0x02;
#[allow(dead_code)]
const YM278_REG_MEMORY_TYPE: u8 = 0x02;
#[allow(dead_code)]
const YM278_REG_WAVE_TABLE_HEADER: u8 = 0x02;
struct Opl4Tone {
	instrument: u8,
	pitch: u16,
	#[allow(dead_code)]
	key_scl: u8,
}
const XG_MODE: bool = false;
static OPL4_TONES: Vec<Opl4Tone> = Vec::new();
fn note_ymf278(tone: u16, fnum: u32, oct: i16) -> f64 {
	if oct == -8 && fnum == 0 {
		return 0xFF.into();
	}
						
	let mut pitch = (1.0 + fnum as f64 / 1024.0).log2();
	let octave = oct + 8;
		pitch -= OPL4_TONES[tone as usize].pitch as f64 / 0x600 as f64;
			
			
	(octave as f64 * 12.0) + pitch * 12.0
}
fn channel_select(snum: u8, tone_ins: u8) -> (u4, bool) {
	let mut select_channel: u4;
	let drum_mode: bool;
		if XG_MODE {
		select_channel = (snum & 0xF).into();
		drum_mode = false;
	} else if tone_ins & 0x80 == 0 {
 			select_channel = (snum & 0xF).into();
 			if select_channel == CHN_DAC {
 				select_channel = 0xF.into();
 			}
 			 			drum_mode = false;
 		} else {
 			select_channel = CHN_DAC;
 			drum_mode = true;
 		}
	(select_channel, drum_mode)
}
pub(crate) struct YMF278State {
	tone_wave: [u16; 0x18],
	tone_ins: [u8; 0x18],
	oct: [i16; 0x18],
	fnum_o: [u32; 0x18],
	fnum_n: [u32; 0x18],
	note_o: [f64; 0x18],
	note_n: [f64; 0x18],
	note_on_o: [u8; 0x18],
	note_on_n: [u8; 0x18],
	slot_volume: [u8; 0x18],
	slot_pan: [u8; 0x18],
	damper_on_o: [u8; 0x18],
	damper_on_n: [u8; 0x18],
	vib: [u8; 0x18],
	drum_pan: [[u8; 0x80]; 0x10],
	drum_pitch: [[u8; 0x80]; 0x10],
	drum_nrpn_m: [u7; 0x10],
	drum_nrpn_l: [u8; 0x10],
	opl4_tones: Vec<Opl4Tone>,
	factored: FactoredState,
}
pub(crate) struct YMF278<'config> {
	pub state: YMF278State,
	config: &'config Config,
}
impl<'config> YMF278<'config> {
	pub(crate) fn new<'c: 'config>(
		config: &'c Config,
		opt_state: Option<YMF278State>,
	) -> YMF278 {
		YMF278 {
			state: opt_state.unwrap_or_default(),
			config,
		}
	}
	pub(crate) fn command_handle(
		&mut self,
		port: u8,
		register: u8,
		data: u8,
		ym3812: &mut YM3812,
		fsam_3812: f64,
		midi: &mut MIDIShim,
	) -> Result<()> {
		if port < 0x2 {
			ym3812.command_handle(
				(port as u16) << 8 | register as u16,
				data,
				fsam_3812,
				midi,
			)?;
			if port == 0x1 && register == 0x5 {
				self.state = Default::default();
			}
			return Ok(());
		}
		if (0x08..=0xF7).contains(®ister) {
			let slot_number = (register - 0x8) % 24;
			let (channel, drum_mode) = channel_select(
				slot_number,
				self.state.tone_ins[slot_number as usize],
			);
						match (register - 0x8) / 24 {
				0x0 => {
					wave_table_number(
						&mut self.state,
						slot_number,
						data,
						channel.into(),
						midi,
					)?;
				},
				0x1 => {
					f_number(&mut self.state, slot_number, data);
				},
				0x2 => {
					octave(
						&mut self.state,
						slot_number,
						data,
						channel.into(),
						midi,
					)?;
				},
				0x3 => {
					total_level(
						&mut self.state,
						slot_number,
						data,
						drum_mode,
						channel.into(),
						midi,
					);
				},
				0x4 => {
					key_on(
						data,
						&mut self.state,
						slot_number,
						drum_mode,
						channel.into(),
						midi,
					)?;
				},
				0x5 => {
					lfo(
						&mut self.state,
						slot_number,
						data,
						drum_mode,
						channel.into(),
						midi,
					);
				},
				0x6 => {
					ar(
						&mut self.state,
						slot_number,
						data,
						channel.into(),
						midi,
					)?;
				},
				0x7 => (), 												0x8 => (), 												0x9 => (), 								_ => strict!(),
			}
		} else {
						match register {
				0x0 => (), 				0x1 => (),
				0x2 => (),
												0x3 => (),
								0x4 => (),
								0x5 => (),
								0x6 => (), 												0xF8 => (),
												0xF9 => (),
												_ => (),
			}
		}
		
		Ok(())
	}
	pub(crate) fn load_opl4_instrument_set(&mut self) -> Result<()> {
		const INSSET_SIG: &[u8] = b"INSSET";
		const COUNT_PTR: usize = INSSET_SIG.len(); 
								let bytes = std::fs::read("yrw801.ins").map_err(|err| anyhow!(err))?;
		if bytes.len() <= INSSET_SIG.len() {
			strict!("OPL4 Instrument file is too short!");
		}
		if &bytes[0..INSSET_SIG.len()] != INSSET_SIG {
			strict!("Invalid OPL4 instrument set");
		}
		let tone_count = bytes[COUNT_PTR];
		for tone in 0..tone_count {
			let instrument = bytes[COUNT_PTR + 1 + (tone * 4) as usize];
			let pitch = u16::from_le_bytes([
				bytes[COUNT_PTR + 1 + (1 + tone * 4) as usize],
				bytes[COUNT_PTR + 1 + (2 + tone * 4) as usize],
			]);
			let key_scl = bytes[COUNT_PTR + 1 + (3 + tone * 4) as usize];
			self.state.opl4_tones.push(Opl4Tone {
				instrument,
				pitch,
				key_scl,
			});
		}
		Ok(())
	}
}
impl Default for YMF278State {
	fn default() -> Self {
		YMF278State {
			tone_wave: [0; 0x18],
			tone_ins: [0; 0x18],
			oct: [0; 0x18],
			fnum_o: [0; 0x18],
			fnum_n: [0; 0x18],
			note_o: [0xFF.into(); 0x18],
			note_n: [0xFF.into(); 0x18],
			note_on_o: [0; 0x18],
			note_on_n: [0; 0x18],
			slot_volume: [0; 0x18],
			slot_pan: [0; 0x18],
			damper_on_o: [0; 0x18],
			damper_on_n: [0; 0x18],
			vib: [0; 0x18],
			drum_pan: [[0xFF; 0x80]; 0x10],
			drum_pitch: [[0xFF; 0x80]; 0x10],
			drum_nrpn_m: [0xFF.into(); 0x10],
			drum_nrpn_l: [0xFF; 0x10],
			opl4_tones: Vec::new(),
			factored: Default::default(),
		}
	}
}
fn ar(
	state: &mut YMF278State,
	slot_number: u8,
	data: u8,
	channel: u8,
	midi: &mut MIDIShim,
) -> Result<()> {
				if state.tone_ins[slot_number as usize] == 0x77 {
				let temp_byte = data / 0x10;
		if temp_byte >= 0xE {
						if state.note_on_o[slot_number as usize] != 0 {
				midi.do_note_on(
					state.note_o[slot_number as usize],
					0xFF.into(),
					channel.into(),
					&mut state.factored.midi_note[slot_number as usize],
					&mut state.factored.midi_wheel[slot_number as usize],
					Some(0xFF),
					Some(temp_byte.into()),
				)?;
				state.note_on_o[slot_number as usize] = 0;
			}
			state.tone_wave[slot_number as usize] = 0x180;
			state.tone_ins[slot_number as usize] = OPL4_TONES
				[state.tone_wave[slot_number as usize] as usize]
				.instrument;
			let (channel, _drum_mode) =
				channel_select(slot_number, state.tone_ins[slot_number as usize]);
			let channel_ptr = channel.as_int() as usize;
			if state.tone_ins[slot_number as usize] & 0x80 != 0 {
				let temp_byte = state.tone_ins[slot_number as usize] & 0x7F;
				let old_val = state.drum_pitch[channel_ptr][temp_byte as usize];
				state.drum_pitch[channel_ptr][temp_byte as usize] =
					state.note_n[slot_number as usize] as u8; 				if state.drum_pitch[channel_ptr][temp_byte as usize] != old_val {
					if state.drum_nrpn_m[channel_ptr] != NRPN_DRUM_PITCH_COARSE
						|| state.drum_nrpn_l[channel_ptr] != temp_byte
					{
						state.drum_nrpn_m[channel_ptr] =
							NRPN_DRUM_PITCH_COARSE;
						state.drum_nrpn_l[channel_ptr] = temp_byte;
						midi.controller_write(
							channel,
							MIDI_NRPN_MSB,
							state.drum_nrpn_m[channel_ptr],
						);
						midi.controller_write(
							channel,
							MIDI_NRPN_LSB,
							state.drum_nrpn_l[channel_ptr].into(),
						);
					}
					midi.controller_write(
						channel,
						MIDI_DATA_ENTRY_MSB,
						state.drum_pitch[channel_ptr][temp_byte as usize]
							.into(),
					);
				}
				state.note_n[slot_number as usize] =
					(state.tone_ins[slot_number as usize] & 0x7F).into();
			}
		}
	}
	Ok(())
}
fn lfo(
	state: &mut YMF278State,
	slot_number: u8,
	data: u8,
	drum_mode: bool,
	channel: u8,
	midi: &mut MIDIShim,
) {
		let old_val = state.vib[slot_number as usize];
	state.vib[slot_number as usize] = (data & 0x7) * 0x10;
	if !drum_mode && state.vib[slot_number as usize] != old_val {
		midi.controller_write(
			channel.into(),
			MIDI_MODULATOR_WHEEL,
			state.vib[slot_number as usize].into(),
		);
	}
	}
fn key_on(
	data: u8,
	state: &mut YMF278State,
	slot_number: u8,
	drum_mode: bool,
	channel: u8,
	midi: &mut MIDIShim,
) -> Result<()> {
		if data & 0x80 != 0 {
		let temp_byte = data & 0xF;
		state.slot_pan[slot_number as usize] = match temp_byte.cmp(&0x08) {
			std::cmp::Ordering::Less => 0x40 - temp_byte / 0x7 * 0x40,
			std::cmp::Ordering::Equal => 0x40,
			std::cmp::Ordering::Greater => {
				let temp = 0x40 + (0x10 - temp_byte) / 0x7 * 0x40;
				if temp == 0x80 {
					0x7F
				} else {
					temp
				}
			},
		};
		if !drum_mode {
			let old_val = state.factored.midi_pan[channel as usize];
			state.factored.midi_pan[channel as usize] =
				state.slot_pan[slot_number as usize].into();
			if state.factored.midi_pan[channel as usize] != old_val {
				midi.controller_write(
					channel.into(),
					MIDI_PAN,
					state.factored.midi_pan[channel as usize],
				);
			}
		} else {
			let temp_byte = state.tone_ins[slot_number as usize] & 0x7F;
			let old_val = state.drum_pan[channel as usize][temp_byte as usize];
			state.drum_pan[channel as usize][temp_byte as usize] =
				state.slot_pan[slot_number as usize];
			if state.drum_pan[channel as usize][temp_byte as usize] != old_val {
				if state.drum_nrpn_m[channel as usize] != NRPN_DRUM_PAN
					|| state.drum_nrpn_l[channel as usize] != temp_byte
				{
					state.drum_nrpn_m[channel as usize] = NRPN_DRUM_PAN;
					state.drum_nrpn_l[channel as usize] = temp_byte;
					midi.controller_write(
						channel.into(),
						MIDI_NRPN_MSB,
						state.drum_nrpn_m[channel as usize],
					);
					midi.controller_write(
						channel.into(),
						MIDI_NRPN_LSB,
						state.drum_nrpn_l[channel as usize].into(),
					);
				}
				midi.controller_write(
					channel.into(),
					MIDI_DATA_ENTRY_MSB,
					state.drum_pan[channel as usize][temp_byte as usize].into(),
				);
			}
		}
	}
	state.damper_on_n[slot_number as usize] = (data & 0x40) / 0x40;
	state.note_on_n[slot_number as usize] = (data & 0x80) / 0x80;
	if state.damper_on_n[slot_number as usize] != state.damper_on_o[slot_number as usize] {
						state.damper_on_o[slot_number as usize] = state.damper_on_n[slot_number as usize]
	}
	if state.note_on_n[slot_number as usize] != state.note_on_o[slot_number as usize] {
		let temp_byte = if !drum_mode {
			0x7F
		} else {
			state.slot_volume[slot_number as usize]
		};
		if state.note_on_n[slot_number as usize] != 0 {
			midi.do_note_on(
				state.note_o[slot_number as usize],
				state.note_n[slot_number as usize],
				channel.into(),
				&mut state.factored.midi_note[slot_number as usize],
				&mut state.factored.midi_wheel[slot_number as usize],
				Some(0xFF),
				Some(temp_byte.into()),
			)?;
		} else {
			midi.do_note_on(
				state.note_o[slot_number as usize],
				0xFF.into(),
				channel.into(),
				&mut state.factored.midi_note[slot_number as usize],
				&mut state.factored.midi_wheel[slot_number as usize],
				Some(0xFF),
				Some(temp_byte.into()),
			)?;
		}
		state.note_on_o[slot_number as usize] = state.note_on_n[slot_number as usize];
	}
	Ok(())
}
fn total_level(
	state: &mut YMF278State,
	slot_number: u8,
	data: u8,
	drum_mode: bool,
	channel: u8,
	midi: &mut MIDIShim,
) {
		let old_val = state.slot_volume[slot_number as usize];
	state.slot_volume[slot_number as usize] = 0x7F - data / 0x2;
	if state.slot_volume[slot_number as usize] == 0x0
		&& state.note_on_n[slot_number as usize] == 0
	{
		state.slot_volume[slot_number as usize] = old_val;
	}
	
					
	if !drum_mode {
		let old_val = state.factored.midi_volume[channel as usize];
		state.factored.midi_volume[channel as usize] =
			state.slot_volume[slot_number as usize].into();
		if state.factored.midi_volume[channel as usize] != old_val {
			midi.controller_write(
				channel.into(),
				MIDI_VOLUME,
				state.factored.midi_volume[channel as usize],
			);
		}
	}
}
fn octave(
	state: &mut YMF278State,
	slot_number: u8,
	data: u8,
	channel: u8,
	midi: &mut MIDIShim,
) -> Result<()> {
			state.factored.fnum_msb[slot_number as usize] = data & 0x7;
			state.oct[slot_number as usize] = ((data & 0xF0) / 0x10).into();
		if state.oct[slot_number as usize] & 0x8 != 0 {
		state.oct[slot_number as usize] |= -8;
	}
	state.fnum_o[slot_number as usize] = state.fnum_n[slot_number as usize];
	state.fnum_n[slot_number as usize] = ((state.factored.fnum_msb[slot_number as usize]
		* 0x80) | state.factored.fnum_lsb
		[slot_number as usize])
		.into();
	state.note_o[slot_number as usize] = state.note_n[slot_number as usize];
	state.note_n[slot_number as usize] = note_ymf278(
		state.tone_wave[slot_number as usize],
		state.fnum_n[slot_number as usize],
		state.oct[slot_number as usize],
	);
	if state.tone_ins[slot_number as usize] & 0x80 != 0 {
		let temp_byte = state.tone_ins[slot_number as usize] & 0x7F;
		let old_val = state.drum_pitch[channel as usize][temp_byte as usize];
		state.drum_pitch[channel as usize][temp_byte as usize] =
			state.note_n[slot_number as usize] as u8; 		if state.drum_pitch[channel as usize][temp_byte as usize] != old_val {
			if state.drum_nrpn_m[channel as usize] != NRPN_DRUM_PITCH_COARSE
				|| state.drum_nrpn_l[channel as usize] != temp_byte
			{
				state.drum_nrpn_m[channel as usize] = NRPN_DRUM_PITCH_COARSE;
				state.drum_nrpn_l[channel as usize] = temp_byte;
				midi.controller_write(
					channel.into(),
					MIDI_NRPN_MSB,
					state.drum_nrpn_m[channel as usize],
				);
				midi.controller_write(
					channel.into(),
					MIDI_NRPN_LSB,
					state.drum_nrpn_l[channel as usize].into(),
				);
			}
			midi.controller_write(
				channel.into(),
				MIDI_DATA_ENTRY_MSB,
				state.drum_pitch[channel as usize][temp_byte as usize].into(),
			);
		}
		state.note_n[slot_number as usize] =
			(state.tone_ins[slot_number as usize] & 0x7F).into();
	}
	if state.note_on_n[slot_number as usize] != 0 {
		midi.do_note_on(
			state.note_o[slot_number as usize],
			state.note_n[slot_number as usize],
			channel.into(),
			&mut state.factored.midi_note[slot_number as usize],
			&mut state.factored.midi_wheel[slot_number as usize],
			None,
			None,
		)?;
	}
	Ok(())
}
fn f_number(state: &mut YMF278State, slot_number: u8, data: u8) {
			state.tone_wave[slot_number as usize] =
		(state.tone_wave[slot_number as usize] & 0xFF) | ((data as u16 & 0x1) * 0x100);
		state.factored.fnum_lsb[slot_number as usize] = data / 0x2;
	}
fn wave_table_number(
	state: &mut YMF278State,
	slot_number: u8,
	data: u8,
	channel: u8,
	midi: &mut MIDIShim,
) -> Result<()> {
					state.tone_wave[slot_number as usize] =
		(state.tone_wave[slot_number as usize] & 0x100) | data as u16;
	state.tone_ins[slot_number as usize] =
		OPL4_TONES[state.tone_wave[slot_number as usize] as usize].instrument;
	let old_val = channel;
	let (channel, _drum_mode) =
		channel_select(slot_number, state.tone_ins[slot_number as usize]);
	let channel_ptr = channel.as_int() as usize;
	if channel != old_val & state.note_on_o[slot_number as usize] {
		midi.do_note_on(
			state.note_o[slot_number as usize],
			0xFF.into(),
			channel,
			&mut state.factored.midi_note[slot_number as usize],
			&mut state.factored.midi_wheel[slot_number as usize],
			Some(0xFF),
			Some(0x00.into()), 		)?;
		state.note_on_o[slot_number as usize] = 0
	}
	let old_instrument = state.factored.midi_instrument[channel_ptr];
	state.factored.midi_instrument[channel_ptr] =
		if state.tone_ins[slot_number as usize] & 0x80 != 0 {
			0x80.into()
		} else {
			(state.tone_ins[slot_number as usize] & 0x7F).into()
		};
	if state.factored.midi_instrument[channel_ptr] != old_instrument {
		if XG_MODE {
			if (state.factored.midi_instrument[channel_ptr].as_int() & 0x80)
				!= (old_instrument.as_int() & 0x80)
			{
				let temp_byte = if state.factored.midi_instrument[channel_ptr]
					.as_int() & 0x80 != 0
				{
					0x7F
				} else {
					0x0
				};
				midi.controller_write(channel, 0x00.into(), temp_byte.into());
			}
			if (state.factored.midi_instrument[channel_ptr].as_int() & 0x80)
				& (old_instrument.as_int() & 0x80)
				== 0
			{
				midi.program_change_write(
					channel,
					state.factored.midi_instrument[channel_ptr],
				);
			}
		} else if state.factored.midi_instrument[channel_ptr].as_int() & 0x80 == 0 {
  				midi.program_change_write(
  					channel,
  					state.factored.midi_instrument[channel_ptr],
  				);
  			}
			}
																									Ok(())
}