using UnityEngine;
using NeonTea.Quakeball.Networking.Packets;
using NeonTea.Quakeball.Util;
using NeonTea.Quakeball.Networking;
using NeonTea.Quakeball.Networking.Instances;
using NeonTea.Quakeball.Combat;
using NeonTea.Quakeball.Interface;
using NeonTea.Quakeball.Animation;
using NeonTea.Quakeball.Audio;

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;

        public MoveStyle[] MoveStyles;
        public Transform Gun;
        public Animator GunAnimator;

        public SoldierProceduralAnimator SoldierProceduralAnimator;

        [Tooltip("GameObjects that are disabled on death and re-enabled on respawn.")]
        public GameObject[] DisabledOnDeath;

        [Header("Shooting")]
        [Tooltip("For raycasting the shoot target.")]
        public Transform CameraRoot;
        public Transform BulletSourcePoint;
        public LayerMask BulletHitLayer;
        public LayerMask BulletPassLayer;
        public float Cooldown;

        [Header("Visuals")]
        public DesyncLerper[] Lerpables;
        public float Lean;
        public GameObject LaserPrefab;

        public ParticleSystem Splatter;

        [Header("Audio")]
        public AudioSource GunShotAudioSource;
        public AudioClip RaygunClip;
        public AudioSource HitAudioSource;
        public AudioSource LocalHitAudioSource;

        [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;

        [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;

        /// <summary> The possible networked Id of this Player instance </summary>
        public ulong NetId;

        /// <summary> Is the player dead</summary>
        public bool IsDead;

        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;

        private float LastShot;
        private float TimeofDeath;

        /// <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) {
            if (!IsDead) {
                Pitch = packet.Pitch;
                Yaw = packet.Yaw;
                MoveDirection = packet.MoveDirection;
                CurrentMoveStyle = packet.MoveStyle;
            }
        }

        /// <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;
        }

        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 void Shoot() {
            float delta = Time.time - LastShot;
            if (Net.Singleton.Instance is Server) {
                float ping = ((Server)Net.Singleton.Instance).Players[NetId].Ping;
                delta += ping;
            }
            if (delta < Cooldown) {
                return;
            }
            LastShot = Time.time;
            Vector3 GunPoint = BulletSourcePoint.position;
            Vector3 ShotDelta = CameraRoot.forward * 1000f;

            Vector3 From = CameraRoot.position;
            Vector3 Direction = CameraRoot.forward;
            RaycastHit[] Hits = Physics.RaycastAll(From, Direction, 1000f, BulletHitLayer | BulletPassLayer);
            System.Array.Sort(Hits, (a, b) => { return a.distance.CompareTo(b.distance); });
            foreach (RaycastHit Hit in Hits) {
                ShotDelta = Hit.point - GunPoint;
                Player Player = Hit.rigidbody != null ? Hit.rigidbody.GetComponent<Player>() : null;
                if (Player == this) {
                    continue;
                }
                if (((1 << Hit.collider.gameObject.layer) & BulletPassLayer) != 0) {
                    ImpactSound ImpactSound = Hit.collider.GetComponent<ImpactSound>();
                    if (ImpactSound != null) {
                        ImpactSound.PlayAt(Hit.point);
                    }
                    continue;
                }
                if (Player != null) {
                    if (Net.Singleton.Instance is Server) {
                        ((Server)Net.Singleton.Instance).SendHit(NetId, Player.NetId);
                        Player.Hit(NetId);
                    }
                }
                break;
            }

            GunAnimator.SetBool("Shot", true);
            GameObject LaserEffect = Instantiate(LaserPrefab);
            Laser Laser = LaserEffect.GetComponent<Laser>();
            Laser.From = GunPoint;
            Laser.To = GunPoint + ShotDelta;

            GunShotAudioSource.PlayOneShot(RaygunClip);
        }

        public void Hit(ulong sourceUid) {
            Terminal.Singleton.Println($"{NetId} hit sourceIid: {sourceUid}");
            if (Net.Singleton.Instance is Server) {
                ((Server)Net.Singleton.Instance).HandlePlayerDeath(NetId, sourceUid);
            }
            bool IsLocal = true;
            if (Net.Singleton.Instance != null) {
                IsLocal = Net.Singleton.Instance.LocalPlayer.Id == sourceUid;
            }
            Splatter.Play();
            HitAudioSource.Play();
            if (IsLocal) {
                Net.Singleton.Instance.LocalPlayer.Controlled.LocalHitAudioSource.Play();
            }
        }

        /// <summary>Called when this Player is dead</summary>
        public void Dead(ulong killer) {
            Terminal.Singleton.Println($"{killer} killed me: {NetId}, deadness currently: {IsDead}");
            if (IsDead) {
                return;
            }
            if (Net.Singleton.Instance != null && Net.Singleton.Instance.LocalPlayer.Id == NetId) {
                string name = $"Connection {killer}";
                GameObject.FindGameObjectWithTag("DeadScreen").GetComponent<DeadScreen>().StartCountdown(name);
                Net.Singleton.Instance.LocalPlayer.Controlled.GetComponent<LocalPlayer>().DisableInput += 1;
            }
            SoldierProceduralAnimator.StartRagdoll();
            TimeofDeath = Time.time;
            MoveDirection = Vector3.zero;
            foreach (GameObject obj in DisabledOnDeath) {
                obj.SetActive(false);
            }
            IsDead = true;
        }

        /// <summary>Called when this Player is respawned, after dying.</summary>
        public void Respawn(Vector3 location) {
            Terminal.Singleton.Println($"Respawned (I am {NetId}), deadness before proper resurrection: {IsDead}");
            if (Net.Singleton.Instance != null && Net.Singleton.Instance.LocalPlayer.Id == NetId && IsDead) {
                GameObject.FindGameObjectWithTag("DeadScreen").GetComponent<DeadScreen>().Open = false;
                Net.Singleton.Instance.LocalPlayer.Controlled.GetComponent<LocalPlayer>().DisableInput -= 1;
            }
            transform.position = location;
            SoldierProceduralAnimator.StopRagdoll();
            foreach (GameObject obj in DisabledOnDeath) {
                obj.SetActive(true);
            }
            IsDead = false;
        }

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

        /// <summary>The normal of the ground below the player. If there is no ground, it's <c>Vector3.up</c> by default.</summary>
        private Vector3 GroundCast() {
            RaycastHit hit;
            Vector3 feetPosition = FeetPosition + transform.position + CharacterController.center + Vector3.down * CharacterController.height / 2;
            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;
            }
        }

        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.");
            }
            LastShot = Time.time - Cooldown;
        }

        private void Update() {
            if (Net.Singleton.Instance is Server) {
                if (IsDead && (Time.time - TimeofDeath > 3)) {
                    ((Server)Net.Singleton.Instance).HandlePlayerRespawn(NetId);
                }
            }

            float TargetLean = -Vector3.Dot(GroundVelocity / MoveStyle.TargetVelocity, CameraRoot.right) * MoveStyle.LeanDegrees;
            Lean = Mathf.Lerp(Lean, TargetLean, 30f * Time.deltaTime);
            CameraRoot.localEulerAngles = new Vector3(Pitch - CameraRoot.parent.eulerAngles.x, Yaw - CameraRoot.parent.eulerAngles.y, Lean);
        }

        private void LateUpdate() {
            GunAnimator.SetBool("Shot", false);
            UpdateMovement();
        }

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

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

            Vector3 GroundNormal = GroundCast();

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

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

            CharacterController.Move((GravitationalVelocity + GroundVelocity) * 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, reset falling velocity downwards (same as "static gravity" when grounded)
                GravitationalVelocity.y = GroundCastLength;
            }

            if (ShowMoveVector) {
                Debug.DrawLine(transform.position + CharacterController.center, transform.position + CharacterController.center + GravitationalVelocity, Color.yellow, 1.0f);
                Debug.DrawLine(transform.position + CharacterController.center, transform.position + CharacterController.center + GroundVelocity, Color.green, 1.0f);
            }

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