using Firebase.Database;
using Firebase.Extensions;
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using Newtonsoft.Json;
using System.Linq;
using System.Collections;
using UnityEngine.Localization.Settings;
using UnityEngine.Localization.Tables;

/// <summary>
/// This is the game state manager on the PC side
/// </summary>
public class RoomManager : MonoBehaviour
{
    public static RoomManager Instance;

    [Header("Waiting For Players Page")]
    public GameObject waitingForPlayersPage;
    /// <summary>
    /// TextMeshPro that show the value of the current rooom code
    /// </summary>
    public TextMeshProUGUI roomCodeLabel;
    public AudioClip playerJoinSFX;
    public List<PlayerSticker> playerStickers = new List<PlayerSticker>();

    [Header("Explanation Page")]
    public GameObject explanationPage;
    public TextMeshProUGUI explanationCounter;
    public float explanationTime;
    private DateTime endOfExplanationDate = DateTime.MinValue;
    public AudioClip counterSFX;

    [Header("Waiting For Proposition Page")]
    public WaitForPropositionsPage waitForPropositionsPage;

    [Header("Waiting For Proposition Page")]
    public VotingPage votingPage;

    [Header("Scoring Page")]
    public ScoringPage scoringPage;

    [Header("Other")]
    public PromptList promptList;

    public Room myRoom { get; private set; } = null;

    public event Action OnRoomCreated;

    DatabaseReference realtimeDB;

    #region Unity Methods

    private void Awake()
    {
        Instance = this;
        FirebaseInitializer.Instance.onFirebaseReady += Initialize;
    }

    private void Start()
    {
        InitializeFirstPage();
    }

    private void Update()
    {
        if (myRoom == null)
            return;

        //While Explanation State
        if (myRoom.currentState == (int)GameState.Explanation && endOfExplanationDate != DateTime.MinValue)
        {
            TimeSpan duration = endOfExplanationDate - DateTime.Now;
            explanationCounter.text = ((int)duration.TotalSeconds).ToString("D1");


            if (duration.TotalMilliseconds <= 0)
            {
                SendRoomState(GameState.MakeProposition);
            }
        }

    }

    private bool deleteRoomFilesCompleted = false;
    private bool deleteRealtimeDBCompleted = false;

    private void OnApplicationQuit()
    {
        StartCoroutine(Coroutine_ClearCurrentRoom(() =>
        {
            Debug.Log("You can go. Bye bye...");
        }));
    }

    #endregion

    #region Public Methods

    /// <summary>
    /// Automatically called at start of game
    /// </summary>
    [ContextMenu("Create New Room")]
    public void CreateNewRoom()
    {
        WhichCodesAreAlreadyUsed(codes =>
        {
            Room newRoom = new Room(GenerateRandomAvailableCode(codes).ToString("D4"));
            myRoom = newRoom;
            try
            {
                string JSON = JsonConvert.SerializeObject(newRoom);

                realtimeDB.Child("rooms").Child(newRoom.code).SetRawJsonValueAsync(JSON).ContinueWithOnMainThread(task =>
                {
                    //then subscribe to it
                    realtimeDB.Child("rooms").Child(newRoom.code).ValueChanged += OnRoomUpdate;
                    roomCodeLabel.text = myRoom.code;
                    OnRoomCreated?.Invoke();
                    Debug.Log($"room {myRoom.code} has been created on the server");
                });
            }
            catch (Exception e)
            {
                Debug.LogException(e);
            }
        });
    }

    public void GeneratePrompts()
    {
        System.Random rnd = new();
        List<Prompt> prompts = promptList.prompts.OrderBy(x => rnd.Next()).Take(myRoom.players.Count()).ToList();
        List<Player> players = myRoom.players.Values.ToList().OrderBy(x => rnd.Next()).ToList();
        Dictionary<int, Question> questions = new();

        for (int i = 0; i < players.Count(); i++)
        {
            Dictionary<int, Proposition> propositions = new();

            for (int j = 0; j < 2; j++)
            {
                propositions.Add(j, new Proposition(players[i + j < players.Count() ? i + j : 0]));
            }

            questions.Add(i, new Question()
            {
                index = i,
                promptId = prompts[i].id,
                propositions = propositions,
                creationDate = DateTime.Now.ToOADate(),
            });
        }

        string JSON = JsonConvert.SerializeObject(questions);
        realtimeDB.Child("rooms").Child(myRoom.code).Child("questions").SetRawJsonValueAsync(JSON);
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="_language">can be "en" or "fr"</param>
    public void SetPromptsLanguage(string _language)
    {
        if (myRoom == null)
            return;

        if(_language == "en" ||_language == "fr")
            realtimeDB.Child("rooms").Child(myRoom.code).Child("promptsLanguage").SetValueAsync(_language);
    }

    #endregion

    #region Private Methods

    private void Initialize()
    {
        FirebaseInitializer.Instance.onFirebaseReady -= Initialize;
        realtimeDB = FirebaseDatabase.DefaultInstance.RootReference;
        Debug.Log("Realtime DB initialized");
        ClearOldAndCreateNewRoom();
    }


    private void InitializeFirstPage()
    {
        explanationPage.SetActive(false);
        waitForPropositionsPage.gameObject.SetActive(false);
        votingPage.gameObject.SetActive(false);
        scoringPage.gameObject.SetActive(false);

        waitingForPlayersPage.SetActive(true);
        ResetAllPlayerLabels();
    }

    private void SendRoomState(GameState _newState)
    {
        //Debug.Log($"sending to RTDB that we are now in the {_newState} state", this);
        realtimeDB.Child("rooms").Child(myRoom.code).Child("currentState").SetValueAsync((int)_newState);
    }

    private void ResetAllPlayerLabels()
    {
        string label = LanguageManager.Instance.currentStringTable.GetTable().GetEntry("ComputerView/Canvas/WaitingForPlayersPage/WaitingForP").LocalizedValue;
        for (int i = 0; i < playerStickers.Count; i++)
        {
            playerStickers[i].Initialize($"{label}{i + 1}", i);
        }
    }

    private void ClearOldAndCreateNewRoom()
    {
        CleanOldRooms();
        StartCoroutine(Coroutine_ClearCurrentRoom(() =>
        {
            CreateNewRoom();
        }));
    }

    private IEnumerator Coroutine_ClearCurrentRoom(Action callback_OnRoomClear = null)
    {
        if (myRoom == null || string.IsNullOrEmpty(myRoom.code))
        {
            yield return null;
            Debug.Log("There is no last room to clean", this);
            callback_OnRoomClear?.Invoke();
        }
        else
        {
            realtimeDB.Child("rooms").Child(myRoom.code).ValueChanged -= OnRoomUpdate;

            Debug.Log($"delete files of room {myRoom.code} from storage", this);
            StorageManager.Instance.DeleteFileOfRoom(myRoom, () =>
            {
                Debug.Log($"cleaning photo files of room({myRoom.code})finish", this);
                deleteRoomFilesCompleted = true;
            });

            Debug.Log($"delete realtimeDB room {myRoom.code}");
            realtimeDB.Child("rooms").Child(myRoom.code).RemoveValueAsync().ContinueWithOnMainThread(task =>
            {
                Debug.Log($"room's({myRoom.code}) data has been deleted for database", this);
                myRoom = null;
                deleteRealtimeDBCompleted = true;
            });

            while (!deleteRoomFilesCompleted || !deleteRealtimeDBCompleted)
            {
                yield return null; // Yield until both tasks are completed
            }
            callback_OnRoomClear?.Invoke();

            Debug.Log("Everything is clean.");
        }
    }


    /// <summary>
    /// Automatically called when something change in your room
    /// </summary>
    private void OnRoomUpdate(object sender, ValueChangedEventArgs value)
    {
        if (value.DatabaseError != null)
        {
            Debug.LogError(value.DatabaseError.Message);
            return;
        }
        if (value.Snapshot.Value == null)
        {
            Debug.Log("Trying to update room, but it's empty. Maybe you are exiting the app, so it's ok", this);
            return;
        }
        string JSON = value.Snapshot.GetRawJsonValue();
        Debug.Log($"your room has been updated :\n{JSON}");
        GameState lastState = (GameState)myRoom.currentState;
        try
        {
            myRoom = JsonConvert.DeserializeObject<Room>(JSON);
        }
        catch (Exception ex)
        {
            Debug.LogException(ex);
        }

        //this is done only when entering a new state
        if (lastState != (GameState)myRoom.currentState)
        {
            OnNewGameStateStarted();
        }
        else
        {
            //this is done each time something change
            switch (myRoom.currentState)
            {
                case (int)GameState.WaitingForOtherPlayersToJoin:
                    UpdateConnectedPlayerList(myRoom.GetPlayerList());
                    break;
                case (int)GameState.MakeProposition:
                    waitForPropositionsPage.OnRoomUpdate(myRoom);
                    break;
                default:
                    break;
            }

        }
    }



    /// <summary>
    /// Check all the rooms in the server and give back the number already taken
    /// </summary>
    private void WhichCodesAreAlreadyUsed(Action<List<int>> callback_OnCodesChecked)
    {
        QueryRoomsInDatabase(onlineRooms =>
        {
            List<int> alreadyUsedCodes = onlineRooms.Select(room => int.Parse(room.code)).ToList();
            Debug.Log($"Codes {string.Join(", ", alreadyUsedCodes)} are already used by other parties", this);
            callback_OnCodesChecked?.Invoke(alreadyUsedCodes);
        });
    }


    /// <summary>
    /// Generate a code between 0 and 1000 that is not in the list
    /// </summary>
    /// <param name="_impossibleCodes">the list of code you don"t want to get</param>
    /// <returns></returns>
    private int GenerateRandomAvailableCode(List<int> _impossibleCodes)
    {
        int random = UnityEngine.Random.Range(0, 1000);
        while (_impossibleCodes.Contains(random))
        {
            Debug.Log($"{random} is already taken, choosing another room code", this);
            random = UnityEngine.Random.Range(0, 1000);
        }
        return random;
    }

    /// <summary>
    /// Called when we enter in a new game state
    /// </summary>
    private void OnNewGameStateStarted()
    {
        switch (myRoom.currentState)
        {
            case (int)GameState.EnteringName: //if game has been reset by a player on mobile
                ClearOldAndCreateNewRoom();
                InitializeFirstPage();
                break;

            case (int)GameState.WaitingForOtherPlayersToJoin:
                Debug.Log("New State : WaitingForOtherPlayersToJoin");
                break;

            case (int)GameState.Explanation:
                Debug.Log("New State : Explanation");
                scoringPage.gameObject.SetActive(false); //if we come back from a new game
                waitingForPlayersPage.SetActive(false);
                explanationPage.SetActive(true);
                endOfExplanationDate = DateTime.Now.AddSeconds(explanationTime);
                AudioManager.Instance.PlaySFX(counterSFX);
                AudioManager.Instance.StopMusic();
                //generate all the questions during the explanation
                GeneratePrompts();
                break;

            case (int)GameState.MakeProposition:
                Debug.Log("New State : MakeProposition");
                AudioManager.Instance.ChangeMusic(MusicTitle.TakingPicture);
                waitForPropositionsPage.Initialize(myRoom, () => SendRoomState(GameState.MakeVote));
                explanationPage.SetActive(false);
                break;

            case (int)GameState.MakeVote:
                AudioManager.Instance.ChangeMusic(MusicTitle.VotingSession);
                votingPage.ShowVotingPage(
                    realtimeDB.Child("rooms").Child(myRoom.code)
                    , myRoom.players
                    , myRoom.questions
                    , () => SendRoomState(GameState.Score));
                break;

            case (int)GameState.Score:
                Debug.Log("It's score time !");
                scoringPage.Initialize(myRoom);
                break;

            default:
                break;
        }
    }

    /// <summary>
    /// Update the player labels on the WaitingForPlayer page
    /// </summary>
    /// <param name="_players"></param>
    private void UpdateConnectedPlayerList(List<Player> _players)
    {
        AudioManager.Instance.PlaySFX(playerJoinSFX);
        ResetAllPlayerLabels();

        //Debug.Log($"players count = {_players.Count}");
        List<Player> orderedPlayers = _players.OrderBy(x => x.creationDate).ToList();
        for (int i = 0; i < orderedPlayers.Count; i++)
        {
            Debug.Log($"player {i} = {orderedPlayers[i].name}");
            playerStickers[i].Initialize(orderedPlayers[i].name, i);
        }
    }

    private void QueryRoomsInDatabase(Action<List<Room>> callback)
    {
        //Debug.Log("Querying rooms from the database", this);
        realtimeDB.Child("rooms").GetValueAsync().ContinueWithOnMainThread(task =>
        {
            if (task.IsFaulted)
            {
                Debug.LogException(task.Exception);
                callback?.Invoke(new List<Room>());
                return;
            }

            DataSnapshot snapshot = task.Result;
            if (snapshot.Value != null)
            {
                string JSON = snapshot.GetRawJsonValue();
                Debug.Log($"Found some rooms:\n{JSON}", this);
                try
                {
                    Dictionary<string, Room> onlineRooms = JsonConvert.DeserializeObject<Dictionary<string, Room>>(JSON);
                    callback?.Invoke(onlineRooms.Values.ToList());
                }
                catch (Exception e)
                {
                    Debug.LogException(e);
                    Debug.Log($"Room {JSON} is broken, deleting it", this);
                    snapshot.Reference.RemoveValueAsync().ContinueWithOnMainThread(task =>
                    {
                        if (task.IsFaulted)
                        {
                            Debug.LogException(task.Exception);
                            return;
                        }
                        Debug.Log($"Broken room has been deleted. Checking again", this);
                        QueryRoomsInDatabase(callback);
                    });
                }
            }
            else
            {
                Debug.Log("No rooms found in the database", this);
                callback?.Invoke(new List<Room>());
            }
        });
    }


    private void CleanOldRooms()
    {
        QueryRoomsInDatabase(onlineRooms =>
        {
            DateTime oneHourLater = DateTime.Now.AddHours(1);

            foreach (Room r in onlineRooms)
            {
                if (DateTime.FromOADate(r.creationDate) < oneHourLater)
                {
                    Debug.Log($"Room {r.code} has been created at {DateTime.FromOADate(r.creationDate)} and it's more than one hour old.");
                    DeleteRoom(r.code);
                }
            }
        });
    }


    private void DeleteRoom(string _roomCode)
    {
        realtimeDB.Child("rooms").Child(_roomCode).RemoveValueAsync().ContinueWithOnMainThread(task =>
        {
            if (task.IsFaulted)
            {
                Debug.LogException(task.Exception);
                return;
            }
            Debug.Log($"Room {_roomCode} has been deleted");
        });
    }

    #endregion
}
