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

public enum ObjectType
{
    None,
    Player,
    NPC,
}

public class MovingObject : MonoBehaviour 
{
    public ObjectType mType;
    /// <summary>
    /// The previous position.
    /// </summary>
    public Vector2 mOldPosition;
    /// <summary>
    /// The current position.
    /// </summary>
    public Vector2 mPosition;
    public Vector2 mReminder;

    private Vector2 mScale;
    public Vector2 Scale
    {
        set {
            mScale = value;
            mAABB.scale = new Vector2(Mathf.Abs(value.x), Mathf.Abs(value.y));
        }
        get { return mScale; }
    }
    public float ScaleX
    {
        set
        {
            mScale.x = value;
            mAABB.scale.x = Mathf.Abs(value);
        }
        get { return mScale.x; }
    }
    public float ScaleY
    {
        set
        {
            mScale.y = value;
            mAABB.scale.y = Mathf.Abs(value);
        }
        get { return mScale.y; }
    }

    /// <summary>
    /// The current speed in pixels/second.
    /// </summary>
    public Vector2 mSpeed;
	
	/// <summary>
	/// The previous speed in pixels/second.
	/// </summary>
	public Vector2 mOldSpeed;

    private Vector2 mAABBOffset;
    public Vector2 AABBOffset
    {
        set { mAABBOffset = value; }
        get { return new Vector2(mAABBOffset.x * mScale.x, mAABBOffset.y * mScale.y); }
    }

    public float AABBOffsetX
    {
        set { mAABBOffset.x = value; }
        get { return mAABBOffset.x * mScale.x; }
    }

    public float AABBOffsetY
    {
        set { mAABBOffset.y = value; }
        get { return mAABBOffset.y * mScale.y; }
    }

    [Serializable]
    public struct PositionState
    {
        public bool pushesRight;
        public bool pushesLeft;
        public bool pushesBottom;
        public bool pushesTop;

        public bool pushedTop;
        public bool pushedBottom;
        public bool pushedRight;
        public bool pushedLeft;

        public bool pushedLeftObject;
        public bool pushedRightObject;
        public bool pushedBottomObject;
        public bool pushedTopObject;

        public bool pushesLeftObject;
        public bool pushesRightObject;
        public bool pushesBottomObject;
        public bool pushesTopObject;

        public bool pushedLeftTile;
        public bool pushedRightTile;
        public bool pushedBottomTile;
        public bool pushedTopTile;

        public bool pushesLeftTile;
        public bool pushesRightTile;
        public bool pushesBottomTile;
        public bool pushesTopTile;

        public bool onOneWay;
        public bool tmpIgnoresOneWay;
        public int oneWayY;

        public Vector2i leftTile;
        public Vector2i rightTile;
        public Vector2i topTile;
        public Vector2i bottomTile;

        public void Reset()
        {
            leftTile = rightTile = topTile = bottomTile = new Vector2i(-1, -1);
            oneWayY = -1;

            pushesRight = false;
            pushesLeft = false;
            pushesBottom = false;
            pushesTop = false;

            pushedTop = false;
            pushedBottom = false;
            pushedRight = false;
            pushedLeft = false;

            pushedLeftObject = false;
            pushedRightObject = false;
            pushedBottomObject = false;
            pushedTopObject = false;

            pushesLeftObject = false;
            pushesRightObject = false;
            pushesBottomObject = false;
            pushesTopObject = false;

            pushedLeftTile = false;
            pushedRightTile = false;
            pushedBottomTile = false;
            pushedTopTile = false;

            pushesLeftTile = false;
            pushesRightTile = false;
            pushesBottomTile = false;
            pushesTopTile = false;

            onOneWay = false;
        }
    }
    /// <summary>
    /// The AABB for collision queries.
    /// </summary>
    public AABB mAABB;

    public PositionState mPS;

    public Game mGame;
    public Map mMap;

    public bool mIgnoresOneWay = false;
    public bool mOnOneWayPlatform = false;
    public bool mSticksToSlope = true;
    public bool mIsKinematic = false;

    [NonSerialized]
    public List<Vector2i> mAreas = new List<Vector2i>();
    [NonSerialized]
    public List<int> mIdsInAreas = new List<int>();
    [NonSerialized]
    protected List<MovingObject> mFilteredObjects = new List<MovingObject>();
    [NonSerialized]
    public List<CollisionData> mAllCollidingObjects = new List<CollisionData>();
    /// <summary>
    /// Depth for z-ordering the sprites.
    /// </summary>
    public float mSpriteDepth = -1.0f;

    public Transform mTransform;

	void OnDrawGizmos()
	{
		DrawMovingObjectGizmos ();
	}

    /// <summary>
    /// Draws the aabb and ceiling, ground and wall sensors .
    /// </summary>
    protected void DrawMovingObjectGizmos()
	{
		//calculate the position of the aabb's center
		var aabbPos = transform.position + (Vector3)AABBOffset;
		
		//draw the aabb rectangle
		Gizmos.color = Color.yellow;
   		Gizmos.DrawWireCube(aabbPos, mAABB.HalfSize*2.0f);
		
		//draw the ground checking sensor
		Vector2 bottomLeft = aabbPos - new Vector3(mAABB.HalfSizeX, mAABB.HalfSizeY, 0.0f) - Vector3.up + Vector3.right;
		var bottomRight = new Vector2(bottomLeft.x + mAABB.HalfSizeX*2.0f - 2.0f, bottomLeft.y);
		
		Gizmos.color = Color.red;
		Gizmos.DrawLine(bottomLeft, bottomRight);
		
		//draw the ceiling checking sensor
		Vector2 topRight = aabbPos + new Vector3(mAABB.HalfSizeX, mAABB.HalfSizeY, 0.0f) + Vector3.up - Vector3.right;
		var topLeft = new Vector2(topRight.x - mAABB.HalfSizeX*2.0f + 2.0f, topRight.y);
		
		Gizmos.color = Color.red;
		Gizmos.DrawLine(topLeft, topRight);
		
		//draw left wall checking sensor
		bottomLeft = aabbPos - new Vector3(mAABB.HalfSizeX, mAABB.HalfSizeY, 0.0f) - Vector3.right;
		topLeft = bottomLeft;
		topLeft.y += mAABB.HalfSizeY * 2.0f;
		
		Gizmos.DrawLine(topLeft, bottomLeft);
		
		//draw right wall checking sensor
		
		bottomRight = aabbPos + new Vector3(mAABB.HalfSizeX, -mAABB.HalfSizeY, 0.0f) + Vector3.right;
		topRight = bottomRight;
		topRight.y += mAABB.HalfSizeY * 2.0f;
		
		Gizmos.DrawLine(topRight, bottomRight);
	}

    public bool HasCollisionDataFor(MovingObject other)
    {
        for (int i = 0; i < mAllCollidingObjects.Count; ++i)
        {
            if (mAllCollidingObjects[i].other == other)
                return true;
        }

        return false;
    }

    Vector2 RoundVector(Vector2 v)
    {
        return new Vector2(Mathf.Round(v.x), Mathf.Round(v.y));
    }

    public bool HasCeiling(Vector2 oldPosition, Vector2 position, out float ceilingY)
    {
        //make sure the aabb is up to date with the position
        var center = position + AABBOffset;
        var oldCenter = oldPosition + AABBOffset;

        //init the groundY
        ceilingY = 0.0f;

        var oldTopRight = RoundVector(oldCenter + mAABB.HalfSize + Vector2.up - Vector2.right);
        
        //set the Vector2is right below us on our left and right sides
        var newTopRight = RoundVector(center + mAABB.HalfSize + Vector2.up - Vector2.right);
        var newTopLeft = RoundVector(new Vector2(newTopRight.x - mAABB.HalfSizeX * 2.0f + 2.0f, newTopRight.y));

        int endY = mMap.GetMapTileYAtPoint(newTopRight.y);
        int begY = Mathf.Min(mMap.GetMapTileYAtPoint(oldTopRight.y) + 1, endY);
        int dist = Mathf.Max(Mathf.Abs(endY - begY), 1);

        //get the indices of a tile below us on our left side
        int tileIndexX;

        for (int tileIndexY = begY; tileIndexY <= endY; ++tileIndexY)
        {
            var topRight = Vector2.Lerp(newTopRight, oldTopRight, (float)Mathf.Abs(endY - tileIndexY) / dist);
            var topLeft = new Vector2(topRight.x - mAABB.HalfSizeX * 2.0f + 2.0f, topRight.y);

            //iterate over all the tiles that the object may collide with from the left to the right
            for (var checkedTile = topLeft; ; checkedTile.x += Map.cTileSize)
            {
                //makre sure that we don't check beyound the top right corner
                checkedTile.x = Mathf.Min(checkedTile.x, topRight.x);

                tileIndexX = mMap.GetMapTileXAtPoint(checkedTile.x);

                if (mMap.IsObstacle(tileIndexX, tileIndexY))
                {
                    ceilingY = (float)tileIndexY * Map.cTileSize - Map.cTileSize / 2.0f + mMap.mPosition.y;
                    return true;
                }

                //if we checked all the possible tiles and there's nothing right above the aabb
                if (checkedTile.x >= topRight.x)
                    break;
            }
        }

        //there's nothing right above the aabb
        return false;
    }

    public bool HasGround(Vector2 oldPosition, Vector2 position, Vector2 speed, out float groundY, ref bool onOneWayPlatform)
    {
        var oldCenter = oldPosition + AABBOffset;
        //make sure the aabb is up to date with the position
        var center = position + AABBOffset;

        //init the groundY
        groundY = 0.0f;

        var oldBottomLeft = RoundVector(oldCenter - mAABB.HalfSize - Vector2.up + Vector2.right);
        //set the Vector2is right below us on our left and right sides
        var newBottomLeft = RoundVector(center - mAABB.HalfSize - Vector2.up + Vector2.right);
        var newBottomRight = RoundVector(new Vector2(newBottomLeft.x + mAABB.HalfSizeX * 2.0f - 2.0f, newBottomLeft.y));

        //left side
        //calculate the indices of a tile below us on our left side

        int endY = mMap.GetMapTileYAtPoint(newBottomLeft.y);
        int begY = Mathf.Max(mMap.GetMapTileYAtPoint(oldBottomLeft.y) - 1, endY);
        int dist = Mathf.Max(Mathf.Abs(endY - begY), 1);

        int tileIndexX;

        for (int tileIndexY = begY; tileIndexY >= endY; --tileIndexY)
        {
            var bottomLeft = Vector2.Lerp(newBottomLeft, oldBottomLeft, (float)Mathf.Abs(endY - tileIndexY) / dist);
            var bottomRight = new Vector2(bottomLeft.x + mAABB.HalfSizeX * 2.0f - 2.0f, bottomLeft.y);

            //iterate over all the tiles that the object may collide with from the left to the right
            for (var checkedTile = bottomLeft; ; checkedTile.x += Map.cTileSize)
            {
                //makre sure that we don't check beyound the bottom right corner
                checkedTile.x = Mathf.Min(checkedTile.x, bottomRight.x);

                tileIndexX = mMap.GetMapTileXAtPoint(checkedTile.x);

                //calculate the y position of the floor tile's top
                groundY = (float)tileIndexY * Map.cTileSize + Map.cTileSize / 2.0f + mMap.mPosition.y;
                //if the tile is not empty, it means we have a floor right below us
                if (mMap.IsObstacle(tileIndexX, tileIndexY))
                {
                    onOneWayPlatform = false;
                    return true;
                }
                //if there's a one way platform below us, treat it as a floor only if we're falling or standing
                else if (mMap.IsOneWayPlatform(tileIndexX, tileIndexY)
                        && Mathf.Abs(checkedTile.y - groundY) <= Constants.cOneWayPlatformThreshold + mOldPosition.y - position.y)
                    onOneWayPlatform = true;

                //if we checked all the possible tiles and there's nothing right below the aabb
                if (checkedTile.x >= bottomRight.x)
                {
                    if (onOneWayPlatform)
                        return true;
                    break;
                }
            }
        }

        //there's nothing right beneath the aabb
        return false;
    }

    public bool CollidesWithRightWall(Vector2 oldPosition, Vector2 position, out float wallX)
    {
        //make sure the aabb is up to date with the position
        var center = position + AABBOffset;
        var oldCenter = oldPosition + AABBOffset;

        //init the wallX
        wallX = 0.0f;

        var oldBottomRight = RoundVector(oldCenter + new Vector2(mAABB.HalfSizeX, -mAABB.HalfSizeY) + Vector2.right);
        //calculate the bottom left and top left vertices of our aabb
        var newBottomRight = RoundVector(center + new Vector2(mAABB.HalfSizeX, -mAABB.HalfSizeY) + Vector2.right);
        var newTopRight = RoundVector(newBottomRight + new Vector2(0.0f, mAABB.HalfSizeY * 2.0f));

        var endX = mMap.GetMapTileXAtPoint(newBottomRight.x);
        var begX = Mathf.Min(mMap.GetMapTileXAtPoint(oldBottomRight.x) + 1, endX);
        int dist = Mathf.Max(Mathf.Abs(endX - begX), 1);

        //get the bottom right vertex's tile indices
        int tileIndexY;

        for (int tileIndexX = begX; tileIndexX <= endX; ++tileIndexX)
        {
            var bottomRight = Vector2.Lerp(newBottomRight, oldBottomRight, (float)Mathf.Abs(endX - tileIndexX) / dist);
            var topRight = bottomRight + new Vector2(0.0f, mAABB.HalfSizeY * 2.0f);

            //iterate over all the tiles that the object may collide with from the top to the bottom
            for (var checkedTile = bottomRight; ; checkedTile.y += Map.cTileSize)
            {
                //make sure that we don't check beyound the top right corner
                checkedTile.y = Mathf.Min(checkedTile.y, topRight.y);

                tileIndexY = mMap.GetMapTileYAtPoint(checkedTile.y);

                if (mMap.IsObstacle(tileIndexX, tileIndexY))
                {
                    //calculate the x position of the left side of the wall
                    wallX = (float)tileIndexX * Map.cTileSize - Map.cTileSize / 2.0f + mMap.mPosition.x;
                    return true;
                }

                //if we checked all the possible tiles and there's nothing right next to the aabb
                if (checkedTile.y >= topRight.y)
                    break;
            }
        }

        return false;
    }

    /// <summary>
    /// Checks if the hero collides with a wall on the left.
    /// </summary>
    /// <returns>
    /// True if the hero collides with the wall on the left, otherwise false.
    /// </returns>
    /// <param name='wallX'>
    /// The X coordinate in world space of the right edge of the wall the hero collides with.
    /// </param>
    public bool CollidesWithLeftWall(Vector2 oldPosition, Vector2 position, out float wallX)
    {
        //make sure the aabb is up to date with the position
        var center = position + AABBOffset;
        var oldCenter = oldPosition + AABBOffset;

        //init the wallX
        wallX = 0.0f;

        var oldBottomLeft = RoundVector(oldCenter - mAABB.HalfSize - Vector2.right);
        //calculate the bottom left and top left vertices of our aabb
        var newBottomLeft = RoundVector(center - mAABB.HalfSize - Vector2.right);
        var newTopLeft = RoundVector(newBottomLeft + new Vector2(0.0f, mAABB.HalfSizeY * 2.0f));

        //get the bottom left vertex's tile indices
        int tileIndexY;

        var endX = mMap.GetMapTileXAtPoint(newBottomLeft.x);
        var begX = Mathf.Max(mMap.GetMapTileXAtPoint(oldBottomLeft.x) - 1, endX);
        int dist = Mathf.Max(Mathf.Abs(endX - begX), 1);

        for (int tileIndexX = begX; tileIndexX >= endX; --tileIndexX)
        {
            var bottomLeft = Vector2.Lerp(newBottomLeft, oldBottomLeft, (float)Mathf.Abs(endX - tileIndexX) / dist);
            var topLeft = bottomLeft + new Vector2(0.0f, mAABB.HalfSizeY * 2.0f);

            //iterate over all the tiles that the object may collide with from the top to the bottom
            for (var checkedTile = bottomLeft; ; checkedTile.y += Map.cTileSize)
            {
                //make sure that we don't check beyound the top right corner
                checkedTile.y = Mathf.Min(checkedTile.y, topLeft.y);

                tileIndexY = mMap.GetMapTileYAtPoint(checkedTile.y);

                if (mMap.IsObstacle(tileIndexX, tileIndexY))
                {
                    //calculate the x position of the right side of the wall
                    wallX = (float)tileIndexX * Map.cTileSize + Map.cTileSize / 2.0f + mMap.mPosition.x;
                    return true;
                }

                //if we checked all the possible tiles and there's nothing right next to the aabb
                if (checkedTile.y >= topLeft.y)
                    break;
            }
        }

        return false;
    }

    public void CollidesWithTiles(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state)
    {
        Vector2 pos = position, tr = topRight, bl = bottomLeft;
        CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state);
        CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state);
        CollidesWithTileLeft(ref pos, ref tr, ref bl, ref state);
        CollidesWithTileRight(ref pos, ref tr, ref bl, ref state);
    }

    public bool CollidesWithTileLeft(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state, bool move = false)
    {
        Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y - 0.5f));
        Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x - 0.5f, bottomLeft.y + 0.5f));
        float slopeOffset = 0.0f, oldSlopeOffset = 0.0f;
        bool wasOneWay = false, isOneWay;
        TileCollisionType slopeCollisionType = TileCollisionType.Empty;

        for (int y = bottomLeftTile.y; y <= topRightTile.y; ++y)
        {
            var tileCollisionType = mMap.GetCollisionType(bottomLeftTile.x, y);
            isOneWay = Slopes.IsOneWay(tileCollisionType);

            if (isOneWay && (!move || mIgnoresOneWay || state.tmpIgnoresOneWay || y != bottomLeftTile.y))
                continue;

            switch (tileCollisionType)
            {
                default://slope

                    Vector2 tileCenter = mMap.GetMapTilePosition(bottomLeftTile.x, y);
                    float leftTileEdge = (tileCenter.x - Map.cTileSize / 2);
                    float rightTileEdge = (leftTileEdge + Map.cTileSize);
                    float bottomTileEdge = (tileCenter.y - Map.cTileSize / 2);

                    oldSlopeOffset = slopeOffset;

                    var offset = Slopes.GetOffset(tileCenter, bottomLeft.x - 0.5f, topRight.x - 0.5f, bottomLeft.y + 0.5f, topRight.y - 0.5f, tileCollisionType);
                    slopeOffset = Mathf.Abs(offset.freeUp) < Mathf.Abs(offset.freeDown) ? offset.freeUp : offset.freeDown;

                    if (!isOneWay && (Mathf.Abs(slopeOffset) >= Constants.cSlopeWallHeight || (slopeOffset < 0 && state.pushesBottomTile) || (slopeOffset > 0 && state.pushesTopTile)))
                    {
                        state.pushesLeftTile = true;
                        state.leftTile = new Vector2i(bottomLeftTile.x, y);
                        return true;
                    }
                    else if (Mathf.Abs(slopeOffset) > Mathf.Abs(oldSlopeOffset))
                    {
                        wasOneWay = isOneWay;
                        slopeCollisionType = tileCollisionType;
                        state.leftTile = new Vector2i(bottomLeftTile.x, y);
                    }
                    else
                        slopeOffset = oldSlopeOffset;


                    break;
                case TileCollisionType.Empty:
                    break;
            }
        }

        if (slopeCollisionType != TileCollisionType.Empty && slopeOffset != 0)
        {
            if (slopeOffset > 0 && slopeOffset < Constants.cSlopeWallHeight)
            {
                Vector2 pos = position, tr = topRight, bl = bottomLeft;
                pos.y += slopeOffset - Mathf.Sign(slopeOffset);
                tr.y += slopeOffset - Mathf.Sign(slopeOffset);
                bl.y += slopeOffset - Mathf.Sign(slopeOffset);
                PositionState s = new PositionState();

                if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s))
                {
                    state.pushesLeftTile = true;
                    return true;
                }
                else if (move)
                {
                    position.y += slopeOffset;
                    bottomLeft.y += slopeOffset;
                    topRight.y += slopeOffset;
                    state.pushesBottomTile = true;
                    state.onOneWay = wasOneWay;
                }
            }
            else if (slopeOffset < 0 && slopeOffset > -Constants.cSlopeWallHeight)
            {
                Vector2 pos = position, tr = topRight, bl = bottomLeft;
                pos.y += slopeOffset - Mathf.Sign(slopeOffset);
                tr.y += slopeOffset - Mathf.Sign(slopeOffset);
                bl.y += slopeOffset - Mathf.Sign(slopeOffset);
                PositionState s = new PositionState();

                if (CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s))
                {
                    state.pushesLeftTile = true;
                    return true;
                }
                else if (move)
                {
                    position.y += slopeOffset;
                    bottomLeft.y += slopeOffset;
                    topRight.y += slopeOffset;
                    state.pushesTopTile = true;
                    state.onOneWay = wasOneWay;
                }
            }
        }

        if (mSticksToSlope && state.pushedBottomTile && move)
        {
            var nextX = mMap.GetMapTileXAtPoint(topRight.x - 1.5f);
            var bottomY = mMap.GetMapTileYAtPoint(bottomLeft.y + 1.0f) - 1;

            var prevPos = mMap.GetMapTilePosition(new Vector2i(topRightTile.x, bottomLeftTile.y));
            var nextPos = mMap.GetMapTilePosition(new Vector2i(nextX, bottomY));

            var prevCollisionType = mMap.GetCollisionType(new Vector2i(topRightTile.x, bottomLeftTile.y));
            var nextCollisionType = mMap.GetCollisionType(new Vector2i(nextX, bottomY));

            int x1 = (int)Mathf.Clamp((topRight.x - 1.0f - (prevPos.x - Map.cTileSize / 2)), 0.0f, 15.0f);
            int x2 = (int)Mathf.Clamp((topRight.x - 1.5f - (nextPos.x - Map.cTileSize / 2)), 0.0f, 15.0f);

            int slopeHeight = Slopes.GetSlopeHeightFromBottom(x1, prevCollisionType);
            int nextSlopeHeight = Slopes.GetSlopeHeightFromBottom(x2, nextCollisionType);

            var offset = slopeHeight + Map.cTileSize - nextSlopeHeight;

            if (offset < Constants.cSlopeWallHeight && offset > 0)
            {
                Vector2 pos = position, tr = topRight, bl = bottomLeft;
                pos.y -= offset - Mathf.Sign(offset);
                tr.y -= offset - Mathf.Sign(offset);
                bl.y -= offset - Mathf.Sign(offset);
                bl.x -= 1.0f;
                tr.x -= 1.0f;
                PositionState s = new PositionState();

                if (!CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s))
                {
                    position.y -= offset;
                    bottomLeft.y -= offset;
                    topRight.y -= offset;
                    state.pushesBottomTile = true;
                    state.onOneWay = wasOneWay;
                }
            }
        }

        return false;
    }

    public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state, bool move = false)
    {
        Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f));
        Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f));
        float slopeOffset = 0.0f, oldSlopeOffset = 0.0f;
        bool wasOneWay = false, isOneWay;
        TileCollisionType slopeCollisionType = TileCollisionType.Empty;

        for (int y = bottomLeftTile.y; y <= topRightTile.y; ++y)
        {
            var tileCollisionType = mMap.GetCollisionType(topRightTile.x, y);
            isOneWay = Slopes.IsOneWay(tileCollisionType);

            if (isOneWay && (!move || mIgnoresOneWay || state.tmpIgnoresOneWay || y != bottomLeftTile.y))
                continue;

            switch (tileCollisionType)
            {
                default://slope

                    //int leftPlusOneTileX = mMap.GetMapTileXAtPoint(bottomLeft.x - 1.0f);

                    Vector2 tileCenter = mMap.GetMapTilePosition(topRightTile.x, y);
                    float leftTileEdge = (tileCenter.x - Map.cTileSize / 2);
                    float rightTileEdge = (leftTileEdge + Map.cTileSize);
                    float bottomTileEdge = (tileCenter.y - Map.cTileSize / 2);

                    //int slopeHeight;
                    oldSlopeOffset = slopeOffset;

                    var offset = Slopes.GetOffset(tileCenter, bottomLeft.x + 0.5f, topRight.x + 0.5f, bottomLeft.y + 0.5f, topRight.y - 0.5f, tileCollisionType);
                    slopeOffset = Mathf.Abs(offset.freeUp) < Mathf.Abs(offset.freeDown) ? offset.freeUp : offset.freeDown;

                    if (!isOneWay && (Mathf.Abs(slopeOffset) >= Constants.cSlopeWallHeight || (slopeOffset < 0 && state.pushesBottomTile) || (slopeOffset > 0 && state.pushesTopTile)))
                    {
                        state.pushesRightTile = true;
                        state.rightTile = new Vector2i(topRightTile.x, y);
                        return true;
                    }
                    else if (Mathf.Abs(slopeOffset) > Mathf.Abs(oldSlopeOffset))
                    {
                        wasOneWay = isOneWay;
                        slopeCollisionType = tileCollisionType;
                        state.rightTile = new Vector2i(topRightTile.x, y);
                    }
                    else
                        slopeOffset = oldSlopeOffset;

                    break;
                case TileCollisionType.Empty:
                    break;
            }
        }

        if (slopeCollisionType != TileCollisionType.Empty && slopeOffset != 0)
        {
            if (slopeOffset > 0 && slopeOffset < Constants.cSlopeWallHeight)
            {
                Vector2 pos = position, tr = topRight, bl = bottomLeft;
                pos.y += slopeOffset - Mathf.Sign(slopeOffset);
                tr.y += slopeOffset - Mathf.Sign(slopeOffset);
                bl.y += slopeOffset - Mathf.Sign(slopeOffset);
                PositionState s = new PositionState();

                if (CollidesWithTileTop(ref pos, ref tr, ref bl, ref s))
                {
                    state.pushesRightTile = true;
                    return true;
                }
                else if (move)
                {
                    position.y += slopeOffset;
                    bottomLeft.y += slopeOffset;
                    topRight.y += slopeOffset;
                    state.pushesBottomTile = true;
                    state.onOneWay = wasOneWay;
                }
            }
            else if (slopeOffset < 0 && slopeOffset > -Constants.cSlopeWallHeight)
            {
                Vector2 pos = position, tr = topRight, bl = bottomLeft;
                pos.y += slopeOffset - Mathf.Sign(slopeOffset);
                tr.y += slopeOffset - Mathf.Sign(slopeOffset);
                bl.y += slopeOffset - Mathf.Sign(slopeOffset);
                PositionState s = new PositionState();

                if (CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s))
                {
                    state.pushesRightTile = true;
                    return true;
                }
                else if (move)
                {
                    position.y += slopeOffset;
                    bottomLeft.y += slopeOffset;
                    topRight.y += slopeOffset;
                    state.pushesTopTile = true;
                    state.onOneWay = wasOneWay;
                }
            }
        }

        if (mSticksToSlope && state.pushedBottomTile && move)
        {
            var nextX = mMap.GetMapTileXAtPoint(bottomLeft.x + 1.0f);
            var bottomY = mMap.GetMapTileYAtPoint(bottomLeft.y + 1.0f) - 1;

            var prevPos = mMap.GetMapTilePosition(new Vector2i(bottomLeftTile.x, bottomLeftTile.y));
            var nextPos = mMap.GetMapTilePosition(new Vector2i(nextX, bottomY));

            var prevCollisionType = mMap.GetCollisionType(new Vector2i(bottomLeftTile.x, bottomLeftTile.y));
            var nextCollisionType = mMap.GetCollisionType(new Vector2i(nextX, bottomY));

            int x1 = (int)Mathf.Clamp((bottomLeft.x - (prevPos.x - Map.cTileSize / 2)), 0.0f, 15.0f);
            int x2 = (int)Mathf.Clamp((bottomLeft.x + 1.0f - (nextPos.x - Map.cTileSize / 2)), 0.0f, 15.0f);

            int slopeHeight = Slopes.GetSlopeHeightFromBottom(x1, prevCollisionType);
            int nextSlopeHeight = Slopes.GetSlopeHeightFromBottom(x2, nextCollisionType);

            var offset = slopeHeight + Map.cTileSize - nextSlopeHeight;

            if (offset < Constants.cSlopeWallHeight && offset > 0)
            {
                Vector2 pos = position, tr = topRight, bl = bottomLeft;
                pos.y -= offset - Mathf.Sign(offset);
                tr.y -= offset - Mathf.Sign(offset);
                bl.y -= offset - Mathf.Sign(offset);
                bl.x += 1.0f;
                tr.x += 1.0f;
                PositionState s = new PositionState();

                if (!CollidesWithTileBottom(ref pos, ref tr, ref bl, ref s))
                {
                    position.y -= offset;
                    bottomLeft.y -= offset;
                    topRight.y -= offset;
                    state.pushesBottomTile = true;
                    state.onOneWay = wasOneWay;
                }
            }
        }

        return false;
    }

    public bool CollidesWithTileTop(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state)
    {
        Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y + 0.5f));
        Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f));
        int freeDown = int.MaxValue;
        int slopeX = -1;

        for (int x = bottomleftTile.x; x <= topRightTile.x; ++x)
        {
            var tileCollisionType = mMap.GetCollisionType(x, topRightTile.y);

            if (Slopes.IsOneWay(tileCollisionType))
                continue;

            switch (tileCollisionType)
            {
                default://slope

                    Vector2 tileCenter = mMap.GetMapTilePosition(x, topRightTile.y);
                    SlopeOffsetI sf = Slopes.GetOffset(tileCenter, bottomLeft.x + 0.5f, topRight.x - 0.5f, bottomLeft.y + 0.5f, topRight.y + 0.5f, tileCollisionType);
                    sf.freeDown += 1;
                    sf.collidingTop += 1;

                    if (sf.freeDown < freeDown && sf.freeDown <= 0 && sf.freeDown == sf.collidingTop)
                    {
                        freeDown = sf.freeDown;
                        slopeX = x;
                    }

                    break;
                case TileCollisionType.Empty:
                    break;
                case TileCollisionType.Full:
                    state.pushesTopTile = true;
                    state.topTile = new Vector2i(x, topRightTile.y);
                    return true;
            }
        }

        if (slopeX != -1)
        {
            state.pushesTopTile = true;
            state.topTile = new Vector2i(slopeX, topRightTile.y);
            position.y += freeDown;
            topRight.y += freeDown;
            bottomLeft.y += freeDown;
            return true;
        }

        return false;
    }

    public bool CollidesWithTileBottom(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state)
    {
        Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y - 0.5f));
        Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y - 0.5f));
        int collidingBottom = int.MinValue;
        int slopeX = -1;
        bool wasOneWay = false, isOneWay;
        

        for (int x = bottomleftTile.x; x <= topRightTile.x; ++x)
        {
            var tileCollisionType = mMap.GetCollisionType(x, bottomleftTile.y);

            isOneWay = Slopes.IsOneWay(tileCollisionType);

            if ((mIgnoresOneWay || state.tmpIgnoresOneWay) && isOneWay)
                continue;

            switch (tileCollisionType)
            {
                default://slope

                    Vector2 tileCenter = mMap.GetMapTilePosition(x, bottomleftTile.y);

                    SlopeOffsetI sf = Slopes.GetOffset(tileCenter, bottomLeft.x + 0.5f, topRight.x - 0.5f, bottomLeft.y - 0.5f, topRight.y - 0.5f, tileCollisionType);
                    sf.freeUp -= 1;
                    sf.collidingBottom -= 1;

                    if (((sf.freeUp >= 0 && sf.collidingBottom == sf.freeUp)
                            || (mSticksToSlope && state.pushedBottom && sf.freeUp - sf.collidingBottom < Constants.cSlopeWallHeight && sf.freeUp >= sf.collidingBottom))
                        && sf.collidingBottom >= collidingBottom
                        && !(isOneWay && Mathf.Abs(sf.collidingBottom) >= Constants.cSlopeWallHeight))
                    {
                        wasOneWay = isOneWay;
                        collidingBottom = sf.collidingBottom;
                        slopeX = x;
                    }

                    break;
                case TileCollisionType.Empty:
                    break;
                case TileCollisionType.Full:
                    state.onOneWay = false;
                    state.pushesBottomTile = true;
                    state.bottomTile = new Vector2i(x, bottomleftTile.y);
                    state.tmpIgnoresOneWay = false;
                    return true;
            }
        }

        if (slopeX != -1)
        {
            state.onOneWay = wasOneWay;
            state.oneWayY = bottomleftTile.y;
            state.pushesBottomTile = true;
            state.bottomTile = new Vector2i(slopeX, bottomleftTile.y);
            position.y += collidingBottom;
            topRight.y += collidingBottom;
            bottomLeft.y += collidingBottom;
            return true;
        }

        return false;
    }

    private void MoveX(ref Vector2 position, ref bool foundObstacleX, float offset, float step, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state)
    {
        while (!foundObstacleX && offset != 0.0f)
        {
            offset -= step;

            if (step > 0.0f)
                foundObstacleX = CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state, true);
            else
                foundObstacleX = CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state, true);

            if (!foundObstacleX)
            {
                position.x += step;
                topRight.x += step;
                bottomLeft.x += step;

                CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state);
                CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state);
            }
        }
    }

    private void MoveY(ref Vector2 position, ref bool foundObstacleY, float offset, float step, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state)
    {
        while (!foundObstacleY && offset != 0.0f)
        {
            offset -= step;

            if (step > 0.0f)
                foundObstacleY = CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state);
            else
                foundObstacleY = CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state);

            if (!foundObstacleY)
            {
                position.y += step;
                topRight.y += step;
                bottomLeft.y += step;

                CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state);
                CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state);
            }
        }
    }

    public void Move(Vector2 offset, Vector2 speed, ref Vector2 position, ref Vector2 reminder, AABB aabb, ref PositionState state)
    {
        reminder += offset;

        Vector2 topRight = aabb.Max();
        Vector2 bottomLeft = aabb.Min();

        bool foundObstacleX = false, foundObstacleY = false;

        var step = new Vector2(Mathf.Sign(offset.x), Mathf.Sign(offset.y));
        var move = new Vector2(Mathf.Round(reminder.x), Mathf.Round(reminder.y));
        reminder -= move;

        if (move.x == 0.0f && move.y == 0.0f)
            return;
        else if (move.x != 0.0f && move.y == 0.0f)
        {
            MoveX(ref position, ref foundObstacleX, move.x, step.x, ref topRight, ref bottomLeft, ref state);

            if (step.x > 0.0f)
                state.pushesLeftTile = CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state);
            else
                state.pushesRightTile = CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state);
        }
        else if (move.y != 0.0f && move.x == 0.0f)
        {
            MoveY(ref position, ref foundObstacleY, move.y, step.y, ref topRight, ref bottomLeft, ref state);

            if (step.y > 0.0f)
                state.pushesBottomTile = CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state);
            else
                state.pushesTopTile = CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state);

            if (!mIgnoresOneWay && state.tmpIgnoresOneWay && mMap.GetMapTileYAtPoint(bottomLeft.y - 0.5f) != state.oneWayY)
                state.tmpIgnoresOneWay = false;
        }
        else
        {
            float speedRatio = Mathf.Abs(speed.y) / Mathf.Abs(speed.x);
            float vertAccum = 0.0f;

            while (!foundObstacleX && !foundObstacleY && (move.x != 0.0f || move.y != 0.0f))
            {
                vertAccum += Mathf.Sign(step.y) * speedRatio;

                MoveX(ref position, ref foundObstacleX, step.x, step.x, ref topRight, ref bottomLeft, ref state);
                move.x -= step.x;

                while (!foundObstacleY && move.y != 0.0f && (Mathf.Abs(vertAccum) >= 1.0f || move.x == 0.0f))
                {
                    move.y -= step.y;
                    vertAccum -= step.y;

                    MoveY(ref position, ref foundObstacleX, step.y, step.y, ref topRight, ref bottomLeft, ref state);
                }
            }

            if (step.x > 0.0f)
                state.pushesLeftTile = CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state);
            else
                state.pushesRightTile = CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state);

            if (step.y > 0.0f)
                state.pushesBottomTile = CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state);
            else
                state.pushesTopTile = CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state);

            if (!mIgnoresOneWay && state.tmpIgnoresOneWay && mMap.GetMapTileYAtPoint(bottomLeft.y - 0.5f) != state.oneWayY)
                state.tmpIgnoresOneWay = false;
        }
    }

    public void UpdatePhysics()
    {
        //assign the previous state of onGround, atCeiling, pushesRightWall, pushesLeftWall
        //before those get recalculated for this frame
        mPS.pushedBottom = mPS.pushesBottom;
        mPS.pushedRight = mPS.pushesRight;
        mPS.pushedLeft = mPS.pushesLeft;
        mPS.pushedTop = mPS.pushesTop;

        mPS.pushedBottomTile = mPS.pushesBottomTile;
        mPS.pushedLeftTile = mPS.pushesLeftTile;
        mPS.pushedRightTile = mPS.pushesRightTile;
        mPS.pushedTopTile = mPS.pushesTopTile;

        mPS.pushesBottomTile = mPS.pushesBottom;
        mPS.pushesTopTile = mPS.pushesTop;
        mPS.pushesRightTile = mPS.pushesRight;
        mPS.pushesLeftTile = mPS.pushesLeft;

            mPS.pushesBottomTile = mPS.pushesLeftTile = mPS.pushesRightTile = mPS.pushesTopTile =
            mPS.pushesBottomObject = mPS.pushesLeftObject = mPS.pushesRightObject = mPS.pushesTopObject = false;

        Vector2 topRight = mAABB.Max();
        Vector2 bottomLeft = mAABB.Min();

        CollidesWithTiles(ref mPosition, ref topRight, ref bottomLeft, ref mPS);

        if (!mPS.pushesRightTile)
        {
            CollidesWithTiles(ref mPosition, ref topRight, ref bottomLeft, ref mPS);
        }
        //save the speed to oldSpeed vector
        mOldSpeed = mSpeed;

        if (mPS.pushesBottomTile)
            mSpeed.y = Mathf.Max(0.0f, mSpeed.y);
        if (mPS.pushesTopTile)
            mSpeed.y = Mathf.Min(0.0f, mSpeed.y);
        if (mPS.pushesLeftTile)
            mSpeed.x = Mathf.Max(0.0f, mSpeed.x);
        if (mPS.pushesRightTile)
            mSpeed.x = Mathf.Min(0.0f, mSpeed.x);

        //save the position to the oldPosition vector
        mOldPosition = mPosition;

        Vector2 newPosition = mPosition + mSpeed * Time.deltaTime;

        Vector2 offset = newPosition - mPosition;

        if (offset != Vector2.zero)
        {
            Move(offset, mSpeed, ref mPosition, ref mReminder, mAABB, ref mPS);
        }

        //update the aabb
        mAABB.center = mPosition;

        mPS.pushesBottom = mPS.pushesBottomTile;
        mPS.pushesRight = mPS.pushesRightTile;
        mPS.pushesLeft = mPS.pushesLeftTile;
        mPS.pushesTop = mPS.pushesTopTile;
    }

    private void UpdatePhysicsResponse()
    {
        if (mIsKinematic)
            return;

        mPS.pushedBottomObject = mPS.pushesBottomObject;
        mPS.pushedRightObject = mPS.pushesRightObject;
        mPS.pushedLeftObject = mPS.pushesLeftObject;
        mPS.pushedTopObject = mPS.pushesTopObject;

        mPS.pushesBottomObject = false;
        mPS.pushesRightObject = false;
        mPS.pushesLeftObject = false;
        mPS.pushesTopObject = false;

        Vector2 offsetSum = Vector2.zero;

        for (int i = 0; i < mAllCollidingObjects.Count; ++i)
        {
            var other = mAllCollidingObjects[i].other;
            var data = mAllCollidingObjects[i];
            var overlap = data.overlap - offsetSum;

            if (overlap.x == 0.0f)
            {
                if (other.mAABB.center.x > mAABB.center.x)
                {
                    mPS.pushesRightObject = true;
                    mSpeed.x = Mathf.Min(mSpeed.x, 0.0f);
                }
                else
                {
                    mPS.pushesLeftObject = true;
                    mSpeed.x = Mathf.Max(mSpeed.x, 0.0f);
                }
                continue;
            }
            else if (overlap.y == 0.0f)
            {
                if (other.mAABB.center.y > mAABB.center.y)
                {
                    mPS.pushesTopObject = true;
                    mSpeed.y = Mathf.Min(mSpeed.y, 0.0f);
                }
                else
                {
                    mPS.pushesBottomObject = true;
                    mSpeed.y = Mathf.Max(mSpeed.y, 0.0f);
                }
                continue;
            }

            Vector2 absSpeed1 = new Vector2(Mathf.Abs(data.pos1.x - data.oldPos1.x), Mathf.Abs(data.pos1.y - data.oldPos1.y));
            Vector2 absSpeed2 = new Vector2(Mathf.Abs(data.pos2.x - data.oldPos2.x), Mathf.Abs(data.pos2.y - data.oldPos2.y));
            Vector2 speedSum = absSpeed1 + absSpeed2;

            
            float speedRatioX, speedRatioY;

            if (other.mIsKinematic)
                speedRatioX = speedRatioY = 1.0f;
            else
            {
                if (speedSum.x == 0.0f && speedSum.y == 0.0f)
                {
                    speedRatioX = speedRatioY = 0.5f;
                }
                else if (speedSum.x == 0.0f)
                {
                    speedRatioX = 0.5f;
                    speedRatioY = absSpeed1.y / speedSum.y;
                }
                else if (speedSum.y == 0.0f)
                {
                    speedRatioX = absSpeed1.x / speedSum.x;
                    speedRatioY = 0.5f;
                }
                else
                {
                    speedRatioX = absSpeed1.x / speedSum.x;
                    speedRatioY = absSpeed1.y / speedSum.y;
                }
            }

            float offsetX = overlap.x * speedRatioX;
            float offsetY = overlap.y * speedRatioY;

            bool overlappedLastFrameX = Mathf.Abs(data.oldPos1.x - data.oldPos2.x) < mAABB.HalfSizeX + other.mAABB.HalfSizeX;
            bool overlappedLastFrameY = Mathf.Abs(data.oldPos1.y - data.oldPos2.y) < mAABB.HalfSizeY + other.mAABB.HalfSizeY;

            if ((!overlappedLastFrameX && overlappedLastFrameY)
                || (!overlappedLastFrameX && !overlappedLastFrameY && Mathf.Abs(overlap.x) <= Mathf.Abs(overlap.y)))
            {
                mPosition.x += offsetX;
                offsetSum.x += offsetX;

                if (overlap.x < 0.0f)
                {
                    mPS.pushesRightObject = true;
                    mSpeed.x = Mathf.Min(mSpeed.x, 0.0f);
                }
                else
                {
                    mPS.pushesLeftObject = true;
                    mSpeed.x = Mathf.Max(mSpeed.x, 0.0f);
                }
            }
            else //if (!overlappedLastFrameY)//if (Mathf.Abs(data.oldPos1.x - data.oldPos2.x) < mAABB.HalfSizeX + other.mAABB.HalfSizeX)
            {
                mPosition.y += offsetY;
                offsetSum.y += offsetY;

                if (overlap.y < 0.0f)
                {
                    mPS.pushesTopObject = true;
                    mSpeed.y = Mathf.Min(mSpeed.y, 0.0f);
                }
                else
                {
                    mPS.pushesBottomObject = true;
                    mSpeed.y = Mathf.Max(mSpeed.y, 0.0f);
                }
            }
        }
    }

    public void UpdatePhysicsP2()
    {
        UpdatePhysicsResponse();

        mPS.pushesBottom = mPS.pushesBottomTile || mPS.pushesBottomObject;
        mPS.pushesRight = mPS.pushesRightTile || mPS.pushesRightObject;
        mPS.pushesLeft = mPS.pushesLeftTile || mPS.pushesLeftObject;
        mPS.pushesTop = mPS.pushesTopTile || mPS.pushesTopObject;

        //update the aabb
        mAABB.center = mPosition + AABBOffset;

        //apply the changes to the transform
        transform.position = new Vector3(Mathf.Round(mPosition.x), Mathf.Round(mPosition.y), mSpriteDepth);
        transform.localScale = new Vector3(ScaleX, ScaleY, 1.0f);
    }
}