// (c)2011 MuchDifferent. All Rights Reserved.

using System;
using System.Collections.Generic;
using UnityEngine;
using uLink;

/// <summary>
/// A script example that can be use for players' objects in a 3d game without gravity. 
/// The object is floating in space just like a spaceship or a submarine does.
/// </summary>
/// <remarks>
/// When using this example script, it should be added as a component to the game object that a player controls.
/// The server should be authoritative when using this script (uLink.Network.isAuthoritativeServer = true).
/// The basic idea is that the server simulates all physics and checks if any player tries to cheat by 
/// sending movment orders as an RPC (The RPC name is ServerMove) with false coordinates to move faster than allowed in the game.
/// The server checks the incoming ServerMove RPC from the client and sends two kinds of RPCs back to the client.
/// If the client did move too fast (due to a cheating attempt or a bug or whatever) the server sends an RPC named
/// AdjustOwnerPos. If the position is good, the server sends an RPC named GoodOwnerPos. They are both sent as unreliable
/// RPCs from the server to the client to minimize server resources.
///
/// This script component also makes sure interpolation and extrapolation is used for the state synchronozation sent from
/// the server to clients. The state synchronization, arriving at the client, is stored in an internal array and the 
/// public properties interpolationBackTime and extrapolationLimit can be used to tune the correct behavior for every game. 
/// Please read the code for more details.
/// </remarks>

[AddComponentMenu("uLink Utilities/Strict Character")]
[RequireComponent(typeof(uLinkNetworkView))]
public class uLinkStrictCharacter : uLink.MonoBehaviour
{
	private struct State
	{
		public double timestamp;
		public Vector3 pos;
		public Vector3 vel;
		public Quaternion rot;
	}

	private struct Move : IComparable<Move>
	{
		public double timestamp;
		public float deltaTime;
		public Vector3 vel;

		public static bool operator ==(Move lhs, Move rhs) { return lhs.timestamp == rhs.timestamp; }
		public static bool operator !=(Move lhs, Move rhs) { return lhs.timestamp != rhs.timestamp; }
		public static bool operator >=(Move lhs, Move rhs) { return lhs.timestamp >= rhs.timestamp; }
		public static bool operator <=(Move lhs, Move rhs) { return lhs.timestamp <= rhs.timestamp; }
		public static bool operator >(Move lhs, Move rhs) { return lhs.timestamp > rhs.timestamp; }
		public static bool operator <(Move lhs, Move rhs) { return lhs.timestamp < rhs.timestamp; }

		public override bool Equals(object other)
		{
			if (other == null || !(other is Move))
				return false;

			return this == (Move)other;
		}

		public override int GetHashCode()
		{
			return timestamp.GetHashCode();
		}

		public int CompareTo(Move other)
		{
			if (this > other)
				return 1;

			if (this < other)
				return -1;

			return 0;
		}
	}

	public double interpolationBackTime = 0.2;
	public double extrapolationLimit = 0.5;

	public float sqrMaxServerError = 300.0f;
	public float sqrMaxServerSpeed = 1000.0f;

	private CharacterController character;
	
	// We store twenty states with "playback" information
	State[] proxyStates = new State[20];
	// Keep track of what slots are used
	int proxyStateCount;

	List<Move> ownerMoves = new List<Move>();

	double serverLastTimestamp = 0;

	void Awake()
	{
		character = GetComponent<CharacterController>();
	}

	void uLink_OnSerializeNetworkView(uLink.BitStream stream, uLink.NetworkMessageInfo info)
	{
		if (stream.isWriting)
		{
			Vector3 pos = transform.position;
			Quaternion rot = transform.rotation;
			Vector3 velocity = character.velocity;

			stream.Serialize(ref pos);
			stream.Serialize(ref velocity);
			stream.Serialize(ref rot);
		}
		else
		{
			Vector3 pos = Vector3.zero;
			Vector3 velocity = Vector3.zero;
			Quaternion rot = Quaternion.identity;

			stream.Serialize(ref pos);
			stream.Serialize(ref velocity);
			stream.Serialize(ref rot);

			// Shift the buffer sideways, deleting state 20
			for (int i = proxyStates.Length - 1; i >= 1; i--)
			{
				proxyStates[i] = proxyStates[i - 1];
			}


			// Record current state in slot 0
			State state;
			state.timestamp = info.timestamp;

			state.pos = pos;
			state.vel = velocity;
			state.rot = rot;
			proxyStates[0] = state;

			// Update used slot count, however never exceed the buffer size
			// Slots aren't actually freed so this just makes sure the buffer is
			// filled up and that uninitalized slots aren't used.
			proxyStateCount = Mathf.Min(proxyStateCount + 1, proxyStates.Length);

			// Check if states are in order
			if (proxyStates[0].timestamp < proxyStates[1].timestamp)
				Debug.LogError("Timestamp inconsistent: " + proxyStates[0].timestamp + " should be greater than " + proxyStates[1].timestamp);
		}
	}
	
	// We have a window of interpolationBackTime where we basically play 
	// By having interpolationBackTime the average ping, you will usually use interpolation.
	// And only if no more data arrives we will use extra polation
	void Update()
	{
		if (uLink.Network.isAuthoritativeServer && uLink.Network.isServerOrCellServer)
		{
			return;
		}

		// This is the target playback time of the rigid body
		double interpolationTime = uLink.Network.time - interpolationBackTime;
		
		// Use interpolation if the target playback time is present in the buffer
		if (proxyStates[0].timestamp > interpolationTime)
		{
			// Go through buffer and find correct state to play back
			for (int i=0;i<proxyStateCount;i++)
			{
				if (proxyStates[i].timestamp <= interpolationTime || i == proxyStateCount-1)
				{
					// The state one slot newer (<100ms) than the best playback state
					State rhs = proxyStates[Mathf.Max(i-1, 0)];
					// The best playback state (closest to 100 ms old (default time))
					State lhs = proxyStates[i];
					
					// Use the time between the two slots to determine if interpolation is necessary
					double length = rhs.timestamp - lhs.timestamp;
					float t = 0.0F;
					// As the time difference gets closer to 100 ms t gets closer to 1 in 
					// which case rhs is only used
					// Example:
					// Time is 10.000, so sampleTime is 9.900 
					// lhs.time is 9.910 rhs.time is 9.980 length is 0.070
					// t is 9.900 - 9.910 / 0.070 = 0.14. So it uses 14% of rhs, 86% of lhs
					if (length > 0.0001)
						t = (float)((interpolationTime - lhs.timestamp) / length);
					
					// if t=0 => lhs is used directly
					transform.localPosition = Vector3.Lerp(lhs.pos, rhs.pos, t);
					transform.localRotation = Quaternion.Slerp(lhs.rot, rhs.rot, t);
					return;
				}
			}
		}
		// Use extrapolation
		else
		{
			State latest = proxyStates[0];
			
			float extrapolationLength = (float)(interpolationTime - latest.timestamp);
			// Don't extrapolation for more than 500 ms, you would need to do that carefully
			if (extrapolationLength < extrapolationLimit)
			{				
				transform.position = latest.pos + latest.vel * extrapolationLength;
				transform.rotation = latest.rot;
				character.SimpleMove(latest.vel);
			}
		}
	}

	void LateUpdate()
	{
		if (!uLink.Network.isAuthoritativeServer || uLink.Network.isServerOrCellServer || !networkView.isMine)
		{
			return;
		}

		// TODO: optimize by not sending rpc if no input and rotation. also add idleTime so server's timestamp is still in sync

		Move move;
		move.timestamp = uLink.Network.time;
		move.deltaTime = (ownerMoves.Count > 0) ? (float)(move.timestamp - ownerMoves[ownerMoves.Count - 1].timestamp) : 0.0f;
		move.vel = character.velocity;

		ownerMoves.Add(move);

		networkView.UnreliableRPC("ServerMove", uLink.NetworkPlayer.server, transform.position, move.vel, transform.rotation);
	}

	[RPC]
	void ServerMove(Vector3 ownerPos, Vector3 vel, Quaternion rot, uLink.NetworkMessageInfo info)
	{
		if (info.timestamp <= serverLastTimestamp || !character.isGrounded)
		{
			return;
		}

		transform.rotation = rot;

		if (vel.sqrMagnitude > sqrMaxServerSpeed)
		{
			vel.x = vel.y = vel.z = Mathf.Sqrt(sqrMaxServerSpeed) / 3.0f;
		}

		float deltaTime = (float)(info.timestamp - serverLastTimestamp);
		Vector3 deltaPos = vel * deltaTime;

		character.Move(deltaPos);

		serverLastTimestamp = info.timestamp;

		Vector3 serverPos = transform.position;
		Vector3 diff = serverPos - ownerPos;

		if (Vector3.SqrMagnitude(diff) > sqrMaxServerError)
		{
			networkView.UnreliableRPC("AdjustOwnerPos", uLink.RPCMode.Owner, serverPos);
		}
		else
		{
			networkView.UnreliableRPC("GoodOwnerPos", uLink.RPCMode.Owner);
		}
	}

	[RPC]
	void GoodOwnerPos(uLink.NetworkMessageInfo info)
	{
		Move goodMove;
		goodMove.timestamp = info.timestamp;
		goodMove.deltaTime = 0;
		goodMove.vel = Vector3.zero;

		int index = ownerMoves.BinarySearch(goodMove);
		if (index < 0) index = ~index;

		ownerMoves.RemoveRange(0, index);
	}

	[RPC]
	void AdjustOwnerPos(Vector3 pos, uLink.NetworkMessageInfo info)
	{
		GoodOwnerPos(info);
		
		transform.position = pos;

		foreach (Move move in ownerMoves)
		{
			character.Move(move.vel * move.deltaTime);
		}
	}
}
