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

namespace mgaia.Utils.LayerSystem
{
	/// <summary>
	/// Classe que deve ser implementada por qualquer objeto de jogo que deseja receber o input controlado pelo InputManager.
	/// Automaticamente recebe input do InputManager e o repassa a seus subelementos de layer (botoes, areas de scroll, etc).
	/// Normalmente este objeto será uma das layers do jogo, caso em que também deve possuir uma camera ortogonal (component).	
	/// Caso nao seja uma layer, basta sobrescrever o metodo OnTouch e tratar o input como desejar.
	/// </summary>
    public abstract class IInputReceivable : MonoBehaviour
    {
        public virtual bool ShouldRemoveOnClean
        {
            get { return true; }
        }

        private bool _removeAndDestroy = false;
        public bool RemoveAndDestroy
        {
            get { return _removeAndDestroy; }
            set { _removeAndDestroy = value; }
        }

        private const int NO_FINGER_ID = -1;

        private Dictionary<string, Dictionary<InputManager.TouchPhases, Action<GameObject>>> mButtonActions = new Dictionary<string, Dictionary<InputManager.TouchPhases, Action<GameObject>>>();
        private ITouchable mCurrentlyPressedBtn = null, mPreviouslyPressedBtn = null;
        private ScrollingArea mCurrentlyPressedScrollingArea = null;
        private int mCurrentFingerId = NO_FINGER_ID;

        /// <summary>
        /// Warning: overriding this method and not calling its base will prevent the button methods from being called!
        /// </summary>
        /// <param name="screenPosition">The position on the screen where the touch occured</param>
        /// <returns>True if the touch event was consumed, false otherwise</returns>
        /// <owner>felipecb</owner>
        public virtual bool OnTouch(Vector3 screenPosition, InputManager.TouchPhases touchPhase, int fingerId)
        {
            switch (touchPhase)
            {
                case InputManager.TouchPhases.Began:
                    if (mCurrentFingerId == NO_FINGER_ID)
                    {
                        mCurrentFingerId = fingerId;
                        return OnTouchBegan(screenPosition);
                    }
                    return false;
                case InputManager.TouchPhases.Moved:
                    if (fingerId == mCurrentFingerId)
                    {
                        return OnTouchMoved(screenPosition);
                    }
                    return false;
                case InputManager.TouchPhases.Ended:
                    if (fingerId == mCurrentFingerId)
                    {
                        mCurrentFingerId = NO_FINGER_ID;
                        return OnTouchEnded(screenPosition);
                    }
                    return false;
            }
            Assert.AssertTrue(false, "IInputReceivable is unable to process this kind of touch: " + touchPhase.ToString());
            return false;
        }

        private void RascastAndGetData(Vector3 position, out RaycastHit[] hitInfo, out GameObject buttonObj, out ITouchable buttonScript, out GameObject scrollObj, out ScrollingArea scrollScript)
        {
            hitInfo = UnityEngine.Physics.RaycastAll(camera.ScreenPointToRay(position));
            buttonObj = hitInfo.Select(info => info.collider.gameObject).First(gameObj => gameObj.GetComponent<ITouchable>() != null);
            buttonScript = buttonObj == null ? null : buttonObj.GetComponent<ITouchable>();
            scrollObj = hitInfo.Select(info => info.collider.gameObject).First(gameObj => gameObj.GetComponent<ScrollingArea>() != null);
            scrollScript = scrollObj == null ? null : scrollObj.GetComponent<ScrollingArea>();
        }

        private bool OnTouchBegan(Vector3 screenPosition)
        {
            bool wasEventConsumed = false;
            GameObject buttonObj;
            ITouchable pressedBtnScript;
            GameObject scrollObj;
            ScrollingArea scrollScript;
            RaycastHit[] hitInfo;
            RascastAndGetData(screenPosition, out hitInfo, out buttonObj, out pressedBtnScript, out scrollObj, out scrollScript);
            if (hitInfo.Length > 0)
            {
                if (pressedBtnScript != null)
                {
                    Assert.AssertTrue(mButtonActions.ContainsKey(buttonObj.name), "An unregistered button has been touched: " + buttonObj.name);
                    Assert.AssertTrue(mButtonActions[buttonObj.name].ContainsKey(InputManager.TouchPhases.Began), "An unregistered button has been touched: " + buttonObj.name);
                    pressedBtnScript.OnTouchBegan();
                    mButtonActions[buttonObj.name][InputManager.TouchPhases.Began](buttonObj);
                    wasEventConsumed = true;
                    mCurrentlyPressedBtn = pressedBtnScript;
                }
                if (scrollScript != null)
                {
                    Assert.AssertTrue(mCurrentlyPressedScrollingArea == null, "A ScrollingArea was pressed while another one was already pressed");
                    RaycastHit scrollHitInfo = hitInfo.Where(info => info.collider.gameObject == scrollObj).First();
                    scrollScript.OnTouchBegan(screenPosition, scrollHitInfo.point, mCurrentlyPressedBtn != null);
                    mCurrentlyPressedScrollingArea = scrollScript;
                }
            }
            return wasEventConsumed;
        }

        private bool OnTouchMoved(Vector3 screenPosition)
        {
            GameObject buttonObj;
            ITouchable pressedBtnScript;
            GameObject scrollObj;
            ScrollingArea scrollScript;
            RaycastHit[] hitInfo;
            RascastAndGetData(screenPosition, out hitInfo, out buttonObj, out pressedBtnScript, out scrollObj, out scrollScript);
            Assert.AssertTrue(buttonObj == null ? true : mButtonActions.ContainsKey(buttonObj.name), "An unregistered button has been touched: " /*+ buttonObj.name*/);
            if (mCurrentlyPressedBtn != null && mCurrentlyPressedBtn != pressedBtnScript)
            {
                mCurrentlyPressedBtn.OnTouchEnded();
                mPreviouslyPressedBtn = mCurrentlyPressedBtn;
                mCurrentlyPressedBtn = null;
            }
            else if (mPreviouslyPressedBtn != null && mPreviouslyPressedBtn == pressedBtnScript)
            {
                mPreviouslyPressedBtn.OnTouchBegan();
                mCurrentlyPressedBtn = mPreviouslyPressedBtn;
                mPreviouslyPressedBtn = null;
            }
            if (mCurrentlyPressedScrollingArea != null)
            {
                if (mCurrentlyPressedScrollingArea.OnTouchMoved(screenPosition))
                {
                    if (mCurrentlyPressedBtn != null)
                    {
                        mCurrentlyPressedBtn.OnTouchEnded();
                    }
                    else if (mPreviouslyPressedBtn != null)
                    {
                        mPreviouslyPressedBtn.OnTouchEnded();
                    }
                    mCurrentlyPressedBtn = mPreviouslyPressedBtn = null;
                }
            }
            return mCurrentlyPressedBtn != null || mPreviouslyPressedBtn != null || mCurrentlyPressedScrollingArea != null;
        }

        private bool OnTouchEnded(Vector3 screenPosition)
        {
            GameObject buttonObj;
            ITouchable pressedBtnScript;
            GameObject scrollObj;
            ScrollingArea scrollScript;
            RaycastHit[] hitInfo;
            RascastAndGetData(screenPosition, out hitInfo, out buttonObj, out pressedBtnScript, out scrollObj, out scrollScript);
            if (hitInfo.Length > 0)
            {
                if (pressedBtnScript != null && pressedBtnScript == mCurrentlyPressedBtn)
                {
                    Assert.AssertTrue(mButtonActions.ContainsKey(buttonObj.name), "An unregistered button has been touched: " + buttonObj.name);
                    Assert.AssertTrue(mButtonActions[buttonObj.name].ContainsKey(InputManager.TouchPhases.Ended), "An unregistered button has been touched " + buttonObj.name);
                    pressedBtnScript.OnTouchEnded();
                    mButtonActions[buttonObj.name][InputManager.TouchPhases.Ended](buttonObj);
                }
                else
                {
                    // FIX: Com movimentos muito rápidos (no mouse é possível) consegue-se dar um TouchEnded fora do botao
                    // atualmente pressionado (antes que o botao atualmente pressionado receba o evento de TouchEnded no TouchMoved)
                    if (mCurrentlyPressedBtn != null)
                    {
                        mCurrentlyPressedBtn.OnTouchEnded();
                    }
                }
            }
            else
            {
                // FIX: Com movimentos muito rápidos (no mouse é possível) consegue-se dar um TouchEnded fora do botao
                // atualmente pressionado (antes que o botao atualmente pressionado receba o evento de TouchEnded no TouchMoved)
                if (mCurrentlyPressedBtn != null)
                {
                    mCurrentlyPressedBtn.OnTouchEnded();
                }
            }
            if (mCurrentlyPressedScrollingArea != null)
            {
                mCurrentlyPressedScrollingArea.OnTouchEnded(screenPosition);
                mCurrentlyPressedScrollingArea = null;
                return true;
            }
            bool retorno = mCurrentlyPressedBtn != null || mPreviouslyPressedBtn != null;
            mCurrentlyPressedBtn = null;
            mPreviouslyPressedBtn = null;
            return retorno;
        }

        public bool GetInputStateOnComponent(string buttonName)
        {
            Transform transformToFind = gameObject.transform.FindChild(buttonName, true);

            Assert.AssertTrue(transformToFind != null, "Cannot find the component with the following name " + buttonName);
            Assert.AssertTrue(transformToFind.GetComponent<ITouchable>() != null || transformToFind.GetComponent<ScrollingArea>(), "The button doesn't have an ITouchable or ScrollingArea behaviour!");

            foreach (Collider collider in transformToFind.GetComponents<Collider>())
            {
                return collider.enabled;
            }

            return false;
        }

		public void SetInputStateOnComponent(string buttonName, bool shouldEnable)
		{
			Transform transformToFind = gameObject.transform.FindChild(buttonName, true);

			Assert.AssertTrue(transformToFind != null, "Cannot find the component with the following name " + buttonName);
			Assert.AssertTrue(transformToFind.GetComponent<ITouchable>() != null || transformToFind.GetComponent<ScrollingArea>(), "The button doesn't have an ITouchable or ScrollingArea behaviour!");

			foreach (Collider collider in transformToFind.GetComponents<Collider>())
			{
				collider.enabled = shouldEnable;
			}
		}

        public void RegisterButtonAction(string buttonName, Action<GameObject> actionDown, Action<GameObject> actionUp)
        {
            Assert.AssertTrue(actionDown != null && actionUp != null, "Button actions cannot be null");
            RegisterPhasedButtonAction(buttonName, actionDown, InputManager.TouchPhases.Began);
            RegisterPhasedButtonAction(buttonName, actionUp, InputManager.TouchPhases.Ended);
        }

        private void RegisterPhasedButtonAction(string buttonName, Action<GameObject> action, InputManager.TouchPhases touchPhase)
        {
            if (!mButtonActions.ContainsKey(buttonName))
            {
                mButtonActions[buttonName] = new Dictionary<InputManager.TouchPhases, Action<GameObject>>();
            }
            Assert.AssertTrue(!mButtonActions[buttonName].ContainsKey(touchPhase), "This button has already registered this phase's action");
            mButtonActions[buttonName][touchPhase] = action;
        }

        public virtual void OnLayerEffectFinished(bool removing) { }
        public virtual void OnLayerEffectStarted(bool removing) { }
        public virtual void OnDataPackReceived(object dataPack) { }

        public virtual bool OnEscapeKeyDown()
        {
            return true;
        }
    }
}