﻿#define DEBUG
using UnityEngine;
using System;
using System.Collections.Generic;


[RequireComponent(typeof(BoxCollider2D))]
public class CharacterController2D : MonoBehaviour
{
	#region internal types
	
	private enum MoveDirection : int
	{
		Right = 1,
		Left = -1,
		Up = 1,
		Down = -1
	}
	
	private struct CharacterRaycastOrigins
	{
		public Vector3 topRight;
		public Vector3 topLeft;
		public Vector3 bottomRight;
		public Vector3 bottomLeft;
	}
	
	public class CharacterCollisionState2D
	{
		public bool right;
		public bool left;
		public bool above;
		public bool below;
		public bool becameGroundedThisFrame;
		
		
		public void reset()
		{
			right = left = above = below = becameGroundedThisFrame = false;
		}
		
		
		public override string ToString()
		{
			return string.Format("[CharacterCollisionState2D] r: {0}, l: {1}, a: {2}, b: {3}", right, left, above, below);
		}
	}
	
	#endregion
	
	
	#region properties and fields
	
	public event Action<RaycastHit2D> onControllerCollidedEvent;
	
	/// <summary>
	/// toggles if the RigidBody2D velocity should be used for movement or if Transform.Translate will be used
	/// </summary>
	public bool usePhysicsForMovement = false;
	
	/// <summary>
	/// defines how far in from the edges of the collider rays are cast from. If cast with a 0 extent it will often result in ray hits that are
	/// not desired (for example a foot collider casting horizontally from directly on the surface can result in a hit)
	/// </summary>
	[Range(0, 0.3f)]
	public float skinWidth = 0.02f;
	
	/// <summary>
	/// mask with all layers that the player should interact with
	/// </summary>
	public LayerMask platformMask = 0;
	
	/// <summary>
	/// mask with all layers that should act as one-way platforms. Note that one-way platforms should always be EdgeCollider2Ds
	/// </summary>
	public LayerMask oneWayPlatformMask = 0;
	
	[Range(0, 90f)]
	public float slopeLimit = 30f;
	
	/// <summary>
	/// curve for multiplying speed based on slope (negative = downwards)
	/// </summary>
	public AnimationCurve slopeSpeedMultiplier = new AnimationCurve(new Keyframe(0, 1), new Keyframe(90, 0));
	
	[Range(2, 20)]
	public int totalHorizontalRays = 8;
	[Range(2, 20)]
	public int totalVerticalRays = 4;
	
	
	[HideInInspector]
	public new Transform transform;
	[HideInInspector]
	public BoxCollider2D boxCollider;
	
	[HideInInspector]
	[NonSerialized]
	public CharacterCollisionState2D collisionState = new CharacterCollisionState2D();
	[HideInInspector]
	[NonSerialized]
	public Vector3 velocity;
    public bool isGrounded 
    { 
        get { return collisionState.below; }
        set { collisionState.below = value; } 
    }

    //public bool isgrounded = false;

	#endregion
	public delegate void DoOnBecomeGrounded();
    public event DoOnBecomeGrounded OnBecomeGrounded = new DoOnBecomeGrounded(()=>{});
	
	/// <summary>
	/// holder for our raycast origin corners (TR, TL, BR, BL)
	/// </summary>
	private CharacterRaycastOrigins _raycastOrigins;
	
	/// <summary>
	/// stores our raycast hit during movement
	/// </summary>
	private RaycastHit2D _raycastHit;
    private RaycastHit2D[] _platformHit;
	
	// horizontal/vertical movement data
	private float _verticalDistanceBetweenRays;
	private float _horizontalDistanceBetweenRays;
	
	public bool _isStandingOnMovingPlatform = false;
    private Transform _parentPlatform = null;
    private Vector3 _parentPlatformOldPosition;

  //  private bool _becomeStandingOnMoving = false;
	
	#region Monobehaviour
	
	void Awake()
	{
		// add our one-way platforms to our normal platform mask so that we can land on them from above
		platformMask |= oneWayPlatformMask;
		
		// cache some components
		transform = GetComponent<Transform>();
		boxCollider = GetComponent<BoxCollider2D>();
		
		// figure out the distance between our rays in both directions
		// horizontal
		var colliderUseableHeight = boxCollider.size.y * Mathf.Abs(transform.localScale.y) - (2f * skinWidth);
		_verticalDistanceBetweenRays = colliderUseableHeight / (totalHorizontalRays - 1);
		
		// vertical
		var colliderUseableWidth = boxCollider.size.x * Mathf.Abs(transform.localScale.x) - (2f * skinWidth);
		_horizontalDistanceBetweenRays = colliderUseableWidth / (totalVerticalRays - 1);
	}
	
	#endregion
		
	[System.Diagnostics.Conditional("DEBUG")]
	private void DrawRay(Vector3 start, Vector3 dir, Color color)
	{
		Debug.DrawRay(start, dir, color);
	}
	
	
	#region Movement
	
	/// <summary>
	/// resets the raycastOrigins to the current extents of the box collider inset by the skinWidth. It is inset
	/// to avoid casting a ray from a position directly touching another collider which results in wonky normal data.
	/// </summary>
	/// <param name="futurePosition">Future position.</param>
	/// <param name="deltaMovement">Delta movement.</param>
	private void primeRaycastOrigins(Vector3 futurePosition, Vector3 deltaMovement)
	{
		var scaledColliderSize = new Vector2(boxCollider.size.x * Mathf.Abs(transform.localScale.x), boxCollider.size.y * Mathf.Abs(transform.localScale.y)) / 2;
		var scaledCenter = new Vector2(boxCollider.offset.x * transform.localScale.x, boxCollider.offset.y * transform.localScale.y);
		
		_raycastOrigins.topRight = transform.position + new Vector3(scaledCenter.x + scaledColliderSize.x, scaledCenter.y + scaledColliderSize.y);
		_raycastOrigins.topRight.x -= skinWidth;
		_raycastOrigins.topRight.y -= skinWidth;
		
		_raycastOrigins.topLeft = transform.position + new Vector3(scaledCenter.x - scaledColliderSize.x, scaledCenter.y + scaledColliderSize.y);
		_raycastOrigins.topLeft.x += skinWidth;
		_raycastOrigins.topLeft.y -= skinWidth;
		
		_raycastOrigins.bottomRight = transform.position + new Vector3(scaledCenter.x + scaledColliderSize.x, scaledCenter.y - scaledColliderSize.y);
		_raycastOrigins.bottomRight.x -= skinWidth;
		_raycastOrigins.bottomRight.y += skinWidth;
		
		_raycastOrigins.bottomLeft = transform.position + new Vector3(scaledCenter.x - scaledColliderSize.x, scaledCenter.y - scaledColliderSize.y);
		_raycastOrigins.bottomLeft.x += skinWidth;
		_raycastOrigins.bottomLeft.y += skinWidth;
	}
	
	
	/// <summary>
	/// we have to use a bit of trickery in this one. The rays must be cast from a small distance inside of our
	/// collider (skinWidth) to avoid zero distance rays which will get the wrong normal. Because of this small offset
	/// we have to increase the ray distance skinWidth then remember to remove skinWidth from deltaMovement before
	/// actually moving the player
	/// </summary>
    /// 

	private void moveHorizontally(ref Vector3 deltaMovement)
	{
 		
		var isGoingRight = deltaMovement.x > 0;
		var rayDistance = Mathf.Abs(deltaMovement.x) + skinWidth;
		var rayDirection = isGoingRight ? Vector2.right : -Vector2.right;
		var initialRayOrigin = isGoingRight ? _raycastOrigins.bottomRight : _raycastOrigins.bottomLeft;

     
		for (var i = 0; i < totalHorizontalRays; i++)
		{
			var ray = new Vector2(initialRayOrigin.x, initialRayOrigin.y + i * _verticalDistanceBetweenRays);
			
			DrawRay(ray, rayDirection * rayDistance, Color.red);
			_raycastHit = Physics2D.Raycast(ray, rayDirection, rayDistance, platformMask & ~oneWayPlatformMask);
			if (_raycastHit)
			{

				// set our new deltaMovement and recalculate the rayDistance taking it into account
                deltaMovement.x = _raycastHit.point.x - ray.x;// -positionFix;
				rayDistance = Mathf.Abs(deltaMovement.x);
				
				// remember to remove the skinWidth from our deltaMovement
				if (isGoingRight)
				{
					deltaMovement.x -= skinWidth;
					collisionState.right = true;
				}
				else
				{
					deltaMovement.x += skinWidth;
					collisionState.left = true;
				}
				
				if (onControllerCollidedEvent != null)
					onControllerCollidedEvent(_raycastHit);
				
				// we add a small fudge factor for the float operations here. if our rayDistance is smaller
				// than the width + fudge bail out because we have a direct impact
				if (rayDistance < skinWidth + 0.001f)
					break;
			}
		}

	}
		
	//public bool flipped = false;
    private bool platformWasUnderLastFrame = false;
    private List<Transform> lastframePlatforms = new List<Transform>();
	
	private void moveVertically(ref Vector3 deltaMovement)
	{
			
		// Figure out which way we are heading
		var isGoingUp = deltaMovement.y > 0;
		var rayDistance = Mathf.Abs(deltaMovement.y) + skinWidth;
		var rayDirection = isGoingUp ? Vector2.up : -Vector2.up;
		var initialRayOrigin = isGoingUp ? _raycastOrigins.topLeft : _raycastOrigins.bottomLeft;
        var platformRayOrigin = _raycastOrigins.bottomLeft;
		
		// if we are moving up, we should ignore the layers in oneWayPlatformMask
		var mask = platformMask;

		if (isGoingUp) mask &= ~oneWayPlatformMask;

        /*
         * This is additional test to handle cases where moving platform passes stationary player when going up
         * Test checks if moving paltform was under players feet in previous frame.
         * If it was under previously and now over, it must have passed player so move player on it
         * */

        bool hitted = false;
        bool foundBelow = false;

        List<Transform> platformsFound = new List<Transform>();

        lastframePlatforms = platformsFound;
        platformWasUnderLastFrame = foundBelow;
        
        hitted = false;
		for (var i = 0; i < totalVerticalRays; i++) {
			var ray = new Vector2 (initialRayOrigin.x + i * _horizontalDistanceBetweenRays, initialRayOrigin.y);
			
			DrawRay (ray, rayDirection * rayDistance, Color.green);
			_raycastHit = Physics2D.Raycast (ray, rayDirection, rayDistance, mask);

			if (_raycastHit) {
                //if (_raycastHit.collider.transform != transform.parent) 
                
                hitted = true;
				// set our new deltaMovement and recalculate the rayDistance taking it into account
				deltaMovement.y = _raycastHit.point.y - ray.y;
				rayDistance = Mathf.Abs (deltaMovement.y);
				
				// remember to remove the skinWidth from our deltaMovement
				if (isGoingUp) {
					deltaMovement.y -= skinWidth;

					collisionState.above = true;

				} else {
					
					deltaMovement.y += skinWidth;
					collisionState.below = true;

				}
				
				if (onControllerCollidedEvent != null)
					onControllerCollidedEvent (_raycastHit);
				
				// we add a small fudge factor for the float operations here. if our rayDistance is smaller
				// than the width + fudge bail out because we have a direct impact
				if (rayDistance < skinWidth + 0.001f)
					return;
			} 

		}//for

        //if didnt hit anything on collision check, drop from possible moving platforms
//        if (_isStandingOnMovingPlatform && !hitted) dropFromMovingPlatform();
        
	}//moveVertically

    private bool checkIfInLastFrameList(Transform t)
    {

        foreach (Transform old in lastframePlatforms)
        {
            if (t == old) return true;
        }

        return false;
    }

	
	#endregion

    public void resetVelocity()
    {
        velocity = Vector3.zero;
    }

    //move function without any velocity etc stuff that affects manual controlling of the character
    public void externalMove(Vector3 deltaMovement) {

        var desiredPosition = transform.position + deltaMovement;
        primeRaycastOrigins(desiredPosition, deltaMovement);


        // first we check movement in the horizontal dir
        if (deltaMovement.x != 0)
            moveHorizontally(ref deltaMovement);

        // next, check movement in the vertical dir
        if (deltaMovement.y != 0)
            moveVertically(ref deltaMovement);

        transform.Translate(deltaMovement);
    }

    private void moveDiagonally(ref Vector3 deltaMovement)
    {
        var isGoingRight = deltaMovement.x > 0;
        var isGoingUp = deltaMovement.y > 0;

        var rayDistance = Mathf.Abs(deltaMovement.x) + skinWidth;

        var rayDirection = (Vector2)deltaMovement;

        var initialRayOrigin = isGoingRight ? _raycastOrigins.bottomRight : _raycastOrigins.bottomLeft;
      
        var platformRayOrigin = _raycastOrigins.bottomLeft;

        //DrawRay(initialRayOrigin, rayDirection, Color.magenta);

        var ray = (Vector2)initialRayOrigin;

        var mask = platformMask - oneWayPlatformMask;

        DrawRay(ray, rayDirection, Color.green);
        _raycastHit = Physics2D.Raycast(ray, rayDirection, deltaMovement.magnitude, mask);


        if (_raycastHit && !isGoingUp)
        {
            Debug.Log("hits corner");
           // Debug.Log(_raycastHit.normal);

            if (_raycastHit.normal.x == 0)
            {
                //ver
                deltaMovement.y = _raycastHit.point.y - ray.y;
                if (isGoingRight)
                {
                    deltaMovement.x -= skinWidth;
                    collisionState.right = true;
                }
                else
                {
                    deltaMovement.x += skinWidth;
                    collisionState.left = true;
                }
            }
            if (_raycastHit.normal.y == 0)
            {
                //hor
                deltaMovement.y = _raycastHit.point.y - ray.y;
				
				// remember to remove the skinWidth from our deltaMovement
               /* if (isGoingUp)
                {
                    deltaMovement.y -= skinWidth;
                    collisionState.above = true;
                }
                else
                {*/

                    deltaMovement.y += skinWidth;
                    collisionState.below = true;
                //}
            }
        }
        
    }

	public void move(Vector3 deltaMovement)
	{
       // Debug.Log(deltaMovement.x.ToString("F3"));

		// save off our current grounded state
		var wasGroundedBeforeMoving = collisionState.below;
		
		// clear our state
		collisionState.reset();


        var desiredPosition = transform.position + deltaMovement;
		primeRaycastOrigins(desiredPosition, deltaMovement);

        Vector3 oldDelta = deltaMovement;
		// first we check movement in the horizontal dir
		if (deltaMovement.x != 0)
			moveHorizontally(ref deltaMovement);
		
		// next, check movement in the vertical dir
		if (deltaMovement.y != 0)
			moveVertically(ref deltaMovement);

        //check diagonal movement in case of going directly to corner
        if (deltaMovement.x != 0 && deltaMovement.y != 0 && oldDelta == deltaMovement)
        {
            moveDiagonally(ref deltaMovement);
        }

		// move then update our state
		/*   if (usePhysicsForMovement)
        {
            GetComponent<Rigidbody2D>().velocity = deltaMovement / Time.fixedDeltaTime;
            velocity = GetComponent<Rigidbody2D>().velocity;
        }
        else
        {*/
		//Debug.Log (deltaMovement);
		if (Time.timeScale != 0 && Time.deltaTime != 0) {
          //  Debug.Log("siirros " + deltaMovement.x.ToString("F3"));
			transform.Translate (deltaMovement);
            velocity = deltaMovement / Time.deltaTime;
		} 
		
		// set our becameGrounded state based on the previous and current collision state
		if (!wasGroundedBeforeMoving && collisionState.below)
		{
			collisionState.becameGroundedThisFrame = true;
            if (OnBecomeGrounded != null) 
                OnBecomeGrounded();
			
		}
		
	}
	/*
    void OnCollisionEnter2D(Collision2D other)

    {
        Debug.Log(other.collider.transform.name);

    }*/
	

	
}

