using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UIElements;

/// <summary>
///     singleton for a single source of truth game state and flow management
/// </summary>
public class GameManager : MonoBehaviour
{
    /// <summary>
    ///     singleton pattern: define instance field for accessing the singleton elsewhere
    /// </summary>
    public static GameManager Instance;

    /// <summary>
    ///     ui manager object for handling ui state and flow
    /// </summary>
    public UIManager ui;

    /// <summary>
    ///     list of callbacks to call when the local player data changes
    /// </summary>
    private readonly List<Action<LocalPlayerData>> _onLocalPlayerDataChangeCallbacks = new();

    /// <summary>
    ///     the local player data object for storing player data
    /// </summary>
    private LocalPlayerData _data;

    /// <summary>
    ///     backend object for handling communication with the firebase backend
    /// </summary>
    public Backend Backend;

    /// <summary>
    ///     gameplay object for handling game loop
    /// </summary>
    public Gameplay Gameplay;

    /// <summary>
    ///     read-only property for accessing the local player data outside this class
    /// </summary>
    public LocalPlayerData Data => _data;

    /// <summary>
    ///     enforces singleton behaviour; sets doesn't destroy on load and checks for multiple instances
    /// </summary>
    private void Awake()
    {
        // check if instance hasn't been set yet
        if (Instance == null)
        {
            Debug.Log("awake as singleton instance, setting self as the forever-alive instance");
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
        // check if instance is already set and it's not this instance
        else if (Instance != null && Instance != this)
        {
            Debug.Log("awake as non-singleton instance, destroying self");
            Destroy(gameObject);
        }
    }

    /// <summary>
    ///     start modifying state
    /// </summary>
    private void Start()
    {
        Debug.Log("GameManager starts here");
        _data.LoadFromTheWorld(FireLocalPlayerDataChangeCallbacks);
    }

    /// <summary>
    ///     initialise variables and ui elements
    /// </summary>
    private void OnEnable()
    {
        ui = UIManager.Instance;

        // load the local player data and refresh the ui
        _data = new LocalPlayerData();

        Backend = new Backend();
        Backend.Initialise(status =>
        {
            Debug.Log("initialised backend, setting connection status text");

            ui.UI.Q<Label>("ConnectionStatusText").text = status switch
            {
                Backend.FirebaseConnectionStatus.Connected => "Status: Connected",
                Backend.FirebaseConnectionStatus.Updating => "Status: Updating... (Retrying in a bit!)",
                Backend.FirebaseConnectionStatus.NotConnected => "Status: Disconnected",
                Backend.FirebaseConnectionStatus.UpdateRequired =>
                    "Status: Disconnected (Device Component Update Required)",
                Backend.FirebaseConnectionStatus.ExternalError => "Status: Disconnected (External/Device Error)",
                Backend.FirebaseConnectionStatus.InternalError => "Status: Disconnected (Internal Error)",
                _ => "Status: Disconnected (unknown fcs state, this is unreachable and a bug)"
            };

            if (status == Backend.FirebaseConnectionStatus.Connected) return;

            // if we're not connected, hide any online 'features'
            ui.UI.Q<Button>("LeaderboardButton").style.display = DisplayStyle.None;
            ui.UI.Q<Button>("AccountButton").style.display = DisplayStyle.None;
        });

        // register a callback to refresh the ui when the player signs in
        Backend.RegisterOnSignInCallback(_ =>
        {
            Debug.Log("sign in callback, refreshing GameManager-controlled SideView UI");
            _data.LoadFromTheWorld(FireLocalPlayerDataChangeCallbacks);
        });

        Backend.RegisterOnConnectionStatusChangedCallback(status =>
        {
            Debug.Log($"post-fcStatus change, deciding to show/hide buttons based on new status: {status}");
            ui.UI.Q<Button>("LeaderboardButton").style.display =
                status == Backend.FirebaseConnectionStatus.Connected
                    ? DisplayStyle.Flex
                    : DisplayStyle.None;
            ui.UI.Q<Button>("AccountButton").style.display =
                status == Backend.FirebaseConnectionStatus.Connected
                    ? DisplayStyle.Flex
                    : DisplayStyle.None;
        });

        Gameplay = new Gameplay(ui.UI);
    }

    /// <summary>
    ///     called when the application is quitting, saves the local player data
    /// </summary>
    private void OnApplicationQuit()
    {
        Debug.Log("running deferred cleanup/save functions");
        Backend.Cleanup();
        _data.SaveToTheWorld();
    }

    /// <summary>
    ///     function to register a callback to be called when the local player data changes
    /// </summary>
    /// <param name="callback">callback function that takes a <c>LocalPlayerData</c> object</param>
    public void RegisterOnLocalPlayerDataChangeCallback(Action<LocalPlayerData> callback)
    {
        _onLocalPlayerDataChangeCallbacks.Add(callback);
        Debug.Log($"registering LocalPlayerDataChangeCallback ({_onLocalPlayerDataChangeCallbacks.Count})");
    }

    /// <summary>
    ///     function to fire all local player data change callbacks
    /// </summary>
    public void FireLocalPlayerDataChangeCallbacks(LocalPlayerData data)
    {
        Debug.Log($"firing LocalPlayerDataChangeCallbacks ({_onLocalPlayerDataChangeCallbacks.Count})");
        foreach (var callback in _onLocalPlayerDataChangeCallbacks)
            try
            {
                callback.Invoke(data);
            }
            catch (Exception e)
            {
                Debug.LogError($"error invoking LocalPlayerDataChangeCallback: {e.Message}");
            }
    }

    public void SignalGameEnd(List<Gameplay.RoundInfo> playedRounds)
    {
        Debug.Log("signalling game end");

        // calculate historical averages
        var historicalLightnessAcc = 0f;
        var historicalChromaAcc = 0f;
        var historicalHueAcc = 0f;
        var historicalRounds = 0;

        foreach (var localScore in _data.RecentLocalScores)
        {
            historicalLightnessAcc += localScore.AvgLightnessAccuracy;
            historicalChromaAcc += localScore.AvgChromaAccuracy;
            historicalHueAcc += localScore.AvgHueAccuracy;
            historicalRounds += localScore.NoOfRounds;
        }

        foreach (var onlineScore in _data.RecentOnlineScores)
        {
            historicalLightnessAcc += onlineScore.AvgLightnessAccuracy;
            historicalChromaAcc += onlineScore.AvgChromaAccuracy;
            historicalHueAcc += onlineScore.AvgHueAccuracy;
            historicalRounds += onlineScore.NoOfRounds;
        }

        foreach (var onlineScore in _data.BestOnlineScores)
        {
            historicalLightnessAcc += onlineScore.AvgLightnessAccuracy;
            historicalChromaAcc += onlineScore.AvgChromaAccuracy;
            historicalHueAcc += onlineScore.AvgHueAccuracy;
            historicalRounds += onlineScore.NoOfRounds;
        }

        historicalLightnessAcc /= historicalRounds;
        historicalChromaAcc /= historicalRounds;
        historicalHueAcc /= historicalRounds;

        // calculate round averages
        var gameLightnessAcc = 0d;
        var gameChromaAcc = 0d;
        var gameHueAcc = 0d;
        var gamePerceivedAcc = 0d;

        var templateColour = Color.clear;
        var responseColour = Color.clear;
        var roundNumber = 1;
        foreach (var distance in playedRounds.Take(Gameplay.RoundsPerGame).Select(round =>
                     Colorimetry.CalculateDistance(templateColour = round.TemplateColour,
                         responseColour = round.ResponseColour)))
        {
            var dLCh = Colorimetry.CalculateLChSimilarityPercentage(distance);

            Debug.Log(
                $"processing round: template={templateColour}, response={responseColour} (dL%={dLCh.L}, dC%={dLCh.C}, dh%={dLCh.h}, dEok={distance.dE:F})");

            var roundLightnessAcc = Math.Clamp(dLCh.L * 100d, 0d, 100d);
            var roundChromaAcc = Math.Clamp(dLCh.C * 100d, 0d, 100d);
            var roundHueAcc = Math.Clamp(dLCh.h * 100d, 0d, 100d);
            var roundPerceivedAcc = Math.Clamp((100d - distance.dE) * 100d, 0d, 100d);

            gameLightnessAcc += roundLightnessAcc;
            gameChromaAcc += roundChromaAcc;
            gameHueAcc += roundHueAcc;
            gamePerceivedAcc += roundPerceivedAcc;

            var showcaseTemplate = ui.UI.Q<VisualElement>($"ShowcasePair{roundNumber}TemplateColour");
            var showcaseResponse = ui.UI.Q<VisualElement>($"ShowcasePair{roundNumber}ResponseColour");
            var showcaseInfo = ui.UI.Q<Label>($"ShowcasePair{roundNumber}Info");

            if (!(showcaseTemplate == null || showcaseResponse == null || showcaseInfo == null))
            {
                showcaseTemplate.style.backgroundColor = templateColour;
                showcaseResponse.style.backgroundColor = responseColour;
                showcaseInfo.text =
                    $"{roundLightnessAcc:N0}% {roundChromaAcc:N0}% {roundHueAcc:N0}% ({roundPerceivedAcc:N0}%)";
            }
            else
            {
                Debug.LogError($"showcase pair {roundNumber} not found");
            }

            roundNumber++; // used for ui querying
        }

        gameLightnessAcc /= Gameplay.RoundsPerGame;
        gameChromaAcc /= Gameplay.RoundsPerGame;
        gameHueAcc /= Gameplay.RoundsPerGame;
        gamePerceivedAcc /= Gameplay.RoundsPerGame;

        // NOTE: this is NOT equiv to user rating, this is just a per-game accuracy score
        // all that math is done in LocalPlayerData.CalculateUserRating
        var gameAccuracy = (gameLightnessAcc + gameChromaAcc + gameHueAcc + gamePerceivedAcc) / 4;

        // make comparison texts
        var lAccDeltaText = (gameLightnessAcc > historicalLightnessAcc ? "+" : "-") +
                            Math.Abs(gameLightnessAcc - historicalLightnessAcc).ToString("F2");
        var cAccDeltaText = (gameChromaAcc > historicalChromaAcc ? "+" : "-") +
                            Math.Abs(gameChromaAcc - historicalChromaAcc).ToString("F2");
        var hAccDeltaText = (gameHueAcc > historicalHueAcc ? "+" : "-") +
                            Math.Abs(gameHueAcc - historicalHueAcc).ToString("F2");

        var score = new LocalPlayerData.Score(DateTime.Now,
            playedRounds.Count,
            (float)gameLightnessAcc,
            (float)gameChromaAcc,
            (float)gameHueAcc,
            (float)gamePerceivedAcc);

        var oldRating = _data.CalculateUserRating();
        _data.RegisterLocalScore(score);
        FireLocalPlayerDataChangeCallbacks(Instance.Data);

        Debug.Log("submitting score to backend");

        Backend.SubmitScore(score,
            submitRes =>
            {
                if (submitRes != Backend.DatabaseTransactionResult.Ok)
                {
                    Debug.Log("couldn't submit score");
                    TransitionToResultsView(_data.CalculateUserRating());
                    return;
                }

                Backend.CalculateUserRating((urcRes, userRating) =>
                {
                    if (urcRes != Backend.DatabaseTransactionResult.Ok)
                    {
                        Debug.Log("couldn't calculate user rating");
                        TransitionToResultsView(_data.CalculateUserRating());
                        FireLocalPlayerDataChangeCallbacks(Instance.Data);
                        return;
                    }

                    Backend.UpdateUserRating(updateRes =>
                    {
                        if (updateRes != Backend.DatabaseTransactionResult.Ok)
                        {
                            Debug.Log("calculated user rating but couldn't update it");
                            TransitionToResultsView(userRating);
                            return;
                        }

                        TransitionToResultsView(userRating);
                    });
                });
            });

        return;

        void TransitionToResultsView(float rating)
        {
            Debug.Log("signal GameManager-UIManager transition to results view");
            
            var ratingDifferenceDescriptor = oldRating > rating ? "decreased" : "increased";
            var ratingText = rating >= 0
                ? $"\nYour rating has {ratingDifferenceDescriptor} by {Math.Abs(rating - oldRating):F2}."
                : "\nYour rating could not be calculated.";

            // build the result text and show the results view
            ui.UI.Q<Label>("ResultsText").text = string.Join(Environment.NewLine, $"Over {playedRounds.Count} rounds,",
                $"you were {gameAccuracy} accurate.", "",
                $"Lightness was {gameLightnessAcc:F2}% accurate. ({lAccDeltaText} from your average)",
                $"Chroma was {gameChromaAcc:F2}% accurate. ({cAccDeltaText} from your average)",
                $"Hue was {gameHueAcc:F2}% accurate. ({hAccDeltaText} from your average)") + ratingText;

            ui.SetDisplayState(UIManager.DisplayState.ResultsView);
        }
    }
}