import Foundation

// MARK: - Score Pitch Map

/// Maps audio frame positions to target pitches from the musical score.
/// Used by PitchCorrector to know what frequency the singer should be hitting at each moment.
struct ScorePitchMap {

    // MARK: - Target Pitch

    struct TargetPitch {
        let frequency: Double       // Target frequency in Hz (0.0 for rests)
        let isRest: Bool
        let noteIndex: Int          // Sequential note index in the part
        let fractionInNote: Double  // 0.0 = start of note, 1.0 = end
    }

    // MARK: - Build Map

    /// Build a lookup table mapping each analysis hop frame to its target pitch.
    ///
    /// - Parameters:
    ///   - part: The musical part containing notes and measures
    ///   - tempo: Playback tempo in BPM
    ///   - sampleRate: Audio sample rate (e.g. 48000)
    ///   - startMeasure: Which measure the recording started at
    ///   - hopSize: Analysis hop size in samples (must match PitchDetector's hop)
    ///   - totalFrames: Total number of audio frames in the recording
    /// - Returns: Array of TargetPitch, one per hop frame
    static func build(
        part: Part,
        tempo: Int,
        sampleRate: Double,
        startMeasure: Int,
        hopSize: Int,
        totalFrames: Int
    ) -> [TargetPitch] {

        let totalHops = (totalFrames - 1) / hopSize + 1
        var targets = [TargetPitch](repeating: TargetPitch(frequency: 0, isRest: true, noteIndex: 0, fractionInNote: 0), count: totalHops)

        // Build note schedule from startMeasure to end of part
        let endMeasure = part.measures.count - 1
        guard startMeasure <= endMeasure else { return targets }

        var noteIndex = 0
        var beatOffset: Double = 0.0

        for measureIndex in startMeasure...endMeasure {
            guard measureIndex < part.measures.count else { continue }
            for note in part.measures[measureIndex].notes {
                let beatEnd = beatOffset + note.beats

                // Convert beat range to frame range
                let startSeconds = beatOffset * 60.0 / Double(tempo)
                let endSeconds = beatEnd * 60.0 / Double(tempo)
                let startFrame = Int(startSeconds * sampleRate)
                let endFrame = Int(endSeconds * sampleRate)
                let noteDurationFrames = max(1, endFrame - startFrame)

                // Convert to hop indices
                let startHop = startFrame / hopSize
                let endHop = min(endFrame / hopSize, totalHops - 1)

                let frequency = note.isRest ? 0.0 : (note.pitch?.frequency ?? 0.0)

                let hopStart = max(0, startHop)
                let hopEnd = min(endHop, totalHops - 1)
                guard hopStart <= hopEnd else {
                    beatOffset = beatEnd
                    noteIndex += 1
                    continue
                }
                for hop in hopStart...hopEnd {
                    let hopFrame = hop * hopSize
                    let fraction = Double(hopFrame - startFrame) / Double(noteDurationFrames)

                    targets[hop] = TargetPitch(
                        frequency: frequency,
                        isRest: note.isRest || frequency == 0.0,
                        noteIndex: noteIndex,
                        fractionInNote: min(max(fraction, 0), 1)
                    )
                }

                beatOffset = beatEnd
                noteIndex += 1
            }
        }

        return targets
    }

    // MARK: - Crossfade at Note Boundaries

    /// Blend target frequencies at note transitions to avoid abrupt correction jumps.
    /// Applies a crossfade zone at the start/end of each note.
    ///
    /// - Parameters:
    ///   - targets: Raw target pitch array from `build()`
    ///   - crossfadeHops: Number of hop frames for the crossfade zone (default ~3 = ~30ms)
    /// - Returns: Smoothed target array with blended frequencies at transitions
    static func smoothTransitions(targets: [TargetPitch], crossfadeHops: Int = 3) -> [TargetPitch] {
        guard targets.count > 1 else { return targets }

        var smoothed = targets

        for i in 1..<targets.count {
            let prev = targets[i - 1]
            let curr = targets[i]

            // Detect note boundary (different noteIndex, both voiced)
            if curr.noteIndex != prev.noteIndex && !curr.isRest && !prev.isRest
                && curr.frequency > 0 && prev.frequency > 0 {

                // Apply crossfade: blend previous note's frequency into current note's start
                for j in 0..<crossfadeHops {
                    let blendIdx = i + j
                    guard blendIdx < targets.count else { break }

                    let t = Double(j + 1) / Double(crossfadeHops + 1) // 0→1
                    let blendedFreq = prev.frequency * (1.0 - t) + curr.frequency * t

                    smoothed[blendIdx] = TargetPitch(
                        frequency: blendedFreq,
                        isRest: false,
                        noteIndex: curr.noteIndex,
                        fractionInNote: curr.fractionInNote
                    )
                }
            }
        }

        return smoothed
    }
}
