using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
using UnityEngine.Audio;

//인스펙터에서 구역별 BGM을 매핑하기 위한 구조체
[Serializable]
public struct ZoneBgm
{
    public Zone Zone;
    public AudioClip Clip;
}

public class SoundManager : MonoBehaviour
{
    public static SoundManager Instance;

    [Header("Mixer")]
    [SerializeField] private AudioMixer _mainMixer; //메인 믹서 하나
    [SerializeField] private AudioMixerGroup _bgmGroup;
    [SerializeField] private AudioMixerGroup _sfxGroup;

    [Header("BGM")]
    [SerializeField] private AudioSource _bgmSource; //BGM 전용 단일 오디오 소스
    [SerializeField, Range(0f, 1f)] private float _bgmVolume = 1f;
    [SerializeField] private float _bgmFadeDuration = 1.5f;
    [SerializeField] private List<ZoneBgm> _zoneBgmList = new();

    [Header("SFX Pool")]
    [SerializeField] private int _sfxPoolSize = 10; //시작 시 미리 생성할 SFX 소스 개수

    [Header("Snapshots")]
    [SerializeField] private AudioMixerSnapshot _normalSnapshot;   //평상시 믹서 상태
    [SerializeField] private AudioMixerSnapshot _minigameSnapshot; //미니게임 중 BGM·환경음을 낮춘 상태
    [SerializeField] private float _snapshotFadeDuration = 0.5f;   //스냅샷 전환 페이드 시간

    //구역 → BGM 빠른 조회용 (런타임 변환)
    private readonly Dictionary<Zone, AudioClip> _zoneBgmMap = new();

    //SFX 풀 - 대기중인 소스
    private readonly Queue<AudioSource> _sfxPool = new();

    //BGM 전환(페이드)을 취소하기 위한 토큰. 구역을 빠르게 오갈 때 이전 전환을 취소함
    private CancellationTokenSource _bgmCts;

    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this; //만들어진 자신을 인스턴스로 설정
            DontDestroyOnLoad(gameObject);

            Initialize();
        }
        else
        {
            Destroy(gameObject); //이미 인스턴스가 있으면 자신을 파괴
        }
    }

    private void Initialize()
    {
        //구역별 BGM 매핑을 딕셔너리로 변환
        foreach (var entry in _zoneBgmList)
        {
            _zoneBgmMap[entry.Zone] = entry.Clip;
        }

        //BGM 소스 기본 설정
        if (_bgmSource != null)
        {
            _bgmSource.outputAudioMixerGroup = _bgmGroup;
            _bgmSource.playOnAwake = false;
            _bgmSource.loop = true;
        }

        //SFX 풀 미리 생성
        for (int i = 0; i < _sfxPoolSize; i++)
        {
            _sfxPool.Enqueue(CreateSfxSource());
        }
    }

    //모든 매니저의 Awake가 끝난 뒤 이벤트를 구독한다
    private void Start()
    {
        if (Instance != this) return; //중복 인스턴스로 파괴 예정이면 구독하지 않음

        if (GameManager.Instance != null)
        {
            GameManager.Instance.OnZoneChanged += HandleZoneChanged;
        }
    }

    private void OnDestroy()
    {
        if (GameManager.Instance != null)
        {
            GameManager.Instance.OnZoneChanged -= HandleZoneChanged;
        }
    }

    //=========================== BGM ===========================

    //구역이 바뀌면 호출됨 (GameManager.OnZoneChanged 구독)
    private void HandleZoneChanged(Zone zone)
    {
        AudioClip clip = _zoneBgmMap.GetValueOrDefault(zone); //없으면 null → 무음 처리

        //이전 전환이 진행중이면 취소
        _bgmCts?.Cancel();
        _bgmCts?.Dispose();
        _bgmCts = CancellationTokenSource.CreateLinkedTokenSource(this.destroyCancellationToken);

        _ = ChangeBGM(clip, _bgmCts.Token);
    }

    //페이드아웃 → 클립 교체 → 페이드인 (단일 소스라 순차 진행)
    private async Awaitable ChangeBGM(AudioClip clip, CancellationToken token)
    {
        try
        {
            //현재 재생중이면 먼저 페이드아웃
            if (_bgmSource.isPlaying)
            {
                await BGMFade(_bgmSource.volume, 0f, token);
                _bgmSource.Stop();
            }

            if (clip == null) return; //해당 구역에 BGM이 없으면 무음 유지

            //새 BGM 페이드인
            _bgmSource.clip = clip;
            _bgmSource.volume = 0f;
            _bgmSource.Play();
            await BGMFade(0f, _bgmVolume, token);
        }
        catch (OperationCanceledException)
        {
            //구역이 다시 바뀌어 전환이 취소됨 - 정상 흐름
        }
    }

    //BGM 소스 볼륨을 from→to로 부드럽게 이동
    private async Awaitable BGMFade(float from, float to, CancellationToken token)
    {
        float t = 0f;
        while (t < _bgmFadeDuration)
        {
            t += Time.deltaTime;
            _bgmSource.volume = Mathf.Lerp(from, to, t / _bgmFadeDuration);
            await Awaitable.NextFrameAsync(token);
        }
        _bgmSource.volume = to;
    }

    //=========================== SFX ===========================

    //2D SFX 재생 (UI 사운드 등 위치 무관)
    public void PlaySFX(AudioClip clip, float volume = 1f)
    {
        if (clip == null) return;

        AudioSource source = GetSfxSource();
        source.transform.localPosition = Vector3.zero;
        source.spatialBlend = 0f; //2D
        Play(source, clip, volume);
        _ = ReturnAfterPlay(source, clip.length); //재생이 끝나면 풀에 반납
    }

    //3D SFX 재생 (VR 공간음향 - 특정 위치에서 들림)
    public void PlaySFXAt(AudioClip clip, Vector3 position, float volume = 1f)
    {
        if (clip == null) return;

        AudioSource source = GetSfxSource();
        source.transform.position = position;
        source.spatialBlend = 1f; //3D
        Play(source, clip, volume);
        _ = ReturnAfterPlay(source, clip.length); //재생이 끝나면 풀에 반납
    }

    private void Play(AudioSource source, AudioClip clip, float volume)
    {
        source.clip = clip;
        source.volume = volume;
        source.Play();
    }

    //풀에서 사용 가능한 SFX 소스를 가져옴 (없으면 새로 생성 - 자동 증설)
    private AudioSource GetSfxSource()
    {
        AudioSource source = _sfxPool.Count > 0 ? _sfxPool.Dequeue() : CreateSfxSource();
        source.gameObject.SetActive(true);
        return source;
    }

    //재생 길이만큼 대기 후 소스를 풀로 반납
    private async Awaitable ReturnAfterPlay(AudioSource source, float duration)
    {
        try
        {
            await Awaitable.WaitForSecondsAsync(duration, this.destroyCancellationToken);
        }
        catch (OperationCanceledException)
        {
            return; //매니저 파괴 시 종료
        }

        source.Stop();
        source.clip = null;
        source.gameObject.SetActive(false);
        _sfxPool.Enqueue(source);
    }

    //SFX용 AudioSource를 자식 오브젝트로 생성
    private AudioSource CreateSfxSource()
    {
        GameObject go = new("SFX_Source");
        go.transform.SetParent(transform);
        go.SetActive(false);

        AudioSource source = go.AddComponent<AudioSource>();
        source.outputAudioMixerGroup = _sfxGroup;
        source.playOnAwake = false;
        return source;
    }

    //=========================== Mixer ===========================

    //믹서 노출 파라미터로 볼륨 조절 (0~1 → dB 변환)
    public void SetVolume(string exposedParam, float normalized)
    {
        //0에 가까우면 -80dB(무음), 그 외에는 로그 스케일로 변환
        float db = normalized <= 0.0001f ? -80f : Mathf.Log10(normalized) * 20f;
        _mainMixer.SetFloat(exposedParam, db);
    }

    //=========================== Snapshot ===========================

    //미니게임 시작: BGM·환경음을 낮춘 스냅샷으로 페이드 전환
    public void EnterMinigameMode()
    {
        if (_minigameSnapshot != null) _minigameSnapshot.TransitionTo(_snapshotFadeDuration);
    }

    //미니게임 종료: 평상시 스냅샷으로 복귀
    public void ExitMinigameMode()
    {
        if (_normalSnapshot != null) _normalSnapshot.TransitionTo(_snapshotFadeDuration);
    }
}
