import Foundation

// MARK: - Accidental

enum Accidental: Equatable, Hashable {
    case natural
    case sharp
    case flat
    case doubleSharp
    case doubleFlat

    /// The semitone offset from natural
    var semitoneOffset: Int {
        switch self {
        case .doubleFlat:  return -2
        case .flat:        return -1
        case .natural:     return 0
        case .sharp:       return 1
        case .doubleSharp: return 2
        }
    }

    /// Display string for the accidental (using Unicode music symbols)
    var displayString: String {
        switch self {
        case .natural:     return "♮"
        case .sharp:       return "♯"
        case .flat:        return "♭"
        case .doubleSharp: return "𝄪"
        case .doubleFlat:  return "𝄫"
        }
    }

    /// Parse from file format: #, b, ##, bb
    static func parse(_ str: String) -> Accidental? {
        switch str {
        case "##": return .doubleSharp
        case "bb": return .doubleFlat
        case "#":  return .sharp
        case "b":  return .flat
        case "":   return .natural
        default:   return nil
        }
    }
}

// MARK: - Pitch

struct Pitch: Equatable, Hashable {
    let noteName: NoteName
    let octave: Int          // 0-9, where 4 is the middle octave (A4 = 440Hz)
    let accidental: Accidental

    /// The 12 note names in chromatic order
    enum NoteName: String, CaseIterable {
        case C, D, E, F, G, A, B

        /// Semitones above C within an octave (natural notes)
        var baseSemitone: Int {
            switch self {
            case .C: return 0
            case .D: return 2
            case .E: return 4
            case .F: return 5
            case .G: return 7
            case .A: return 9
            case .B: return 11
            }
        }

        /// Position on the staff (0 = C, 1 = D, ... 6 = B)
        var staffPosition: Int {
            switch self {
            case .C: return 0
            case .D: return 1
            case .E: return 2
            case .F: return 3
            case .G: return 4
            case .A: return 5
            case .B: return 6
            }
        }
    }

    /// MIDI note number (C4 = 60, A4 = 69)
    var midiNumber: Int {
        let baseMidi = (octave + 1) * 12 + noteName.baseSemitone + accidental.semitoneOffset
        return baseMidi
    }

    /// Frequency in Hz using equal temperament (A4 = 440Hz)
    var frequency: Double {
        let semitonesFromA4 = Double(midiNumber - 69)
        return 440.0 * pow(2.0, semitonesFromA4 / 12.0)
    }

    /// Staff position relative to middle C (C4 = 0).
    /// Each step is one line/space on the staff.
    /// Used for vertical positioning of notes.
    var staffPositionFromMiddleC: Int {
        let octaveOffset = (octave - 4) * 7  // 7 staff positions per octave
        return octaveOffset + noteName.staffPosition
    }

    /// Create a pitch from a string like "C4", "F#5", "Bb3", "F##4", "Dbb2"
    static func parse(_ str: String) -> Pitch? {
        guard !str.isEmpty else { return nil }

        // First character is the note name
        let noteChar = String(str.prefix(1)).uppercased()
        guard let noteName = NoteName(rawValue: noteChar) else { return nil }

        // Last character(s) are the octave number
        // Middle characters are accidentals
        let remaining = String(str.dropFirst())

        // Split into accidental part and octave part
        // Octave is the trailing digits, accidental is everything before
        var accidentalStr = ""
        var octaveStr = ""

        for char in remaining {
            if char.isNumber || (char == "-" && octaveStr.isEmpty) {
                octaveStr.append(char)
            } else {
                accidentalStr.append(char)
            }
        }

        guard let octave = Int(octaveStr) else { return nil }
        guard let accidental = Accidental.parse(accidentalStr) else { return nil }

        return Pitch(noteName: noteName, octave: octave, accidental: accidental)
    }
}

extension Pitch: CustomStringConvertible {
    var description: String {
        let accStr: String
        switch accidental {
        case .natural:     accStr = ""
        case .sharp:       accStr = "#"
        case .flat:        accStr = "b"
        case .doubleSharp: accStr = "##"
        case .doubleFlat:  accStr = "bb"
        }
        return "\(noteName.rawValue)\(accStr)\(octave)"
    }
}
