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

public class ScrollingArea : MonoBehaviour
{
    public GameObject ScrollAreaTop, ScrollAreaBottom;
    
    private float mMaxYPosition = 2.3f;
    private float mMinYPosition = 0.3f;

    private const float ATENUATOR_BEGINNING_VALUE = 0.8f; //1.0f
    private const float ATENUATOR_MIN_VALUE = 0.4f; //0.5f
    private const float OVERFLOW_DIVISOR = 2.0f; //3.0f
    private const float DEFAULT_ABS_DECELERATION = 600; //200
    private const float KEEP_HISTORY_TIME = 0.15f; //0.25f
    private const float UPDATE_ARRAY_TIME = 1.0f;
    private const float MIN_MOVEMENT = 0.05f; //0.1f
    private const int MIN_ARRAY_VALUE_QTT = 2;
    
    private float mCurrentVerticalAbsDeceleration = 200;
    private float mVerticalSpeed = 0.0f;
    private float mSpeedSign;
    private Vector3? mTouchBeganPosition = null;
    private Vector3 mTransfLocalPositionOnTouchBegan;
    private bool mMovementBegan = false;
    private float mElapsed, mAuxiliarElapsed;
    private float mLastOverflowValue;
    private List<KeyValuePair<float, Vector3>> mTimePositionArray = new List<KeyValuePair<float, Vector3>>();
    private bool mAlreadyRecalculatedDeceleration = false, mAlreadyRecalculatedSpeed = true;
    private bool mIsTouching = false;

    private void Start()
    {
        ScrollAreaTop = gameObject;
        Util.AssertTrue(ScrollAreaTop != null && ScrollAreaBottom != null, "Some gameobject is missing its assignment");
        Util.AssertTrue(ScrollAreaTop.transform.parent == transform.parent && ScrollAreaBottom.transform.parent == transform.parent, "The Area Bottom and Top objects must be brothers of the ScrollingArea object");
        Util.AssertTrue(ScrollAreaTop.transform.localPosition.y.Compare(ScrollAreaBottom.transform.localPosition.y) >= 0, "TopAreaPosition must be higher than or equals to BottomAreaPosition");

        float scrollingAreaTotalSize = gameObject.GetSize(true, true).y;
        float scrollingMaxViewSize = Mathf.Abs(ScrollAreaTop.transform.localPosition.y - ScrollAreaBottom.transform.localPosition.y);
        float scrollingEffectiveSize = Mathf.Min(scrollingAreaTotalSize, scrollingMaxViewSize);
        float scrollingAreaExtraSize = scrollingAreaTotalSize - scrollingMaxViewSize;
        mMinYPosition = ScrollAreaTop.transform.localPosition.y;
        mMaxYPosition = mMinYPosition + (scrollingAreaExtraSize.Compare(0.0f) == 1 ? scrollingAreaExtraSize : 0.0f);

        BoxCollider collider = gameObject.AddComponent<BoxCollider>();
        collider.size = new Vector3(gameObject.GetSize(true, true).x, scrollingAreaTotalSize, 1.0f);
        collider.center = new Vector3(0.0f, -scrollingAreaTotalSize / 2, 0.0f);

        Util.AssertTrue(mMaxYPosition.Compare(mMinYPosition) >= 0, "MaxYPosition must be higher than or equals to MinYPosition");
    }

    public void OnTouchBegan(Vector3 position, Vector3 hitPosition, bool isAnyButtonPressed)
    {
        Util.AssertTrue(mTouchBeganPosition == null, "ScrollingArea received a TouchBegan call while another touch was already being processed");
        mTouchBeganPosition = position;
        mTransfLocalPositionOnTouchBegan = transform.localPosition;
        mVerticalSpeed = 0.0f;
        mCurrentVerticalAbsDeceleration = DEFAULT_ABS_DECELERATION;
        mAlreadyRecalculatedDeceleration = false;
        mAlreadyRecalculatedSpeed = true;
        mIsTouching = true;
        mElapsed = 0.0f;
        mAuxiliarElapsed = 0.0f;
        mTimePositionArray.Clear();
        mTimePositionArray.Add(new KeyValuePair<float, Vector3>(mElapsed, position));
        mMovementBegan = !isAnyButtonPressed;
    }

    /// <summary>
    /// Returns true if the touch movement caused the scroll movement to begin, false otherwise.
    /// </summary>
    public bool OnTouchMoved(Vector3 position)
    {
        Util.AssertTrue(mTouchBeganPosition != null, "ScrollingArea received a TouchMoved call without receiving the TouchBegan first");
        float distanceFromTouchBegan = CalculateVerticalDistance(position, mTouchBeganPosition.Value);
        if (!mMovementBegan)
        {            
            if (Mathf.Abs(distanceFromTouchBegan).Compare(MIN_MOVEMENT) >= 0)
            {
                mMovementBegan = true;
                transform.localPosition = new Vector3(transform.localPosition.x, Mathf.Clamp(transform.localPosition.y + distanceFromTouchBegan, mMinYPosition, mMaxYPosition), transform.localPosition.z);
                return true;
            }
        }
        else if (mMovementBegan)
        {
            float newYposition = mTransfLocalPositionOnTouchBegan.y + distanceFromTouchBegan;
            float overflowValue = 0.0f, atenuator = 0.0f, atenuatedOverflow = 0.0f;
            if (newYposition.Compare(mMaxYPosition) > 0)
            {
                overflowValue = newYposition - mMaxYPosition;
                newYposition = newYposition - overflowValue;
                atenuator = ATENUATOR_BEGINNING_VALUE - overflowValue / OVERFLOW_DIVISOR;
                atenuatedOverflow = overflowValue * atenuator;
                if (atenuator <= ATENUATOR_MIN_VALUE)
                {
                    atenuatedOverflow = GetAbsMaxAtenuatedOverflow();
                }
            }
            else if (newYposition.Compare(mMinYPosition) < 0)
            {
                overflowValue = newYposition - mMinYPosition;
                newYposition = newYposition - overflowValue;
                atenuator = ATENUATOR_BEGINNING_VALUE + overflowValue / OVERFLOW_DIVISOR;
                atenuatedOverflow = overflowValue * atenuator;
                if (atenuator <= ATENUATOR_MIN_VALUE)
                {
                    atenuatedOverflow = -GetAbsMaxAtenuatedOverflow();
                }
            }
            transform.localPosition = new Vector3(transform.localPosition.x, newYposition + atenuatedOverflow, transform.localPosition.z);            
        }
        //using(GenericReusablePool<KeyValuePair<float, Vector3>> reusable = new GenericReusablePool<KeyValuePair<float,Vector3>>.Instance.A
        if (mTimePositionArray.Count == 0 || !mTimePositionArray[mTimePositionArray.Count - 1].Value.y.Aproximate(position.y))
        {   // adiciona se tiver havido movimento
            mTimePositionArray.Add(new KeyValuePair<float, Vector3>(mElapsed, position));
        }
        return false;
    }

    private float GetAbsMaxAtenuatedOverflow()
    {
        return -OVERFLOW_DIVISOR * (ATENUATOR_MIN_VALUE - ATENUATOR_BEGINNING_VALUE) * ATENUATOR_MIN_VALUE;
    }

    public void OnTouchEnded(Vector3 position)
    {
        Util.AssertTrue(mTouchBeganPosition != null, "ScrollingArea received a OnTouchEnded call without receiving the TouchBegan first");
        mIsTouching = false;
        mTouchBeganPosition = null;
        mMovementBegan = false;
        UpdateTimePositionsArray();
        if (transform.localPosition.y.Compare(mMaxYPosition) > 0)
        {
            float deltaS = (mMaxYPosition - transform.localPosition.y) / LayerManager.ScreenHeightNormalizer;
            mVerticalSpeed = -Mathf.Sqrt(Mathf.Abs(2 * mCurrentVerticalAbsDeceleration * deltaS));
        }
        else if (transform.localPosition.y.Compare(mMinYPosition) < 0)
        {
            float deltaS = (mMinYPosition - transform.localPosition.y) / LayerManager.ScreenHeightNormalizer;
            mVerticalSpeed = Mathf.Sqrt(Mathf.Abs(2 * mCurrentVerticalAbsDeceleration * deltaS));
        }
        else if (mTimePositionArray.Count >= MIN_ARRAY_VALUE_QTT)
        {
            mVerticalSpeed = (mTimePositionArray[mTimePositionArray.Count - 1].Value.y - mTimePositionArray[0].Value.y) / (mElapsed - mTimePositionArray[0].Key);
        }
        else
        {
            mVerticalSpeed = 0.0f;
        }
        mSpeedSign = Mathf.Sign(mVerticalSpeed);
        mTimePositionArray.Clear();
        mAlreadyRecalculatedSpeed = false;
    }

    private float CalculateVerticalDistance(Vector3 to, Vector3 from)
    {
        return (to.y - from.y) * LayerManager.ScreenHeightNormalizer;
    }

    private void Update()
    {
        if (mIsTouching)
        {
            mElapsed += Time.deltaTime;
            mAuxiliarElapsed += Time.deltaTime;
            if (mAuxiliarElapsed.Compare(UPDATE_ARRAY_TIME) >= 0)
            {
                mAuxiliarElapsed -= UPDATE_ARRAY_TIME;
                UpdateTimePositionsArray();
            }
        }
        if (mMovementBegan)
        {

        }
        else if (!mVerticalSpeed.Aproximate(0.0f))
        {
            transform.localPosition = new Vector3(transform.localPosition.x, transform.localPosition.y + mVerticalSpeed * Time.deltaTime * LayerManager.ScreenHeightNormalizer, transform.localPosition.z);
            if (mSpeedSign.Aproximate(1.0f))
            {
                if (!mAlreadyRecalculatedDeceleration)
                {
                    if (transform.localPosition.y.Compare(mMaxYPosition) > 0)
                    {
                        mAlreadyRecalculatedDeceleration = true;
                        float newPossibleDeceleration = (mVerticalSpeed * mVerticalSpeed) / (2 * Mathf.Abs(transform.localPosition.y - (mMaxYPosition + GetAbsMaxAtenuatedOverflow())) / LayerManager.ScreenHeightNormalizer);
                        mCurrentVerticalAbsDeceleration = Mathf.Max(mCurrentVerticalAbsDeceleration, newPossibleDeceleration);
                        mCurrentVerticalAbsDeceleration = Mathf.Max(mCurrentVerticalAbsDeceleration, 5000.0f);
                    }
                }
                mVerticalSpeed = Mathf.Clamp(mVerticalSpeed - mSpeedSign * Time.deltaTime * mCurrentVerticalAbsDeceleration, 0.0f, Mathf.Infinity);
            }
            else
            {
                if (!mAlreadyRecalculatedDeceleration)
                {
                    if (transform.localPosition.y.Compare(mMinYPosition) < 0)
                    {
                        mAlreadyRecalculatedDeceleration = true;
                        float newPossibleDeceleration = (mVerticalSpeed * mVerticalSpeed) / (2 * Mathf.Abs(transform.localPosition.y - (mMinYPosition - GetAbsMaxAtenuatedOverflow())) / LayerManager.ScreenHeightNormalizer);
                        mCurrentVerticalAbsDeceleration = Mathf.Max(mCurrentVerticalAbsDeceleration, newPossibleDeceleration);
                        mCurrentVerticalAbsDeceleration = Mathf.Max(mCurrentVerticalAbsDeceleration, 5000.0f);
                    }
                }
                mVerticalSpeed = Mathf.Clamp(mVerticalSpeed - mSpeedSign * Time.deltaTime * mCurrentVerticalAbsDeceleration, -Mathf.Infinity, 0.0f);
            }
        }
        else
        {
            if (!mAlreadyRecalculatedSpeed)
            {
                mAlreadyRecalculatedSpeed = true;
                if (transform.localPosition.y.Compare(mMaxYPosition) > 0)
                {
                    mCurrentVerticalAbsDeceleration = DEFAULT_ABS_DECELERATION;
                    float deltaS = Mathf.Abs(mMaxYPosition - transform.localPosition.y) / LayerManager.ScreenHeightNormalizer;
                    mVerticalSpeed = -Mathf.Sqrt(Mathf.Abs(2 * mCurrentVerticalAbsDeceleration * deltaS));
                    mSpeedSign = Mathf.Sign(mVerticalSpeed);
                }
                else if (transform.localPosition.y.Compare(mMinYPosition) < 0)
                {
                    mCurrentVerticalAbsDeceleration = DEFAULT_ABS_DECELERATION;
                    float deltaS = Mathf.Abs(mMinYPosition - transform.localPosition.y) / LayerManager.ScreenHeightNormalizer;
                    mVerticalSpeed = Mathf.Sqrt(Mathf.Abs(2 * mCurrentVerticalAbsDeceleration * deltaS));
                    mSpeedSign = Mathf.Sign(mVerticalSpeed);
                }
            }
        }
    }

    private void UpdateTimePositionsArray()
    {
        if (mTimePositionArray.Count >= MIN_ARRAY_VALUE_QTT)
        {
            int i;

            // elimina os tempos antigos
            for (i = mTimePositionArray.Count - 1; i >= 0; i--)
            {
                if ((mElapsed - mTimePositionArray[i].Key).Compare(KEEP_HISTORY_TIME) >= 0)
                {
                    break;
                }
            }
            for (int j = i; j >= 0; j--)
            {
                mTimePositionArray.RemoveAt(j);
            }

            // elimina valores anteriores a possivel mudança de direcao
            if (mTimePositionArray.Count >= MIN_ARRAY_VALUE_QTT)
            {
                int lastVerticalSign = mTimePositionArray[mTimePositionArray.Count - 1].Value.y >= mTimePositionArray[mTimePositionArray.Count - 2].Value.y ? 1 : -1;
                for (i = mTimePositionArray.Count - 2; i > 0; i--)
                {
                    int sinal = mTimePositionArray[i].Value.y >= mTimePositionArray[i - 1].Value.y ? 1 : -1;
                    if (sinal != lastVerticalSign)
                    {
                        break;
                    }
                }
                for (int j = i - 1; j >= 0; j--)
                {
                    mTimePositionArray.RemoveAt(j);
                }
            }
        }
    }
}