using UnityEngine;
using NeonTea.Quakeball.Networking.Packets;
using NeonTea.Quakeball.Util;

namespace NeonTea.Quakeball.Players {
    /// <summary>The central glue class for players (both local and remote).</summary>
    /// <remarks>Other classes will handle netcode/inputs, and then speak to this class in order to make the little people run around.</remarks>
    [RequireComponent(typeof(CharacterController))]
    public class Player : MonoBehaviour {

        /// <summary>The duration after running off a cliff, during which the player should still be considered grounded.</summary>
        public float CoyoteTime;
        public float PingBias;

        /// <summary>How often should the player's movement be updated? Used by Local and Remote -Player classes.</summary>
        public float UpdateFrequency = 1f;

        public MoveStyle[] MoveStyles;
        public Transform Head;
        public Transform Body;
        public Transform Gun;
        public Animator GunBobber;

        [Header("Visuals")]
        public DesyncLerper[] Lerpables;

        [Header("Player rotation status")]
        /// <summary>The pitch of the player's head.</summary>
        public float Pitch;
        /// <summary>The total yaw of the player. Head yaw is Yaw - BodyYaw.</summary>
        public float Yaw;
        /// <summary>The yaw of the player body. Calculated from MoveDirection.</summary>
        public float BodyYaw;

        [Header("Player movement status")]
        /// <summary>The direction the player is going.</summary>
        public Vector3 MoveDirection;

        /// <summary>The amount of movement the player wants to happen.</summary>
        /// <remarks>Without analog controls, always 0 or 1.</remarks>
        public float InputSpeed;

        /// <summary>The way the player is moving.</summary>
        public byte CurrentMoveStyle = 0;
        public MoveStyle MoveStyle => MoveStyles[CurrentMoveStyle];

        [Header("Runtime computed values")]
        /// <summary>The speed at which the player is currently moving across the ground.</summary>
        public Vector3 GroundVelocity;

        /// <summary>The speed at which the player is rising or falling.</summary>
        public Vector3 GravitationalVelocity;

        /// <summary>The timestamp of when the player was last on the ground.</summary>
        public float GroundedTime;

        public float LatestGroundedY;

        [Header("Misc. technical knobs")]
        public float GroundCastLength = 0.2f;
        public LayerMask GroundLayer;

        [Header("Debug settings")]
        public bool ShowGroundCast;
        public bool ShowMoveVector;

        private CharacterController CharacterController;
        private Vector3 FeetPosition;

        /// <summary>Creates a PlayerUpdatePckt representing this Player's current status, for sending to other peers.</summary>
        public PlayerUpdatePckt CreateUpdatePacket(ulong id = 0) {
            return new PlayerUpdatePckt(MoveDirection, CurrentMoveStyle, Pitch, Yaw, id);
        }

        /// <summary>Updates this Player with the given packet.</summary>
        public void ProcessUpdatePacket(PlayerUpdatePckt packet) {
            Pitch = packet.Pitch;
            Yaw = packet.Yaw;
            MoveDirection = packet.MoveDirection;
            CurrentMoveStyle = packet.MoveStyle;
            packet = null;
        }

        /// <summary>Creates a PlayerSyncPacket representing this Player's position and velocity, for sending to the server.</summary>
        public PlayerSyncPacket CreateSyncPacket(ulong id = 0, bool unsynced = false) {
            return new PlayerSyncPacket(id, unsynced, transform.position, GroundVelocity);
        }

        /// <summary>Applies the sync packet, checking it for cheatiness if shouldApply is false.</summary>
        public bool ProcessSyncPacket(PlayerSyncPacket syncPckt, bool shouldApplyWithoutInspection = true) {
            bool ShouldApply = shouldApplyWithoutInspection;
            if (!shouldApplyWithoutInspection) {
                // TODO: Gaze into the crystal ball to determine the nefariousness level of the packet and update ShouldApply accordingly.
                ShouldApply = true;
            }
            if (ShouldApply) {
                Vector3 Delta = transform.position - syncPckt.Location;
                if (Delta.magnitude < 1) {
                    // Player is close enough to the sync packet, lerp them over.
                    foreach (DesyncLerper Lerper in Lerpables) {
                        Lerper.Offset(Delta);
                    }
                }
                transform.position = syncPckt.Location;
                GroundVelocity = syncPckt.GroundVelocity;
            }
            return ShouldApply;
        }

        /// <summary>The normal of the ground below the player. If there is no ground, it's <c>Vector3.up</c> by default.</summary>
        public Vector3 GroundCast() {
            RaycastHit Hit;
            if (ShowGroundCast) {
                Debug.DrawLine(FeetPosition, FeetPosition - Vector3.up, Color.red, 1f);
            }
            if (Physics.Raycast(FeetPosition, -Vector3.up, out Hit, GroundCastLength, GroundLayer)) {
                return Hit.normal;
            } else {
                return Vector3.up;
            }
        }

        public bool Jump() {
            bool IsCoyoteTime = Time.time - GroundedTime <= CoyoteTime + PingBias;
            if (IsCoyoteTime || IsGrounded()) {
                GravitationalVelocity = Vector3.up * MoveStyle.JumpVelocity;
                Vector3 Pos = transform.position;
                Pos.y = LatestGroundedY;
                transform.position = Pos;
                return true;
            } else {
                return false;
            }
        }

        public bool IsGrounded() {
            return CharacterController.isGrounded && Vector3.Dot(GravitationalVelocity, Vector3.down) >= 0;
        }

        private void Awake() {
            CharacterController = GetComponent<CharacterController>();
            FeetPosition = transform.position + CharacterController.center - Vector3.up * (CharacterController.height / 2 + CharacterController.skinWidth / 2);
            if (GameObject.FindObjectOfType<Util.PhysicsSyncer>() == null) {
                Debug.LogWarning("Player.Awake: There is no PhysicsSyncer in this scene! Some code will not work as expected.");
            }
        }

        private void Update() {
            if (MoveDirection.magnitude > 0) {
                BodyYaw = Vector3.SignedAngle(Vector3.forward, MoveDirection, Vector3.up);
            }
            Body.localRotation = Quaternion.Lerp(Body.localRotation, Quaternion.Euler(0, BodyYaw, 0), 20f * Time.deltaTime);
            Head.localRotation = Quaternion.Lerp(Head.localRotation, Quaternion.Euler(Pitch, Yaw, 0), 15f * Time.deltaTime);
            UpdateMovement();
        }

        private void UpdateMovement() {
            bool Grounded = IsGrounded();

            bool FallingDown = Vector3.Dot(Vector3.down, GravitationalVelocity) > 0;
            if (Grounded && FallingDown) {
                GravitationalVelocity = Vector3.zero;
            } else if (!Grounded) {
                GravitationalVelocity += Physics.gravity * Time.deltaTime;
            }

            float FrictionVelocityFactor = Mathf.Max(GroundVelocity.magnitude, MoveStyle.StopVelocity);
            float Deccel = FrictionVelocityFactor * Time.deltaTime;
            if (Grounded) {
                Deccel *= MoveStyle.Friction;
            } else {
                Deccel *= MoveStyle.AirFriction;
            }
            float FrictionedVelocity = Mathf.Max(0, GroundVelocity.magnitude - Deccel);
            GroundVelocity = GroundVelocity.normalized * FrictionedVelocity;

            Vector3 GroundNormal = GroundCast();
            Vector3 FixedHeading = Vector3.ProjectOnPlane(MoveDirection, GroundNormal).normalized;
            float CurrentSpeed = Vector3.Dot(GroundVelocity, FixedHeading);
            float Acceleration = MoveStyle.TargetVelocity * Time.deltaTime;
            if (Grounded) {
                Acceleration *= MoveStyle.Acceleration;
            } else {
                Acceleration *= MoveStyle.AirAcceleration;
            }
            Acceleration = Mathf.Min(Acceleration, MoveStyle.TargetVelocity - CurrentSpeed);
            GroundVelocity += FixedHeading * Acceleration;

            Vector3 FinalMoveVector = GroundVelocity + GravitationalVelocity;
            CharacterController.Move(FinalMoveVector * Time.deltaTime);
            if (CharacterController.isGrounded) {
                GroundedTime = Time.time;
                LatestGroundedY = transform.position.y;
            }
            if (GravitationalVelocity.y > 0.1 && Mathf.Abs(CharacterController.velocity.y) < 0.1) {
                // Hit a roof while jumping
                GravitationalVelocity.y = 0;
            }
            if (ShowMoveVector) {
                Debug.DrawLine(
                    transform.position + CharacterController.center,
                    transform.position + CharacterController.center + FinalMoveVector,
                    Color.green, 1.0f
                );
            }

            float TargetBobbiness = Grounded ? GroundVelocity.magnitude / MoveStyle.TargetVelocity : 0;
            GunBobber.SetLayerWeight(1, Mathf.Lerp(GunBobber.GetLayerWeight(1), TargetBobbiness, 10f * Time.deltaTime));
        }
    }
}
