﻿using Cinemachine;
using DG.Tweening;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine.Rendering.PostProcessing;
using PhotonHashTable = ExitGames.Client.Photon.Hashtable;

public class KartController : MonoBehaviourPunCallbacks, IPunObservable
{

    [Header("Track Info")]
    public List<Vector3> kartSpots;
    public List<Quaternion> kartRotations;
    public bool startCounting = true;
    public bool startRace = false;
    public Transform lastTarget;
    public int lapCount = 1;
    public int totalLaps = 3;
    public bool lapCompleted = false;
    public int currentCheckpoint = 0;
    public float checkpointDistance = 0f;
    public bool complete = false;
    public int completeOffset = 0;
    public int positionInRace = 0;
    public float startTimer = 3f;
    public float raceTime = 0f;
    public float lastTime = 0f;
    public float totalRaceTime = 0f;
    public static float bestTime = 0f;

    [Header("Track Components")]
    public Collider checkpointCollider;
    public LayerMask trackMask;
    public LayerMask layerMask;
    public PostProcessVolume postVolume;
    public PostProcessProfile postProfile;
    public SpectatorController spectatorController;
    public AudioManager audioManager;
    public AudioSource music;
    public AudioSource lapSounds;
    public AudioClip countNormal;
    public AudioClip countEnd;

    [Header("UI Components")]
    public Transform countdownCanvas;
    public GameObject playAgainButton;
    public TextMeshProUGUI timer;
    public TimeDisplayHandler timeDisplay;
    public TextMeshProUGUI lapCountText;
    public TextMeshProUGUI positionDisplay;
    public ParticleSystem boostVisual;

    [Header("Racer Defaults")]
    public float acceleration = 30f;
    public float steering = 80f;
    public float driftSteering = 80f;
    public float gravity = 10f;
    public float lerpFloat = 0.2f;
    public Color[] turboColors;

    [Header("Kart Components")]
    public Transform kartModel;
    public Transform kartNormal;
    public List<Transform> wheel;
    public Transform steeringWheel;
    public Transform tube1;
    public Transform tube2;
    public Rigidbody rb;
    public Collider myCollider;
    public SkinnedMeshRenderer playerKart;
    public AudioSource engineAudio;
    public AudioClip sparkActivate;
    public AudioClip rocketBoost;
    public Transform wheelParticles, flashParticles;
    public List<ParticleSystem> primaryParticles = new List<ParticleSystem>();
    public List<ParticleSystem> secondaryParticles = new List<ParticleSystem>();

    [Header("Character Components")]
    public Animator anim;
    public MeshFilter hairStyle;
    public SkinnedMeshRenderer playerCharacter;
    public MeshRenderer playerHair;
    public PlayerStats playerStats;

    [Header("Runtime Values")]
    public float framerateOffset = 0.022f;                // Adjusts gameplay elements so that they run as if the local framerate was equal to 60fps (need this multiplied by Time.deltaTime)
    [ReadOnly]
    public bool canReset = true;
    private bool? shouldAccelerate = null;
    private bool? shouldReverse = null;
    private float? horizontalMovement = null;
    private bool? shouldJump = null;
    [ReadOnly]
    bool everythingIsSet = false;
    [ReadOnly]
    public bool isSpectated;
    [ReadOnly]
    public float speed;
    [ReadOnly]
    public float currentSpeed;
    [ReadOnly]
    public float rotate;
    [ReadOnly]
    public float currentRotate;
    [ReadOnly]
    private bool wasJumpingLastFrame = false;
    [ReadOnly]
    public bool hasLanded;
    [ReadOnly]
    public float engineEffect = 0f;
    [ReadOnly]
    public bool drifting;
    [ReadOnly]
    public int driftDirection;
    [ReadOnly]
    public int driftMode = 0;
    [ReadOnly]
    public float driftPower;
    [ReadOnly]
    public bool first;
    [ReadOnly]
    public bool second;
    [ReadOnly]
    public bool third;
    [HideInInspector]
    public bool boostCalled = false;
    [ReadOnly]
    public bool nextLapCalled = false;
    [ReadOnly]
    public Color c;


    void Awake()
    {
        isSpectated = false;
    }

    void Start()
    {

        // Standard Photon check
        if (photonView && !photonView.IsMine && PhotonNetwork.IsConnected) { return; }

        // Set all the variables that need to be assigned after the Kart has been instantiated
        while (SetEverything() == false) { everythingIsSet = false; }

        // Confirm everything has been set
        everythingIsSet = true;
    }

    void Update()
    {


        // If this is another player's kart...
        if (photonView && !photonView.IsMine && PhotonNetwork.IsConnected)
        {
            SnapToFloor();
        }

        if (!spectatorController)
        {
            spectatorController = SpectatorController.FindObjectOfType<SpectatorController>();
        }

        if (spectatorController)
        {
            if (spectatorController.isActiveAndEnabled) { return; }
        }

        // This should not hit but is in place as a last resort;
        if (!everythingIsSet) { return; }

        // Adjust the framrate offset
        framerateOffset = Time.deltaTime / 0.022f;

        // Will reset the racers position to that of the last checkpoint
        if (lastTarget && Input.GetKeyDown(KeyCode.R) && !drifting && canReset)
        {
            StartCoroutine(ResetPosition());
        }

        // This should be looked at to be added to the Time Display Handler
        if (startCounting)
        {
            startCounting = false;
            StartCoroutine(StartRace());
        }

        // This needs to be updated once testing has been complete
        if (SceneManager.GetActiveScene().name != "PhotonTesting 1" && SceneManager.GetActiveScene().name != "RKR-Lobby" && SceneManager.GetActiveScene().name != "Esports-Lobby")
        {

            lapCountText.SetText((LapCount(lapCount)));

            if (lapCount > totalLaps)
            {
                if (playAgainButton)
                {
                    if (!playAgainButton.activeInHierarchy)
                    {
                        playAgainButton.SetActive(true);
                    }
                    rb.gameObject.SetActive(false);
                    kartNormal.GetChild(0).gameObject.SetActive(false);
                    transform.RotateAround(transform.position, transform.up, 0.1f);
                }
            }

            if (!startRace || lapCount > totalLaps)
            {
                return;
            }


            if (timer.gameObject.activeInHierarchy)
            {
                timer.SetText("0:00:00");
                totalRaceTime += Time.deltaTime;
                raceTime += Time.deltaTime;
            }

            timer.SetText(FormatTime("Current", raceTime));
        }

        //Follow Collider
        transform.position = rb.transform.position;



        //Accelerate
        bool isReversing = shouldReverse ?? false;
        bool isAccelerating = shouldAccelerate ?? false;
        if (!isAccelerating && isReversing)
        {
            speed = -acceleration / 2f;
        }
        else if (isAccelerating)
        {
            speed = acceleration;
        }

        //Steer
        float horizontalMovementThisFrame = horizontalMovement ?? 0f;
        if (horizontalMovementThisFrame != 0)
        {
            int dir = horizontalMovementThisFrame > 0 ? 1 : -1;
            float amount = Mathf.Abs((horizontalMovementThisFrame));
            Steer(dir, amount);
        }

        //Drift
        bool isJumping = shouldJump ?? false;
        bool jumpStateChangedThisFrame = isJumping != wasJumpingLastFrame;
        bool startedJumpingThisFrame = jumpStateChangedThisFrame && isJumping == true;
        if (startedJumpingThisFrame && !drifting)// && horizontalMovementThisFrame != 0)
        {
            drifting = true;
            hasLanded = false;

            foreach (ParticleSystem p in primaryParticles)
            {
                var pmain = p.main;
                pmain.startColor = Color.clear;
                p.Play();
            }

            kartModel.parent.DOComplete();
            kartModel.parent.DOPunchPosition(transform.up * .2f, .3f * framerateOffset, 5, 1);

            driftDirection = horizontalMovementThisFrame > 0 ? 1 : horizontalMovementThisFrame == 0 ? 0 : -1;

            if (horizontalMovementThisFrame == 0 || currentSpeed < 1f)
            {
                drifting = false;
            }
            hasLanded = true;

        }

        if (drifting)
        {
            float control = (driftDirection == 1) ? ExtensionMethods.Remap(horizontalMovementThisFrame, -1, 1, 0.5f, 2) : ExtensionMethods.Remap(horizontalMovementThisFrame, -1, 1, 2, 0.5f);
            float powerControl = (driftDirection == 1) ? ExtensionMethods.Remap(horizontalMovementThisFrame, -1, 1, 0.85f, 1.5f) : ExtensionMethods.Remap(horizontalMovementThisFrame, -1, 1, 1.5f, 0.85f);
            Steer(driftDirection, control);

            float addDriftPower = 0f;

            if (powerControl == 0.85f)
            {
                addDriftPower = 1.5f;
            }
            else if (powerControl == 1.5f)
            {
                addDriftPower = 0.75f;
            }
            else
            {
                addDriftPower = powerControl * framerateOffset;
            }

            driftPower += powerControl * framerateOffset;

            ColorDrift();
        }

        bool stoppedJumpingThisFrame = jumpStateChangedThisFrame && isJumping == false;
        if (stoppedJumpingThisFrame && drifting)
        {
            Boost();
        }

        currentSpeed = Mathf.SmoothStep(currentSpeed, speed, Time.deltaTime * 12f); speed = 0f;
        currentRotate = Mathf.Lerp(currentRotate, rotate, Time.deltaTime * 4f); rotate = 0f;

        //Animations    

        //a) Kart
        if (!drifting)
        {
            kartModel.localEulerAngles = Vector3.Lerp(kartModel.localEulerAngles, new Vector3(0, 90 + (horizontalMovementThisFrame * 15), kartModel.localEulerAngles.z), 0.2f * framerateOffset);
        }
        else
        {
            float control = (driftDirection == 1) ? ExtensionMethods.Remap(horizontalMovementThisFrame, -1, 1, .75f, 2) : ExtensionMethods.Remap(horizontalMovementThisFrame, -1, 1, 2, 0.75f * framerateOffset);
            kartModel.parent.localRotation = Quaternion.Euler(0, Mathf.LerpAngle(kartModel.parent.localEulerAngles.y, (control * 15) * driftDirection, 0.2f * framerateOffset), 0);
        }

        //b) Wheels
        if (wheel.Count == 4)
        {

            for (int i = 0; i < 4; i++)
            {
                if (i < 2)
                {
                    //wheel [i].localEulerAngles = new Vector3 (wheel [i].localEulerAngles.x, (horizontalMovementThisFrame * 15), 0);
                }
                //wheel[i].localEulerAngles += new Vector3 (1, 0, 0);
            }

            //c) Steering Wheel
            //steeringWheel.localEulerAngles = new Vector3 (steeringWheel.localEulerAngles.x, steeringWheel.localEulerAngles.y, ((horizontalMovementThisFrame * 45)));
        }


        // ZAS: Clear command states after use
        shouldReverse = null;
        shouldAccelerate = null;
        horizontalMovement = null;
        shouldJump = null;

        // ZAS: Store the jump state each frame for reference in the next frame to detect changes in state
        wasJumpingLastFrame = isJumping;

        checkpointDistance = Vector3.Distance(checkpointCollider.ClosestPoint(transform.position), transform.position);
    }

    private void FixedUpdate()
    {

        // Standard Photon check
        if (photonView && !photonView.IsMine && PhotonNetwork.IsConnected) { return; }

        if (!startRace || lapCount > totalLaps)
        {
            return;
        }

        //Forward Acceleration
        if (!drifting)
            rb.AddForce(-kartModel.transform.right * currentSpeed, ForceMode.Acceleration);
        else
            rb.AddForce(transform.forward * currentSpeed, ForceMode.Acceleration);

        //Steering
        transform.eulerAngles = Vector3.Lerp(transform.eulerAngles, new Vector3(0, transform.eulerAngles.y + currentRotate, 0), Time.deltaTime * 5f);

        // For Debugging
        GetRays();

        HandleTrackNormals();

        //Normal Rotation
        kartNormal.Rotate(0, transform.eulerAngles.y, 0);
        if (!Physics.Raycast(rb.transform.position, -kartNormal.up, 1f, trackMask))
        {
            rb.AddForce(-kartNormal.up * gravity, ForceMode.Acceleration);
        }
    }


    // ZAS: Informs the kart to accelerate during the next update call
    public void Accelerate(bool forward)
    {
        if (forward)
        {
            shouldAccelerate = true;
        }
        else
        {
            shouldReverse = true;
        }
    }

    bool SetEverything()
    {

        // If the character has previously been customized, load those values
        if (CheckCharacterSave()) { LoadPlayerPreference(); }

        // Values independent of the others
        anim.SetBool("Grounded", true);
        engineEffect = 0f;
        hasLanded = true;

        // Assign the Starting track piece as the Last Target
        if (!lastTarget) { lastTarget = GameObject.FindGameObjectWithTag("StartFinish").transform; }

        // Get the Checkpoint Collider
        if (!checkpointCollider) { checkpointCollider = lastTarget.GetComponent<ModularTrack>().track.pivotObject.GetComponent<Collider>(); }

        // Get the Countdown Canvas
        if (!countdownCanvas) { countdownCanvas = GameObject.FindGameObjectWithTag("CountdownCanvas").transform; }

        // Get the Time Display
        if (!timeDisplay) { timeDisplay = FindObjectOfType<TimeDisplayHandler>(); }

        // Get the Audio Manager
        if (!audioManager) { audioManager = FindObjectOfType<AudioManager>(); }

        // Get the Post Volume
        if (!postVolume) { postVolume = FindObjectOfType<PostProcessVolume>(); }

        if (!playerStats) { playerStats = FindObjectOfType<PlayerStats>(); }

        // If any of these variables are assigned, do not continue
        if (!lastTarget || !checkpointCollider || !countdownCanvas || !timeDisplay || !audioManager || !postVolume)
        {
            return false;
        }

        // Variables dependent on the above variables being assigned
        timer = timeDisplay.currentLapTime;
        lapCountText = timeDisplay.lapCount;
        music = audioManager.musicSource;
        lapSounds = audioManager.lapSoundsSource;
        if (engineAudio)
        {
            audioManager.engineSource = engineAudio;
        }
        postProfile = postVolume.profile;

        // Set up the particles
        for (int i = 0; i < wheelParticles.GetChild(0).childCount; i++)
        {
            primaryParticles.Add(wheelParticles.GetChild(0).GetChild(i).GetComponent<ParticleSystem>());
        }

        for (int i = 0; i < wheelParticles.GetChild(1).childCount; i++)
        {
            primaryParticles.Add(wheelParticles.GetChild(1).GetChild(i).GetComponent<ParticleSystem>());
        }

        foreach (ParticleSystem p in flashParticles.GetComponentsInChildren<ParticleSystem>())
        {
            secondaryParticles.Add(p);
        }

        // Handle the Lap Display
        HandleLapDisplay(0f);

        return true;
    }

    // ZAS: Informs the kart to jump during the next update call
    public void Jump() { shouldJump = true; }

    // ZAS: Informs the kart to steer during the next update call. Range is -1f to 1f
    public void Steer(float amount) { horizontalMovement = amount; }

    // Check for a previously saved character
    bool CheckCharacterSave()
    {

        string[] myKeys = new string[] {
            "HairStyle",
            "HairColorR",
            "HairColorG",
            "HairColorB",
            "PlayerTexture",
            "KartTexture",
            "KartColorR",
            "KartColorG",
            "KartColorB"
        };

        for (int i = 0; i < myKeys.Length; i++)
        {
            if (!PlayerPrefs.HasKey(myKeys[i]))
            {
                return false;
            }
        }
        return true;
    }

    // Load the previously saved character and send an RPC to all players to adjust the racer's appearence
    void LoadPlayerPreference()
    {

        if (photonView && !photonView.IsMine && PhotonNetwork.IsConnected)
            return;
        Mesh hairMesh = (Mesh)Resources.Load("Hair Models/" + PlayerPrefs.GetString("HairStyle"));
        if (hairStyle)
        {
            hairStyle.mesh = hairMesh;
        }

        hairStyle.mesh = (Mesh)Resources.Load("Hair Models/" + PlayerPrefs.GetString("HairStyle"));
        float hr = PlayerPrefs.GetFloat("HairColorR");
        float hg = PlayerPrefs.GetFloat("HairColorG");
        float hb = PlayerPrefs.GetFloat("HairColorB");

        playerCharacter.material.mainTexture = (Texture2D)Resources.Load("Player Textures/" + PlayerPrefs.GetString("PlayerTexture"));
        playerKart.material.mainTexture = (Texture2D)Resources.Load("Kart Textures/" + PlayerPrefs.GetString("KartTexture"));
        PlayerPrefs.GetString("KartTexture");
        float kr = PlayerPrefs.GetFloat("KartColorR");
        float kg = PlayerPrefs.GetFloat("KartColorG");
        float kb = PlayerPrefs.GetFloat("KartColorB");


        if (playerHair)
        {
            playerHair.material.color = new Color(hr, hg, hb);
        }

        if (playerKart)
        {
            playerKart.material.color = new Color(kr, kg, kb);
        }

        Vector3 hairColors = new Vector3(hr, hg, hb);
        Vector3 kartColors = new Vector3(kr, kg, kb);
        string[] hairPlayerKart = new string[] {
            "Hair Models/" + PlayerPrefs.GetString ("HairStyle"),
            "Player Textures/" + PlayerPrefs.GetString ("PlayerTexture"),
            "Kart Textures/" + PlayerPrefs.GetString ("KartTexture")
        };

        if (photonView && photonView.IsMine && PhotonNetwork.IsConnected)
        {
            photonView.RPC("AssignRacerDefaults", RpcTarget.OthersBuffered, photonView.ViewID, hairColors, kartColors, hairPlayerKart);
        }

    }

    // Snap instanced karts to the floor
    private void SnapToFloor()
    {
        float radius = rb.transform.GetComponent<SphereCollider>().radius;
        RaycastHit hit;
        if (!Physics.Raycast(rb.transform.position + (Vector3.up * radius), -kartNormal.up, out hit, radius * 3, trackMask))
        {
            rb.transform.position = hit.point + (Vector3.up * radius);
        }
    }

    private void CheckPositionInRace()
    {
        KartController[] karts = FindObjectsOfType<KartController>();
        //Debug.Log(transform.name + " has completed " + completeOffset + " laps.");
        int kartRank = 1 + completeOffset;

        checkpointDistance = Vector3.Distance(checkpointCollider.ClosestPoint(transform.position), transform.position);

        if (complete)
        {
            return;
        }

        foreach (KartController kart in karts)
        {
            if (kart == this || kart.complete)
            {
                continue;
            }
            else
            {
                if (kart.complete)
                {
                    kartRank++;
                }
                else if (currentCheckpoint < kart.currentCheckpoint)
                {
                    kartRank++;
                }
                else if (currentCheckpoint == kart.currentCheckpoint)
                {
                    if (checkpointDistance > kart.checkpointDistance)
                    {
                        kartRank++;
                    }
                }
            }
        }

        string placeEnding = "th";
        if (kartRank > 3)
        {
            placeEnding = "th";
        }
        else if (kartRank == 3)
        {
            placeEnding = "rd";
        }
        else if (kartRank == 2)
        {
            placeEnding = "nd";
        }
        else if (kartRank == 1)
        {
            placeEnding = "st";
        }

        positionInRace = kartRank;
        if (timeDisplay)
        {
            //Debug.Log (transform.name + " is in " + racePlacement + placeEnding + " place!");
            if (timeDisplay.racePlacement)
            {
                timeDisplay.racePlacement.SetText(positionInRace + placeEnding);
                timeDisplay.racePlacement.color = timeDisplay.placeColors[positionInRace - 1];
            }
        }
    }

    string FormatTime(string type, float time)
    {
        int intTime = (int)time;
        int minutes = (int)time / 60;
        int seconds = (int)time % 60;
        int milliseconds = (int)(time * 1000) % 1000;
        string timeText = type + " Lap Time:\n" + string.Format("{0:00}:{1:00}:{2:000}", minutes, seconds, milliseconds);
        return timeText;
    }

    string LapCount(int lap)
    {
        if (lap > totalLaps)
        {
            lap = totalLaps;
        }
        string lapText = "Lap " + lap.ToString() + "/" + totalLaps.ToString();
        return lapText;
    }

    public void CallBlur()
    {
        //Debug.Log ("called");
        StartCoroutine(BoostBlur());
    }

    public IEnumerator BoostBlur()
    {
        //Debug.Log ("here too");
        lapSounds.PlayOneShot(rocketBoost);
        boostVisual.Play();
        if (postProfile)
        {
            postProfile.GetSetting<LensDistortion>().intensity.value = -45;
            postProfile.GetSetting<MotionBlur>().enabled.value = true;
        }
        float t = 0f;
        engineEffect = 1.5f;//(driftMode == 3) ? 1.5f : (driftMode == 2) ? 1.25f : 1f;
        while (t < 1)
        {
            t += Time.deltaTime;
            engineEffect = Mathf.Lerp(engineEffect, 0f, t);
            yield return null;
        }
        boostCalled = false;
        t = 0f;
        if (postProfile)
        {
            float currentInt = postProfile.GetSetting<LensDistortion>().intensity.value;
            while (t < 1)
            {
                if (boostCalled)
                {
                    postProfile.GetSetting<LensDistortion>().intensity.value = -45;
                    yield break;
                }
                if (t > 0.5f)
                {
                    postProfile.GetSetting<MotionBlur>().enabled.value = false;
                }
                t += Time.deltaTime;
                postProfile.GetSetting<LensDistortion>().intensity.Interp(currentInt, 0, t);
                yield return null;
            }
        }
        canReset = true;

        Debug.Log("here too");
        yield return null;

    }

    public void Boost()
    {
        canReset = false;
        drifting = false;
        boostCalled = true;
        if (driftPower > 130)
        {
            driftPower = 130;
        }

        if (driftMode > 0)
        {
            StartCoroutine(BoostBlur());
            DOVirtual.Float(acceleration * (2f + ((float)driftMode / 3f)), currentSpeed, 0.425f * driftMode * framerateOffset, Speed);
            tube1.GetComponentInChildren<ParticleSystem>().Play();
            tube2.GetComponentInChildren<ParticleSystem>().Play();
        }

        driftPower = 0;
        driftMode = 0;
        first = false; second = false; third = false;

        foreach (ParticleSystem p in primaryParticles)
        {
            var pmain = p.main;
            pmain.startColor = Color.clear;
            p.Stop();
        }

        kartModel.parent.DOLocalRotate(Vector3.zero, 0.5f * framerateOffset).SetEase(Ease.OutBack);

    }

    public void Steer(int direction, float amount)
    {
        rotate = (drifting == true) ? (driftSteering * direction) * amount : (steering * direction) * amount;
    }

    public void ColorDrift()
    {
        if (!first)
            c = Color.clear;

        if (driftPower > 40 && driftPower < 85 - 1 && !first)
        {
            first = true;
            c = turboColors[0];
            driftMode = 1;
            lapSounds.PlayOneShot(sparkActivate);
            PlayFlashParticle(c);
        }

        if (driftPower > 85 && driftPower < 130 - 1 && !second)
        {
            second = true;
            c = turboColors[1];
            driftMode = 2;
            lapSounds.PlayOneShot(sparkActivate);
            PlayFlashParticle(c);
        }

        if (driftPower > 130 && !third)
        {
            third = true;
            c = turboColors[2];
            driftMode = 3;
            lapSounds.PlayOneShot(sparkActivate);
            PlayFlashParticle(c);
        }

        foreach (ParticleSystem p in primaryParticles)
        {
            var pmain = p.main;
            pmain.startColor = c;
        }

        foreach (ParticleSystem p in secondaryParticles)
        {
            var pmain = p.main;
            pmain.startColor = c;
        }
    }

    void PlayFlashParticle(Color c)
    {
        GameObject.Find("CM vcam1").GetComponent<CinemachineImpulseSource>().GenerateImpulse();

        foreach (ParticleSystem p in secondaryParticles)
        {
            var pmain = p.main;
            pmain.startColor = c;
            p.Play();
        }
    }

    void GetRays()
    {
        Debug.DrawRay(transform.position, -kartNormal.up, Color.magenta);
        Debug.DrawRay(wheel[0].position, -kartNormal.up, Color.magenta);
        Debug.DrawRay(wheel[1].position, -kartNormal.up, Color.magenta);
        Debug.DrawRay(wheel[2].position, -kartNormal.up, Color.magenta);
        Debug.DrawRay(wheel[3].position, -kartNormal.up, Color.magenta);
    }

    void HandleTrackNormals()
    {
        List<RaycastHit> rayHits = new List<RaycastHit>();
        RaycastHit groundCheck;

        bool sameTarget = true;
        Transform target = null;

        if (Physics.Raycast(transform.position, -kartNormal.up, out groundCheck, Mathf.Infinity, trackMask))
        {
            rayHits.Add(groundCheck);
            target = groundCheck.transform;
        }

        for (int i = 0; i < 2; i++)
        {
            RaycastHit tempHit;
            if (Physics.Raycast(wheel[i].position, -kartNormal.up, out tempHit, Mathf.Infinity, trackMask))
            {
                rayHits.Add(tempHit);
            }
        }

        Vector3 adjustedKartNormal = Vector3.zero;
        for (int r = 0; r < rayHits.Count; r++)
        {
            if (target != rayHits[r].transform)
            {
                sameTarget = false;
            }
            adjustedKartNormal += rayHits[r].normal;
        }
        if (rayHits.Count > 0)
        {
            adjustedKartNormal = adjustedKartNormal / (float)rayHits.Count;
        }

        if (SceneManager.GetActiveScene().name != "PhotonTesting 1" && SceneManager.GetActiveScene().name != "RKR - Esports Lobby")
        {

            if (sameTarget && (target.GetComponent<ModularTrack>() || target.GetComponentInParent<ModularTrack>()))
            {
                if (lastTarget.GetComponent<ModularTrack>().endLink == null && target.GetComponentInParent<ModularTrack>() == lastTarget.GetComponent<ModularTrack>())
                {
                }
                else if (lastTarget.GetComponent<ModularTrack>().endLink == null && target.GetComponent<ModularTrack>().startLink == null)
                {
                    lastTarget = target.GetComponent<ModularTrack>().endLink.transform;
                    checkpointCollider = lastTarget.GetComponent<ModularTrack>().track.pivotObject.GetComponent<Collider>();
                    StartCoroutine(NextLap(target));
                }
                else if (lastTarget.GetComponent<ModularTrack>().endLink == target.GetComponent<ModularTrack>())
                {
                    lastTarget = target;
                    checkpointCollider = lastTarget.GetComponent<ModularTrack>().track.pivotObject.GetComponent<Collider>();
                    currentCheckpoint++;
                }
                else if (lastTarget.GetComponent<ModularTrack>().endLink == target.GetComponentInParent<ModularTrack>())
                {
                    lastTarget = target.parent;
                    checkpointCollider = lastTarget.GetComponent<ModularTrack>().track.pivotObject.GetComponent<Collider>();
                    currentCheckpoint++;
                }
            }

        }

        kartNormal.up = Vector3.Lerp(kartNormal.up, adjustedKartNormal, lerpFloat * framerateOffset);
        //Debug.Log ("Last Target: " + lastTarget.forward.ToString() + ", Last Target Opposite: " + (-lastTarget.forward).ToString() + ", Self: " + transform.forward);

    }

    void HandleLapDisplay(float currentTime)
    {
        if (currentTime != 0f)
        {
            lastTime = currentTime;
            // Last Lap
            timer.transform.GetChild(0).GetComponent<TextMeshProUGUI>().SetText(FormatTime("Last", currentTime));
            // Best Lap
            if ((bestTime != 0f && bestTime > currentTime) || bestTime == 0f)
            {
                timer.transform.GetChild(0).GetChild(0).GetComponent<TextMeshProUGUI>().SetText(FormatTime("Best", currentTime));
                bestTime = currentTime;
            }
        }
        else
        {
            if (bestTime != 0f && lapCount == 1)
            {
                timer.transform.GetChild(0).GetChild(0).GetComponent<TextMeshProUGUI>().SetText(FormatTime("Best", bestTime));
            }
        }
    }

    public static Transform GetNewCameraTargetTransform()
    {
        if (!PhotonNetwork.IsConnected)
        {
            return null;
        }

        KartController[] racers = FindObjectsOfType<KartController>();
        for (int i = 0; i < racers.Length; i++)
        {
            PhotonView view = racers[i].gameObject.GetPhotonView();
            if (view && !view.IsMine)
                return racers[i].transform;
        }

        return null;
    }

    public void SetSpectating(bool set)
    {
        isSpectated = set;
    }

    public bool IsSpectating()
    {
        return isSpectated;
    }

    // Sets the speed
    private void Speed(float x) { currentSpeed = x; }

    // Remove the parent object as well so that no Null Reference Exceptions start being thrown
    void OnDestroy() { Destroy(transform.parent.gameObject); }


    IEnumerator StartRace()
    {

        if (SceneManager.GetActiveScene().name == "PhotonTesting 1" || SceneManager.GetActiveScene().name == "RKR - Esports Lobby")
        {
            startRace = true;
            yield break;
        }
        countdownCanvas.GetChild(0).gameObject.SetActive(true);
        yield return new WaitForSecondsRealtime(3.0f);
        lapSounds.Play();
        countdownCanvas.GetChild(0).gameObject.SetActive(false);
        countdownCanvas.GetChild(1).gameObject.SetActive(true);
        yield return new WaitForSecondsRealtime(1.0f);
        lapSounds.Play();
        countdownCanvas.GetChild(1).gameObject.SetActive(false);
        countdownCanvas.GetChild(2).gameObject.SetActive(true);
        yield return new WaitForSecondsRealtime(1.0f);
        lapSounds.Play();
        countdownCanvas.GetChild(2).gameObject.SetActive(false);
        countdownCanvas.GetChild(3).gameObject.SetActive(true);
        yield return new WaitForSecondsRealtime(1.0f);
        lapSounds.clip = countEnd;
        lapSounds.Play();
        countdownCanvas.GetChild(3).gameObject.SetActive(false);
        countdownCanvas.GetChild(4).gameObject.SetActive(true);
        startRace = true;
        //StartCoroutine (GhostRace ());
        yield return new WaitForSecondsRealtime(1.0f);
        countdownCanvas.GetChild(4).gameObject.SetActive(false);
        yield return null;
    }

    IEnumerator NextLap(Transform target)
    {
        if (nextLapCalled)
        {
            yield break;
        }
        currentCheckpoint++;
        while (!lapCompleted)
        {
            nextLapCalled = true;
            yield return null;
        }
        nextLapCalled = false;
        lapCount++;
        lapCompleted = false;
        lastTarget = target;
        checkpointCollider = lastTarget.GetComponent<ModularTrack>().track.pivotObject.GetComponent<Collider>();
        HandleLapDisplay(raceTime);
        if (lapCount == totalLaps)
        {
            StartCoroutine(LastLap());
        }

        if (lapCount == totalLaps + 1)
        {
            StartCoroutine(RaceComplete());
        }
        raceTime = 0f;
        yield return null;
    }

    IEnumerator LastLap()
    {
        countdownCanvas.GetChild(5).gameObject.SetActive(true);
        lapSounds.PlayOneShot(countEnd);
        yield return new WaitForSecondsRealtime(0.2f);
        lapSounds.PlayOneShot(countEnd);
        yield return new WaitForSecondsRealtime(0.2f);
        lapSounds.PlayOneShot(countEnd);
        yield return new WaitForSecondsRealtime(0.2f);
        lapSounds.PlayOneShot(countEnd);
        countdownCanvas.GetChild(5).gameObject.SetActive(false);
        float t = 0f;
        float pitch = music.pitch;
        while (t < 1)
        {
            t += Time.deltaTime / 5f;
            music.pitch = Mathf.Lerp(pitch, 1.1f, t);
        }

    }

    IEnumerator RaceComplete()
    {
        countdownCanvas.GetChild(5).GetComponent<TextMeshProUGUI>().SetText("Finish!");
        countdownCanvas.GetChild(5).gameObject.SetActive(true);
        lapSounds.pitch = 0.8f;
        lapSounds.PlayOneShot(countEnd);
        complete = true;

        // Send race info to playfab
        playerStats.ComparePlayerTimes(SceneManager.GetActiveScene().name, bestTime, totalRaceTime);


        if (PhotonNetwork.IsConnected)
        {
            PlayfabCloudScriptAPI.SetRaceComplete();
            if (positionInRace < 4)
            {
                if (positionInRace == 1)
                {
                    PlayfabCloudScriptAPI.SetRaceStats("1st Place Wins");
                }
                else if (positionInRace == 2)
                {
                    PlayfabCloudScriptAPI.SetRaceStats("2nd Place Wins");
                }
                else if (positionInRace == 3)
                {
                    PlayfabCloudScriptAPI.SetRaceStats("3rd Place Wins");
                }
            }

            photonView.RPC("RacerComplete", RpcTarget.OthersBuffered, photonView.ViewID);
        }

        yield return new WaitForSecondsRealtime(0.2f);
        lapSounds.PlayOneShot(countEnd);
        float t = 0f;
        float pitch = music.pitch;
        while (t < 1)
        {
            t += Time.deltaTime / 3f;
            music.pitch = Mathf.Lerp(pitch, 0.9f, t);
        }
        yield return new WaitForSecondsRealtime(1f);
        countdownCanvas.GetChild(5).gameObject.SetActive(false);
        float h = 0f;
        while (h < 1)
        {
            h += Time.deltaTime / 3f;
        }

    }

    IEnumerator ResetPosition()
    {
        canReset = false;
        float lastAcceleration = acceleration;
        rb.velocity = Vector3.zero;
        rb.angularVelocity = Vector3.zero;
        currentSpeed = 0f;
        rotate = 0f;
        currentRotate = 0f;
        speed = 0f;
        acceleration = 0f;
        driftMode = 0;
        driftPower = 0;
        rb.transform.position = new Vector3(lastTarget.position.x, lastTarget.position.y + 1.13f, lastTarget.position.z);
        transform.rotation = lastTarget.rotation;
        yield return new WaitForSecondsRealtime(0.5f);
        acceleration = lastAcceleration;
        canReset = true;
        yield return null;
    }

    // Handles sending information between photon instances
    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting)
        {
            CheckPositionInRace();
            if (photonView && photonView.IsMine)
            {
                stream.SendNext(positionInRace);
                stream.SendNext(currentCheckpoint);
                stream.SendNext(checkpointDistance);
                stream.SendNext(complete);
            }
        }
        else
        {
            if (photonView && !photonView.IsMine)
            {
                positionInRace = (int)stream.ReceiveNext();
                currentCheckpoint = (int)stream.ReceiveNext();
                checkpointDistance = (float)stream.ReceiveNext();
                complete = (bool)stream.ReceiveNext();
            }
        }
    }

    // Sends the player's customization values to the other racers
    [PunRPC]
    public void AssignRacerDefaults(int id, Vector3 hairColors, Vector3 kartColors, string[] hairPlayerKart)
    {

        if (photonView.ViewID != id)
        {
            return;
        }
        else
        {
            hairStyle.mesh = (Mesh)Resources.Load(hairPlayerKart[0]);
            playerCharacter.material.mainTexture = (Texture2D)Resources.Load(hairPlayerKart[1]);
            playerKart.material.mainTexture = (Texture2D)Resources.Load(hairPlayerKart[2]);
            playerHair.material.color = new Color(hairColors.x, hairColors.y, hairColors.z);
            playerKart.material.color = new Color(kartColors.x, kartColors.y, kartColors.z);
        }
    }

    // Alerts other players when this player has completed the race
    [PunRPC]
    public void RacerComplete(int playerID)
    {
        KartController[] karts = FindObjectsOfType<KartController>();
        if (photonView.ViewID == playerID)
        {
            complete = true;
            foreach (KartController kart in karts)
            {
                if (kart.complete)
                {
                    continue;
                }
                kart.completeOffset++;
            }
        }
    }
}
