//
//  MusicBackend.swift
//  DashSmash
//
//  Common abstraction over the two music sources we support: the in-app
//  procedural drum/bass synth and Apple Music via MusicKit. Adding a third
//  source later (local file picker, mic-driven beat track) only needs another
//  conformer here — GameViewController doesn't change.
//

import Foundation
import MusicKit
import MediaPlayer
import AVFoundation

protocol MusicBackend: AnyObject {
    /// Begin playback from the start of the track.
    func start()
    /// Stop playback. Safe to call when already stopped.
    func stop()
    /// Restart from the beginning. Awaitable so the caller can wait for the
    /// audio to actually be at beat 0 before presenting the next scene frame
    /// (important for Apple Music, which restarts asynchronously).
    func restart() async
    /// Whether this backend can actually play at a non-1.0 rate. Streaming
    /// Apple Music can't be slowed reliably on-device, so the UI hides the
    /// "retry slower" affordance in that case.
    var supportsRateChange: Bool { get }
    /// Set the playback rate. 1.0 = normal, 0.9 = 10% slower. Must be applied
    /// before the next `start()`/`restart()` to take effect.
    func setPlaybackRate(_ rate: Double)
}

extension MusicBackend {
    var supportsRateChange: Bool { false }
    func setPlaybackRate(_ rate: Double) {}
}

// MARK: - Synth conformance

final class SynthBackend: MusicBackend {
    private let synth = MusicSynthesizer()
    private let level: Level
    private var rate: Double = 1.0

    init(level: Level) {
        self.level = level
        synth.prepare(level: level)
        synth.setRate(Float(rate))
    }

    func start() { synth.start() }
    func stop() { synth.stop() }
    func restart() async {
        synth.stop()
        synth.prepare(level: level)
        synth.setRate(Float(rate))
        synth.start()
    }

    var supportsRateChange: Bool { true }

    func setPlaybackRate(_ rate: Double) {
        self.rate = rate
        synth.setRate(Float(rate))
    }
}

// MARK: - Apple Music conformance

/// Wraps ApplicationMusicPlayer for a single song. The song is assumed to be
/// already queued (the tap-tempo flow does that) — `start` just ensures
/// playback is running from beat 0.
final class AppleMusicBackend: MusicBackend {

    func start() {
        // The tap-tempo screen already restarted the song from beat 0 before
        // pushing the game, so nothing more is needed here.
    }

    func stop() {
        AppleMusicCoordinator.shared.stop()
    }

    func restart() async {
        try? await AppleMusicCoordinator.shared.restartFromBeginning()
    }
}

// MARK: - Local library conformance

/// Plays a song from the device's Apple Music library via
/// `MPMusicPlayerController`. Works without the MusicKit capability.
final class MIDIBackend: MusicBackend {
    private let synth = MIDISynthesizer()
    private let song: MIDISong
    private var rate: Double = 1.0

    init(song: MIDISong) {
        self.song = song
        synth.prepare(song: song)
        synth.setRate(Float(rate))
    }

    func start() { synth.start() }
    func stop() { synth.stop() }
    func restart() async {
        synth.setRate(Float(rate))
        synth.restart()
    }

    var supportsRateChange: Bool { true }

    func setPlaybackRate(_ rate: Double) {
        self.rate = rate
        synth.setRate(Float(rate))
    }
}

final class LibraryMusicBackend: MusicBackend {

    private let item: MPMediaItem
    private let player = MPMusicPlayerController.applicationMusicPlayer
    private var rate: Float = 1.0

    init(item: MPMediaItem) {
        self.item = item
        player.setQueue(with: MPMediaItemCollection(items: [item]))
        // prepareToPlay() is async on iOS 26+; not strictly required —
        // play() works without it, just with marginally higher start latency.
    }

    func start() {
        applyRate()
    }

    func stop() {
        player.stop()
    }

    func restart() async {
        player.skipToBeginning()
        player.play()
        applyRate()
    }

    var supportsRateChange: Bool { true }

    func setPlaybackRate(_ rate: Double) {
        self.rate = Float(rate)
        applyRate()
    }

    private func applyRate() {
        // MPMusicPlayerController honours currentPlaybackRate for local
        // library items; setting before play() can be ignored on some iOS
        // versions, so we also re-apply after play() is invoked.
        player.currentPlaybackRate = rate
    }
}
