using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using DG.Tweening;
using NaughtyAttributes;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using TMPro;

enum LoadState
{
    Idle,
    FadeOut,
    UnityUnload,
    Cleanup,
    UnityLoad,
    SceneSetup,
    Sync,
    FadeIn,

    COUNT
}

public class Bootstrap : MonoBehaviour
{
    [Header( "Debug" )] 
    [SerializeField]
    private bool _debugMode;

    [Header( "Config" )] 
    [SerializeField] [Expandable]
    private BootstrapConfig _config;

    private static Bootstrap _instance;

    private LoadState _loadState;
    private Coroutine _currentSceneLoad;
    private SceneType _activeSceneType;

    private SceneLoader _activeScene;
    private string _activeSceneName;

    [Header( "References" )] 
    [SerializeField]
    private Image _fader;

    [SerializeField]
    private TMP_Text _debugDisplay;

    //private LevelDescriptor _activeLevelDescriptor;
    
    private List<LevelDescriptor> _loadedLevels = new List<LevelDescriptor>();
    
    private Queue<LevelDescriptor> _toLoadLevels = new Queue<LevelDescriptor>();
    private AsyncOperation _levelLoadAsync = null;
    
    private Queue<LevelDescriptor> _toUnloadLevels = new Queue<LevelDescriptor>();
    private AsyncOperation _levelUnloadAsync = null;

    public bool IsLoading => _loadState != LoadState.Idle;

    private void QueryLoadState()
    {
        if ( _activeScene != null && _activeScene.ToLoad != SceneType.None )
        {
            LoadScene( _activeScene.ToLoad );
        }
    }

    private void Awake()
    {
        _instance = this;
        DOTween.Init();

        if ( _debugMode )
        {
            StartCoroutine( DebugCR() );
        }
    }

    void Start()
    {
        LoadScene( SceneType.BootSequence );
    }

    public static void LoadScene( SceneType scene )
    {
        if ( _instance.IsLoading )
        {
            Debug.LogError( "[Bootstrap]: couldn't load scene: another load operation already active." );
            return;
        }

        _instance._currentSceneLoad = _instance.StartCoroutine( _instance.LoadSceneCR( scene ) );
    }

    public static void LoadLevel( LevelDescriptor descriptor )
    {
        _instance._toLoadLevels.Enqueue( descriptor );
    }

    private IEnumerator DebugCR()
    {
        LoadState loadState = LoadState.COUNT;
        StringBuilder sb = new StringBuilder();

        while ( true )
        {
            if ( _loadState != loadState )
            {
                loadState = _loadState;
                sb.Clear();

                sb.Append( "LoadState::" );
                sb.AppendLine( _loadState.ToString() );

                if ( _activeSceneName != null )
                {
                    sb.AppendFormat( "S: {0} ({1})", _activeSceneType, _activeSceneName );
                }
                else
                {
                    sb.AppendFormat( "S: [none]" );
                }

                sb.Append( "\n" );

                if ( (_loadedLevels?.Count ?? 0) > 0 )
                {
                    sb.Append( "L: " );
                    for ( int i = 0; i < _loadedLevels.Count; ++i )
                    {
                        sb.Append( _loadedLevels[ i ].LevelScene );
                        if ( i != _loadedLevels.Count - 1 )
                        {
                            sb.Append( ", " );
                        }
                    }
                }
                else
                {
                    sb.AppendFormat( "L: [none]" );
                }
                
                sb.Append( "\n" );
                sb.AppendFormat( "Load: {0} ({1} queued)\n", _levelLoadAsync?.isDone ?? true ? "idle" : "busy" , _toLoadLevels.Count );
                sb.AppendFormat( "Unload: {0} ({1} queued)\n", _levelUnloadAsync?.isDone ?? true ? "idle" : "busy" , _toUnloadLevels.Count );

                _debugDisplay.SetText( sb.ToString() );
            }

            yield return null;
        }
    }

    private bool ProcessLevelUnloads()
    {
        if (_levelUnloadAsync?.isDone ?? true)
        {
            while (true)
            {
                if ( _toUnloadLevels.Count == 0 )
                {
                    break;
                }
                
                var target = _toUnloadLevels.Dequeue();
                if ( _loadedLevels.Contains( target ) == false )
                {
                    continue;
                }
                
                _levelUnloadAsync = SceneManager.UnloadSceneAsync( target.LevelScene );
                _loadedLevels.Remove( target );
                return false;
            }
        }

        return (_levelUnloadAsync?.isDone ?? true) && _toUnloadLevels.Count == 0;
    }
    
    private bool ProcessLevelLoads()
    {
        if (_levelLoadAsync?.isDone ?? true)
        {
            while (true)
            {
                if ( _toLoadLevels.Count == 0 )
                {
                    break;
                }
                
                var target = _toLoadLevels.Dequeue();
                if ( _loadedLevels.Contains( target ) )
                {
                    continue;
                }
                
                _levelLoadAsync = SceneManager.LoadSceneAsync( target.LevelScene, LoadSceneMode.Additive );
                _loadedLevels.Add( target );
                return false;
            }
        }

        return (_levelLoadAsync?.isDone ?? true) && _toLoadLevels.Count == 0;
    }

    private IEnumerator LoadSceneCR( SceneType scene )
    {
        AsyncOperation asyncOp;

        // fade out
        _loadState = LoadState.FadeOut;
        _fader.CrossFadeAlpha( 1f, _config.TransitionDuration, true );
        yield return new WaitForSecondsRealtime( _config.TransitionDuration );

        // unload unity's scenes
        _loadState = LoadState.UnityUnload;

        if ( _activeSceneName != null )
        {
            // unload main scene
            asyncOp = SceneManager.UnloadSceneAsync( _activeSceneName );

            while ( asyncOp.isDone == false )
                yield return null;

            _activeScene = null;
            _activeSceneName = null;
        }

        // cleanup
        _loadState = LoadState.Cleanup;

        if ( scene == SceneType.Gameplay )
        {
            _toUnloadLevels.Clear();
            _toLoadLevels.Clear();
            _loadedLevels.Clear();
            _toLoadLevels.Enqueue( _config.StartLevel );
        }
        else
        {
            _toLoadLevels.Clear();
                
            for ( int i = 0; i < _loadedLevels.Count; ++i )
            {
                if ( _toUnloadLevels.Contains( _loadedLevels[ i ] ) == false )
                {
                    _toUnloadLevels.Enqueue( _loadedLevels[ i ] );
                }
            }
        }

        // 6 collections, as that is the deepest unity hierarchies go.
        for ( int i = 0; i < 6; ++i )
        {
            GC.Collect();
            yield return null;
        }

        // unity loading
        _loadState = LoadState.UnityLoad;
        _activeSceneType = scene;
        _activeSceneName = _config.GetScene( scene );
        
        asyncOp = SceneManager.LoadSceneAsync( _activeSceneName, LoadSceneMode.Additive );

        while ( asyncOp.isDone == false )
            yield return null;

        // setup
        _loadState = LoadState.SceneSetup;

        _activeScene = FindObjectOfType<SceneLoader>();

        while ( _activeScene.LoadComplete == false )
            yield return null;
        
        // sync
        _loadState = LoadState.Sync;
        while ( ProcessLevelUnloads() == false )
        {
            yield return null;
        }
        
        while ( ProcessLevelLoads() == false )
        {
            yield return null;
        }

        // fade in
        _loadState = LoadState.FadeIn;
        _fader.CrossFadeAlpha( 0f, _config.TransitionDuration, true );
        yield return new WaitForSecondsRealtime( _config.TransitionDuration );

        yield return null;
        _loadState = LoadState.Idle;
        _currentSceneLoad = null;
    }


    private void Update()
    {
        if ( !IsLoading )
        {
            QueryLoadState();

            if ( _activeSceneType == SceneType.Gameplay )
            {
                if ( ProcessLevelUnloads() )
                {
                    ProcessLevelLoads();
                }
            }
        }
    }
}