//
//  SoundManager.swift
//  CasinoKit
//
//  Manages game sound effects for casino games.
//

import AVFoundation
import AudioToolbox
import SwiftUI

/// Types of sound effects used in casino games.
public enum GameSound: String, CaseIterable, Sendable {
    case chipPlace = "chip_place"       // When placing a bet
    case chipStack = "chip_stack"       // Stacking multiple chips
    case cardDeal = "card_deal"         // Dealing a card
    case cardFlip = "card_flip"         // Flipping a card face up
    case cardShuffle = "card_shuffle"   // Shuffling the deck
    case win = "win"                    // Player wins
    case lose = "lose"                  // Player loses
    case push = "push"                  // Tie/push result
    case bigWin = "big_win"             // Large payout
    case buttonTap = "button_tap"       // UI button tap
    case newRound = "new_round"         // Starting a new round
    case clearBets = "clear_bets"       // Clearing all bets
    case gameOver = "game_over"         // Out of chips / game over
    
    /// File extension for the sound file.
    public var fileExtension: String { "mp3" }
    
    /// System sound ID to use as fallback when custom sound file is missing.
    /// These are built-in iOS sounds that approximate the intended effect.
    public var fallbackSystemSound: SystemSoundID {
        switch self {
        case .chipPlace:    return 1104  // Key press click
        case .chipStack:    return 1105  // Keyboard key
        case .cardDeal:     return 1306  // Swoosh
        case .cardFlip:     return 1104  // Click
        case .cardShuffle:  return 1110  // Swish
        case .win:          return 1025  // Success chime
        case .lose:         return 1053  // Error tone
        case .push:         return 1057  // Neutral beep
        case .bigWin:       return 1026  // Fanfare
        case .buttonTap:    return 1104  // Tap
        case .newRound:     return 1113  // Begin
        case .clearBets:    return 1155  // Sweep
        case .gameOver:     return 1073  // Sad trombone / failure
        }
    }
    
    /// Display name for settings.
    public var displayName: String {
        switch self {
        case .chipPlace: return "Chip Place"
        case .chipStack: return "Chip Stack"
        case .cardDeal: return "Card Deal"
        case .cardFlip: return "Card Flip"
        case .cardShuffle: return "Card Shuffle"
        case .win: return "Win"
        case .lose: return "Lose"
        case .push: return "Push/Tie"
        case .bigWin: return "Big Win"
        case .buttonTap: return "Button Tap"
        case .newRound: return "New Round"
        case .clearBets: return "Clear Bets"
        case .gameOver: return "Game Over"
        }
    }
}

/// Manages playing game sound effects for casino games.
/// 
/// Use the shared instance and configure it with your app's settings:
/// ```swift
/// SoundManager.shared.soundEnabled = settings.soundEnabled
/// SoundManager.shared.volume = settings.soundVolume
/// ```
@MainActor
@Observable
public final class SoundManager {
    
    // MARK: - Singleton
    
    public static let shared = SoundManager()
    
    // MARK: - Properties
    
    /// Whether sound effects are enabled.
    public var soundEnabled: Bool = true {
        didSet {
            UserDefaults.standard.set(soundEnabled, forKey: "casinokit.soundEnabled")
        }
    }
    
    /// Whether haptic feedback is enabled.
    public var hapticsEnabled: Bool = true {
        didSet {
            UserDefaults.standard.set(hapticsEnabled, forKey: "casinokit.hapticsEnabled")
        }
    }
    
    /// Master volume (0.0 to 1.0). This is the linear slider value.
    public var volume: Float = 1.0 {
        didSet {
            UserDefaults.standard.set(volume, forKey: "casinokit.soundVolume")
            updatePlayerVolumes()
        }
    }
    
    /// Perceived volume using an exponential curve.
    /// Human hearing is logarithmic, so linear volume feels wrong.
    /// This curve makes 50% on the slider sound like 50% to human ears.
    private var perceivedVolume: Float {
        // Using a power of 3 gives a natural-feeling curve
        // 0.0 -> 0.0, 0.5 -> 0.125, 1.0 -> 1.0
        pow(volume, 3)
    }
    
    /// The bundle to load sound files from. Defaults to CasinoKit's bundle.
    /// Set this to `.main` if sounds are in your app bundle instead.
    public var soundBundle: Bundle = .module
    
    /// Cache of audio players for quick playback.
    private var audioPlayers: [GameSound: AVAudioPlayer] = [:]
    
    // MARK: - Initialization
    
    private init() {
        // Load saved preferences
        soundEnabled = UserDefaults.standard.object(forKey: "casinokit.soundEnabled") as? Bool ?? true
        hapticsEnabled = UserDefaults.standard.object(forKey: "casinokit.hapticsEnabled") as? Bool ?? true
        volume = UserDefaults.standard.object(forKey: "casinokit.soundVolume") as? Float ?? 1.0
        
        // Configure audio session
        configureAudioSession()
        
        // Preload sounds
        preloadSounds()
    }
    
    // MARK: - Audio Session
    
    private func configureAudioSession() {
        do {
            try AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default)
            try AVAudioSession.sharedInstance().setActive(true)
        } catch {
            print("CasinoKit: Failed to configure audio session: \(error)")
        }
    }
    
    // MARK: - Preloading
    
    /// Preloads all sound files for faster playback.
    /// Call this after setting `soundBundle` if you're using a custom bundle.
    public func preloadSounds() {
        audioPlayers.removeAll()
        
        for sound in GameSound.allCases {
            if let player = createPlayer(for: sound) {
                audioPlayers[sound] = player
            }
        }
    }
    
    private func createPlayer(for sound: GameSound) -> AVAudioPlayer? {
        guard let url = soundBundle.url(
            forResource: sound.rawValue,
            withExtension: sound.fileExtension
        ) else {
            // Sound file not found - will use system sound fallback
            return nil
        }
        
        do {
            let player = try AVAudioPlayer(contentsOf: url)
            player.prepareToPlay()
            player.volume = perceivedVolume
            return player
        } catch {
            print("CasinoKit: Failed to create audio player for \(sound.rawValue): \(error)")
            return nil
        }
    }
    
    // MARK: - Playback
    
    /// Plays a sound effect.
    /// - Parameter sound: The sound to play.
    public func play(_ sound: GameSound) {
        guard soundEnabled else { return }
        
        // Try custom sound first
        if let player = audioPlayers[sound] {
            player.currentTime = 0
            player.play()
        } else {
            // Fall back to system sound
            playSystemSound(sound.fallbackSystemSound)
        }
    }
    
    /// Plays a system sound by ID.
    private func playSystemSound(_ soundID: SystemSoundID) {
        // Only play if volume is above threshold (system sounds can't be volume-adjusted)
        guard perceivedVolume > 0.01 else { return }
        AudioServicesPlaySystemSound(soundID)
    }
    
    /// Plays a sound effect with a delay.
    /// - Parameters:
    ///   - sound: The sound to play.
    ///   - delay: Delay in seconds before playing.
    public func play(_ sound: GameSound, delay: TimeInterval) {
        Task {
            try? await Task.sleep(for: .seconds(delay))
            play(sound)
        }
    }
    
    /// Plays multiple sounds in sequence.
    /// - Parameters:
    ///   - sounds: Array of sounds to play.
    ///   - interval: Time between each sound.
    public func playSequence(_ sounds: [GameSound], interval: TimeInterval = 0.1) {
        for (index, sound) in sounds.enumerated() {
            play(sound, delay: TimeInterval(index) * interval)
        }
    }
    
    // MARK: - Volume
    
    private func updatePlayerVolumes() {
        for player in audioPlayers.values {
            player.volume = perceivedVolume
        }
    }
    
    // MARK: - Haptics
    
    /// Plays a light haptic feedback.
    public func hapticLight() {
        guard hapticsEnabled else { return }
        let impact = UIImpactFeedbackGenerator(style: .light)
        impact.impactOccurred()
    }
    
    /// Plays a medium haptic feedback.
    public func hapticMedium() {
        guard hapticsEnabled else { return }
        let impact = UIImpactFeedbackGenerator(style: .medium)
        impact.impactOccurred()
    }
    
    /// Plays a heavy haptic feedback.
    public func hapticHeavy() {
        guard hapticsEnabled else { return }
        let impact = UIImpactFeedbackGenerator(style: .heavy)
        impact.impactOccurred()
    }
    
    /// Plays a success haptic notification.
    public func hapticSuccess() {
        guard hapticsEnabled else { return }
        let notification = UINotificationFeedbackGenerator()
        notification.notificationOccurred(.success)
    }
    
    /// Plays an error haptic notification.
    public func hapticError() {
        guard hapticsEnabled else { return }
        let notification = UINotificationFeedbackGenerator()
        notification.notificationOccurred(.error)
    }
    
    /// Plays a warning haptic notification.
    public func hapticWarning() {
        guard hapticsEnabled else { return }
        let notification = UINotificationFeedbackGenerator()
        notification.notificationOccurred(.warning)
    }
}

// MARK: - Convenience Extensions

public extension SoundManager {
    
    /// Plays chip placement sound with haptic.
    func playChipPlace() {
        play(.chipPlace)
        hapticLight()
    }
    
    /// Plays card deal sound.
    func playCardDeal() {
        play(.cardDeal)
    }
    
    /// Plays card flip sound with haptic.
    func playCardFlip() {
        play(.cardFlip)
        hapticLight()
    }
    
    /// Plays win sound with haptic.
    func playWin(isBigWin: Bool = false) {
        play(isBigWin ? .bigWin : .win)
        hapticSuccess()
    }
    
    /// Plays lose sound with haptic.
    func playLose() {
        play(.lose)
        hapticError()
    }
    
    /// Plays push/tie sound with haptic.
    func playPush() {
        play(.push)
        hapticMedium()
    }
    
    /// Plays game over sound with haptic.
    func playGameOver() {
        play(.gameOver)
        hapticError()
    }
    
    /// Plays new round sound with haptic.
    func playNewRound() {
        play(.newRound)
        hapticMedium()
    }
    
    /// Plays clear bets sound with haptic.
    func playClearBets() {
        play(.clearBets)
        hapticMedium()
    }
}

