// LoopFollow
// AlarmSound.swift

import AVFoundation
import Foundation
import MediaPlayer
import UIKit

/*
 * Class that handles the playing and the volume of the alarm sound.
 */
class AlarmSound {
    static var isPlaying: Bool {
        return audioPlayer?.isPlaying == true
    }

    static var isMuted: Bool {
        return muted
    }

    static var whichAlarm: String = "none"
    static var soundFile = "Indeed"
    static var isTesting: Bool = false

    fileprivate static var systemOutputVolumeBeforeOverride: Float?

    fileprivate static var soundURL = Bundle.main.url(forResource: "Indeed", withExtension: "caf")!
    fileprivate static var audioPlayer: AVAudioPlayer?
    fileprivate static let audioPlayerDelegate = AudioPlayerDelegate()

    fileprivate static var muted = false

    fileprivate static var alarmPlayingForTimer = Timer()
    fileprivate static let alarmPlayingForInterval = 290

    fileprivate static var repeatDelay: TimeInterval = 0

    fileprivate func startAlarmPlayingForTimer(time: TimeInterval) {
        AlarmSound.alarmPlayingForTimer = Timer.scheduledTimer(timeInterval: time,
                                                               target: self,
                                                               selector: #selector(AlarmSound.alarmPlayingForTimerDidEnd(_:)),
                                                               userInfo: nil,
                                                               repeats: true)
    }

    @objc func alarmPlayingForTimerDidEnd(_: Timer) {
        if !AlarmSound.isPlaying { return }
        AlarmSound.stop()
    }

    /*
     * Sets the audio volume to 0.
     */
    static func muteVolume() {
        audioPlayer?.volume = 0
        muted = true
        restoreSystemOutputVolume()
    }

    static func setSoundFile(str: String) {
        soundURL = Bundle.main.url(forResource: str, withExtension: "caf")!
    }

    /*
     * Sets the volume of the alarm back to the volume before it has been muted.
     */
    static func unmuteVolume() {
        audioPlayer?.volume = 1.0
        muted = false
    }

    static func stop() {
        Observable.shared.alarmSoundPlaying.value = false

        repeatDelay = 0
        audioPlayer?.stop()
        audioPlayer = nil

        restoreSystemOutputVolume()
    }

    static func playTest() {
        guard !isPlaying else {
            return
        }

        do {
            audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
            audioPlayer!.delegate = audioPlayerDelegate

            activateAudioSessionWithFallback()

            audioPlayer?.numberOfLoops = 0

            if !audioPlayer!.prepareToPlay() {
                LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed preparing to play")
            }

            if audioPlayer!.play() {
                if !isPlaying {
                    LogManager.shared.log(category: .alarm, message: "AlarmSound - not playing after calling play")
                    LogManager.shared.log(category: .alarm, message: "AlarmSound - rate value: \(audioPlayer!.rate)")
                }
            } else {
                LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed to play")
            }

        } catch {
            LogManager.shared.log(category: .alarm, message: "AlarmSound - unable to play sound; error: \(error)")
        }
    }

    static func play(repeating: Bool, delay: Int = 0) {
        guard !isPlaying else {
            return
        }

        // If repeating with delay, we'll handle it manually via the delegate
        // Only set repeatDelay if both repeating and delay > 0
        repeatDelay = (repeating && delay > 0) ? TimeInterval(delay) : 0

        do {
            audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
            audioPlayer!.delegate = audioPlayerDelegate

            activateAudioSessionWithFallback()

            // Only use numberOfLoops if we're not using delay-based repeating
            // When repeatDelay > 0, we play once and then use the delegate to schedule the next play with delay
            audioPlayer!.numberOfLoops = (repeating && repeatDelay == 0) ? -1 : 0

            // Store existing volume
            if systemOutputVolumeBeforeOverride == nil {
                systemOutputVolumeBeforeOverride = AVAudioSession.sharedInstance().outputVolume
            }

            if !audioPlayer!.prepareToPlay() {
                LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed preparing to play")
            }

            // First sound plays immediately - delay only applies between repeated sounds
            if audioPlayer!.play() {
                if !isPlaying {
                    LogManager.shared.log(category: .alarm, message: "AlarmSound - not playing after calling play (rate \(audioPlayer!.rate))")
                } else {
                    Observable.shared.alarmSoundPlaying.value = true
                    if repeatDelay > 0 {
                        LogManager.shared.log(category: .alarm, message: "AlarmSound - first sound playing immediately, delay (\(repeatDelay)s) will apply between repeats")
                    }
                }
            } else {
                LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed to play")
            }

            if Storage.shared.alarmConfiguration.value.overrideSystemOutputVolume {
                MPVolumeView.setVolume(Storage.shared.alarmConfiguration.value.forcedOutputVolume)
            }
        } catch {
            LogManager.shared.log(category: .alarm, message: "AlarmSound - unable to play sound; error: \(error)")
        }
    }

    fileprivate static func playNextWithDelay() {
        guard repeatDelay > 0 else {
            return
        }

        DispatchQueue.main.asyncAfter(deadline: .now() + repeatDelay) {
            // Check if we should still be playing (user might have stopped it)
            guard repeatDelay > 0 else {
                return
            }

            // Clean up the previous player
            audioPlayer?.stop()
            audioPlayer = nil

            do {
                audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
                audioPlayer!.delegate = audioPlayerDelegate

                activateAudioSessionWithFallback()

                audioPlayer!.numberOfLoops = 0

                if !audioPlayer!.prepareToPlay() {
                    LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed preparing to play (delayed repeat)")
                }

                if audioPlayer!.play() {
                    Observable.shared.alarmSoundPlaying.value = true
                } else {
                    LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed to play (delayed repeat)")
                }
            } catch {
                LogManager.shared.log(category: .alarm, message: "AlarmSound - unable to play sound (delayed repeat); error: \(error)")
            }
        }
    }

    static func playTerminated() {
        guard !isPlaying else {
            return
        }

        do {
            audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
            audioPlayer!.delegate = audioPlayerDelegate

            activateAudioSessionWithFallback()

            // Play endless loops
            audioPlayer!.numberOfLoops = 2

            // Store existing volume
            if systemOutputVolumeBeforeOverride == nil {
                systemOutputVolumeBeforeOverride = AVAudioSession.sharedInstance().outputVolume
            }

            if !audioPlayer!.prepareToPlay() {
                LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - audio player failed preparing to play")
            }

            if audioPlayer!.play() {
                if !isPlaying {
                    LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - not playing after calling play")
                    LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - rate value: \(audioPlayer!.rate)")
                }
            } else {
                LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - audio player failed to play")
            }

            MPVolumeView.setVolume(1.0)

        } catch {
            LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - unable to play sound; error: \(error)")
        }
    }

    fileprivate static func restoreSystemOutputVolume() {
        guard Storage.shared.alarmConfiguration.value.overrideSystemOutputVolume else {
            return
        }

        // cancel any volume change observations
        // self.volumeChangeDetector.isActive = false

        // restore system output volume with its value before overriding it
        if let volumeBeforeOverride = systemOutputVolumeBeforeOverride {
            MPVolumeView.setVolume(volumeBeforeOverride)
        }

        systemOutputVolumeBeforeOverride = nil
    }

    // Background activation of a non-mixable .playback session is denied by iOS
    // (cannotInterruptOthers, 560557684) unless the app is already actively playing
    // audio. In foreground, or with Silent Tune holding a mixable session alive,
    // options: [] succeeds and lets the alarm dominate other audio. For
    // Bluetooth-heartbeat users with no Silent Tune we skip [] (it would always
    // be denied) and ladder through mixable options so activation is still
    // permitted from background. Each attempt is logged so we can see in the
    // field which fallback (if any) the user landed on.
    fileprivate static func activateAudioSessionWithFallback() {
        let isBackgroundWithoutSilentTune = UIApplication.shared.applicationState == .background
            && Storage.shared.backgroundRefreshType.value != .silentTune

        let dominate: (label: String, options: AVAudioSession.CategoryOptions) = ("[]", [])
        let duck: (label: String, options: AVAudioSession.CategoryOptions) = (".duckOthers", .duckOthers)
        let mix: (label: String, options: AVAudioSession.CategoryOptions) = (".mixWithOthers", .mixWithOthers)

        let candidates = isBackgroundWithoutSilentTune ? [duck, mix] : [dominate, duck, mix]
        for candidate in candidates {
            do {
                try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: candidate.options)
                try AVAudioSession.sharedInstance().setActive(true)
                LogManager.shared.log(category: .alarm, message: "AlarmSound - audio session active (options: \(candidate.label))")
                return
            } catch {
                let nsError = error as NSError
                LogManager.shared.log(category: .alarm, message: "AlarmSound - audio session activation failed (options: \(candidate.label)) [code \(nsError.code)]: \(error.localizedDescription)")
            }
        }
        LogManager.shared.log(category: .alarm, message: "AlarmSound - all audio session option fallbacks exhausted")
    }
}

class AudioPlayerDelegate: NSObject, AVAudioPlayerDelegate {
    /* audioPlayerDidFinishPlaying:successfully: is called when a sound has finished playing. This method is NOT called if the player is stopped due to an interruption. */
    func audioPlayerDidFinishPlaying(_: AVAudioPlayer, successfully flag: Bool) {
        LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerDidFinishPlaying (\(flag))", isDebug: true)

        // If we're repeating with delay, schedule the next play
        if AlarmSound.repeatDelay > 0 {
            AlarmSound.playNextWithDelay()
        } else {
            Observable.shared.alarmSoundPlaying.value = false
        }
    }

    /* if an error occurs while decoding it will be reported to the delegate. */
    func audioPlayerDecodeErrorDidOccur(_: AVAudioPlayer, error: Error?) {
        if let error = error {
            LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerDecodeErrorDidOccur: \(error)")
        } else {
            LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerDecodeErrorDidOccur")
        }
    }

    /* AVAudioPlayer INTERRUPTION NOTIFICATIONS ARE DEPRECATED - Use AVAudioSession instead. */

    /* audioPlayerBeginInterruption: is called when the audio session has been interrupted while the player was playing. The player will have been paused. */
    func audioPlayerBeginInterruption(_: AVAudioPlayer) {
        LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerBeginInterruption")
        Observable.shared.alarmSoundPlaying.value = false
    }

    /* audioPlayerEndInterruption:withOptions: is called when the audio session interruption has ended and this player had been interrupted while playing. */
    /* Currently the only flag is AVAudioSessionInterruptionFlags_ShouldResume. */
    func audioPlayerEndInterruption(_: AVAudioPlayer, withOptions flags: Int) {
        LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerEndInterruption withOptions: \(flags)")
        Observable.shared.alarmSoundPlaying.value = false
    }
}

// Helper function inserted by Swift 4.2 migrator.
private func convertFromAVAudioSessionCategory(_ input: AVAudioSession.Category) -> String {
    return input.rawValue
}

extension MPVolumeView {
    static func setVolume(_ volume: Float) {
        // Need to use the MPVolumeView in order to change volume, but don't care about UI set so frame to .zero
        let volumeView = MPVolumeView(frame: .zero)
        // Search for the slider
        let slider = volumeView.subviews.first(where: { $0 is UISlider }) as? UISlider
        // Update the slider value with the desired volume.
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01) {
            slider?.value = volume
        }
        // Optional - Remove the HUD
        let activeWindow = UIApplication.shared.connectedScenes
            .compactMap { $0 as? UIWindowScene }
            .first { $0.activationState == .foregroundActive }?
            .windows.first(where: \.isKeyWindow)
        if let window = activeWindow {
            volumeView.alpha = 0.000001
            window.addSubview(volumeView)
        }
    }
}
