using System.Collections.Generic;
using UnityEngine;
using Mirror;
using System.Linq;
using UnityEngine.UI;

public class SpaceshipController : NetworkBehaviour
{
    [SyncVar(hook=nameof(OnPnameChanged))]
    public string pname;
    [SyncVar(hook=nameof(OnScoresChanged))]
    public int Scores;
    [SyncVar(hook=nameof(OnTrailTimeChanged))]
    public float trailTime;
    public float trailIncrementRate = 0.5f;
    [SyncVar]
    public bool dead;
    [SyncVar]
    public float speed;
    [SyncVar(hook=nameof(OnScaleChanged))]
    public float scaleMultiplier=1;
    public Text pnameTxt;
    public Transform body;
    public TrailMgr trailMgr;
    public float movingSpeed = 0.1f;
    public float turningSmoothFactor = 0.1f;
    public Joystick joystick;
    public Vector2 joyInput;
    [SyncVar]
    public bool boosting;

    [Header("Client Prediction")]
    public SortedDictionary<int, StatePayload> serverStateBuffer = new SortedDictionary<int, StatePayload>();
    public Vector3 DetourError = new Vector3(0,0.2f,0);
    public Vector3 Detour = Vector3.zero;
    public Quaternion RotationDetour = Quaternion.identity;
    public float DetourCorrectionFactor = 0.5f;
    public float timeDelayErrorFix = 1f;
    public bool showDebugHUD = false;

    public float distanceFromCenter= 0;

    [Command]
    void CmdSetPname(string value){
        pname = value;
    }
    void OnPnameChanged(string oldName, string newName){
        pnameTxt.text = newName;
    }


    void OnScoresChanged(int oldScores, int newScores){
        Debug.Log($"Add scores { newScores - oldScores}, (total: {newScores})");
    }

    void OnTrailTimeChanged(float oldValue, float newValue){
        trailMgr.trail.time = newValue;
    }

    void OnBoostDown(){
        if(isLocalPlayer){
            if(isServer){
                boosting=true;
            }else{
                CmdSetBoosting(true);
            }
        }
    }

    void OnBoostUp(){
        if(isLocalPlayer){
            if(isServer){
                boosting=false;
            }else{
                CmdSetBoosting(false);
            }
        }
    }

    void OnScaleChanged(float oldScale, float newScale){
        if(isLocalPlayer){
            SceneData.holder.boostBtn.gameObject.SetActive(newScale>1);
        }
    }

    [Command]
    void CmdSetBoosting(bool value){
        boosting=value;
    }

    void Start()
    {
        scaleMultiplier=1;
        if (isLocalPlayer)
        {
            if (joystick == null) { joystick = FindObjectOfType<Joystick>(); }
            FindObjectOfType<CameraFollower>().SetTarget(transform);
            string myName = PlayerPrefs.GetString("username");
            SceneData.localPlayer = gameObject;
            SceneData.OnBoostDown.AddListener(OnBoostDown);
            SceneData.OnBoostUp.AddListener(OnBoostUp);
            if(isServer){pname=myName;}else{
                CmdSetPname(myName);
            }

            pnameTxt.text = myName;
            pnameTxt.gameObject.SetActive(false);

        }
        if (isServer)
        {
            
        }
    }

    // Update is called once per frame
    int timeInMillis => (int)(NetworkTime.time * 1000);
    int roundedTime => Mathf.FloorToInt((float)timeInMillis / 100f) * 100;

    int lastClientUpdateTime = 0;
    void FixedUpdate()
    {
        distanceFromCenter = Vector3.Distance(transform.position, Vector3.zero);
        pnameTxt.rectTransform.rotation = Quaternion.Euler(Vector3.zero);

        //Update size of trail and spaceship
        transform.localScale = Vector3.Lerp(transform.localScale,new Vector3(scaleMultiplier,scaleMultiplier,scaleMultiplier),0.1f);
        trailMgr.trail.startWidth = Mathf.Lerp(trailMgr.trail.startWidth,scaleMultiplier,0.1f);

        if(dead){return;}

        if (isLocalPlayer)
        {
            joyInput = joystick.input;

            //Cheats => TODO: Remove this
            if(Input.GetKeyDown(KeyCode.F)){
                CmdCheatKills();
            }
        }
        //Simulate on both client and server
        if (isLocalPlayer || isServer)
        {
            body.Translate(new Vector3(0, speed), body);
            if (joyInput != Vector2.zero)
            {
                Turn(joyInput);
            }
        }

        if(isServer){
            //boost check
            if(boosting && scaleMultiplier > 1){
                speed= movingSpeed*2;
                scaleMultiplier-=Time.deltaTime;
                
                if(scaleMultiplier<1){scaleMultiplier=1;} //Clamp in case gets lower
            }else{
                speed=movingSpeed;
            }
        }
        ///Diff = rot1 . rot2
        ///1 = rot1 . rot2 / Diff
        ///1 * Diff * Diff = rot1.rot2. Diff

        if (isLocalPlayer)
        {
            CmdUpdateJoyInput(joyInput);

            //Fix Detours
            if (Mathf.Abs(Detour.magnitude) > 0.1f || true)
            {
                Vector3 newPosition = body.position + Detour;
                Quaternion newRotation = body.rotation * RotationDetour;
                if(Detour.magnitude > 0.5f){
                    trailMgr.trail.emitting =false;
                }
                body.position = Vector3.Lerp(body.position, newPosition, (Mathf.Abs(Detour.magnitude) > 0.2f) ?  DetourCorrectionFactor * 2 * Detour.magnitude  : DetourCorrectionFactor);
                Detour = newPosition - body.position;
                body.rotation = Quaternion.Lerp(body.rotation, newRotation, DetourCorrectionFactor * ((joystick.touchDown) ? 0.1f : 1));
                RotationDetour = Quaternion.Inverse(transform.rotation) * newRotation;

                trailMgr.trail.emitting=true;
            }
        }

        //Fill input and position buffer for client predictions

        StatePayload currentState = new StatePayload(timeInMillis, transform.position, transform.rotation, joyInput);

        if (isLocalPlayer)
        {
            if (lastClientUpdateTime < roundedTime)
            {
                CmdValidateMovement(roundedTime, currentState.Position, currentState.Rotation, currentState.Input);
                lastClientUpdateTime = roundedTime;
            }
        }
        else if (isServer)
        {
            int lastUpdatedTime = serverStateBuffer.Count > 0 ? serverStateBuffer.Keys.Last() : 0;
            if (timeInMillis >= lastUpdatedTime + 100)
            {
                serverStateBuffer.Add(roundedTime, currentState);
                if (serverStateBuffer.Count > 1024)
                {
                    serverStateBuffer.Remove(serverStateBuffer.Keys.First());
                }
            }
            RpcUpdatePosition(joyInput, transform.position, transform.rotation, timeInMillis);
            RpcUpdateTrail(trailMgr.positions);
        }else{
            transform.position = Vector3.Lerp(transform.position, targetPosition, 0.1f);
            transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, 0.1f);
        }
    }

    Vector3 targetPosition;
    Quaternion targetRotation;
    

    void Turn(Vector2 input)
    {
        body.rotation = Quaternion.Lerp(body.rotation, getTurnAngle(input), turningSmoothFactor * input.magnitude);
    }

    Quaternion getTurnAngle(Vector2 input)
    {
        var angle1 = Mathf.Atan2(-input.y, -input.x) * Mathf.Rad2Deg;
        return Quaternion.AngleAxis(angle1 + 90, Vector3.forward);
    }

    [Command]
    void CmdUpdateJoyInput(Vector2 input)
    {
        joyInput = input;
    }

    [Command]
    void CmdValidateMovement(int time, Vector3 position, Quaternion rotation, Vector2 input)
    {
        StatePayload payload = new StatePayload(time, position, rotation, input);
        StatePayload lastServerState = serverStateBuffer.Values.Last();
        StatePayload payloadToCompare;
        if (serverStateBuffer.Count < 1) { Debug.Log("still Initiating server buffer"); return; }
        joyInput = payload.Input;
        if (!serverStateBuffer.ContainsKey(time))
        {
            payloadToCompare = lastServerState;
            if (lastServerState.Time < time)
            {
               // Debug.Log("Server doesn't have that data yet\nYou asked for " + time + " best I can do is " + lastServerState.Time);
            }
            else
            {
                for (int i = 0; i < 50; i++)
                {
                    if (serverStateBuffer.ContainsKey(time - i))
                    {
                        payloadToCompare = serverStateBuffer[time - i];
                        Debug.Log($"Found closest state at {payloadToCompare.Time}, requested {time}");
                        break;
                    }
                }
            }

        }
        else
        {
            payloadToCompare = serverStateBuffer[time];
        }
        Vector3 positionDiff = payloadToCompare.Position - payload.Position;
        int timeDiff = lastServerState.Time - time;

        if (Mathf.Abs(positionDiff.magnitude) < 0.2f && lastServerState.Time - time < 500)
        {
            //Validated, move on
        }
        else
        {
          //  Debug.Log($"RB : {positionDiff.magnitude} [{timeDiff}ms]");
            RpcRubberBand(joyInput, serverStateBuffer.Values.Last().Position, serverStateBuffer.Values.Last().Rotation, trailMgr.positions, timeInMillis);
        }


    }
    [ClientRpc]
    void RpcUpdatePosition(Vector2 input, Vector3 position, Quaternion rotation, double sentTime)
    {
        if(isLocalPlayer || isServer){return;}
        double delay = (timeInMillis - sentTime) * timeDelayErrorFix;
        int numberOfFrames = (int)((float)(delay/1000f) *50f);
        
        Quaternion newRotation = rotation;
        Vector3 direction = (rotation * Vector3.forward).normalized;
        Vector3 newPosition = position + ((direction * speed) * numberOfFrames);
        
        for (int i = 0; i < numberOfFrames; i++)
        {
            newRotation = Quaternion.Lerp(newRotation, getTurnAngle(input), turningSmoothFactor * input.magnitude);
        }

        targetPosition = newPosition - DetourError;
        
        targetRotation = newRotation;
    }
    double lastRubberBandTime = 0;
    [ClientRpc]
    void RpcRubberBand(Vector2 input, Vector3 position, Quaternion rotation, Vector3[] trailPositions, double sentTime)
    {
        if (true)
        {

            if (sentTime < lastRubberBandTime) { Debug.Log("Old rubber band rpc, ignoree..."); return; }
            //Lag comprehension
            double delay = (timeInMillis - sentTime) * timeDelayErrorFix;
            int numberOfFrames = (int)((float)(delay/1000f) * 50f);
            
            Quaternion newRotation = rotation;
            Vector3 direction = (rotation * Vector3.forward).normalized;
            Vector3 newPosition = position + ((direction * speed) * numberOfFrames);
            
            for (int i = 0; i < numberOfFrames; i++)
            {
                newRotation = Quaternion.Lerp(newRotation, getTurnAngle(input), turningSmoothFactor * input.magnitude);
            }
            int distanceSinceSent = (int)Vector3.Distance(newPosition, position);

            #region trailSyncOld
            // Vector3[] newTrailPositions = new Vector3[trailPositions.Length];
            // for(int i=0; i < trailPositions.Length; i++){
            //     if(i > trailPositions.Length - distanceSinceSent-1){
            //         //fill with current data
            //         newTrailPositions[i] = trailMgr.positions[i];
            //     }else{
            //         Vector3 newTrailPoint = trailPositions[i-distanceSinceSent];
            //         if(trailMgr.positions.Length > i){
            //             if(Vector3.Distance(newTrailPoint, trailMgr.positions[i]) < 0.4f){
            //                 newTrailPoint = trailMgr.positions[i];
            //             }
            //         }
            //         newTrailPositions[i] = newTrailPoint;
            //     }
            // }
            // trailMgr.trail.SetPositions(newTrailPositions);
            #endregion
            // Vector3 newPosition = position + new Vector3(0, movingSpeed * );

            // Vector3 newPosition = position;
            Detour = newPosition - transform.position - DetourError;

            // RotationDetour = rotation;
            RotationDetour = Quaternion.Inverse(transform.rotation) * newRotation;
            lastRubberBandTime = sentTime;

//            Debug.Log($"Rubber banded (Detour of pos:{Detour}, rotation: {RotationDetour}) you to {transform.position}, @ {sentTime} (delay: {delay}");
        }
    }

    [ClientRpc]
    void RpcUpdateTrail(Vector3[] positions){
        //trailMgr.trail.SetPositions(positions);
    }

    void OnGUI()
    {
        if (!isLocalPlayer) { return; }
        if (showDebugHUD)
        {
            GUI.Label(new Rect(Screen.width - 120, 10, 100, 20), transform.position.ToString());
            GUI.Label(new Rect(Screen.width - 120, 30, 100, 20), timeInMillis.ToString());
            GUI.Label(new Rect(Screen.width - 100, Screen.height - 30, 50, 20), (NetworkTime.rtt * 1000).ToString() + " ms");
        }

    }



    public float Angle(Vector2 vector2)
    {
        return 360 - (Mathf.Atan2(vector2.x, vector2.y) * Mathf.Rad2Deg * Mathf.Sign(vector2.x));
    }


    //Auto assign default variables [Editor only]
    void OnValidate()
    {
        if (body == null)
        {
            body = transform;
        }
        if(trailMgr==null){
            trailMgr = GetComponent<TrailMgr>();
        }
    }



    public void TrailCollided(Collider2D hit){
        if(!isServer){
            // Debug.Log("This cannot run on client, That's illegal!"); // <-- What this log says
            return;
        }

        SpaceshipController deadPlayer = hit.GetComponent<SpaceshipController>();

        if(deadPlayer!=null && !deadPlayer.dead){ // <-- okay we killed someone | KILLCODE
            deadPlayer.Die(pname);
            Debug.Log($"{pname} killed {deadPlayer.pname}");
            OnKill();            
        }
    }

    void OnKill(){
        Scores+= 10; //TODO: Need to change Scores on kills?
        scaleMultiplier+=0.05f;
        OnScaleChanged(scaleMultiplier,scaleMultiplier);
        IncreaseTrail(trailIncrementRate);
    }

    void IncreaseTrail(float rate){        
        trailTime = trailMgr.trail.time+ rate;
        trailMgr.trail.time = trailTime;
        Debug.Log("Increasing trail of" + pname);
    }

    [Command]
    void CmdCheatKills(){
        OnKill();
    }

    public void CollectPickup(PickupItem.PickupType type){
        if(isClient){Debug.Log("Server function ran on client. That's illegal!");} // <-- What this log says
        switch(type){
            case PickupItem.PickupType.Moon:
                IncreaseTrail(trailIncrementRate);
                break;
            
            case PickupItem.PickupType.Star:
                IncreaseTrail(trailIncrementRate/2f);
                break;
        }

        RpcCollectPickup(type);
    }

    [ClientRpc]
    void RpcCollectPickup(PickupItem.PickupType type){
        if(isLocalPlayer){
            MinigameManager.instance.GainMetals((type==PickupItem.PickupType.Star) ? 10 : 30);
        }
    }

    public void Die(string killer){
        
        MinigameManager.instance.SpawnLeftoverPickups(transform.position, Scores);
        Debug.Log($"Sending death signal to {pname} by {killer}");

        //Handle Respawning
        OnScaleChanged(scaleMultiplier,1);
        scaleMultiplier=1;
        dead=true;
        Scores=0;
        trailTime = 1;
        trailMgr.trail.time = trailTime;
        RpcDie(killer);
        MinigameManager.instance.SetRespawn(gameObject);
    }

    [ClientRpc]
    public void RpcDie(string killer){
        Debug.Log($"{killer} killed {pname} : isItMe? -> {isLocalPlayer}");
        KillfeedMgr.instance.AddNewEntry($"<b>{pname}</b> was killed by <b>{killer}</b>");
        gameObject.SetActive(false);
        if(isLocalPlayer){
            //TODO: Death message goes here
            SceneData.holder.deadScreen.SetActive(true);
        }
    }

    public void Respawn(Vector3 respawnPoint){
        dead=false;
        trailMgr.trail.emitting =false;
        trailMgr.trail.Clear();
        RpcRespawn(respawnPoint);
        transform.position = respawnPoint;
        trailMgr.trail.emitting=true;
        gameObject.SetActive(true);
    }

    [ClientRpc]
    public void RpcRespawn(Vector3 respawnPoint){
       
        GetComponent<NetworkTrail>().enableValidation=false;
        trailMgr.trail.Clear();
        trailMgr.positions = new Vector3[0];
        trailMgr.trail.emitting = false;
        transform.position = respawnPoint;
        trailMgr.trail.emitting=true;        
        GetComponent<NetworkTrail>().enableValidation=true;
        gameObject.SetActive(true);

        if(isLocalPlayer){
            SceneData.holder.deadScreen.SetActive(false);
        }
    }
}



public class StatePayload
{
    public int Time;
    public Vector3 Position;
    public Quaternion Rotation;
    public Vector2 Input;

    public StatePayload(int time, Vector3 position, Quaternion rotation, Vector2 input)
    {
        Time = time;
        Position = position;
        Rotation = rotation;
        Input = input;
    }
}