import Foundation

// MARK: - Song File Parser

/// Parses a .txt song file into a Song structure.
///
/// File format:
///   Line 1: Title
///   Line 2: "TimeSignature, Key, Tempo" e.g. "4/4, B minor, 80 bpm"
///   Lines 3+: "PartName Note1 Dur1 Note2 Dur2 ..." e.g. "Alto F4 2 G#4 8. C5 16 R 16"
///
/// Notes:
///   - Pitch format: NoteName[Accidental]Octave, e.g. C4, F#5, Bb3, F##4, Dbb2
///   - R = rest
///   - Duration: 1=whole, 2=half, 4=quarter, 8=eighth, 16=sixteenth
///   - Period suffix = dotted (e.g., "8." = dotted eighth)
///   - Notes represent literal pitches (F4 = F natural regardless of key)
///
struct SongFileParser {

    /// Parse a song file from a URL
    static func parse(url: URL) throws -> Song {
        let contents: String
        do {
            contents = try String(contentsOf: url, encoding: .utf8)
        } catch {
            throw ParseError.readError(error.localizedDescription)
        }
        return try parse(text: contents)
    }

    /// Parse a song from a text string
    static func parse(text: String) throws -> Song {
        let lines = text.components(separatedBy: .newlines)
            .map { $0.trimmingCharacters(in: .whitespaces) }
            .filter { !$0.isEmpty }

        guard !lines.isEmpty else { throw ParseError.emptyFile }
        guard lines.count >= 3 else { throw ParseError.noParts }

        // Line 1: Title
        let title = lines[0]
        guard !title.isEmpty else { throw ParseError.missingTitle }

        // Line 2: Metadata — "TimeSignature, Key, Tempo"
        let (timeSignature, keySignature, tempo) = try parseMetadataLine(lines[1])

        // Lines 3+: Parts
        var parts: [Part] = []
        for i in 2..<lines.count {
            let part = try parsePartLine(lines[i], lineNumber: i + 1, timeSignature: timeSignature, colorIndex: i - 2)
            parts.append(part)
        }

        guard !parts.isEmpty else { throw ParseError.noParts }

        return Song(
            title: title,
            timeSignature: timeSignature,
            keySignature: keySignature,
            defaultTempo: tempo,
            parts: parts
        )
    }

    // MARK: - Metadata parsing

    /// Parse "4/4, B minor, 80 bpm" into (TimeSignature, KeySignature, Int)
    private static func parseMetadataLine(_ line: String) throws -> (TimeSignature, KeySignature, Int) {
        // Split by commas
        let components = line.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
        guard components.count == 3 else {
            throw ParseError.invalidMetadataLine(line)
        }

        // Time signature
        guard let timeSig = TimeSignature.parse(components[0]) else {
            throw ParseError.invalidTimeSignature(components[0])
        }

        // Key signature
        guard let keySig = KeySignature.parse(components[1]) else {
            throw ParseError.invalidKeySignature(components[1])
        }

        // Tempo
        let tempo = try parseTempo(components[2])

        return (timeSig, keySig, tempo)
    }

    /// Parse "80 bpm" or "80" into an Int BPM value
    private static func parseTempo(_ str: String) throws -> Int {
        let cleaned = str.lowercased()
            .replacingOccurrences(of: "bpm", with: "")
            .trimmingCharacters(in: .whitespaces)
        guard let bpm = Int(cleaned), bpm > 0 else {
            throw ParseError.invalidTempo(str)
        }
        return bpm
    }

    // MARK: - Part line parsing

    /// Parse "Alto F4 2 G#4 8. C5 16 R 16" into a Part
    private static func parsePartLine(
        _ line: String,
        lineNumber: Int,
        timeSignature: TimeSignature,
        colorIndex: Int
    ) throws -> Part {
        let tokens = line.split(separator: " ").map { String($0) }
        guard tokens.count >= 3 else {
            throw ParseError.invalidPartLine(lineNumber: lineNumber, content: line)
        }

        // First token is the part name
        let partName = tokens[0]

        // Remaining tokens are note-duration pairs
        let noteTokens = Array(tokens.dropFirst())
        guard noteTokens.count % 2 == 0 else {
            throw ParseError.invalidPartLine(lineNumber: lineNumber, content: line)
        }

        var notes: [Note] = []
        for i in stride(from: 0, to: noteTokens.count, by: 2) {
            let pitchToken = noteTokens[i]
            let durationToken = noteTokens[i + 1]

            // Parse duration
            guard let duration = Duration.parse(durationToken) else {
                throw ParseError.invalidDuration(lineNumber: lineNumber, token: durationToken)
            }

            // Parse pitch or rest
            if pitchToken.uppercased() == "R" {
                notes.append(Note.rest(duration: duration))
            } else {
                guard let pitch = Pitch.parse(pitchToken) else {
                    throw ParseError.invalidNote(lineNumber: lineNumber, token: pitchToken)
                }
                notes.append(Note.pitched(pitch, duration: duration))
            }
        }

        return Part.create(
            name: partName,
            notes: notes,
            timeSignature: timeSignature,
            colorIndex: colorIndex
        )
    }
}
