using Framework.Debugging;
using Framework.Entities;
using Framework.PlasmaStateSystem;
using Framework.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using Framework.Utility;
using Framework.StatSystem;
using Assets.Framework.Classes.Experiments;

namespace Framework.StatSystem
{
    public static partial class StatType
    {
        public static readonly Id WalkSpeed = Allocate();
        public static readonly Id RunSpeed = Allocate();
        public static readonly Id JumpHeight = Allocate();
        public static readonly Id MaxJumpCount = Allocate();
    }
}

namespace Framework.ActorComponents
{
    public class Move : Spark { }
    public class MoveFast : Spark { }
    public class Stop : Spark { }
    public class Jump : Spark { }
    public class EndJump : Spark { }
    public class LeaveGround : Spark { }
    public class TouchGround : Spark { }

    /// <summary>
    /// This component moves the gameObject it is attached to as though
    /// the gameObject has legs, this includes walking, running, crawling etc.
    /// TODO: Implement running
    /// </summary>
    [AddComponentMenu("Scripts/Framework/Actor Components/Legs")]
    public class Legs : PhysicsMover
    {
        public float stoppingDistance = 0.5f; // The distance at which the gameObject should stop moving from its target position
        public string stateString; // A string representation of the current state

        public bool moving = false; // Whether or not the gameObject is trying to move
        public Vector3 targetPosition; // The target move position passed to this component by another component through a move command
        public int jumpCount = 0; // The current number of times that the gameObject has jumped since it last touched the ground
        public UnityTimer jumpFinishTimer = new UnityTimer(0.5f, false);

        public bool airborneFromJumping = false;

        private Stat walkSpeed; // The speed at which this gameObject can walk
        private Stat jumpHeight; // The height at which this gameObject can jump
        private Stat maxJumpCount; //The maximum number of times this gameObject can jump before having to touch the ground again

        private State main; // The parent state of all other states on this component

        private State grounded; // The state of being on the ground
        private State standing; // The state of being stood while on the ground
        private State walking; // The state of walking while on the ground
        private State running; // The state of running while on the ground

        private State airborne; // The state of being in the air
        private State jumping; // The state of jumping, just after leaving the ground
        private State falling; // The state of falling in the air

        private State floating; // The state of being in water
        private State treading; // The state of treading water
        private State swimming; // The state of swimming while in water

        /// <summary>
        /// Called when this component is awoken after the Unity
        /// Awake is called. Should be overriden.
        /// </summary>
        protected override void InnerAwake()
        {
            this.targetPosition = this.transform.position;

            this.walkSpeed = new Stat(1f);
            this.maxJumpCount = new Stat(1f);
            this.jumpHeight = new Stat(1f);

            // Setup action handlers
            AddActionHandler<MoveCommand>(MoveReaction);
            AddActionHandler<JumpCommand>(JumpReaction);
            AddActionHandler<ExposeAttributes>(SetStats);

            // Setup states
            // Main state
            this.main = new State("Main", null, UpdateMain, null);

            // Ground states
            this.grounded = new State("Grounded", this.main, EnterGrounded);

            this.standing = new State("Standing", this.grounded, null, UpdateStanding, null);
            this.walking = new State("Walking", this.grounded, null, UpdateWalking, ExitWalking);
            this.running = new State("Running", this.grounded);
            this.grounded.SetInitialStates(this.standing);

            // Air states
            this.airborne = new State("Airborne", this.main, null, UpdateAirborne, null);
            this.jumping = new State("Jumping", this.airborne, EnterJumping, UpdateJumping, null);
            this.falling = new State("Falling", this.airborne);
            this.airborne.SetInitialStates(this.falling);
            
            // Water states
            this.floating = new State("Floating", this.main);
            this.treading = new State("Treading Water", this.floating);
            this.swimming = new State("Swimming", this.floating);
            this.floating.SetInitialStates(this.treading);
            this.main.SetInitialStates(this.grounded);

            // Setup of transitions
            this.grounded.SetTransition<Jump>(this.jumping, () => { return this.jumpCount < this.maxJumpCount; });
            this.airborne.SetTransition<Jump>(this.jumping, () => { return this.jumpCount < this.maxJumpCount && this.airborneFromJumping; });
            this.standing.SetTransition<Move>(this.walking);
            this.walking.SetTransition<MoveFast>(this.running);
            this.grounded.SetTransition<Stop>(this.standing);
            this.grounded.SetTransition<LeaveGround>(this.airborne);
            this.treading.SetTransition<Move>(this.swimming);
            this.floating.SetTransition<Stop>(this.treading);
            this.falling.SetTransition<TouchGround>(this.grounded);
            this.jumping.SetTransition<EndJump>(this.airborne);

            this.stateMachine = new StateMachine();
            this.stateMachine.SetInitialState(this.main);
        }

        /// <summary>
        /// Called when this component is updated after the Unity
        /// Update is called. 
        /// </summary>
        protected override void InnerUpdate()
        {
            this.stateString = string.Join(",", this.stateMachine.GetState().Select(i => i.ToString()).ToArray()) + ", " + Vector3.Distance(this.transform.position, this.targetPosition);
            //GetComponent<Animator>().SetFloat("Speed", GetGroundRelativeVelocity().ProjectRelative(this.transform, new Vector3(1, 0, 1)).magnitude);

            if (this.stateMachine.HasState(this.airborne) && this.onGround)
            {
                this.stateMachine.Ignite(new TouchGround());
            }

            if (this.stateMachine.HasState(this.grounded) && !this.onGround)
            {
                this.stateMachine.Ignite(new LeaveGround());
            }
        }

        /// <summary>
        /// Handles the <see cref="MoveCommand"/> action by setting the target position
        /// of this component.
        /// </summary>
        /// <param name="action">The action.</param>
        private void MoveReaction(MoveCommand action)
        {
            this.targetPosition = action.GetTargetPosition();
        }

        /// <summary>
        /// Handles the <see cref="JumpCommand"/> action by igniting the jump spark of this 
        /// component's state machine.
        /// </summary>
        /// <param name="action">The action.</param>
        private void JumpReaction(JumpCommand action)
        {
            this.stateMachine.Ignite(new Jump());
        }

        /// <summary>
        /// Handles the <see cref="ExposeAttributes"/> action by setting relevant stats on
        /// this component.
        /// </summary>
        /// <param name="action">The action.</param>
        private void SetStats(ExposeAttributes action)
        {
            this.walkSpeed = (Stat)action.GetAttributes().GetStat(StatType.WalkSpeed);
            this.jumpHeight = (Stat)action.GetAttributes().GetStat(StatType.JumpHeight);
            this.maxJumpCount = (Stat)action.GetAttributes().GetStat(StatType.MaxJumpCount);
        }

        /// <summary>
        /// Called when the grounded state is entered.
        /// </summary>
        private void EnterGrounded()
        {
            Log("Grounded");
            this.jumpCount = 0;
            this.airborneFromJumping = false;
        }

        /// <summary>
        /// Called when the jumping state is entered.
        /// </summary>
        private void EnterJumping()
        {
            //UnityEditor.EditorApplication.isPaused = true;
            Log("Jumping");
            LeapToward(this.transform.position + this.transform.up, this.jumpHeight * 250f);
            this.jumpCount += 1;
            this.onGround = false;
            this.airborneFromJumping = true;
        }

        /// <summary>
        /// Called when the main state is updated, checks if the target's position is further
        /// than the stopping distance, if it is then ignites the move spark id not then it 
        /// ignites the stop spark.
        /// </summary>
        private void UpdateMain()
        {
            //this.targetPosition = this.transform.position + this.targetPosition;

            if (Vector3.Distance(this.transform.position, this.targetPosition) > 0.5f && !this.moving)
            {
                this.stateMachine.Ignite(new Move());
            }
            else if (Vector3.Distance(this.transform.position, this.targetPosition) <= 0.5f && this.moving)
            {
                this.stateMachine.Ignite(new Stop());
            }
        }

        /// <summary>
        /// Called when the walking state is updated.
        /// </summary>
        private void UpdateWalking()
        {
            Log("Moving");
            this.moving = true;
            Log("WalkSpeed: " + this.walkSpeed.GetValue());
            MoveToward(this.targetPosition, this.walkSpeed * 20f, 3f, new Vector3(1, 0, 1));
            TurnToward(this.targetPosition, 6f, new Vector3(1, 0, 1));
        }

        /// <summary>
        /// Called when the walking state is updated.
        /// </summary>
        private void UpdateStanding()
        {
            this.moving = false;
            if (GetGroundRelativeVelocity().magnitude > 0.1f)
            {
                ChangeVelocity(Vector3.zero, 10f, new Vector3(1, 0, 1));
            }
        }

        /// <summary>
        /// Called when the jumping state is updated.
        /// </summary>
        private void UpdateJumping()
        {
            this.jumpFinishTimer.Advance();
            if(this.jumpFinishTimer.IsComplete())
            {
                this.jumpFinishTimer.Reset();
                this.stateMachine.Ignite(new EndJump());
            }
        }

        /// <summary>
        /// Called when the falling state is updated.
        /// </summary>
        private void UpdateAirborne()
        {
            Log("Airborne");
            this.moving = true;
            RaycastHit hit;
            Draw.Line(this.transform.position, this.targetPosition, Color.blue);
            if (!Physics.Raycast(this.transform.position, this.targetPosition - this.transform.position, out hit, 1f))
            {
                MoveToward(this.targetPosition, 25f, 0.5f, new Vector3(1, 0, 1));
            }
            TurnToward(this.targetPosition, 10f, new Vector3(1, 0, 1));
        }

        /// <summary>
        /// Called when the walking state is exited.
        /// </summary>
        private void ExitWalking()
        {
            Log("Stopped");
            this.moving = false;
            //Stop();
            //GetComponent<Animator>().SetFloat("Speed", 0);
        }

        protected new void Log(string message)
        {
            //Debug.Log(message);
        }
    }
}
