﻿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 {

	public bool complete = false;
	public int completeOffset = 0;
	public TextMeshProUGUI placeInRace;
	public Collider checkpointCollider;
	public int racePlacement;
	public int currentCheckpoint = 0;
	public float checkpointDistance = 0f;

	public Animator anim;
	public MeshFilter hairStyle;
	public SkinnedMeshRenderer playerCharacter, playerKart;
	public MeshRenderer playerHair;

	//[HideInInspector]
	public LayerMask trackMask;

	//[HideInInspector]
    public PostProcessVolume postVolume;
	//[HideInInspector]
    public PostProcessProfile postProfile;

	//[HideInInspector]
    public Transform kartModel, kartNormal, tube1, tube2;
	//[HideInInspector]
    public Rigidbody sphere;
	//[HideInInspector]
	public Collider myCollider;

	float frOffset = 0.022f;				// Adjusts gameplay elements so that they run as if the local framerate was equal to 60fps (need this multiplied by Time.deltaTime)

	bool canReset = true, nextLapCalled = false;


	//[HideInInspector]
    public List<ParticleSystem> primaryParticles = new List<ParticleSystem>(), secondaryParticles = new List<ParticleSystem>();

	//[HideInInspector]
	public float speed, currentSpeed, rotate, currentRotate, driftPower;
	//[HideInInspector]
	public int driftDirection, driftMode = 0;
	//[HideInInspector]
	public bool first, second, third, hasLanded;
	//[HideInInspector]
	public Color c;

	//[HideInInspector]
	public Transform lastTarget;
	//[HideInInspector]
	public int lapCount = 1, totalLaps = 3;
	//[HideInInspector]
	public AudioManager audioManager;
	public AudioSource music, lapSounds;
	//[HideInInspector]
	public AudioClip countNormal, countEnd, sparkActivate, rocketBoost;
	//[HideInInspector]
	public bool startCounting = true, startRace = false, lapCompleted = false;
	//[HideInInspector]
	public float startTimer = 3f, raceTime = 0f, lastTime = 0f, engineEffect = 0f;
	public static float bestTime = 0f;
	//[HideInInspector]
	public Transform countdownCanvas;
	public TimeDisplayHandler timeDisplay;
	//[HideInInspector]
	public TextMeshProUGUI timer, lapCountText;
	//[HideInInspector]
	public List<Vector3> kartSpots;
	//[HideInInspector]
	public List<Quaternion> kartRotations;
	//[HideInInspector]
	public ParticleSystem boostVisual;

	//[HideInInspector]
	public GameObject playAgainButton;

	//[HideInInspector]
	public bool boostCalled = false;

	[Header("Bools")]
	//[HideInInspector]
    public bool drifting;

    [Header("Parameters")]

	//[HideInInspector]
	public float acceleration = 30f, steering = 80f, driftSteering = 80f, gravity = 10f;
	//[HideInInspector]
    public LayerMask layerMask;

    [Header("Model Parts")]

	//[HideInInspector]
	public List<Transform> wheel;
	//[HideInInspector]
	public Transform steeringWheel;

	[Header("Particles")]
	//[HideInInspector]
	public Transform wheelParticles, flashParticles;
	//[HideInInspector]
    public Color[] turboColors;

	//[HideInInspector]
	public float lerpFloat = 0.2f;

	bool everythingIsSet = false;

	// ZAS: Nullable so that if no command is sent it is clear that no value is set
	private bool? shouldAccelerate = null;
	private bool? shouldReverse = null;
	private float? horizontalMovement = null;
	private bool? shouldJump = null;

	// ZAS: These variables help us identify the one frame when jumping changes... similar to how GetButtonUp behaves
	private bool wasJumpingLastFrame = false;

	// ZAS: Informs the kart to accelerate during the next update call
	public void Accelerate(bool forward) {
		if (forward) {
			shouldAccelerate = true;
		} else {
			shouldReverse = 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; }


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

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

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

	}

	void Update() {

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

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

		// Adjust the framrate offset
		frOffset = 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") {

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

			if (lapCount > totalLaps) {
				if (playAgainButton) {
					if (!playAgainButton.activeInHierarchy) {
						playAgainButton.SetActive (true);
					}
					sphere.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");
				raceTime += Time.deltaTime;
			}

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

		//Follow Collider
		transform.position = sphere.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 * frOffset, 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 * frOffset;
			}

			driftPower += powerControl  * frOffset;

			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 * frOffset);
		}
		else
		{
			float control = (driftDirection == 1) ? ExtensionMethods.Remap(horizontalMovementThisFrame, -1, 1, .75f, 2) : ExtensionMethods.Remap(horizontalMovementThisFrame, -1, 1, 2, 0.75f * frOffset);
			kartModel.parent.localRotation = Quaternion.Euler(0, Mathf.LerpAngle(kartModel.parent.localEulerAngles.y, (control * 15) * driftDirection, 0.2f * frOffset), 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)
			sphere.AddForce(-kartModel.transform.right * currentSpeed, ForceMode.Acceleration);
		else
			sphere.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 (sphere.transform.position, -kartNormal.up, 1f, trackMask)) {
			sphere.AddForce (-kartNormal.up * gravity, ForceMode.Acceleration);
		}
	}

	void CheckPositionInRace () {
		KartController[] karts = FindObjectsOfType<KartController> ();
		int kartRank = 1;// + completeOffset;

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

		if (complete) {
			return;
		}

		foreach (KartController kart in karts) {
			if (kart == this) {
				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";
		}

		racePlacement = kartRank;
		if (timeDisplay) {
			Debug.Log (racePlacement);
			if (timeDisplay.racePlacement) {
				timeDisplay.racePlacement.SetText (racePlacement + placeEnding);
				timeDisplay.racePlacement.color = timeDisplay.placeColors [racePlacement - 1];
			}
		}
	}

	IEnumerator StartRace () {

		if (SceneManager.GetActiveScene ().name == "PhotonTesting 1" || SceneManager.GetActiveScene ().name == "RKR-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;
	}

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

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

	}

	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;

	}

	IEnumerator RaceComplete () {
		countdownCanvas.GetChild (5).GetComponent<TextMeshProUGUI> ().SetText ("Finish!");
		countdownCanvas.GetChild (5).gameObject.SetActive (true);
		lapSounds.pitch = 0.8f;
		lapSounds.PlayOneShot (countEnd);
		complete = true;
		if (PhotonNetwork.IsConnected) {
			photonView.RPC ("RacerComplete", RpcTarget.OthersBuffered);
		}
		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;
		sphere.velocity = Vector3.zero;
		sphere.angularVelocity = Vector3.zero;
		currentSpeed = 0f;
		rotate = 0f;
		currentRotate = 0f;
		speed = 0f;
		acceleration = 0f;
		driftMode = 0; 
		driftPower = 0;
		sphere.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;
	}

	public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) {
		if (stream.IsWriting) {
			CheckPositionInRace ();
			stream.SendNext(currentCheckpoint);
			stream.SendNext(checkpointDistance);
			stream.SendNext(complete);
		} else {
			currentCheckpoint = (int)stream.ReceiveNext();
			checkpointDistance = (float)stream.ReceiveNext();
			complete = (bool)stream.ReceiveNext();
		}
	}

    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 * frOffset, 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 * frOffset).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-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 * frOffset);
		//Debug.Log ("Last Target: " + lastTarget.forward.ToString() + ", Last Target Opposite: " + (-lastTarget.forward).ToString() + ", Self: " + transform.forward);

	}

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

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

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



	//RPC calls are located below here



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

	[PunRPC]	
	public void RacerComplete () {
		completeOffset++;
	}
}
