using UnityEngine;

public class BeatClock : MonoBehaviour
{
    [Header("Reference")] 
    [SerializeField] private MusicManager _musicManager;
    [Header("Audio")]
    [SerializeField] private AudioSource musicSource;

    [Header("Beat Settings")]
    [Tooltip("Song tempo in beats per minute.")]
    [SerializeField] private double bpm = 130.0;

    [Tooltip("Seconds from song start until the FIRST beat you want to count (usually near 0, but not always).")]
    [SerializeField] private double firstBeatOffsetSec = 0.5;

    [Header("Judgement Windows (seconds)")]
    [SerializeField] private double perfectWindow = 0.120; // 120 ms
    [SerializeField] private double goodWindow    = 0.160; // 160 ms

    public enum BeatResult { Miss, Good, Perfect }

    public double SongStartDspTime { get; private set; }
    public bool IsPlaying { get; private set; }

    private double SecPerBeat => 60.0 / bpm;

    private void Awake()
    {
        _musicManager = GetComponent<MusicManager>();
        
        if (musicSource == null)
        {
            Debug.LogError($"{nameof(BeatClock)}: Music AudioSource not assigned.");
        }
    }

    private void OnEnable()
    {
        _musicManager.onMusicChanged += OnSoundtrackChanged;
    }

    private void OnDisable()
    {
        _musicManager.onMusicChanged -= OnSoundtrackChanged;
    }

    private void OnSoundtrackChanged(AudioSource currentMusicSource)
    {
        musicSource = currentMusicSource;
    }
    
    /// <summary>
    /// Schedules music to start precisely. Call this when you enter the level / start gameplay.
    /// </summary>
    public void StartMusicScheduled(double leadInSeconds = 0.1)
    {
        if (musicSource == null || musicSource.clip == null)
        {
            Debug.LogError($"{nameof(BeatClock)}: Missing AudioSource or clip.");
            return;
        }

        // Ensure consistent behavior
        musicSource.Stop();

        SongStartDspTime = AudioSettings.dspTime + leadInSeconds;
        musicSource.PlayScheduled(SongStartDspTime);
        
        IsPlaying = true;
    }

    /// <summary>
    /// Song time in seconds since scheduled start.
    /// </summary>
    public double GetSongTime()
    {
        if (!IsPlaying) return 0.0;
        return AudioSettings.dspTime - SongStartDspTime;
    }

    /// <summary>
    /// Returns the DSP time RIGHT NOW (useful for stamping inputs).
    /// </summary>
    public static double NowDspTime() => AudioSettings.dspTime;

    /// <summary>
    /// Returns the nearest beat index to a given song time.
    /// Beat index 0 corresponds to firstBeatOffsetSec.
    /// </summary>
    public long GetNearestBeatIndex(double songTime)
    {
        // Shift so beat 0 happens at firstBeatOffsetSec
        double t = songTime - firstBeatOffsetSec;
        if (t < 0) return 0;
        
        return (long)System.Math.Round(t / SecPerBeat);
    }

    /// <summary>
    /// Returns the exact time (in song seconds) when beatIndex occurs.
    /// </summary>
    public double GetBeatTime(long beatIndex)
    {
        return firstBeatOffsetSec + beatIndex * SecPerBeat;
    }

    /// <summary>
    /// Evaluate a hit occurring at a given DSP time (recommended: use input DSP time).
    /// </summary>
    public BeatResult JudgeAtDspTime(double eventDspTime, out double signedErrorSec)
    {
        if (!IsPlaying)
        {
            signedErrorSec = double.MaxValue;
            return BeatResult.Miss;
        }

        // Convert DSP time -> song time
        double songTime = eventDspTime - SongStartDspTime;

        long nearestBeat = GetNearestBeatIndex(songTime);
        double nearestBeatTime = GetBeatTime(nearestBeat);

        signedErrorSec = songTime - nearestBeatTime;
        double absErr = System.Math.Abs(signedErrorSec);

        if (absErr <= perfectWindow) return BeatResult.Perfect;
        if (absErr <= goodWindow)    return BeatResult.Good;
        return BeatResult.Miss;
    }

    /// <summary>
    /// Convenience: Judge "right now".
    /// </summary>
    public BeatResult JudgeNow(out double signedErrorSec)
    {
        return JudgeAtDspTime(AudioSettings.dspTime, out signedErrorSec);
    }
    
    // -----------------------// -----------------------// -----------------------// -----------------------// -----------------------// -----------------------// -----------------------
    // ----------------------- Added for BPM HUD UI// -----------------------// -----------------------
    [SerializeField] private int beatsPerBar = 4; // usually 4/4

    public int BeatsPerBar => beatsPerBar;

    public long GetCurrentBeatIndex()
    {
        double songTime = GetSongTime();
        return GetNearestBeatIndex(songTime);
    }

    public int GetBeatInBar(long beatIndex)
    {
        // 1..beatsPerBar
        return (int)(beatIndex % beatsPerBar) + 1;
    }

    public long GetBarIndex(long beatIndex)
    {
        // 0-based bar index
        return beatIndex / beatsPerBar;
    }

    /// <summary>
    /// Returns time (song seconds) until the next beat (>= 0).
    /// </summary>
    public double GetTimeToNextBeat()
    {
        double songTime = GetSongTime();
        long nearest = GetNearestBeatIndex(songTime);

        // Determine next beat time (not nearest)
        double nearestTime = GetBeatTime(nearest);

        // If we're past the nearest beat, next = nearest+1; else next = nearest
        long nextBeat = (songTime > nearestTime) ? nearest + 1 : nearest;
        double nextBeatTime = GetBeatTime(nextBeat);

        return System.Math.Max(0.0, nextBeatTime - songTime);
    }

}
