use std::{collections::HashSet, error::Error, cmp::{min, max}};
use linked_hash_set::LinkedHashSet;
use crate::reskit::{soundtrack::types::{PatternRow, Note, Channel, OctaveFrequency, Effect, Instrument, NoiseType, DcsgChannelMode, InstrumentType, CombinedAssets, Sample}, utility::print_warning};

// https://github.com/sikthehedgehog/Echo/blob/master/doc/esf.txt
const ESF_NOTE_ON: u8 = 0x00;
const ESF_NOTE_OFF: u8 = 0x10;
const ESF_SET_VOLUME: u8 = 0x20;
const ESF_SET_FREQUENCY: u8 = 0x30;
const ESF_SET_INSTRUMENT: u8 = 0x40;
const ESF_DELAY_SHORT: u8 = 0xD0;
const ESF_SFX_LOCK: u8 = 0xE0;
const ESF_SET_FM_PARAMS: u8 = 0xF0;
const ESF_SET_FM_DIRECT_0: u8 = 0xF8;
const ESF_SET_FM_DIRECT_1: u8 = 0xF9;
const ESF_SET_FLAGS: u8 = 0xFA;
const ESF_CLEAR_FLAGS: u8 = 0xFB;
pub const ESF_GO_TO_LOOP: u8 = 0xFC;
pub const ESF_SET_LOOP: u8 = 0xFD;
const ESF_DELAY_LONG: u8 = 0xFE;
pub const ESF_STOP: u8 = 0xFF;

pub const ESF_FM_1: u8 = 0x00;
pub const ESF_FM_2: u8 = 0x01;
pub const ESF_FM_3: u8 = 0x02;
pub const ESF_FM_4: u8 = 0x04;
pub const ESF_FM_5: u8 = 0x05;
pub const ESF_FM_6: u8 = 0x06;

pub const ESF_PSG_1: u8 = 0x08;
pub const ESF_PSG_2: u8 = 0x09;
pub const ESF_PSG_3: u8 = 0x0A;
pub const ESF_PSG_4: u8 = 0x0B;

pub const ESF_NOISE_PERIODIC_HIGH: u8   = 0x00;
pub const ESF_NOISE_PERIODIC_MED: u8    = 0x01;
pub const ESF_NOISE_PERIODIC_LOW: u8    = 0x02;
pub const ESF_NOISE_PERIODIC_PSG_3: u8  = 0x03;
pub const ESF_NOISE_WHITE_HIGH: u8      = 0x04;
pub const ESF_NOISE_WHITE_MED: u8       = 0x05;
pub const ESF_NOISE_WHITE_LOW: u8       = 0x06;
pub const ESF_NOISE_WHITE_PSG_3: u8     = 0x07;

pub const ESF_FM_6_PCM: u8 = 0x0C;

const ESF_FM_PARAM_LEFT_SPEAKER: u8 = 0x80;
const ESF_FM_PARAM_RIGHT_SPEAKER: u8 = 0x40;

const ESF_SEMITONE_C: u8        = 0;
const ESF_SEMITONE_C_SHARP: u8  = 1;
const ESF_SEMITONE_D: u8        = 2;
const ESF_SEMITONE_D_SHARP: u8  = 3;
const ESF_SEMITONE_E: u8        = 4;
const ESF_SEMITONE_F: u8        = 5;
const ESF_SEMITONE_F_SHARP: u8  = 6;
const ESF_SEMITONE_G: u8        = 7;
const ESF_SEMITONE_G_SHARP: u8  = 8;
const ESF_SEMITONE_A: u8        = 9;
const ESF_SEMITONE_A_SHARP: u8  = 10;
const ESF_SEMITONE_B: u8        = 11;

struct InstrumentSet<'a> {
    fm_ids: HashSet<u16>,
    psg_ids: HashSet<u16>,
    module_instruments: &'a Vec<Instrument>,
    total_instruments: &'a Vec<Instrument>,
    module_samples: &'a Vec<Sample>,
    total_samples: &'a Vec<Sample>
}

pub type EchoEvent = Vec<u8>;

pub trait EchoFormat {

    fn get_esf( &self, combined_assets: &CombinedAssets ) -> Result<Vec<u8>, Box<dyn Error>>;

}

pub trait EchoArtifact {

    fn to_bytes( &self ) -> Result<Vec<u8>, Box<dyn Error>>;

}

fn get_semitone( note: &Note ) -> Result<u8, Box<dyn Error>> {
    Ok( match note {
        Note::NoteOff => return Err( "internal error: attempted to get semitone for a Note::NoteOff" )?,
        Note::C(_) => ESF_SEMITONE_C,
        Note::CSharp(_) => ESF_SEMITONE_C_SHARP,
        Note::D(_) => ESF_SEMITONE_D,
        Note::DSharp(_) => ESF_SEMITONE_D_SHARP,
        Note::E(_) => ESF_SEMITONE_E,
        Note::F(_) => ESF_SEMITONE_F,
        Note::FSharp(_) => ESF_SEMITONE_F_SHARP,
        Note::G(_) => ESF_SEMITONE_G,
        Note::GSharp(_) => ESF_SEMITONE_G_SHARP,
        Note::A(_) => ESF_SEMITONE_A,
        Note::ASharp(_) => ESF_SEMITONE_A_SHARP,
        Note::B(_) => ESF_SEMITONE_B
    } )
}

/**
 * Notes are used to index PCM sample playback. C = 1, etc.
 */
fn get_pcm_index( note: &Note ) -> Result<u8, Box<dyn Error>> {
    Ok( match note {
        Note::NoteOff => return Err( "internal error: attempted to get pcm index for a Note::NoteOff" )?,
        Note::C(_) => 0,
        Note::CSharp(_) => 1,
        Note::D(_) => 2,
        Note::DSharp(_) => 3,
        Note::E(_) => 4,
        Note::F(_) => 5,
        Note::FSharp(_) => 6,
        Note::G(_) => 7,
        Note::GSharp(_) => 8,
        Note::A(_) => 9,
        Note::ASharp(_) => 10,
        Note::B(_) => 11
    } )
}

/**
  Gets the semitone frequency used by Echo.
  https://github.com/sikthehedgehog/Echo/blob/master/doc/esf.txt#L243
    C  - 644 | E  - 810 | G# - 1021
    C# - 681 | F  - 858 | A  - 1081
    D  - 722 | F# - 910 | A# - 1146
    D# - 765 | G  - 964 | B  - 1214
*/
fn get_fm_semitone_frequency( semitone: u8 ) -> Result<u16, Box<dyn Error>> {
    Ok( match semitone {
        ESF_SEMITONE_C => 644,
        ESF_SEMITONE_C_SHARP => 681,
        ESF_SEMITONE_D => 722,
        ESF_SEMITONE_D_SHARP => 765,
        ESF_SEMITONE_E => 810,
        ESF_SEMITONE_F => 858,
        ESF_SEMITONE_F_SHARP => 910,
        ESF_SEMITONE_G => 964,
        ESF_SEMITONE_G_SHARP => 1021,
        ESF_SEMITONE_A => 1081,
        ESF_SEMITONE_A_SHARP => 1146,
        ESF_SEMITONE_B => 1214,
        _ => return Err( "internal error: invalid semitone value provided to get_fm_semitone_frequency" )?
    } )
}

/**
 * Gets the semitone frequency used by Echo (PSG).
   https://github.com/sikthehedgehog/Echo/blob/master/doc/esf.txt#L273
       |Oct.0|Oct.1|Oct.2|Oct.3|Oct.4|Oct.5
    ---|-----|-----|-----|-----|-----|-----
    C  | 851 | 425 | 212 | 106 |  53 |  26
    C# | 803 | 401 | 200 | 100 |  50 |  25
    D  | 758 | 379 | 189 |  94 |  47 |  23
    D# | 715 | 357 | 178 |  89 |  44 |  22
    E  | 675 | 337 | 168 |  84 |  42 |  21
    F  | 637 | 318 | 159 |  79 |  39 |  19
    F# | 601 | 300 | 150 |  75 |  37 |  18
    G  | 568 | 284 | 142 |  71 |  35 |  17
    G# | 536 | 268 | 134 |  67 |  33 |  16
    A  | 506 | 253 | 126 |  63 |  31 |  15
    A# | 477 | 238 | 119 |  59 |  29 |  14
    B  | 450 | 225 | 112 |  56 |  28 |  14
 */
fn get_psg_semitone_frequency( octave: u8, semitone: u8 ) -> Result<u16, Box<dyn Error>> {
    Ok( match semitone {
        ESF_SEMITONE_C => match octave { 0 => 851, 1 => 425, 2 => 212, 3 => 106, 4 => 53, 5 | _ => 26 },
        ESF_SEMITONE_C_SHARP => match octave { 0 => 803, 1 => 401, 2 => 200, 3 => 100, 4 => 50, 5 | _ => 25 },
        ESF_SEMITONE_D => match octave { 0 => 758, 1 => 379, 2 => 189, 3 => 94, 4 => 47, 5 | _ => 23 },
        ESF_SEMITONE_D_SHARP => match octave { 0 => 715, 1 => 357, 2 => 178, 3 => 89, 4 => 44, 5 | _ => 22 },
        ESF_SEMITONE_E => match octave { 0 => 675, 1 => 337, 2 => 168, 3 => 84, 4 => 42, 5 | _ => 21 },
        ESF_SEMITONE_F => match octave { 0 => 637, 1 => 318, 2 => 159, 3 => 79, 4 => 39, 5 | _ => 19 },
        ESF_SEMITONE_F_SHARP => match octave { 0 => 601, 1 => 300, 2 => 150, 3 => 75, 4 => 37, 5 | _ => 18 },
        ESF_SEMITONE_G => match octave { 0 => 568, 1 => 284, 2 => 142, 3 => 71, 4 => 35, 5 | _ => 17 },
        ESF_SEMITONE_G_SHARP => match octave { 0 => 536, 1 => 268, 2 => 134, 3 => 67, 4 => 33, 5 | _ => 16 },
        ESF_SEMITONE_A => match octave { 0 => 506, 1 => 253, 2 => 126, 3 => 63, 4 => 31, 5 | _ => 15 },
        ESF_SEMITONE_A_SHARP => match octave { 0 => 477, 1 => 238, 2 => 119, 3 => 59, 4 => 29, 5 | _ => 14 },
        ESF_SEMITONE_B => match octave { 0 => 450, 1 => 225, 2 => 112, 3 => 56, 4 => 28, 5 | _ => 14 },
        _ => return Err( "internal error: invalid semitone value provided to get_psg_semitone_frequency" )?
    } )
}

/**
 * Convenience method to get the octave of a given PSG frequency
 */
fn get_psg_octave_from_frequency( frequency: i16 ) -> u8 {
    if frequency > 425 {
        0
    } else if frequency > 212 && frequency <= 425 {
        1
    } else if frequency > 106 && frequency <= 212 {
        2
    } else if frequency > 53 && frequency <= 106 {
        3
    } else if frequency > 26 && frequency <= 53 {
        4
    } else {
        5
    }
}

/**
 * Get the setting for the noise generator based on the active noise mode effect.
 */
fn get_noise_generator_setting( note: &Note, noise_mode: Option<&Effect> ) -> Result<Option<u8>, Box<dyn Error>> {
    Ok(
        if let Some( noise_mode ) = noise_mode {
            if let Effect::DcsgNoiseMode { mode, noise_type } = noise_mode {
                match noise_type {
                    NoiseType::PeriodicNoise => match mode {
                        DcsgChannelMode::Ch3Frequency => Some( ESF_NOISE_PERIODIC_PSG_3 ),
                        DcsgChannelMode::FixedFrequency => match note {
                            Note::NoteOff => return Err( "internal error: attempted to get noise generator setting for NoteOff" )?,
                            Note::C(_) => Some( ESF_NOISE_PERIODIC_LOW ),
                            Note::CSharp(_) => Some( ESF_NOISE_PERIODIC_MED ),
                            Note::D(_) => Some( ESF_NOISE_PERIODIC_HIGH ),
                            _ => None
                        }
                    },
                    NoiseType::WhiteNoise => match mode {
                        DcsgChannelMode::Ch3Frequency => Some( ESF_NOISE_WHITE_PSG_3 ),
                        DcsgChannelMode::FixedFrequency => match note {
                            Note::NoteOff => return Err( "internal error: attempted to get noise generator setting for NoteOff" )?,
                            Note::C(_) => Some( ESF_NOISE_WHITE_LOW ),
                            Note::CSharp(_) => Some( ESF_NOISE_WHITE_MED ),
                            Note::D(_) => Some( ESF_NOISE_WHITE_HIGH ),
                            _ => None
                        }
                    }
                }
            } else {
                return Err( "internal error: non-DcsgNoiseMode effect passed to get_noise_generator_setting" )?
            }
        } else {
            match note {
                Note::NoteOff => return Err( "internal error: attempted to get noise generator setting for NoteOff" )?,
                _ => Some( ESF_NOISE_WHITE_PSG_3 )
            }
        }
    )
}

fn get_fm_note_byte( octave: u8, semitone: u8 ) -> u8 {
    32 * octave + 2 * semitone + 1
}

fn get_psg_note_byte( octave: u8, semitone: u8 ) -> u8 {
    24 * octave + 2 * semitone
}

fn get_note( note: &Note, channel: u8 ) -> Result<u8, Box<dyn Error>> {
    Ok( match channel {
        ESF_FM_1..=ESF_FM_3 | ESF_FM_4..=ESF_FM_6 => get_fm_note_byte( note.get_octave()?, get_semitone( note )? ),
        ESF_PSG_1..=ESF_PSG_4 => get_psg_note_byte( note.get_octave()?, get_semitone( note )? ),
        invalid_channel => return Err( format!( "internal error: get_note: invalid channel {:#04X}", invalid_channel ) )?
    } )
}

fn get_volume( channel: u8, volume: u8 ) -> Result<u8, Box<dyn Error>> {
    Ok( match channel {
        ESF_FM_1..=ESF_FM_3 | ESF_FM_4..=ESF_FM_6 => 0x7F - volume,
        ESF_PSG_1..=ESF_PSG_4 => 0x0F - volume,
        invalid_channel => return Err( format!( "internal error: get_note: invalid channel {:#04X}", invalid_channel ) )?
    } )
}

/**
 * Generate an echo delay event. "delay" is the amount to wait, in 1/60 of a second ticks.
 */
fn get_delay( delay: u8 ) -> Result<EchoEvent, Box<dyn Error>> {
    let events: EchoEvent = if delay == 0 {
        // No delay to generate! Probably should be an error
        vec![]
    } else if delay <= 16 {
        vec![ ESF_DELAY_SHORT | ( delay - 1 ) ]
    } else {
        vec![ ESF_DELAY_LONG, delay ]
    };

    Ok( events )
}

/**
 * For a specific row and channel, get the events this row and channel contribute to the stream.
 * While doing so, update the state of the channel for future event generation.
 */
fn get_events_for_channel( channels: &mut [Channel], active_channel: usize, row: &PatternRow, asset_set: &InstrumentSet ) -> Result<Vec<EchoEvent>, Box<dyn Error>> {
    let mut events: Vec<EchoEvent> = Vec::new();

    // Adjust volume
    if let Some( volume ) = &row.volume {
        // https://github.com/sikthehedgehog/Echo/blob/master/doc/esf.txt#L206-L207
        let volume = get_volume( channels[ active_channel ].id, *volume )?;
        channels[ active_channel ].current_volume = Some( volume );
        events.push( vec![ ESF_SET_VOLUME | channels[ active_channel ].id, volume ] );
    }

    // Set the instrument
    if let Some( instrument ) = &row.instrument_index {
        // Hop one layer of indirection from `instrument` by getting it from instrument_set.module_instruments,
        // then finding the index of that instrument in instrument_set.total_instruments.
        let original_instrument = &asset_set.module_instruments[ *instrument as usize ];
        let instrument = asset_set.total_instruments.iter().position( | instrument | instrument == original_instrument ).ok_or( "internal error: could not locate instrument in combined instrument set" )?;
        let instrument = instrument as u16;

        // Validate the instrument is applicable to the current channel
        let correct_type = match channels[ active_channel ].id {
            ESF_FM_1..=ESF_FM_3 | ESF_FM_4..=ESF_FM_6 => asset_set.fm_ids.contains( &instrument ),
            ESF_FM_6_PCM => {
                print_warning( "attempted to set instrument on FM6 when it is in pcm mode. this is valid, but it won't do anything until the next time it is set back to fm mode." );
                asset_set.fm_ids.contains( &instrument )
            },
            ESF_PSG_1..=ESF_PSG_4 => asset_set.psg_ids.contains( &instrument ),
            invalid_channel => return Err( format!( "internal error: get_events_for_channel: invalid channel {:#04X}", invalid_channel ) )?
        };

        // Do not set the same instrument if it was already set before
        if &channels[ active_channel ].active_instrument != &Some( instrument as u16 ) {
            if correct_type {
                events.push( vec![ ESF_SET_INSTRUMENT | channels[ active_channel ].id, instrument as u8 ] );
                channels[ active_channel ].active_instrument = Some( instrument as u16 );
            } else {
                print_warning( "attempted to set an fm instrument on a psg channel, or vice versa. the instrument was not set - your soundtrack may sound different than expected." );
                // Now if current channel is PSG1 through PSG4, it's gonna need an instrument set on it.
                if channels[ active_channel ].active_instrument.is_none() && ( ESF_PSG_1..=ESF_PSG_4 ).contains( &channels[ active_channel ].id ) {
                    print_warning( "this is a psg channel, seting the default psg instrument so your note can still play..." );
                    if row.note.is_some() {
                        // Set "__reskit_default_psg_instrument"
                        let default_psg_id = if let Some( position ) = asset_set.total_instruments.iter().position( | item | item.name == "__reskit_default_psg_instrument" ) {
                            Some( position as u16 )
                        } else {
                            None
                        };
                        let instrument = default_psg_id.ok_or( "internal error: no default psg instrument to apply" )?;

                        events.push( vec![ ESF_SET_INSTRUMENT | channels[ active_channel ].id, instrument as u8 ] );
                        channels[ active_channel ].active_instrument = Some( instrument );
                    }
                }
            }
        }
    } else {
        // This is SN1, SN2, SN3, or SN4 with no active instrument
        // Check if the channel needs "__reskit_default_psg_instrument" set before a note is set
        if channels[ active_channel ].active_instrument.is_none() && ( ESF_PSG_1..=ESF_PSG_4 ).contains( &channels[ active_channel ].id ) {
            if row.note.is_some() {
                // Set "__reskit_default_psg_instrument"
                let default_psg_id = if let Some( position ) = asset_set.total_instruments.iter().position( | item | item.name == "__reskit_default_psg_instrument" ) {
                    Some( position as u16 )
                } else {
                    None
                };
                let instrument = default_psg_id.ok_or( "internal error: no default psg instrument to apply" )?;

                events.push( vec![ ESF_SET_INSTRUMENT | channels[ active_channel ].id, instrument as u8 ] );
                channels[ active_channel ].active_instrument = Some( instrument );
            }
        }
    }

    // Key on or key off note
    if let Some( note ) = &row.note {
        if let Note::NoteOff = note {
            channels[ active_channel ].note_keyed = false;
            events.push( vec![ ESF_NOTE_OFF | channels[ active_channel ].id ] );
        } else {
            // Determine if we can key on. We can't key on if we are in FM1..FM6, and no instrument was ever set
            // on the channel either here or previously. (We can do that for psg, using the default instrument.)
            channels[ active_channel ].note_keyed = if ( ESF_FM_1..=ESF_FM_3 ).contains( &channels[ active_channel ].id ) | ( ESF_FM_4..=ESF_FM_6 ).contains( &channels[ active_channel ].id ) {
                if channels[ active_channel ].active_instrument.is_none() {
                    print_warning( "no instrument set on this fm channel - cannot trigger the note!" );
                    false
                } else {
                    true
                }
            } else {
                true
            };
            channels[ active_channel ].active_note = Some(
                OctaveFrequency {
                    octave: note.get_octave()?,
                    frequency: if ( ESF_PSG_1..=ESF_PSG_4 ).contains( &channels[ active_channel ].id ) {
                        get_psg_semitone_frequency( note.get_octave()?, get_semitone( note )? )?
                    } else {
                        get_fm_semitone_frequency( get_semitone( note )? )?
                    }
                }
            );

            if channels[ active_channel ].id == ESF_FM_6_PCM {
                let module_pcm_index = get_pcm_index( note )?;
                if asset_set.module_samples.get( module_pcm_index as usize ).is_none() {
                    print_warning( "FM6 is in PCM mode but you specified a note that is not linked to a sample. this has no effect, skipping..." );
                } else {
                    let sample = &asset_set.module_samples[ module_pcm_index as usize ];
                    let global_sample_index = asset_set.total_samples.iter().position( | global_sample | global_sample == sample ).ok_or( "internal error: could not locate sample in combined sample set" )?;

                    // Remember, instruments will always be listed first in the output source file, followed by pointers to samples
                    // So the minimum sample offset is the number of instruments
                    events.push( vec![ ESF_NOTE_ON | channels[ active_channel ].id, global_sample_index as u8 + asset_set.total_instruments.len() as u8 ] );
                }
            } else if channels[ active_channel ].id == ESF_PSG_4 {
                // Find the single (and it should only ever be a single) Effect::DcsgNoiseMode attached to the channel
                let dcsg_noise_mode = channels[ active_channel ].active_effects.iter().find( | effect | matches!( effect, Effect::DcsgNoiseMode { mode: _, noise_type: _ } ) );

                // Do we need to copy channel.active_note to psg3 and set its frequency? psg3 sets the actual note played for psg4 in special circumstances
                if let Some( dcsg_noise_mode ) = dcsg_noise_mode {
                    if let Effect::DcsgNoiseMode { mode, noise_type: _ } = dcsg_noise_mode {
                        if mode == &DcsgChannelMode::Ch3Frequency {
                            let index = channels.iter().position( | channel | channel.id == ESF_PSG_3 ).ok_or( "internal error: where is psg3??" )?;
                            // Issue a set frequency for psg3 but don't tell the channel state (matches behaviour in deflemask)
                            events.push( vec![
                                ESF_SET_FREQUENCY | channels[ index ].id,
                                ( 0x000F & channels[ active_channel ].active_note.ok_or( "internal error: wtf happened here (1)??" )?.frequency ) as u8,
                                ( ( 0x3F0 & channels[ active_channel ].active_note.ok_or( "internal error: wtf happened here (2)??" )?.frequency ) >> 4 ) as u8
                            ] );
                        }
                    } else {
                        return Err( "internal error: wtf happened here (3)??" )?
                    }
                } else {
                    let index = channels.iter().position( | channel | channel.id == ESF_PSG_3 ).ok_or( "internal error: where is psg3??" )?;
                    // Issue a set frequency for psg3 but don't tell the channel state (matches behaviour in deflemask)
                    events.push( vec![
                        ESF_SET_FREQUENCY | channels[ index ].id,
                        ( 0x000F & channels[ active_channel ].active_note.ok_or( "internal error: wtf happened here (1)??" )?.frequency ) as u8,
                        ( ( 0x3F0 & channels[ active_channel ].active_note.ok_or( "internal error: wtf happened here (2)??" )?.frequency ) >> 4 ) as u8
                    ] );
                }

                if let Some( noise_generator_setting ) = get_noise_generator_setting( note, dcsg_noise_mode )? {
                    events.push( vec![ ESF_NOTE_ON | channels[ active_channel ].id, noise_generator_setting ] );
                }
            } else {
                let note = get_note( note, channels[ active_channel ].id )?;
                events.push( vec![ ESF_NOTE_ON | channels[ active_channel ].id, note ] );
            }
        }
    }

    Ok( events )
}

/**
 * Add effects from a given row to a particular channel and perform activities due on insert. This may generate Echo
 * events that should be applied to the stream.
 */
fn apply_effects_to_channel( channel: &mut Channel, effects: &LinkedHashSet<Effect> ) -> Result<Vec<EchoEvent>, Box<dyn Error>> {
    let mut events: Vec<EchoEvent> = Vec::new();

    for effect in effects {
        match effect {
            Effect::DacEnable { enabled } => {
                if !enabled {
                    if channel.id == ESF_FM_6 || channel.id == ESF_FM_6_PCM {
                        channel.id = ESF_FM_6;
                    } else {
                        print_warning( "Effect::DacEnable can only be applied to FM6. ignoring..." );
                    }

                    channel.active_effects.remove( &Effect::DacEnable { enabled: true } );
                } else {
                    if channel.id == ESF_FM_6 || channel.id == ESF_FM_6_PCM {
                        channel.id = ESF_FM_6_PCM;
                    } else {
                        print_warning( "Effect::DacEnable can only be applied to FM6. ignoring..." );
                    }

                    channel.active_effects.insert_if_absent( Effect::DacEnable { enabled: true } );
                }
            },
            Effect::PortamentoUp { speed } => {
                let speed = *speed;

                // Remove all prior portamentos (you cannot portamento up and down at the same time)
                channel.active_effects = channel.active_effects
                                                .clone()
                                                .into_iter()
                                                .filter( | effect |
                                                    !(
                                                        matches!( effect, Effect::PortamentoDown { speed: _ } ) ||
                                                        matches!( effect, Effect::PortamentoUp { speed: _ } )
                                                    )
                                                )
                                                .collect();

                if speed > 0 {
                    channel.active_effects.insert( Effect::PortamentoUp { speed } );
                }
            },
            Effect::PortamentoDown { speed } => {
                let speed = *speed;

                // Remove all prior portamentos (you cannot portamento up and down at the same time)
                channel.active_effects = channel.active_effects
                                                .clone()
                                                .into_iter()
                                                .filter( | effect |
                                                    !(
                                                        matches!( effect, Effect::PortamentoDown { speed: _ } ) ||
                                                        matches!( effect, Effect::PortamentoUp { speed: _ } )
                                                    )
                                                )
                                                .collect();

                if speed > 0 {
                    channel.active_effects.insert( Effect::PortamentoDown { speed } );
                }
            },
            Effect::NoteCut { after_ticks } => {
                let after_ticks = *after_ticks;

                // Remove all previous note cuts
                channel.active_effects = channel.active_effects
                                                .clone()
                                                .into_iter()
                                                .filter( | effect | !( matches!( effect, Effect::NoteCut { after_ticks: _ } ) ) )
                                                .collect();

                // Add new note cut. Being an effect requiring delay generation, it will generate (and expire) in get_delays
                channel.active_effects.insert( Effect::NoteCut { after_ticks } );
            },
            Effect::SetPanning { left, right } => {
                let left = *left;
                let right = *right;

                if channel.id == ESF_FM_6_PCM || ( ESF_PSG_1..=ESF_PSG_4 ).contains( &channel.id ) {
                    print_warning( "Effect::SetPanning unsupported on PCM or PSG channel" );
                } else {
                    if !channel.active_effects.contains( &Effect::SetPanning { left, right } ) {
                        // Remove other active SetPanning effects
                        channel.active_effects.remove( &Effect::SetPanning { left: true, right: true } );
                        channel.active_effects.remove( &Effect::SetPanning { left: true, right: false } );
                        channel.active_effects.remove( &Effect::SetPanning { left: false, right: true } );
                        channel.active_effects.remove( &Effect::SetPanning { left: false, right: false } );

                        // Insert new panning commands
                        let panning: u8 = if left && right {
                            ESF_FM_PARAM_LEFT_SPEAKER | ESF_FM_PARAM_RIGHT_SPEAKER
                        } else if left {
                            ESF_FM_PARAM_LEFT_SPEAKER
                        } else if right {
                            ESF_FM_PARAM_RIGHT_SPEAKER
                        } else {
                            0x00
                        };
                        events.push( vec![ ESF_SET_FM_PARAMS | channel.id, panning ] );

                        // Insert new active SetPanning effect - will not be doubly added
                        channel.active_effects.insert( Effect::SetPanning { left, right } );
                    }
                }
            },
            Effect::DcsgNoiseMode { mode, noise_type } => {
                let mode = *mode;
                let noise_type = *noise_type;

                if channel.id == ESF_PSG_4 {
                    channel.active_effects = channel.active_effects
                                                    .clone()
                                                    .into_iter()
                                                    .filter( | effect | !( matches!( effect, Effect::DcsgNoiseMode { mode: _, noise_type: _ } ) ) )
                                                    .collect();

                    channel.active_effects.insert( Effect::DcsgNoiseMode { mode, noise_type } );
                } else {
                    print_warning( "Effect:DcsgNoiseMode can only be applied to SN4. ignoring..." );
                }
            },
            Effect::PositionJump { pattern: _ } => {
                // Nothing to do - handled as we iterate through the rows.
            },
            Effect::JumpToNextPattern { start_row } => {
                // Nothing to do - handled as we iterate through the rows.
            },
            unsupported_effect => print_warning( &format!( "effect unsupported: {:?}. your soundtrack may sound different than expected.", unsupported_effect ) )
        }
    }

    Ok( events )
}

fn sequence_to_compacted_delay( current_sequence: &Vec<EchoEvent> ) -> Result<Vec<EchoEvent>, Box<dyn Error>> {
    let mut new_events: Vec<EchoEvent> = Vec::new();

    let mut cumulative_delay: u16 = 0;
    for delay_event in current_sequence {
        let mut iter = delay_event.iter();
        let delay_type = *iter.next().ok_or( "internal error: empty event" )?;

        if delay_type & 0xF0 == ESF_DELAY_SHORT {
            cumulative_delay += ( ( delay_type & 0x0F ) as u16 ) + 1;
        } else if delay_type == ESF_DELAY_LONG {
            cumulative_delay += ( *iter.next().ok_or( "internal error: expected delay byte" )? ) as u16;
        }
    }

    if cumulative_delay > 0 {
        // Push amount of full-256 wait events
        for _ in 0..cumulative_delay / 256 {
            new_events.push( vec![ ESF_DELAY_LONG, 0x00 ] );
        }

        // Push the remainder under 256 event
        new_events.push( get_delay( ( cumulative_delay % 256 ) as u8 )? );
    }

    Ok( new_events )
}

/**
 * Replace concurrent sequences of waits with single larger waits. This reduces the ESF stream size.
 */
pub fn compact_delays( events: Vec<EchoEvent> ) -> Result<Vec<EchoEvent>, Box<dyn Error>> {
    let mut new_events: Vec<EchoEvent> = Vec::new();

    let mut current_sequence: Vec<EchoEvent> = Vec::new();
    for event in events {
        if let Some( esf_event_type ) = event.iter().next() {
            if *esf_event_type & 0xF0 == ESF_DELAY_SHORT || *esf_event_type == ESF_DELAY_LONG {
                // Continue to push delay events into current_sequence
                current_sequence.push( event );
            } else {
                let compacted_delay = sequence_to_compacted_delay( &current_sequence )?;
                if !compacted_delay.is_empty() {
                    new_events.extend( compacted_delay.into_iter() );
                    current_sequence.clear();
                }

                new_events.push( event );
            }
        }
    }

    new_events.extend( sequence_to_compacted_delay( &current_sequence )?.into_iter() );

    Ok( new_events )
}

/**
 * Generate an Echo frequency shift up event for a given channel, updating active_note to be ready
 * for the next event.
 */
fn get_portamento( channel: &mut Channel, portamento_effect: &Effect ) -> Result<EchoEvent, Box<dyn Error>> {
    if channel.id != ESF_FM_6_PCM && channel.id != ESF_PSG_4 {
        // A portamento was called for on this channel, and if none was provided, we need to start with C, octave 0
        // This replicates the behaviour in deflemask
        if let None = channel.active_note {
            channel.active_note = match channel.id {
                ESF_FM_1..=ESF_FM_3 | ESF_FM_4..=ESF_FM_6 => Some( OctaveFrequency { octave: 0, frequency: get_fm_semitone_frequency( ESF_SEMITONE_C )? } ),
                ESF_PSG_1..=ESF_PSG_3 => Some( OctaveFrequency { octave: 0, frequency: get_psg_semitone_frequency( 0, ESF_SEMITONE_C )? } ),
                invalid_channel => return Err( format!( "internal error: get_note: invalid default portamento setting channel {:#04X}", invalid_channel ) )?
            }
        }
    }

    Ok(
        if channel.id == ESF_FM_6_PCM || channel.id == ESF_PSG_4 {
            // Generate no events if channel is set to pcm
            print_warning( "attempted to portamento on FM6 when it is set to pcm, or on SN4. this makes no sense and does nothing" );
            vec![]
        } else if let Some( octave_frequency ) = channel.active_note {
            let ( octave, frequency ) = {
                let by_amount: i16 = match portamento_effect {
                    Effect::PortamentoUp { speed } => *speed as i16,
                    Effect::PortamentoDown { speed } => -( *speed as i16 ),
                    _ => return Err( "internal error: provided effect is not a supported portamento effect" )?
                };

                if ( ESF_PSG_1..=ESF_PSG_3 ).contains( &channel.id ) {
                    // Frequency note shifts are backward in PSG
                    let new_frequency: i16 = octave_frequency.frequency as i16 + ( by_amount * -1 );

                    // Clamp frequency
                    let new_frequency = max( min( new_frequency, 1024 ), 0 );

                    // Get psg octave from frequency (which will clamp octave to 0-5) and return the new frequency
                    ( get_psg_octave_from_frequency( new_frequency ), new_frequency as u16 )
                } else {
                    let new_frequency: i16 = octave_frequency.frequency as i16 + by_amount;

                    if new_frequency > get_fm_semitone_frequency( ESF_SEMITONE_B )? as i16 {
                        if octave_frequency.octave == 7 {
                            // Nowhere else to go up
                            ( 7, get_fm_semitone_frequency( ESF_SEMITONE_B )? )
                        } else {
                            // Go up an octave then add the difference to middle C
                            let difference = new_frequency - get_fm_semitone_frequency( ESF_SEMITONE_B )? as i16;
                            ( octave_frequency.octave + 1, get_fm_semitone_frequency( ESF_SEMITONE_C )? + difference as u16 )
                        }
                    } else if new_frequency < get_fm_semitone_frequency( ESF_SEMITONE_C )? as i16 {
                        if octave_frequency.octave == 0 {
                            // Nowhere else to go down
                            ( 0, get_fm_semitone_frequency( ESF_SEMITONE_C )? )
                        } else {
                            // Go down an octave then subtract the overshoot of C from B
                            let difference = get_fm_semitone_frequency( ESF_SEMITONE_C )? as i16 - new_frequency;
                            ( octave_frequency.octave - 1, get_fm_semitone_frequency( ESF_SEMITONE_B )? - difference as u16 )
                        }
                    } else {
                        // Move within the same octave
                        ( octave_frequency.octave, new_frequency as u16 )
                    }
                }
            };

            // Set the new OctaveFrequency on the channel
            channel.active_note = Some( OctaveFrequency { octave, frequency } );

            // Generate the note slide ESF events
            // YM2612 register format for frequency: https://plutiedev.com/ym2612-registers#reg-A0
            vec![
                ESF_SET_FREQUENCY | channel.id,
                if ( ESF_PSG_1..=ESF_PSG_3 ).contains( &channel.id ) {
                    ( 0x000F & frequency ) as u8
                } else {
                    octave << 3 | ( ( ( 0x0700 & frequency ) >> 8 ) as u8 )
                },
                if ( ESF_PSG_1..=ESF_PSG_3 ).contains( &channel.id ) {
                    ( ( 0x3F0 & frequency ) >> 4 ) as u8
                } else {
                    ( 0x00FF & frequency ) as u8
                }
            ]
        } else {
            // No active note, nothing to generate
            vec![]
        }
    )
}

/**
 * Generate a note cut effect for this channel and modify channel state to reflect it
 */
fn get_note_cut( channel: &mut Channel, note_cut_effect: &Effect, tick: u8 ) -> Result<EchoEvent, Box<dyn Error>> {
    let after_ticks = match note_cut_effect {
        Effect::NoteCut { after_ticks } => *after_ticks,
        _ => return Err( "internal error: provided effect is not the note cut effect" )?
    };

    if tick >= after_ticks {
        channel.note_keyed = false;

        // Consume the note cut after we are done with it - it does not remain active
        channel.active_effects = channel.active_effects
                                        .clone()
                                        .into_iter()
                                        .filter( | effect | !( matches!( effect, Effect::NoteCut { after_ticks: _ } ) ) )
                                        .collect();
        return Ok( vec![ ESF_NOTE_OFF | channel.id ] );
    }

    // Nothing to generate this tick
    Ok( vec![] )
}

/**
 * Get effects that require a specific action when computing the event flushes to Echo.
 */
fn get_actionable_effects( channels: &mut [Channel] ) -> Result<Vec<(usize, Effect)>, Box<dyn Error>> {
    // All portamento effects deploy per tick, not per row. So we need to aggregate all portamentos across all
    // channels for this row, then flush them once per `ticks_to_wait` for this row.
    let mut actionable_effects: Vec<(usize, Effect)> = Vec::new();
    for channel_id in 0..channels.len() {
        let channel = &channels[ channel_id ];

        for effect in &channel.active_effects {
            let is_actionable = match effect {
                Effect::PortamentoUp { speed: _ } | Effect::PortamentoDown { speed: _ } => true,
                Effect::NoteCut { after_ticks: _ } => true,
                _ => false // effect is not an actionable effect
            };

            if is_actionable {
                actionable_effects.push( ( channel_id, *effect ) );
            }
        }
    }

    Ok( actionable_effects )
}

/**
 * Calculate delays generated before and after effects applied across all channels. If there are no effects for this row,
 * an empty vec will be returned. This means you should just apply ticks_to_wait as the delay for that row. If there
 * -are- effects generated by this function, you should not apply ticks_to_wait, since this function will spend out
 * the tick budget for that row instead.
 */
fn get_actionable_effect_sequence( channels: &mut [Channel], ticks_to_wait: u8 ) -> Result<Vec<EchoEvent>, Box<dyn Error>> {
    let mut events: Vec<EchoEvent> = Vec::new();

    let actionable_effects = get_actionable_effects( channels )?;
    if !actionable_effects.is_empty() {
        for tick in 0..ticks_to_wait {
            for ( channel_id, actionable_effect ) in &actionable_effects {
                let event_for_tick = match actionable_effect {
                    Effect::PortamentoUp { speed: _ } | Effect::PortamentoDown { speed: _ } => Some( get_portamento( &mut channels[ *channel_id ], &actionable_effect )? ),
                    Effect::NoteCut { after_ticks: _ } => Some( get_note_cut( &mut channels[ *channel_id ], &actionable_effect, tick )? ),
                    _ => None
                };

                if let Some( event_for_tick ) = event_for_tick {
                    if !event_for_tick.is_empty() {
                        events.push( event_for_tick );
                    }
                }
            }

            events.push( get_delay( 1 )? );
        }
    }

    Ok( events )
}

/**
 * Get the delays due at the end of a row. These delays are what flushes the tick to Echo so that it can play.
 */
fn get_delays( events: Vec<EchoEvent>, channels: &mut [Channel], ticks_to_wait: u8 ) -> Result<Vec<EchoEvent>, Box<dyn Error>> {
    let mut events: Vec<EchoEvent> = events;

    let applied_effects = get_actionable_effect_sequence( channels, ticks_to_wait )?;
    if applied_effects.is_empty() {
        // Push the amount of ticks to wait for this row. ticks_to_wait is speed_a or speed_b, times base_speed.
        events.push( get_delay( ticks_to_wait )? );
    } else {
        // Do not push the default "wait n ticks" event but rather push the delay events generated by effects
        events.extend( applied_effects.into_iter() );
    }

    Ok( events )
}

/**
 * For an entire row across all channels, generate the events that apply to the row as a whole. This usually means applying
 * the waits so that the row can be flushed to Echo and played - ESF ticks are until the nearest wait or stop event.
 */
pub fn get_events_for_row( channels: &mut [Channel], module_instruments: &Vec<Instrument>, module_samples: &Vec<Sample>, asset_set: &CombinedAssets, subrows: Vec<&PatternRow>, ticks_to_wait: u8 ) -> Result<Vec<EchoEvent>, Box<dyn Error>> {
    let mut events: Vec<EchoEvent> = Vec::new();

    // Generate InstrumentSet which is used to validate when instruments are set on channels
    let mut index = 0;
    let fm_ids = asset_set.instruments.iter()
                            .filter_map( | instrument | if matches!( instrument.instrument_type, InstrumentType::Fm2612( _ ) ) {
                                let result = index;
                                index += 1;
                                Some( result )
                            } else {
                                index += 1;
                                None
                            } )
                            .collect();

    index = 0;
    let psg_ids = asset_set.instruments.iter()
                            .filter_map( | instrument | if matches!( instrument.instrument_type, InstrumentType::PsgDcsg( _ ) ) {
                                let result = index;
                                index += 1;
                                Some( result )
                            } else {
                                index += 1;
                                None
                            } )
                            .collect();

    let instrument_set = InstrumentSet {
        fm_ids,
        psg_ids,
        module_instruments,
        total_instruments: &asset_set.instruments,
        module_samples,
        total_samples: &asset_set.samples
    };

    // Get events for each subrow (part of the total row for each channel)
    for i in 0..channels.len() {
        // Apply effects to channel's current state
        events.extend( apply_effects_to_channel( &mut channels[ i ], &subrows[ i ].effects )? );

        // Get the ESF events for this channel's part of the row
        events.extend( get_events_for_channel( channels, i, subrows[ i ], &instrument_set )? );
    }

    Ok( get_delays( events, channels, ticks_to_wait )? )
}