using UnityEngine;
using Mirror;
using System.Collections.Generic;
using System.Collections;
using CustomExtensions;

public class Player : NetworkBehaviour
{

    private float timer;
    private int currentTick;
    private float minTimeBetweenTicks;
    private const float SERVER_TICK_RATE = 30f;
    private const int BUFFER_SIZE = 1024;

    // Client specific
    private StatePayload[] client_stateBuffer;
    private InputPayload[] inputBuffer;
    private StatePayload latestServerState;
    private StatePayload lastProcessedState;
    private float horizontalInput;
    private float verticalInput;

    private StatePayload[] stateBuffer;
    private Queue<InputPayload> inputQueue;

    void Start()
    {
        minTimeBetweenTicks = 1f / SERVER_TICK_RATE;

        stateBuffer = new StatePayload[BUFFER_SIZE];
        inputBuffer = new InputPayload[BUFFER_SIZE];
        inputQueue = new Queue<InputPayload>();
    }

    void Update()
    {
        horizontalInput = Input.GetAxis("Horizontal");
        verticalInput = Input.GetAxis("Vertical");
        timer += Time.deltaTime;

        while (timer >= minTimeBetweenTicks)
        {
            timer -= minTimeBetweenTicks;
            if(isServer){
                HandleTick();
            }else if(isLocalPlayer){
                HandleTickClient();
            }
            currentTick++;
        }
    }
    public void OnClientInput(InputPayload inputPayload){
        if(isServer){
            m_OnClientInput(inputPayload);
        }else{
            CmdOnClientInput(inputPayload);
        }
    }

    [Command]
    public void CmdOnClientInput(InputPayload inputPayload){
        m_OnClientInput(inputPayload);
    }

    public void m_OnClientInput(InputPayload inputPayload)
    {
        inputQueue.Enqueue(inputPayload);
    }

    IEnumerator SendToClient(StatePayload statePayload)
    {
        yield return new WaitForSeconds(0.02f);

        OnServerMovementState(statePayload);
    }

    void HandleTick()
    {
        // Process the input queue
        int bufferIndex = -1;
        while(inputQueue.Count > 0)
        {
            InputPayload inputPayload = inputQueue.Dequeue();

            bufferIndex = inputPayload.tick % BUFFER_SIZE;

            StatePayload statePayload = ProcessMovement(inputPayload);
            stateBuffer[bufferIndex] = statePayload;
        }

        if (bufferIndex != -1)
        {
            StartCoroutine(SendToClient(stateBuffer[bufferIndex]));
        }
    }

    void HandleTickClient()
    {
        if (!latestServerState.Equals(default(StatePayload)) &&
            (lastProcessedState.Equals(default(StatePayload)) ||
            !latestServerState.Equals(lastProcessedState)))
        {
            HandleServerReconciliation();
        }

        int bufferIndex = currentTick % BUFFER_SIZE;

        // Add payload to inputBuffer
        InputPayload inputPayload = new InputPayload();
        inputPayload.tick = currentTick;
        inputPayload.inputVector = new Vector3(horizontalInput, 0, verticalInput);
        inputBuffer[bufferIndex] = inputPayload;

        // Add payload to stateBuffer
        stateBuffer[bufferIndex] = ProcessMovement(inputPayload);

        // Send input to server
        StartCoroutine(SendToServer(inputPayload));
    }

     void HandleServerReconciliation()
    {
        lastProcessedState = latestServerState;

        int serverStateBufferIndex = latestServerState.tick % BUFFER_SIZE;
        float positionError = Vector3.Distance(latestServerState.position, stateBuffer[serverStateBufferIndex].position);
        float rotationError = CustomExtensions.QuaternionExtensions.Difference(latestServerState.rotation, stateBuffer[serverStateBufferIndex].rotation);

        if (positionError > 0.001f || rotationError > 0.001f)
        {
            Debug.Log($"We have to reconcile bro, Errors\npos:{positionError}, rot:{rotationError}");
            // Rewind & Replay
            transform.position = latestServerState.position;
            transform.rotation = latestServerState.rotation;

            // Update buffer at index of latest server state
            stateBuffer[serverStateBufferIndex] = latestServerState;

            // Now re-simulate the rest of the ticks up to the current tick on the client
            int tickToProcess = latestServerState.tick + 1;

            while (tickToProcess < currentTick)
            {
                int bufferIndex = tickToProcess % BUFFER_SIZE;

                // Process new movement with reconciled state
                StatePayload statePayload = ProcessMovement(inputBuffer[bufferIndex]);

                // Update buffer with recalculated state
                stateBuffer[bufferIndex] = statePayload;

                tickToProcess++;
            }
        }
    }

    public void m_OnServerMovementState(StatePayload serverState)
    {
        latestServerState = serverState;
    }

    public void OnServerMovementState(StatePayload serverState){
        if(isServer){
            RpcOnServerMovementState(serverState);
        }else{
            m_OnServerMovementState(serverState);
            // CmdOnServerMovementState(serverState);
        }
    }

    [ClientRpc]
    public void RpcOnServerMovementState(StatePayload serverState){
        m_OnServerMovementState(serverState);        
        Debug.Log(serverState.position + ":" + transform.position);
        
    }

    IEnumerator SendToServer(InputPayload inputPayload)
    {
        yield return new WaitForSeconds(0.02f);

        OnClientInput(inputPayload);
    }

    StatePayload ProcessMovement(InputPayload input)
    {
        // Should always be in sync with same function on Client
        transform.position += input.inputVector * 5f * minTimeBetweenTicks;
        transform.Rotate(input.inputVector);

        return new StatePayload()
        {
            tick = input.tick,
            position = transform.position,
            rotation = transform.rotation
        };
    }
}


public struct InputPayload
{
    public int tick;
    public Vector3 inputVector;

    public override string ToString()
    {
        return JsonUtility.ToJson(this);
    }

    public static InputPayload Parse(string json){
        return JsonUtility.FromJson<InputPayload>(json);
    }
}

public struct StatePayload
{
    public int tick;
    public Vector3 position;
    public Quaternion rotation;

    public override string ToString()
    {
        return JsonUtility.ToJson(this);
    }

    public static StatePayload Parse(string json){
        return JsonUtility.FromJson<StatePayload>(json);
    }
}
