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

namespace mgaia.Utils.LayerSystem
{
	/// <summary>
	/// Classe que gerencia o sistema de layers (telas de interface) do jogo
	/// </summary>	
    public class LayerManager : MonoBehaviour
    {
        public delegate void LayerHandler(GameObject layer);

        /// <summary>
        /// Evento disparado quando uma layer é adicionada
        /// </summary>
        public event LayerHandler OnLayerAdd;

        public static LayerManager Instance { get; private set; }

        public static float RequiredOrtographicSize { get; private set; }
        
        public static int ScreenWidth
        { 
            get
            {
                if (!Application.isPlaying)
                {
                    return _sEditorWidth;
                }
                else
                {
                    return Screen.width;
                }
            }
        }

        public static int ScreenHeight
        { 
            get 
            {
                if (!Application.isPlaying)
                {
                    return _sEditorHeight;
                }
                else
                {
                    return Screen.height;
                }
            }
        }
        public static float NormalizedScreenHeight { get; private set; }
        public static float NormalizedScreenWidth { get; private set; }
        public static float ScreenHeightNormalizer { get; private set; }
        public static float ScreenWidthNormalizer { get; private set; }

        private const float BASE_HEIGHT = 200;
        private const float HEIGHT_INC = 10;
        private InputManager mInputManager = new InputManager();
        private List<GameObject> mLayersList = new List<GameObject>();
        private int mCurrentDepth = 0;
        private float mCurrentHeight = BASE_HEIGHT;
        private Dictionary<KeyValuePair<GameObject, bool>, List<LayerSlice>> mTransitioningLayers = new Dictionary<KeyValuePair<GameObject, bool>, List<LayerSlice>>();
        private List<GameObject> mRemovalQueue = new List<GameObject>();
        private List<GameObject> mLayerStack = new List<GameObject>();
        private bool _mListenInputs = true;
        private static int _sEditorWidth;
        private static int _sEditorHeight;

        public bool ListenInputs
        {
            get { return _mListenInputs; }
            set { _mListenInputs = value; }
        }

        static LayerManager()
        {
#if UNITY_EDITOR
            if (!Application.isPlaying)
            {
                float aspectRatio = 0;
                float width, height;

                EditorHelper.GetGameViewSize(out width, out height, out aspectRatio);

                _sEditorWidth = (int)width;
                _sEditorHeight = (int)height;
            }
#endif
        }

        public static void Initialize(int targetWidth, int targetHeight, float targetOrtographicSize)
        {
            GameObject layerMngObj = new GameObject("LayerManager");
            DontDestroyOnLoad(layerMngObj);
            Instance = layerMngObj.AddComponent<LayerManager>();

            CalculateRequiredOrtographicSize(targetWidth, targetHeight, targetOrtographicSize);
            CalculateNormalizedValues();
        }

        private static void CalculateRequiredOrtographicSize(int targetWidth, int targetHeight, float ortographicSize)
        {
            float heightRatio = (float)ScreenHeight / (float)targetHeight;
            float widthRatio = (float)ScreenWidth / (float)targetWidth;
            RequiredOrtographicSize = ortographicSize;
            if (heightRatio > widthRatio)
            {
                RequiredOrtographicSize *= heightRatio / widthRatio;
            }
        }

        private static void CalculateNormalizedValues()
        {
            float aspectRatio = (float)ScreenWidth / (float)ScreenHeight;
            NormalizedScreenHeight = 2 * RequiredOrtographicSize;
            NormalizedScreenWidth = NormalizedScreenHeight * aspectRatio;
            ScreenWidthNormalizer = NormalizedScreenWidth / (float)ScreenWidth;
            ScreenHeightNormalizer = NormalizedScreenHeight / (float)ScreenHeight;
        }

        public void RegisterInputReceivableOnTop(IInputReceivable receivable, bool inputShallNotPass, bool listenOnEffects = false)
        {
            mInputManager.RegisterInputReceivableOnTop(receivable, inputShallNotPass, listenOnEffects);
        }

        public void UnregisterInputReceivable(IInputReceivable receivable)
        {
            mInputManager.UnregisterInputReceivable(receivable);
        }

        public GameObject GetCurrentTopLayer()
        {
            int index = mLayersList.Count - 1;
            if (index >= 0)
            {
                return mLayersList[index];
            }
            return null;
        }

        public int GetCurrentMaxDepth()
        {
            return mLayerStack.Count;
        }

        public GameObject AddLayerOnTop(GameObject newLayerObj, bool inputShallNotPass, object dataPack = null, bool unstacking = false, bool listenOnEffects = false)
        {
            Camera newLayerCamera = newLayerObj.GetComponent<Camera>();
            newLayerCamera.enabled = true;
            Assert.AssertTrue(newLayerCamera != null, "The layer gameobject must have a camera attached to it");
            Assert.AssertTrue(newLayerCamera.isOrthoGraphic, "All layers' cameras must be ortographic");

            IInputReceivable inputReceivableBehaviour = newLayerObj.GetComponent<IInputReceivable>();

            newLayerCamera.orthographicSize = RequiredOrtographicSize * newLayerCamera.rect.height;
            newLayerCamera.clearFlags = CameraClearFlags.Depth;
            newLayerCamera.depth = mLayerStack.Count;
            newLayerObj.transform.position = new Vector3(0, BASE_HEIGHT + HEIGHT_INC * mLayerStack.Count, 0);
            mLayerStack.Add(newLayerObj);

            if (dataPack != null)
            {
                inputReceivableBehaviour.OnDataPackReceived(dataPack);
            }

            newLayerObj.GetComponentsInChildren<AnchoredPositioningBehaviour>(true).ForEach(anchor => anchor.Commit());

            newLayerObj.BroadcastMessage("OnLayerAdd", SendMessageOptions.DontRequireReceiver);

            GameObject[] layerSliceObjs = newLayerObj.transform.GetChildren().Select(transf => transf.gameObject).ToArray();
            Assert.AssertTrue(layerSliceObjs.TrueForAll(sliceObj => sliceObj.GetComponent<LayerSlice>() != null), "All direct sons of a layer must be layerslices");
            List<LayerSlice> layerSliceScripts = new List<LayerSlice>();
            layerSliceScripts.AddRange(layerSliceObjs.Select(sliceObj => sliceObj.GetComponent<LayerSlice>()));
            layerSliceScripts.ForEach(sliceScript => sliceScript.OnLayerPositionSet());
            mLayersList.Add(newLayerObj);

            mInputManager.RegisterInputReceivableOnTop(inputReceivableBehaviour, inputShallNotPass, listenOnEffects);
            OrganizeEffects(newLayerObj, false, unstacking);

            if (OnLayerAdd != null)
            {
                OnLayerAdd(newLayerObj);
            }

            return newLayerObj;
        }

        private bool IsLayerUnderAdding(GameObject layer)
        {
            return mTransitioningLayers.Keys.Any(key => key.Key == layer && !key.Value);
        }

        public void ClearAllLayers()
        {
            List<GameObject> layersToRemove = new List<GameObject>();

            foreach (GameObject layer in mLayersList)
            {
                IInputReceivable inputReceivable = layer.GetComponent<IInputReceivable>();
                if (inputReceivable.ShouldRemoveOnClean)
                {
                    layersToRemove.Add(layer);
                }
            }

            foreach (GameObject layer in layersToRemove)
            {
                RemoveLayer(layer, false, true);
            }

            List<KeyValuePair<GameObject, bool>> transitioningLayersToRemove = new List<KeyValuePair<GameObject, bool>>();

            foreach (KeyValuePair<GameObject, bool> transitioningLayer in mTransitioningLayers.Keys)
            {
                IInputReceivable inputReceivable = transitioningLayer.Key.GetComponent<IInputReceivable>();
                if (inputReceivable.ShouldRemoveOnClean)
                {
                    transitioningLayersToRemove.Add(transitioningLayer);
                }
            }

            foreach (KeyValuePair<GameObject, bool> transitioningLayer in transitioningLayersToRemove)
            {
                mTransitioningLayers.Remove(transitioningLayer);
            }
        }

        public void RemoveLayer(GameObject layer, bool unstackPrevious = false, bool removeAndDestroy = false)
        {
            int index = mLayersList.IndexOf(layer);
            RemoveLayer(index, unstackPrevious, removeAndDestroy);
        }

        public void RemoveTopLayer(bool unstackPrevious = false, bool destroy = false)
        {
            int index = mLayersList.Count - 1;
            RemoveLayer(index, unstackPrevious, destroy);
        }

        public void RemoveLayer(int index, bool unstackPrevious = false, bool removeAndDestroy = false)
        {
            Assert.AssertTrue(index >= 0, "Invalid layer index: " + index);
            Assert.AssertTrue(mLayersList.Count > 0, "There is no layer to be removed");
            GameObject toDestroyLayerObj = mLayersList[index];
            Assert.AssertTrue(toDestroyLayerObj.GetComponent<IInputReceivable>() != null, "This gameobject does not have an 'IInputReceivable' and so is not recognized as a layer");

            IInputReceivable inputReceivable = toDestroyLayerObj.GetComponent<IInputReceivable>();

            inputReceivable.RemoveAndDestroy = removeAndDestroy;

            if (IsLayerUnderAdding(toDestroyLayerObj))
            {
                mRemovalQueue.Add(toDestroyLayerObj);
            }
            else
            {
                mLayersList.RemoveAt(index);

                mInputManager.UnregisterInputReceivable(inputReceivable);
                if (removeAndDestroy)
                {
                    mCurrentDepth--;
                    mCurrentHeight -= HEIGHT_INC;
                }
                OrganizeEffects(toDestroyLayerObj, true, removeAndDestroy);
            }

            if (unstackPrevious)
            {
                AddLayerOnTop(mLayerStack[mLayerStack.IndexOf(toDestroyLayerObj) - 1], true, null, true);
            }

            if (inputReceivable.RemoveAndDestroy)
            {
                mLayerStack.Remove(toDestroyLayerObj);
            }
        }

        public void DestroyHiddenStackedLayers()
        {
            while (mLayerStack.Count > 1)
            {
                if (mLayersList.Contains(mLayerStack[0]))
                {
                    break;
                }
                MonoBehaviour.Destroy(mLayerStack[0]);
                mLayerStack.RemoveAt(0);
            }
        }

        private void OrganizeEffects(GameObject layerObj, bool removing, bool unstacking)
        {
            layerObj.GetComponent<IInputReceivable>().OnLayerEffectStarted(removing);
            GameObject[] layerSliceObjs = layerObj.transform.GetChildren().Select(transf => transf.gameObject).ToArray();
            Assert.AssertTrue(layerSliceObjs.TrueForAll(sliceObj => sliceObj.GetComponent<LayerSlice>() != null), "All direct sons of a layer must be layerslices");
            List<LayerSlice> layerSliceScripts = new List<LayerSlice>();
            layerSliceScripts.AddRange(layerSliceObjs.Select(sliceObj => sliceObj.GetComponent<LayerSlice>()));
            layerSliceScripts.Sort((layer1, layer2) =>
            {
                if (layer1.InsertionOrder < layer2.InsertionOrder) if (removing) return 1; else return -1;
                else if (layer1.InsertionOrder == layer2.InsertionOrder) return 0;
                else if (removing) return -1; else return 1;
            });
            layerSliceScripts.ForEach(slice => slice.Init(removing, unstacking));
            if (removing)
            {
                layerSliceScripts.ForEach(slice => slice.InsertionOrder = 1);
            }
            mTransitioningLayers.Add(new KeyValuePair<GameObject, bool>(layerObj, removing), layerSliceScripts);
            mInputManager.IsInputBlocked = mTransitioningLayers.Count != 0;
        }

        private void Update()
        {
            if (ListenInputs)
            {
                mInputManager.Update(Time.deltaTime);
            }

            KeyValuePair<GameObject, bool>[] keys = mTransitioningLayers.Keys.ToArray();

            for (int i = 0; i < keys.Length; i++)
            {
                KeyValuePair<GameObject, bool> layerObjKvp = keys[i];
                List<LayerSlice> layerSliceScripts = mTransitioningLayers[layerObjKvp];
                
                GameObject layerObj = layerObjKvp.Key;
                bool removing = layerObjKvp.Value;
                int currentPriority = layerSliceScripts[0].InsertionOrder;
                int j = 0;
                while (j < layerSliceScripts.Count && layerSliceScripts[j].InsertionOrder == currentPriority)
                {
                    if (layerSliceScripts[j].UpdateSlice(Time.deltaTime))
                    {
                        layerSliceScripts.RemoveAt(j);
                    }
                    j++;
                }
                if (layerSliceScripts.Count == 0)
                {
                    layerObj.GetComponent<IInputReceivable>().OnLayerEffectFinished(removing);
                    if (removing)
                    {
                        if (layerObj.GetComponent<IInputReceivable>().RemoveAndDestroy)
                        {
                            MonoBehaviour.Destroy(layerObj);
                        }
                        layerObj.GetComponent<Camera>().enabled = false;
                    }
                    mTransitioningLayers.Remove(layerObjKvp);
                    mInputManager.IsInputBlocked = mTransitioningLayers.Count != 0;
                    if (mRemovalQueue.Count > 0)
                    {
                        GameObject toRemoveLayer = mRemovalQueue[0];
                        mRemovalQueue.RemoveAt(0);
                        RemoveLayer(toRemoveLayer);
                    }
                }
            }
        }
    }
}