using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Photon.Pun;
using TMPro;

public class SpectatorController : MonoBehaviour {

    [Header("Spectator Target Data")]
    [ReadOnly]
    public Transform lookTarget;                    // Our current follow target
    [ReadOnly]
    public RacerCore[] m_racers;        // A list of all karts
    [ReadOnly]
    public int currentSpectatingPlayerViewID;       // The view ID of a spectated player
    [ReadOnly]
    public string spectatingPlayerName;             // The spectated target's display nme
    [ReadOnly]
    public TextMeshProUGUI usernameText;            // The username text box
    [ReadOnly]
    public Image usernameBoxBG;                     // The username box background
    private Transform lastTarget;                   // The last target we had followed

    [Header("Main Camera Data")]
    [ReadOnly]
    public Camera mainCamera;                       // The main camera
    [ReadOnly]
    public CameraControllerV2 cameraController;       // The camera controller script
    [ReadOnly]
    public Rigidbody cinematicRB;                   // The camera's rigidbody
    [ReadOnly]
    public Cinemachine.CinemachineBrain brain;      // The cinemachine brain

    [Header("Follow Camera Settings")]
    public Vector3 camFollowOffset;
    public float camFollowSpeed;
    public float followHeightOffset;
    public float minHeightFromTrack;
    [ReadOnly]
    public float distanceToTarget;

    [Header("Flying Camera Settings")]
    [ReadOnly]
    public GameObject modeDisplay;                  // The camera mode icon's display object
    [ReadOnly]
    public Image displayImage;                      // The mode display's outer circle
    [ReadOnly]
    public TextMeshProUGUI displayText;             // The mode display's text
    [ReadOnly]
    public bool freeFlying;                         // Whether we are in free flight mode
    [ReadOnly]
    public bool cinemaMode;                         // Whether or not we are in cinema mode
    [ReadOnly]
    public Vector3 moveVelocity;                    // Holds the current speed vector of the camera's rigidbody
    [ReadOnly]
    public float boostTime = 1.0f;                  // The amount of time it takes to boost in cinema mode
    [Range(20, 100)]
    public float defaultFlySpeed = 50.0f;           // Default flight speed
    [Range(1, 10)]
    public float cinemaFlySpeed = 5f;              // Cinema mode flight speed
    [Range(1f, 5)]
    public float shiftAccelFactor = 2;              // Speed multiplier to use when holding shift
    public float maxSpeed = 500.0f;                 // Maximum camera speed
    [Range(0.1f, 2)]
    public float mouseSensitivity = 0.25f;          // How sensitive the mouse is

    [Header("UI Options")]
    public bool spectatorEnabled = false;
    public Color[] modeColors;                      // Colors for each camera mode
    public Color[] spectateColors;                  // Colors for the spectator bar

    [Header("Debug")]
    [SerializeField] DebugChannelSO m_debugChannel;

    void Start () {
        /*
        if (VerifyDefaults())   // If we are able to verify our default components...
        {
            SetDefaults();      // Set up their default values
        }
        else
        {
            Debug.LogError("Spectator failed to initialize!");
        }
        */
    }

    private void OnEnable()
    {
        // When the Spectator Controller is enabled, check our defaults recursively
        if (VerifyDefaults())
        {
            SetDefaults();
            return;
        }
    }

    // Verifies if we have the necessary components to run our spectator controls
    public bool VerifyDefaults()
    {
        // Critical component checks
        if (!mainCamera)
        {
            mainCamera = GameObject.FindGameObjectWithTag("MainCamera").GetComponent<Camera>();
            if (!mainCamera)
                return false;
        }

        if (!cameraController)
        {
            cameraController = mainCamera.GetComponent<CameraControllerV2>();
            if (!cameraController)
                return false;
        }

        if (!cinematicRB)
        {
            cinematicRB = mainCamera.GetComponent<Rigidbody>();
            if (!cinematicRB)
                return false;
        }

        if (!brain)
        {
            brain = FindObjectOfType<Cinemachine.CinemachineBrain>();
            if (!brain)
                return false;
        }

        // Peripheral component checks
        if (!modeDisplay)
        {
            modeDisplay = transform.Find("TopLeft").Find("ModeDisplay").gameObject;
            if (!modeDisplay)
                return false;
        }

        if (!displayText)
        {
            displayText = modeDisplay.GetComponentInChildren<TextMeshProUGUI>();
            if (!displayText)
                return false;
        }

        if (!displayImage)
        {
            displayImage = modeDisplay.GetComponent<Image>();
            if (!displayImage)
                return false;
        }

        if (!usernameBoxBG || !usernameText)
            return false;

        return true;
    }

    // Sets the default values for the spectator
    private void SetDefaults()
    {
        brain.enabled = false;                          // Enables the cinemachine brain for Follow Mode
        cinematicRB.isKinematic = true;                 // Disables the rigidbody cinematic camera motion by default
        displayImage.color = modeColors[1];             // Sets the mode display ring color
        displayText.text = "Free Mode";                 // Sets the mode display text
        usernameText.text = "";                         // Sets the spectator username text
        usernameBoxBG.color = spectateColors[1];        // Sets the username box to its default color
        freeFlying = true;                              // We are NOT free flying by default
        spectatorEnabled = true;                        // Enable spectating
    }
    
    void Update ()
    {
        if (!VerifyDefaults() || !spectatorEnabled)     // Verify we have everything the spectator needs
        {
            return;
        }
        
        if (Input.GetKeyDown(KeyCode.Tab))              // If we have pressed Tab...
        {
            ToggleFreeFlying();                         // ...toggle free flying
        }
        
        if (freeFlying)                                 // If we are in free or cinema mode...
        {
            if (Input.GetKeyDown(KeyCode.LeftControl))  // ...and if we have pressed Left Control...
            {
                ToggleCinemaMode();                     // ...toggle cinema mode
            }
            HandleFreeFlight();
        }
        else if (Input.GetMouseButton(1))
        {
            ToggleFreeFlying(true, true);
            HandleFollowCam();
        }
        else
        {
            HandleFollowCam();
        }
        

        HandleCameraAngle();

        HandleSpectatorDisplay();
    }

    private void OnDisable()
    {
        if (cameraController)
        {
            // Set the override to false so that we can control local and test karts
            //cameraController.spectatorOverride = false;
            cameraController.toggleCinemaCam = true;
        }

        if (brain)
        {
            // Enables the cinemachine brain for Follow Mode
            brain.enabled = true;
        }

        if (cinematicRB)
        {
            // Disables the rigidbody cinematic camera motion by default
            cinematicRB.isKinematic = true;
        }

        if (displayImage && modeColors.Length > 0)
        {
            // Sets the mode display ring color
            displayImage.color = modeColors[0];
        }

        if (displayText)
        {
            // Sets the mode display text
            displayText.text = "Follow Mode";
        }

        if (usernameBoxBG && spectateColors.Length > 1)
        {
            // Sets the username box to its default color
            usernameBoxBG.color = spectateColors[1];
        }
        freeFlying = cinemaMode = false;                // We are NOT free flying or cinema modes by default

        //Debug.Log("Spectator defaults reset!");
    }


    // PRIVATE METHODS //

    // Toggles free flying mode
    private void ToggleFreeFlying(bool forceOnOff = false, bool forceValue = false)
    {
        freeFlying = forceOnOff ? forceValue : !freeFlying; // Toggle free flying, or use an enforced value

        if (freeFlying)                     // If we are now free flying...
        {
            cinemaMode = false;             // Toggling disables cinema mode by default
        }
        else                                // If we are trying to leave free flying...
        {
            SetCurrentTarget();             // Find a target to follow

            if (lookTarget)                 // If we found a target...
            {
                Cursor.visible = true;      // ...make sure that the cursor is enabled and unlocked
                Cursor.lockState = CursorLockMode.None;
                cinemaMode = false;         // Toggling disables cinema mode by default
                mainCamera.transform.rotation = lookTarget.rotation;
            }
            else
            {
                freeFlying = true;          // Otherwise, change our mode back to free flying
            }
        }

        //cameraController.toggleCinemaCam = brain.enabled = !freeFlying;     // Our cinemacam and brain should be disabled when we are freeflying

        displayImage.color = freeFlying ? modeColors[1] : modeColors[0];    // Update the display color to match our mode
        displayText.text = freeFlying ? "Free Mode" : "Follow Mode";        // Update the display text as well
    }

    // Toggles cinema mode
    private void ToggleCinemaMode()
    {
        cinemaMode = !cinemaMode;   // Toggle cinema mode

        displayImage.color = cinemaMode ? modeColors[2] : modeColors[1];    // Update the display color to match our mode
        displayText.text = cinemaMode ? "Cinema Mode" : "Free Mode";        // Update the display text as well
    }

    // Handles spectator display updates
    private void HandleSpectatorDisplay()
    {
        if (lookTarget)                     // If we have a target...
        {
            if (spectatingPlayerName == "") // ...but our spectator name is empty...
            {
                UpdateSpectatorName();      // Update the spectator name
            }
        }
        else                                // If we DON'T have a target...
        {
            if (spectatingPlayerName != "") // ...but our spectator name is NOT empty...
            {
                UpdateSpectatorName("");    // Empty the spectator name field
            }
        }
    }

    // Sets up the camera angles if we are not being controlled by cinemachine
    private void HandleCameraAngle()
    {
        //Debug.Log("Handling Camera Angle");
        if (Input.GetMouseButton(1))                            // If we are pressing the right mouse button...
        {
            if (lookTarget)                                     // ...and we currently have a look target...
            {
                lookTarget = null;                              // ...clear the look target
            }
            
            Cursor.visible = false;                             // Lock and hide the cursor
            Cursor.lockState = CursorLockMode.Locked;

            // Store how much the mouse has moved from its raw inputs as Vector3 offset
            Vector3 mouseOffset = new Vector3(Input.GetAxis("Mouse Y") * -5f, Input.GetAxis("Mouse X") * 5f, 0f);

            Camera.main.transform.eulerAngles += mouseOffset;   // Rotate the camera by the offset amount

            return;                                             // Return out of the function
        }
        else                                                    // If we are NOT pressing the right mouse button...
        {
            Cursor.visible = true;                              // ...release our cursor
            Cursor.lockState = CursorLockMode.None;
        }
        
        // This behavior is contextual based on our button clicks on the Leaderboard Display
        if (lookTarget) 
        {
            FreeLookFollow();
        }
    }

    // Rotates the camera towards a look target while in free flying mode
    private void FreeLookFollow()
    {
        //Debug.Log("Following the target...");
        if (cinemaMode || !freeFlying) // If we are in cinema or follow mode...
        {
            Vector3 targetVector;

            if (!freeFlying)
            {
                // ...start by getting the direction from our camera and our target
                targetVector = ((lookTarget.position + (Vector3.up * followHeightOffset)) - mainCamera.transform.position).normalized;
            }
            else
            {
                // ...start by getting the direction from our camera and our target
                targetVector = (lookTarget.position - mainCamera.transform.position).normalized;
            }

            // Next we derive Quaternions from our camera's rotation...
            Quaternion currentRotation = mainCamera.transform.rotation;
            // ...the direction vector towards our target...
            Quaternion targetRotation = Quaternion.LookRotation(targetVector, Vector3.up);
            // ...and the angle distance between them
            float degreesBetweenAngles = Quaternion.Angle(currentRotation, targetRotation);
            
            float percentageToRotateBy = Time.deltaTime * 2f;   // Rotate according to the amount of time passed between frames
            Mathf.Clamp(percentageToRotateBy, 0, 1);            // Clamp the value so that we don't overshoot our goal if our framerate is low

            // Set the camera's rotation to an angle between its current and target rotations, according to the percentage change
            mainCamera.transform.rotation = Quaternion.Slerp(currentRotation, targetRotation, percentageToRotateBy);
            return;                                             // Return out of the function
        }

        mainCamera.transform.LookAt(lookTarget, Vector3.up);    // Otherwise, we'll just snap to looking at our target
    }

    // Handles free-flight movement
    private void HandleFreeFlight()
    {
        Vector3 p = GetBaseInput();                                         // Get the base inputs of the player

        if (Input.GetKey(KeyCode.LeftShift))                                // If Left Shift has been pressed...
        {
            if (cinemaMode)                                                 // ...and if we are in cinema mode...
            {
                if (boostTime < 1)
                {
                    boostTime += Time.deltaTime;                            // ...incriment our boost speed by the time between frames
                    Mathf.Clamp(boostTime, 0, 1);
                }
                float boostFactor = boostTime * (shiftAccelFactor);
                p = p * (cinemaFlySpeed + (cinemaFlySpeed * boostFactor));                     // Set our input value to its value multiplied by our boost time and accel value
            }
            else                                                            // If we are in free mode...
            {
                p = p * (defaultFlySpeed * shiftAccelFactor);               // Set our input value to its value multiplied by our accel value
            }

            Vector3.ClampMagnitude(p, maxSpeed);                            // Clamp the input value to our max speed
        }
        else                                                                // If Left Shift has NOT been pressed...
        {
            boostTime = 0f;                                                 // Reduce our boost time
            if (cinemaMode)
            {
                p = p * cinemaFlySpeed;
            }
            else
            {
                p = p * defaultFlySpeed;                                    // Multiply our input value by our flight speed
            }
        }

        Vector3 normalizedInput = InputToNormal(p, mainCamera.transform);   // Applies the input to the angle of the camera

        //Debug.Log(normalizedInput.ToString());

        if (cinemaMode)                             // If we are in cinema mode...
        {
            cinematicRB.isKinematic = false;        // ...enable the rigidbody's default functionality
            cinematicRB.AddForce(normalizedInput);  // Add force according to the input and the rotation of the camera
            moveVelocity = cinematicRB.velocity;    // Update our movement velocity tracker
            return;                                 // Return out of the function
        }
        else if (!cinematicRB.isKinematic)
        {
            cinematicRB.isKinematic = true;         // Disable the rigidbody's default functionality
            moveVelocity = Vector3.zero;            // Reset the move velocity
        }

        normalizedInput = normalizedInput * Time.deltaTime; // Scale the input by time between frames

        mainCamera.transform.position += normalizedInput;   // Apply the input
    }

    // Handles follow-cam movement
    private void HandleFollowCam()
    {
        if (lookTarget)
        {
            Vector3 camCurrentPos = mainCamera.transform.position;                      // Get the camera's current position
            Vector3 camTargetPos = lookTarget.position;                                 // Get the look target's position

            Vector3 targetForwardOffset = lookTarget.forward * camFollowOffset.z;       // Combine the direction the look target is...
            Vector3 targetRightOffset = lookTarget.right * camFollowOffset.x;           // ...facing and multiply each value by the...
            Vector3 targetUpOffset = lookTarget.up * camFollowOffset.y;                 // ...camera offset vector

            camTargetPos += (targetForwardOffset + targetRightOffset + targetUpOffset); // Combine them all together again to make our desired cam position

            Vector3 floorPoint = GetFloorPoint(camTargetPos);                           // Get the floor's position under the target position
            if (floorPoint != Vector3.up)                                               // If we find a floor...
            {
                Vector3 targetUp = floorPoint + (Vector3.up * minHeightFromTrack);      // ...get how much we should be moving up from our current target position
                camTargetPos += (targetUp - camTargetPos) * Time.deltaTime;             // Increment the target position to be at the proper height
            }

            distanceToTarget = (lookTarget.position - camCurrentPos).magnitude;         // Get the distance between the camera and the look target

            // Get how much of that distance we should cover for this frame
            float percentOfMotionComplete = Time.deltaTime * ((distanceToTarget / camFollowOffset.magnitude) * camFollowSpeed);
            Mathf.Clamp(percentOfMotionComplete, 0, 1);                                 // Clamp the percentage between 0 and 1
            
            // Update the camera's position to an interpolated value
            mainCamera.transform.position = Vector3.Lerp(camCurrentPos, camTargetPos, percentOfMotionComplete);

            /*
            floorPoint = GetFloorPoint(mainCamera.transform.position);
            if (floorPoint != Vector3.up)
            {
                Vector3 targetUp = floorPoint + (Vector3.up * minHeightFromTrack);
                mainCamera.transform.position += (targetUp - camCurrentPos) * Time.deltaTime;
            }
            */
        }
    }

    // Returns the value of our inputs as a Vector3
    private Vector3 GetBaseInput()
    {
        Vector3 p_Velocity = new Vector3();     // Create a new Vector3 value

        if (Input.GetKey(KeyCode.W))
        {
            p_Velocity += new Vector3(0, 0, 1);
        }
        if (Input.GetKey(KeyCode.S))
        {
            p_Velocity += new Vector3(0, 0, -1);
        }
        if (Input.GetKey(KeyCode.A))
        {
            p_Velocity += new Vector3(-1, 0, 0);
        }
        if (Input.GetKey(KeyCode.D))
        {
            p_Velocity += new Vector3(1, 0, 0);
        }
        if (Input.GetKey(KeyCode.Q))
        {
            p_Velocity += new Vector3(0, -1, 0);
        }
        if (Input.GetKey(KeyCode.E))
        {
            p_Velocity += new Vector3(0, 1, 0);
        }
        return p_Velocity;                      // Return the derived value
    }

    // Rotates an input vector to match an object's transform rotation
    private Vector3 InputToNormal(Vector3 inputDir, Transform objectTransform)
    {
        Quaternion newRotation = objectTransform.rotation;                  // Store our transform's rotation

        if (Input.GetKey(KeyCode.Space))                                    // If we are holding the space button...
        {
            Vector3 eulers = newRotation.eulerAngles;                       // Get the euler representation of our Quaternion
            eulers.x = 0;                                                   // Zero out the pitch
            newRotation.eulerAngles = eulers;                               // Apply the corrected rotation
        }

        float magnitude = inputDir.magnitude;                               // Get the magnitude of our input
        Vector3 newForward = (newRotation * Vector3.forward) * inputDir.z;  // Get direction the object is facing and multiply it by the forward input value
        Vector3 newRight = (newRotation * Vector3.right) * inputDir.x;      // Get direction to the object's right and multiply it by the right input value
        Vector3 newUp = (newRotation * Vector3.up) * inputDir.y;            // Get direction to the object's up and multiply it by the up input value

        Vector3 newInputDir = newForward + newRight + newUp;                // Add up our new vectors

        //Debug.Log("Was " + inputDir.magnitude + ", is " + newInputDir.magnitude); // !!! Make the magnitude consistent when holding multiple keys
        return newInputDir;                                                 // Return the new input
    }

    // Finds the floor below a position
    private Vector3 GetFloorPoint(Vector3 origin)
    {
        RaycastHit hit;
        if (Physics.Raycast(origin + (Vector3.up * 3), -Vector3.up, out hit, minHeightFromTrack * 3))
        {
            if (hit.collider.GetType() == typeof(MeshCollider))
            return hit.point;
        }
        return Vector3.up;
    }


    // PUBLIC METHODS //

    // Recursively sets a spectator's look target
    public void SpectateAnyPlayer() {
        m_racers = FindObjectsOfType<RacerCore>();

        if(m_racers.Length > 0) {
            SetCurrentTarget(m_racers[0].transform);
            return;
        }
        SetCurrentTarget();
    }

    // Manually sets a spectator's look target
    public void SetCurrentTarget(Transform target = null, string name = "Mystery Racer")
    {
        if (target)
        {
            if (m_debugChannel)
                m_debugChannel.Raise(this, "Setting target to " + target.name);

            lookTarget = target;
            lastTarget = lookTarget;
        }
        else if (m_debugChannel)
            m_debugChannel.Raise(this, "Target given was null.");

        if (!lookTarget)
        {
            if (m_debugChannel)
                m_debugChannel.Raise(this, "No Look Target assigned. Acquiring a new Target.");

            if (!freeFlying && lastTarget)
            {
                if (m_debugChannel)
                    m_debugChannel.Raise(this, "No Look Target assigned. Acquiring a new Target.");

                lookTarget = lastTarget;
            }
            else
            {
                m_racers = FindObjectsOfType<RacerCore>();

                if (m_racers.Length > 0)
                {
                    lastTarget = m_racers[0].transform.GetChild(0).GetChild(0).GetChild(0);

                    if (!freeFlying)
                    {
                        lookTarget = lastTarget;
                    }
                }
            }
        }

        foreach (RacerCore m_Racer in m_racers)
        {
            if (lookTarget.GetComponent<RacerCore>() == m_Racer)
            {
                //m_Racer.SetSpectating(true);

                if (name != "Mystery Racer")
                {
                    UpdateSpectatorName(name);
                }
                else if (m_Racer.photonView)
                {
                    currentSpectatingPlayerViewID = m_Racer.photonView.ViewID;

                    object prop;

                    m_Racer.photonView.Owner.CustomProperties.TryGetValue("DisplayName", out prop);

                    if (prop != null)
                        UpdateSpectatorName(prop as string);
                    else
                        UpdateSpectatorName("Mystery Racer");
                }

                continue;
            }
            currentSpectatingPlayerViewID = 0;
            //m_Racer.SetSpectating(false);
        }
    }
    
    // Updates the spectator name display value
    public void UpdateSpectatorName(string enforcedName = null)
    {
        if (!usernameText || !usernameBoxBG) { return; }   // Since this is a public method, make sure we have the necessary components

        if (enforcedName != null)                           // If we have been passed a name manually...
        {
            spectatingPlayerName = enforcedName;            // ...set the spectator name to the enforced value
        }
        else
        {
            RacerCore _racer = lookTarget.GetComponent<RacerCore>();    // Get the target's kart

            if (_racer)                                                           // If we found a kart...
            {
                if (_racer.photonView && PhotonNetwork.IsConnected)               // ...and this is a player...
                {
                    object prop;                                                // ...set the spectator name to the player's display name
                    _racer.photonView.Owner.CustomProperties.TryGetValue("DisplayName", out prop);
                    spectatingPlayerName = prop as string;
                }
                else                                                            // If we are NOT a player or online...
                {
                    spectatingPlayerName = "Racer";                             // ...use the default racer name
                }
            }
            else                                                                // If we DON'T find a kart...
            {
                spectatingPlayerName = "";                                      // ...clear the spectator name
            }
        }

        usernameText.text = spectatingPlayerName;                               // Set the text to match the spectator name value
        usernameBoxBG.color = spectatingPlayerName != "" ? spectateColors[0] : spectateColors[1];  // Change the spectator box color
    }
}
