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

/// <summary>
///     singleton for a single source of truth game state and flow management
/// </summary>
public class GameManager : MonoBehaviour
{
    /// <summary>
    ///     enum for available menus in the game, for use with <c>ShowMenu()</c>
    /// </summary>
    public enum DisplayState
    {
        UnassociatedState, // initial state, then we transition to Nothing to initialise the ui
        Nothing,
        GameView,
        LeaderboardView,
        AccountView
    }

    /// <summary>
    ///     singleton pattern: define instance field for accessing the singleton elsewhere
    /// </summary>
    public static GameManager Instance;

    /// <summary>
    ///     the current display state of the game
    /// </summary>
    [SerializeField] private DisplayState state = DisplayState.UnassociatedState;

    /// <summary>
    ///     the game object for the ui
    /// </summary>
    [SerializeField] private GameObject uiGameObject;

    // /// <summary>
    // ///     callback functions to be invoked when the display state changes
    // /// </summary>
    // private readonly List<Action<DisplayState>> _onDisplayStateChange = new();

    /// <summary>
    ///     callback functions to be invoked when the local player data is updated
    /// </summary>
    private readonly List<Action<LocalPlayerData>> _onLocalPlayerDataUpdateCallbacks = new();

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

    /// <summary>
    ///     the visual element for the ui
    /// </summary>
    private VisualElement _ui;

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

    /// <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);
        }

        if (uiGameObject == null)
            throw new NullReferenceException("a reference UI GameObject is not set in the inspector");

        _ui = uiGameObject.GetComponent<UIDocument>()?.rootVisualElement;
        if (_ui == null)
            throw new NullReferenceException("could not grab the UIDocument in the reference UI GameObject");
    }

    /// <summary>
    ///     called before the first frame update
    /// </summary>
    private void Start()
    {
        // transition to the initial state
        SetDisplayState(DisplayState.Nothing);

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

            _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.Q<Button>("LeaderboardButton").style.display = DisplayStyle.None;
            _ui.Q<Button>("AccountButton").style.display = DisplayStyle.None;
        });

        // load the local player data and refresh the ui
        _data = new LocalPlayerData();
        _data.LoadFromTheWorld(data =>
        {
            foreach (var callback in _onLocalPlayerDataUpdateCallbacks) callback(data);
        });

        // register a callback to refresh the ui when the player signs in
        Backend.RegisterOnSignInCallback(_ =>
        {
            Debug.Log("post-auth callback, refreshing player data from the world");
            _data.LoadFromTheWorld(data =>
            {
                Debug.Log("firing lpdata update callbacks");
                foreach (var callback in _onLocalPlayerDataUpdateCallbacks) callback(data);
            });
        });
    }

    /// <summary>
    ///     called when the game object is disabled
    /// </summary>
    private void OnDestroy()
    {
        Backend.Cleanup();
    }

    // /// <summary>
    // ///     function to register a callback for when the display state changes
    // /// </summary>
    // /// <param name="callback">callback function that takes in a <c>DisplayState</c> enum</param>
    // public void RegisterOnDisplayStateChange(Action<DisplayState> callback)
    // {
    //     _onDisplayStateChange.Add(callback);
    // }

    /// <summary>
    ///     function to register a callback for when the local player data is updated
    /// </summary>
    /// <param name="callback">callback function that takes in a <c>LocalPlayerData</c> object</param>
    public void RegisterOnLocalPlayerDataUpdate(Action<LocalPlayerData> callback)
    {
        _onLocalPlayerDataUpdateCallbacks.Add(callback);
        Debug.Log(
            $"registering on lpdata update callback, there are now {_onLocalPlayerDataUpdateCallbacks.Count} callbacks");
    }

    /// <summary>
    ///     function to show a menu based on the enum passed,
    ///     and any other necessary actions
    /// </summary>
    /// <param name="newDisplayState">the game menu to show</param>
    public void SetDisplayState(DisplayState newDisplayState)
    {
        var currentDisplayState = state;

        // if the new state is the same as the current state, do nothing
        if (currentDisplayState == newDisplayState)
        {
            Debug.Log($"staying at {currentDisplayState} (illogical transition)");
            return;
        }

        Debug.Log($"switching from {currentDisplayState} to {newDisplayState}");

        var gameView = _ui.Q<VisualElement>("GameView");
        var leaderboardView = _ui.Q<VisualElement>("LeaderboardView");
        var accountView = _ui.Q<VisualElement>("AccountView");

        switch (newDisplayState)
        {
            case DisplayState.Nothing:
                gameView.style.display = DisplayStyle.None;
                leaderboardView.style.display = DisplayStyle.None;
                accountView.style.display = DisplayStyle.None;
                break;

            case DisplayState.GameView:
                gameView.style.display = DisplayStyle.Flex;
                leaderboardView.style.display = DisplayStyle.None;
                accountView.style.display = DisplayStyle.None;
                break;

            case DisplayState.LeaderboardView:
                gameView.style.display = DisplayStyle.None;
                leaderboardView.style.display = DisplayStyle.Flex;
                accountView.style.display = DisplayStyle.None;
                break;

            case DisplayState.AccountView:
                gameView.style.display = DisplayStyle.None;
                leaderboardView.style.display = DisplayStyle.None;
                accountView.style.display = DisplayStyle.Flex;
                break;

            case DisplayState.UnassociatedState:
            default:
                throw new ArgumentOutOfRangeException(nameof(newDisplayState), newDisplayState, null);
        }
    }
}