use super::song::{
    Clip, Curve, Effect, Envelope, Harmonic, PatternStep, Song, Sound, Track, Waveform,
    is_audio_filename,
};
use std::{
    collections::{BTreeMap, HashSet},
    path::{Path, PathBuf},
};

/// Errors returned while parsing a `.track` source string or resolving
/// imported files.
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
    /// Syntactic or semantic error on a specific line of the source.
    #[error("line {line}: {message}")]
    Line {
        /// 1-based line number.
        line: usize,
        /// Human-readable description.
        message: String,
    },
    /// An imported file could not be read.
    #[error("import '{path}': read error: {error}")]
    ImportRead {
        /// Path that failed.
        path: PathBuf,
        /// Underlying I/O error.
        error: std::io::Error,
    },
    /// An imported file failed to parse.
    #[error("import '{path}': {error}")]
    ImportParse {
        /// Path that failed.
        path: PathBuf,
        /// Inner parse error.
        error: Box<Self>,
    },
    /// Imports form a cycle.
    #[error("import cycle: '{path}'")]
    ImportCycle {
        /// Path that closed the cycle.
        path: PathBuf,
    },
}

fn err(line: usize, message: impl Into<String>) -> ParseError {
    ParseError::Line {
        line,
        message: message.into(),
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Section {
    Top,
    Sounds,
    Track,
}

/// Parses a `.track` source string into a [`Song`].
///
/// Imports referenced via `@import` are recorded in `song.imports` but not
/// loaded; call [`resolve_imports`] afterwards to merge their sounds.
pub fn parse(source: &str) -> Result<Song, ParseError> {
    let mut song = Song::default();
    let mut section = Section::Top;
    let mut current_track: Option<Track> = None;

    for (index, raw) in source.lines().enumerate() {
        let line_number = index + 1;
        let line = raw.trim();
        if line.is_empty() {
            continue;
        }

        if let Some(rest) = line.strip_prefix('#') {
            let header = rest.trim();
            if header.eq_ignore_ascii_case("Sounds") {
                commit_track(&mut song, &mut current_track);
                section = Section::Sounds;
            } else if let Some(track_name) = header.strip_prefix("Track ") {
                commit_track(&mut song, &mut current_track);
                section = Section::Track;
                current_track = Some(Track::new(track_name.trim()));
            }
            continue;
        }

        if line.starts_with('@') {
            apply_directives(
                &mut song,
                current_track.as_mut(),
                section,
                line,
                line_number,
            )?;
            continue;
        }

        match section {
            Section::Top => {
                return Err(err(
                    line_number,
                    "content before any section header (use '# Sounds' or '# Track Name')",
                ));
            }
            Section::Sounds => {
                let (name, sound) = parse_sound_line(line, line_number)?;
                song.local_sound_names.insert(name.clone());
                song.sounds.insert(name, sound);
            }
            Section::Track => {
                let track = current_track
                    .as_mut()
                    .ok_or_else(|| err(line_number, "no current track"))?;
                track.clips.push(parse_clip_line(line, line_number)?);
            }
        }
    }

    commit_track(&mut song, &mut current_track);
    Ok(song)
}

fn commit_track(song: &mut Song, current: &mut Option<Track>) {
    if let Some(track) = current.take() {
        song.tracks.push(track);
    }
}

fn apply_directives(
    song: &mut Song,
    mut track: Option<&mut Track>,
    section: Section,
    line: &str,
    line_number: usize,
) -> Result<(), ParseError> {
    for directive in split_directives(line) {
        let mut parts = directive.splitn(2, char::is_whitespace);
        let key = parts
            .next()
            .ok_or_else(|| err(line_number, "empty directive"))?;
        let value = parts.next().unwrap_or("").trim();
        match section {
            Section::Track => {
                let track = track
                    .as_deref_mut()
                    .ok_or_else(|| err(line_number, "track directive outside track"))?;
                apply_track_directive(track, key, value, line_number)?;
            }
            _ => apply_song_directive(song, key, value, line_number)?,
        }
    }
    Ok(())
}

fn split_directives(line: &str) -> Vec<&str> {
    line.split('@')
        .map(str::trim)
        .filter(|piece| !piece.is_empty())
        .collect()
}

fn apply_song_directive(
    song: &mut Song,
    key: &str,
    value: &str,
    line_number: usize,
) -> Result<(), ParseError> {
    match key {
        "tempo" => song.tempo = parse_float(value, line_number, "tempo")?,
        "import" => {
            if value.is_empty() {
                return Err(err(line_number, "@import needs a path"));
            }
            song.imports.push(value.to_string());
        }
        other => {
            return Err(err(
                line_number,
                format!("unknown global directive '{other}'"),
            ));
        }
    }
    Ok(())
}

fn apply_track_directive(
    track: &mut Track,
    key: &str,
    value: &str,
    line_number: usize,
) -> Result<(), ParseError> {
    match key {
        "volume" => track.volume = parse_float(value, line_number, "volume")?,
        "pan" => track.pan = parse_float(value, line_number, "pan")?,
        "mute" => track.muted = parse_bool(value, line_number, "mute")?,
        "solo" => track.solo = parse_bool(value, line_number, "solo")?,
        "loop" => track.loop_length = Some(parse_float(value, line_number, "loop")?),
        "effect" => track.effects.push(parse_effect(value, line_number)?),
        other => {
            return Err(err(
                line_number,
                format!("unknown track directive '{other}'"),
            ));
        }
    }
    Ok(())
}

fn parse_float(value: &str, line_number: usize, key: &str) -> Result<f32, ParseError> {
    value
        .parse::<f32>()
        .map_err(|_| err(line_number, format!("invalid number for {key}: '{value}'")))
}

fn parse_bool(value: &str, line_number: usize, key: &str) -> Result<bool, ParseError> {
    match value {
        "true" | "1" | "on" | "yes" => Ok(true),
        "false" | "0" | "off" | "no" | "" => Ok(false),
        other => Err(err(
            line_number,
            format!("invalid bool for {key}: '{other}'"),
        )),
    }
}

fn parse_sound_line(line: &str, line_number: usize) -> Result<(String, Sound), ParseError> {
    let (name, rest) = line
        .split_once(':')
        .ok_or_else(|| err(line_number, "sound definition needs ':'"))?;
    let name = name.trim().to_string();
    if name.is_empty() {
        return Err(err(line_number, "sound name empty"));
    }
    let mut sound = Sound::default();
    for entry in rest.split(',') {
        let entry = entry.trim();
        if entry.is_empty() {
            continue;
        }
        let mut parts = entry.split_whitespace();
        let key = parts
            .next()
            .ok_or_else(|| err(line_number, "empty sound parameter"))?;
        let value_text = parts
            .next()
            .ok_or_else(|| err(line_number, format!("missing value for '{key}'")))?;
        match key {
            "distortion" => sound.distortion = parse_float(value_text, line_number, key)?,
            "volume" => sound.volume = parse_float(value_text, line_number, key)?,
            "attack" => {
                sound.attack = Some(parse_envelope(value_text, parts.next(), line_number)?);
            }
            "decay" => {
                sound.decay = Some(parse_envelope(value_text, parts.next(), line_number)?);
            }
            other => {
                let value = parse_float(value_text, line_number, key)?;
                let harmonic = parse_harmonic(other, value, line_number)?;
                sound.harmonics.push(harmonic);
            }
        }
    }
    Ok((name, sound))
}

fn parse_effect(value: &str, line_number: usize) -> Result<Effect, ParseError> {
    let mut parts = value.split_whitespace();
    let kind = parts
        .next()
        .ok_or_else(|| err(line_number, "effect needs a type"))?
        .to_string();
    match kind.as_str() {
        "lowpass" => Ok(Effect::Lowpass {
            cutoff: take_float(&mut parts, "cutoff", &kind, line_number)?,
        }),
        "highpass" => Ok(Effect::Highpass {
            cutoff: take_float(&mut parts, "cutoff", &kind, line_number)?,
        }),
        "echo" => Ok(Effect::Echo {
            delay: take_float(&mut parts, "delay", &kind, line_number)?,
            feedback: take_float(&mut parts, "feedback", &kind, line_number)?,
            mix: take_float(&mut parts, "mix", &kind, line_number)?,
        }),
        "tremolo" => Ok(Effect::Tremolo {
            rate: take_float(&mut parts, "rate", &kind, line_number)?,
            depth: take_float(&mut parts, "depth", &kind, line_number)?,
        }),
        "bitcrusher" => {
            let bits = take_float(&mut parts, "bits", &kind, line_number)?;
            Ok(Effect::Bitcrusher {
                bits: bits.clamp(1.0, 16.0) as u8,
            })
        }
        other => Err(err(line_number, format!("unknown effect '{other}'"))),
    }
}

fn take_float<'a>(
    parts: &mut impl Iterator<Item = &'a str>,
    label: &str,
    kind: &str,
    line_number: usize,
) -> Result<f32, ParseError> {
    let text = parts
        .next()
        .ok_or_else(|| err(line_number, format!("effect '{kind}' missing {label}")))?;
    parse_float(text, line_number, label)
}

fn parse_envelope(
    duration_text: &str,
    curve_text: Option<&str>,
    line_number: usize,
) -> Result<Envelope, ParseError> {
    let duration = parse_float(duration_text, line_number, "duration")?;
    let curve = match curve_text {
        Some("linear") => Curve::Linear,
        Some("sine") => Curve::Sine,
        Some(other) => {
            return Err(err(
                line_number,
                format!("unknown curve '{other}' (expected linear or sine)"),
            ));
        }
        None => Curve::Linear,
    };
    Ok(Envelope { duration, curve })
}

fn parse_harmonic(key: &str, weight: f32, line_number: usize) -> Result<Harmonic, ParseError> {
    let (waveform_token, multiplier) = match key.split_once('@') {
        Some((waveform, multiplier_text)) => (
            waveform,
            parse_float(multiplier_text, line_number, "multiplier")?,
        ),
        None => match key {
            "octave" => ("sine", 2.0),
            "fifth" => ("sine", 1.5),
            "sub" => ("triangle", 0.5),
            other => (other, 1.0),
        },
    };
    let waveform = match waveform_token {
        "sine" => Waveform::Sine,
        "triangle" | "tri" => Waveform::Triangle,
        "soft_square" | "square" => Waveform::SoftSquare,
        other => {
            return Err(err(
                line_number,
                format!("unknown sound parameter '{other}'"),
            ));
        }
    };
    Ok(Harmonic {
        waveform,
        multiplier,
        weight,
    })
}

fn parse_clip_line(line: &str, line_number: usize) -> Result<Clip, ParseError> {
    let mut parts = line.split_whitespace();
    let first = parts
        .next()
        .ok_or_else(|| err(line_number, "empty clip line"))?;
    if is_audio_filename(first) {
        let start_text = parts
            .next()
            .ok_or_else(|| err(line_number, "audio clip needs start"))?;
        let start = parse_float(start_text, line_number, "start")?;
        if parts.next().is_some() {
            return Err(err(line_number, "audio clip takes only <file> <start>"));
        }
        return Ok(Clip::Audio {
            file: first.to_string(),
            start,
        });
    }
    let start_text = parts
        .next()
        .ok_or_else(|| err(line_number, "note needs start"))?;
    let start = parse_float(start_text, line_number, "start")?;
    let third = parts
        .next()
        .ok_or_else(|| err(line_number, "note needs pitch or pattern"))?;
    if third.starts_with('[') {
        return parse_pattern_clip(first, start, third, &mut parts, line_number);
    }
    let pitch = parse_float(third, line_number, "pitch")?;
    let duration_text = parts
        .next()
        .ok_or_else(|| err(line_number, "note needs duration"))?;
    let duration = parse_float(duration_text, line_number, "duration")?;
    if parts.next().is_some() {
        return Err(err(line_number, "extra tokens after duration"));
    }
    Ok(Clip::Note {
        sound: first.to_string(),
        start,
        pitch,
        duration,
    })
}

fn parse_pattern_clip<'a>(
    sound: &str,
    start: f32,
    first_token: &'a str,
    parts: &mut impl Iterator<Item = &'a str>,
    line_number: usize,
) -> Result<Clip, ParseError> {
    let mut steps = Vec::new();
    let mut current = first_token;
    loop {
        let trimmed = current.trim_start_matches('[');
        let (body, ends_here) = match trimmed.strip_suffix(']') {
            Some(without) => (without, true),
            None => (trimmed, false),
        };
        if !body.is_empty() {
            steps.push(parse_pattern_step(body, line_number)?);
        }
        if ends_here {
            break;
        }
        current = parts
            .next()
            .ok_or_else(|| err(line_number, "pattern missing closing ']'"))?;
    }
    if steps.is_empty() {
        return Err(err(line_number, "pattern must have at least one step"));
    }
    let duration_text = parts
        .next()
        .ok_or_else(|| err(line_number, "pattern needs total duration"))?;
    let total_duration = parse_float(duration_text, line_number, "duration")?;
    if parts.next().is_some() {
        return Err(err(line_number, "extra tokens after pattern duration"));
    }
    Ok(Clip::Pattern {
        sound: sound.to_string(),
        start,
        steps,
        total_duration,
    })
}

fn parse_pattern_step(token: &str, line_number: usize) -> Result<PatternStep, ParseError> {
    match token {
        "H" | "h" => Ok(PatternStep::Hold),
        "_" => Ok(PatternStep::Pause),
        other => parse_float(other, line_number, "pitch").map(PatternStep::Pitch),
    }
}

/// Parses `source` with sounds from `sounds_source` already merged in.
///
/// Parses both sources, then merges all sounds defined in `sounds_source`
/// into the main song. Local sound definitions in `source` take precedence
/// over sounds with the same name from `sounds_source`.
pub fn parse_with_sounds(source: &str, sounds_source: &str) -> Result<Song, ParseError> {
    let mut song = parse(source)?;
    let library = parse(sounds_source)?;
    for (name, sound) in library.sounds {
        song.sounds.entry(name).or_insert(sound);
    }
    Ok(song)
}

/// Resolves all `@import` references in `song`, loading external files
/// (relative to `base`) and merging their sound definitions.
///
/// Sounds from imports are added to `song.sounds` but not to
/// `song.local_sound_names`. Local definitions take precedence over
/// imported ones with the same name.
pub fn resolve_imports(song: &mut Song, base: Option<&Path>) -> Result<(), ParseError> {
    let mut visited = HashSet::new();
    for path in song.imports.clone() {
        let resolved = resolve_import_path(base, &path);
        load_import(&mut song.sounds, &resolved, &mut visited)?;
    }
    Ok(())
}

fn resolve_import_path(base: Option<&Path>, path: &str) -> PathBuf {
    let candidate = PathBuf::from(path);
    if candidate.is_absolute() {
        return candidate;
    }
    match base {
        Some(base) => base.join(path),
        None => candidate,
    }
}

fn load_import(
    target: &mut BTreeMap<String, Sound>,
    path: &Path,
    visited: &mut HashSet<PathBuf>,
) -> Result<(), ParseError> {
    let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
    if !visited.insert(canonical.clone()) {
        return Err(ParseError::ImportCycle { path: canonical });
    }
    let source = std::fs::read_to_string(path).map_err(|error| ParseError::ImportRead {
        path: path.to_path_buf(),
        error,
    })?;
    let nested = parse(&source).map_err(|error| ParseError::ImportParse {
        path: path.to_path_buf(),
        error: Box::new(error),
    })?;
    for (name, sound) in nested.sounds {
        target.entry(name).or_insert(sound);
    }
    let nested_base = path.parent();
    for nested_path in nested.imports {
        let resolved = resolve_import_path(nested_base, &nested_path);
        load_import(target, &resolved, visited)?;
    }
    Ok(())
}
