using System;
using System.Collections.Generic;

using UnityEngine;

using Sfs2X.Entities.Data;


public delegate void ControlShipEvent(ControlEventArgs e);

public class ControlEventArgs : EventArgs
{
	public int rotationDir { get; set; }
	public bool activate { get; set; }
	public int fire { get; set; }
}

/**
 * Script attached to the SpaceWar2 Engine object in the Game scene.
 */
public class GameEngine : MonoBehaviour
{
	public Background background;
	public WeaponShot weaponTorpedo;
	public WeaponShot weaponMine;

	private Planet planet;
	private Starship myStarship;

	private bool isThrustKeyDown;
	private bool isFire1KeyDown;
	private bool isFire2KeyDown;

	private Dictionary<int, WeaponShot> weaponShots;
	private Dictionary<int, Starship> starships;

	private readonly int SCROLL_AREA_PADDING = 15; // % of the viewport size

	public event ControlShipEvent Rotate;
	public event ControlShipEvent Thrust;
	public event ControlShipEvent Fire;

	//----------------------------------------------------------
	// Unity callback methods
	//----------------------------------------------------------

	public void Start()
	{
		weaponShots = new Dictionary<int, WeaponShot>();
		starships = new Dictionary<int, Starship>();
	}

	void Update()
	{
        if (starships != null)
        {
            foreach (var ship in starships)
                RenderStarship(ship.Value);

            if (weaponShots != null)
            {
                foreach (var shot in weaponShots)
                    RenderWeaponShot(shot.Value);
            }

            OnKeyboardDown();
            OnKeyboardUp();
        }
    }

	void OnKeyboardDown()
	{
		if (myStarship == null)
			return;

		if (Input.GetKeyDown("left") || Input.GetKeyDown("right"))
		{
			int dir = Input.GetKeyDown("left") ? 1 : -1;

			if (dir != myStarship.rotatingDir)
			{
				// Stop rotation
				SetStarshipRotating(myStarship.userId, dir);

				// Fire event to send a request to the server
				ControlEventArgs eventArgs = new ControlEventArgs();
				eventArgs.rotationDir = myStarship.rotatingDir;

                Rotate?.Invoke(eventArgs);
            }
		}

		if (Input.GetKeyDown("up"))
		{
			if (!isThrustKeyDown)
			{
				isThrustKeyDown = true;

				// Thrust activation is made of 3 steps:
				// 1) on key down the ship shows a small flame; no actual force is applied to the ship (no trajectory change)
				// 2) request is sent to the server which activates the thrust and sends a position reset event
				// 3) when the event is received the ship shows a bigger flame and the thrust force is applied during the simulation
				myStarship.thrusterValue = 1;

				// Fire event to send a request to the server
				ControlEventArgs eventArgs = new ControlEventArgs();
				eventArgs.activate = true;

                Thrust?.Invoke(eventArgs);
            }
		}

		if (Input.GetKeyDown("space"))
		{
			if (!isFire1KeyDown)
			{
				isFire1KeyDown = true;

				// Fire event to send a request to the server
				ControlEventArgs eventArgs = new ControlEventArgs();
				eventArgs.fire = 1;

                Fire?.Invoke(eventArgs);
            }
		}

		if (Input.GetKeyDown("c"))
		{
			if (!isFire2KeyDown)
			{
				isFire2KeyDown = true;

				// Fire event to send a request to the server
				ControlEventArgs eventArgs = new ControlEventArgs();
				eventArgs.fire = 2;

				Fire?.Invoke(eventArgs);
			}
		}
	}

	void OnKeyboardUp()
	{
		if (myStarship == null)
			return;

		if (Input.GetKeyUp("left") || Input.GetKeyUp("right"))
		{
			int dir = Input.GetKeyUp("left") ? 1 : -1;

			if (dir == myStarship.rotatingDir)
			{
				// Stop rotation
				SetStarshipRotating(myStarship.userId, 0);

				// Fire event to send a request to the server
				ControlEventArgs eventArgs = new ControlEventArgs();
				eventArgs.rotationDir = myStarship.rotatingDir;

                Rotate?.Invoke(eventArgs);
            }
		}

		if (Input.GetKeyUp("up"))
		{
			if (isThrustKeyDown)
			{
				isThrustKeyDown = false;

				// Thrust deactivation is made of 3 steps:
				// 1) on key up the ship shows a small flame; the actual force is still applied to the ship (trajectory keeps changing)
				// 2) request is sent to the server which deactivates the thrust and sends a position reset event
				// 3) when the event is received the ship stops showing the flame and the thrust force is not applied anymore
				myStarship.thrusterValue = 1;

				// Fire event to send a request to the server
				ControlEventArgs eventArgs = new ControlEventArgs();
				eventArgs.activate = false;

                Thrust?.Invoke(eventArgs);
            }
		}

		if (Input.GetKeyUp("space"))
		{
			isFire1KeyDown = false;
		}

		if (Input.GetKeyUp("c"))
		{
			isFire2KeyDown = false;
		}
	}

	//----------------------------------------------------------
	// Public methods
	//----------------------------------------------------------

	public int GetTimer()
	{
		return (int)Mathf.Round(Time.time * 1000f);
	}

	public void CreatePlanet(ISFSObject planetData)
    {
		// Load resource
		var resource = Resources.Load<Planet>("Planets/" + planetData.GetUtfString("name"));

		// Create instance
		planet = Instantiate(resource);

		// Set mass
		planet.mass = planetData.GetInt("mass");

		// Set sorting layer and order
		SpriteRenderer sr = planet.GetComponent<SpriteRenderer>();
		sr.sortingLayerName = "Planet";
	}

	public void CreateStarship(int userId, string userName, bool isMine, string type, ISFSObject settings)
	{
		if (starships.ContainsKey(userId))
			return;

		// Load resource
		var resource = Resources.Load<Starship>("Ships/" + type);

		// Create instance
		Starship ship = Instantiate(resource);
		ship.userId = userId;
		ship.username = userName;
		ship.Configure(settings, isMine);

		if (isMine)
			myStarship = ship;

		// Add starship to array container
		starships.Add(userId, ship);
	}

	public void SetStarshipPosition(int userId, float x, float y, float vx, float vy, float d, bool t, int s, int elapsed)
	{
		if (starships.ContainsKey(userId))
		{
			Starship ship = starships[userId];

			// Set position and velocity
			ship.xx = x;
			ship.yy = y;
			ship.velocity.vx = vx;
			ship.velocity.vy = vy;
			ship.lastRenderTime = GetTimer() - elapsed;

			// Set thruster
			ship.doThrust = t;

			// Set rotation angle
			ship.rotation = d;

			// Set shield
			ship.shield = s;

			// Render the starship
			// This simulates the starship movement taking into account the elapsed time since the server sent the new position/speed
			// and places the starship in the current coordinates
			RenderStarship(ship);
		}
	}

	public void SetStarshipRotating(int userId, int r)
	{
		if (starships.ContainsKey(userId))
		{
			Starship ship = starships[userId];

			ship.rotatingDir = r;
		}
	}

	public void RemoveStarship(int userId, bool doExplode)
	{
		if (!starships.ContainsKey(userId))
			return;

		Starship ship = starships[userId];

		starships.Remove(userId);

		// Show explosion...
		if (doExplode)
        	ship.Explode();

		// ...or remove immediately
		else
			Destroy(ship.gameObject);

		if (ship == myStarship)
			myStarship = null;
	}

	public void CreateWeaponShot(int id, ISFSObject settings, float x, float y, float vx, float vy, int elapsed)
	{
		if (weaponShots.ContainsKey(id))
			return;

		// Load resource
		var resource = Resources.Load<WeaponShot>("Weapons/" + settings.GetUtfString("model"));

		// Create instance
		WeaponShot shot = Instantiate(resource);
		shot.id = id;

		// Add weapon shot to array container
		weaponShots.Add(id, shot);

		// Set position and velocity
		shot.xx = x;
		shot.yy = y;
		shot.velocity.vx = vx;
		shot.velocity.vy = vy;
		shot.lastRenderTime = GetTimer() - elapsed;
	}

	public void RemoveWeaponShot(int id, bool doExplode, Int32? posX = null, Int32? posY = null)
	{
		if (!weaponShots.ContainsKey(id))
			return;

		WeaponShot shot = weaponShots[id];

		// The shot could have already been removed if the explosion
        // was notified by the server before the proximity update
		if (shot != null)
		{
			weaponShots.Remove(id);

			// Show explosion...
			if (doExplode)
            {
				if (posX != null && posY != null)
					shot.Explode(ConvertCoords((float)posX, (float)posY));
				else
					shot.Explode();
			}

			// ...or remove immediately
			else
				Destroy(shot.gameObject);
		}
	}

	//----------------------------------------------------------
	// Private methods
	//----------------------------------------------------------

	/**
	 * Move a starship to the next position based on applied forces.
	 */
	private void RenderStarship(Starship ship)
	{
		float now = GetTimer();
		float elapsed = now - ship.lastRenderTime;

		for (int i = 0; i < elapsed; i++)
		{
			// Ship rotation
			ship.rotation += ship.rotatingDir * ship.rotationSpeed;

			// Thruster force
			if (ship.doThrust)
			{
				ship.velocity.vx += (float)Math.Cos(ship.rotation) * ship.thrustAcceleration;
				ship.velocity.vy += (float)Math.Sin(ship.rotation) * ship.thrustAcceleration;
			}

			// Limit speed
			ship.velocity.LimitSpeed(ship.maxSpeed);

			// Planet gravitational attraction
			ApplyGravityForce(ship);

			// Update ship position due to the calculated velocity
			ship.xx += ship.velocity.vx;
			ship.yy += ship.velocity.vy;
		}

		// Update starship sprite position
		ship.position = ConvertCoords(ship.xx, ship.yy);

		ship.lastRenderTime = now;

		// Move camera if ship is near borders
		if (ship.isMine)
			UpdateCameraPosition(ship.position);
	}

	/**
	 * Move a weapon shot to the next position based on applied forces.
	 * Same note about fake xx and yy coordinates apply here.
	 */
	private void RenderWeaponShot(WeaponShot shot)
	{
		
		//shot.xx += shot.velocity.vx;
		//shot.yy += shot.velocity.vy;
		//shot.transform.position = new Vector3(shot.xx, shot.yy, shot.transform.position.z);

		float now = GetTimer();
		float elapsed = now - shot.lastRenderTime;

		for (int i = 0; i < elapsed; i++)
		{
			// Planet gravitational attraction
			ApplyGravityForce(shot);

			// Update shot position due to the calculated velocity
			shot.xx += shot.velocity.vx;
			shot.yy += shot.velocity.vy;
		}

		// Update shot sprite position
		shot.position = ConvertCoords(shot.xx, shot.yy);
		
		shot.lastRenderTime = now;
	}

	private void ApplyGravityForce(GameItem gameItem)
	{
		if (planet.mass > 0)
		{
			// Square distance from planet center (always 0,0)
			double dist2 = Math.Pow(gameItem.xx, 2) + Math.Pow(gameItem.yy, 2);
			double angle = Math.Atan2(gameItem.yy, gameItem.xx); // In radians
			float attr = planet.mass / (float)dist2;

			gameItem.velocity.vx -= (float)(Math.Cos(angle) * attr);
			gameItem.velocity.vy -= (float)(Math.Sin(angle) * attr);
		}
	}

	/**
	 * Move camera if player' ship is near viewport borders.
	 */
	private void UpdateCameraPosition(Vector2 shipPosition)
    {
		Vector2 shipOnViewport = Camera.main.WorldToViewportPoint(shipPosition);

		float perc = SCROLL_AREA_PADDING / 100f;

		Vector2 currCamPos = Camera.main.transform.position;
		Vector3 newCamPos = Camera.main.transform.position;

        if (shipOnViewport.x > (1 - perc))
            newCamPos.x += shipPosition.x - Camera.main.ViewportToWorldPoint(new Vector2(1 - perc, 0)).x;
		else if (shipOnViewport.x < perc)
            newCamPos.x -= Camera.main.ViewportToWorldPoint(new Vector2(perc, 0)).x - shipPosition.x;

		if (shipOnViewport.y > (1 - perc))
			newCamPos.y += shipPosition.y - Camera.main.ViewportToWorldPoint(new Vector2(0, 1 - perc)).y;
		else if (shipOnViewport.y < perc)
			newCamPos.y -= Camera.main.ViewportToWorldPoint(new Vector2(0, perc)).y - shipPosition.y;

        // Scroll background
        background.Scroll(newCamPos.x - currCamPos.x, newCamPos.y - currCamPos.y);

        // Move camera
        Camera.main.transform.position = newCamPos;
		
	}

	private Vector2 ConvertCoords(float xx, float yy)
    {
		return new Vector2(xx/100, yy/100);
	}
}
