using UnityEngine;
using System.Collections.Generic;
using UnityEngine.UI;

public class DeployedUnit : Unit {
	private bool selected;
	private bool visible;
	private bool isMouseOver;
	private GameObject gameObject;
	private SpriteRenderer unitSpriteRenderer;
	private GameObject mapInfo;
	private LineRenderer pathLineRenderer;
	private LineRenderer directionLineRenderer;
	private LineRenderer targetLineRenderer;
	private static float currentScale = 1.0f;
	/*private DeployedUnit stackedParent = null;
	private Set<DeployedUnit> stackedChildren = new Set<DeployedUnit>();*/

	private Path path;
	private DeployedUnit target;
	private bool isMoving = false;
	private Vertex position;
	private float angle; // in radians
	private float verticalAngle; // in radians
	private float currentSpeed = 0.0f; // in m/s
	private float userTargetSpeed = 0.0f;
	private float targetSpeed = 0.0f;
	private float userCruiseAltitude = 0.0f;
	private bool isUserCruiseAltitudeSet = false;
	private bool isLanding = false;

	private List<Tuple<Class, DeployedUnit>> toBeFired;
	private List<Tuple<Class, Loadout>> toBeLaunched; //list of classes which were requested to launch
	private List<Tuple<StoredUnit, LoadoutData>> beingPrepared; //list of units which are being loaded with missiles
	private List<StoredUnit> readyForLaunch; //list of units on the 'bridge', ready to be deployed
	private Set<DeployedUnit> alreadyTargeted;

	private Dictionary<Country, Visibility> visibility;
	private Dictionary<Country, Visibility> newVisibility;

	public DeployedUnit(Class unitClass, Vertex position) : base(unitClass) {
		this.position = position;
		init();
	}

	public DeployedUnit(StoredUnit stored, Vertex position) : base (stored) {
		this.position = position;
		init();
	}

	public DeployedUnit(int id, string className) : base (id, className) {
		this.position = new Vertex(-100000000, -100000000, -100000000);
		init();
	}

	private void init() {
		gameObject = GameObject.Instantiate(GameController.getDefaultUnit(), Converter.metersToGameCoo(position).getVector(-10), Quaternion.identity);

		gameObject.GetComponent<UnitBehavior>().setUnit(this);

		unitSpriteRenderer = gameObject.GetComponentsInChildren<SpriteRenderer>()[0];
		mapInfo = gameObject.transform.FindChild("Info").gameObject;

		directionLineRenderer = gameObject.GetComponentsInChildren<LineRenderer>()[0];
		targetLineRenderer = gameObject.GetComponentsInChildren<LineRenderer>()[1];
		pathLineRenderer = gameObject.GetComponentsInChildren<LineRenderer>()[2];

		setSelected(false);

		toBeFired = new List<Tuple<Class, DeployedUnit>>();
		toBeLaunched = new List<Tuple<Class, Loadout>>();
		beingPrepared = new List<Tuple<StoredUnit, LoadoutData>>();
		readyForLaunch = new List<StoredUnit>();
		alreadyTargeted = new Set<DeployedUnit>();

		gameObject.transform.localScale *= currentScale;
		pathLineRenderer.widthMultiplier *= currentScale;
		directionLineRenderer.widthMultiplier *= currentScale;
		targetLineRenderer.widthMultiplier *= currentScale;

		resetVisibility();
		switchVisibility();

		updateVisibility();
		updateSprite();
	}

	public Vertex getPosition() {
		return position;
	}

	public float getAngle() {
		return angle;
	}

	public float getVerticalAngle() {
		return verticalAngle;
	}

	public DeployedUnit getTarget() {
		return target;
	}

	public Path getPath() {
		return path;
	}

	public float getcurrentSpeed() {
		return currentSpeed;
	}

	public float getCruiseAltitude() {
		if (!isUserCruiseAltitudeSet) {
			return modules.getCruiseAltitude();
		}
		else {
			return userCruiseAltitude;
		}
	}

	public MissileTechnique getMissileTechnique() {
		return modules.getMissileTechnique();
	}

	public List<Tuple<float, float>> getLandingPattern() {
		return modules.getLandingPattern();
	}

	public void setSelected(bool selected) {
		this.selected = selected;
	}

	public static void staticScale(float amount) {
		currentScale *= amount;
	}

	public void scaleSprite(float amount) {
		gameObject.transform.localScale *= amount;
		pathLineRenderer.widthMultiplier *= amount;
		directionLineRenderer.widthMultiplier *= amount;
		targetLineRenderer.widthMultiplier *= amount;
	}

	private void stop() {
		path = null;
		isMoving = false;
		targetSpeed = modules.getMinSpeed();
		isLanding = false;
	}

	public void setPosition(Vertex newPosition) {
		this.position = newPosition;
	}

	public void setAngle(float angle) {
		this.angle = angle;
	}

	public void setVerticalAngle(float angle) {
		this.verticalAngle = angle;
	}

	public void setPath(Path newPath, bool keepIsLanding) {
		this.path = newPath;
		isMoving = true;

		if (!keepIsLanding) {
			isLanding = false;
		}

		if (userTargetSpeed == 0.0f) {
			userTargetSpeed = modules.getMaxSpeed();
		}

		targetSpeed = userTargetSpeed;
	}

	public void removePath() {
		stop();
	}

	public void setTarget(DeployedUnit target) {
		this.target = target;

		if (isLanding) {
			stop();
		}
	}

	public void setTargetSpeed(float newSpeed) {
		userTargetSpeed = newSpeed;
		targetSpeed = newSpeed;
	}

	public void setCruiseAltitude(float newCruiseAltitude) {
		userCruiseAltitude = newCruiseAltitude;
		isUserCruiseAltitudeSet = true;
	}

	public void updateVisibility() {

		visible = visibility[GameController.getPlayerCountry()] != Visibility.Invisible;
		gameObject.SetActive(visible);

		/*if (!visible) {
			setStackedParent(null); //if we don't do this, the player may have some info about invisible units
		}*/

		/*if (stackedParent != null && !stackedParent.visible) {
			setStackedParent(null);
		}*/
	}

	public void updateDirectionLine() {
		/*if (hasStackedChildren() || isStacked()) {
			directionLineRenderer.enabled = false;
			return;
		}*/

		directionLineRenderer.enabled = true;

		if (getVisibility(GameController.getPlayerCountry()) == Visibility.Visible) {
			directionLineRenderer.material = GameController.getRelationMaterial(getOwner().getRelation(GameController.getPlayerCountry()));
		}
		else {
			directionLineRenderer.material = GameController.getRelationMaterial(Relation.Unknown);
		}


		directionLineRenderer.numPositions = 2;
		directionLineRenderer.SetPosition(0, Converter.metersToGameCoo(position).getVector(-11));
		directionLineRenderer.SetPosition(1, (Converter.metersToGameCoo(position) + new Vector(Mathf.Cos(angle), Mathf.Sin(angle), -2) * Constants.DIRECTION_LENGTH * currentScale).getVector(-11));
	}

	public void updateTargetLine() {
		/*if (hasStackedChildren() || isStacked()) {
			targetLineRenderer.enabled = false;
			return;
		}*/

		targetLineRenderer.enabled = selected && (target != null);

		if (!targetLineRenderer.enabled) {
			return;
		}

		targetLineRenderer.material = GameController.getRelationMaterial(getOwner().getRelation(target.getOwner()));

		targetLineRenderer.numPositions = 2;
		targetLineRenderer.SetPosition(0, Converter.metersToGameCoo(position).getVector(-11));
		targetLineRenderer.SetPosition(1, Converter.metersToGameCoo(target.position).getVector(-11));
	}

	public void updatePathLine() {
		/*if (hasStackedChildren() || isStacked()) {
			pathLineRenderer.enabled = false;
			return;
		}*/

		if (!selected || !isMoving) {
			pathLineRenderer.enabled = false;
			return;
		}

		pathLineRenderer.enabled = true;

		List<Vertex> vertices = new List<Vertex>();
		vertices.Add(position);

		foreach(Waypoint waypoint in path.toList()) {
			vertices.Add(waypoint.getVertex());
		}

		Vector3[] vectors = new Vector3[vertices.Count];
		for (int i = 0; i < vertices.Count; i++) {
			vectors[i] = Converter.metersToGameCoo(vertices[i]).getVector(-5);
		}

		pathLineRenderer.numPositions = vertices.Count;
		pathLineRenderer.SetPositions(vectors);
	}

	public void updateSprite() {
		/*if (hasStackedChildren()) {
			mapInfo.SetActive(false);
		}
		else if (isStacked()){
			unitSpriteRenderer.enabled = false;
			mapInfo.SetActive(false);
		}
		else {*/
			unitSpriteRenderer.enabled = visibility[GameController.getPlayerCountry()] != Visibility.Invisible;
			mapInfo.SetActive(unitSpriteRenderer.enabled && (selected || isMouseOver));
		//}

		Relation relation = getOwner().getRelation(GameController.getPlayerCountry());
		Vertex meanPosition = position;
		bool differentRelations = false;

		/*foreach(DeployedUnit stackedChild in stackedChildren) {
			meanPosition += stackedChild.position;

			if (stackedChild.getOwner().getRelation(GameController.getPlayerCountry()) != relation) {
				differentRelations = true;
			}
		}

		meanPosition /= (1 + stackedChildren.count());*/

		gameObject.transform.position = Converter.metersToGameCoo(position).getVector(-10); //sets the collider to the real position
		unitSpriteRenderer.transform.position = Converter.metersToGameCoo(meanPosition).getVector(-10); //sets the sprite to the mean position of the stack

		unitSpriteRenderer.sprite = unitClass.getSprite(visibility[GameController.getPlayerCountry()], relation, false, differentRelations);
	}

	public void updateMapInfos() {
		if (!mapInfo.activeSelf) {
			return;
		}

		List<GameObject> toBeDeleted = new List<GameObject>();

		foreach(Transform child in mapInfo.transform.FindChild("Canvas")) {
			toBeDeleted.Add(child.gameObject);
		}

		foreach(GameObject child in toBeDeleted) {
			if (child.activeSelf) {
				GameObject.Destroy(child);
			}
		}

		if (visibility[GameController.getPlayerCountry()] == Visibility.Unknown) {
			addInfoText("????");
		}
		else {
			addInfoText(getClass().getName());
		}

		if (currentSpeed != 0) {
			addInfoText("Speed : " + (int)currentSpeed + " m/s");
		}
		if (position.z() != 0) {
			addInfoText("Alt. : " + (int)position.z() + " m");
		}

	}

	private void addInfoText(string text) {
		GameObject clone = GameObject.Instantiate(mapInfo.transform.FindChild("Canvas").FindChild("Text").gameObject, mapInfo.transform.FindChild("Canvas"));

		clone.SetActive(true);
		clone.GetComponent<Text>().text = text;
	}

	// Modules interactions

	public bool canFire(Class missileClass, int number) {
		return modules.canLaunch(missileClass, number, null);
	}

	public bool canLaunch(Class unitClass, int number, Loadout loadout = null) {
		return modules.canLaunch(unitClass, number, loadout);
	}

	public bool canBeLandedOn(DeployedUnit unit) {
		return modules.canBeLandedOn(unit);
	}

	public void fire(Class missileClass, int number, DeployedUnit missileTarget) {
		for (int i = 0; i < number; i++) {
			toBeFired.Add(new Tuple<Class, DeployedUnit>(missileClass, missileTarget));
		}
	}

	public void launch(Class unitClass, int number, Loadout loadout) {
		for (int i = 0; i < number; i++) {
			toBeLaunched.Add(new Tuple<Class, Loadout>(unitClass, loadout));
		}
	}

	public void landOn(DeployedUnit unit) {
		Requests.moveTowardsTarget(this, unit);
		isLanding = true;
	}

	public void deployUnit(int moduleDataID, int storedUnitID) {
		modules.deployUnit(moduleDataID, storedUnitID);
	}

	public void destroy() {
		/*setStackedParent(null);

		foreach(DeployedUnit stackedChild in stackedChildren) {
			stackedChild.stackedParent = null;
		}*/

		GameObject.Destroy(gameObject);
	}


	// Update methods

	public void move(float delta) {
		if (path == null) {

			float minSpeed = modules.getMinSpeed();
			if (minSpeed > 0.0) {
				angle += modules.getAngularSpeed() / 10 * delta; //do not turn at full rate
			}

			updateSpeed(delta);
			position += (new Vector(Mathf.Cos(angle), Mathf.Sin(angle), 0)) * currentSpeed * delta;

			return;
		}

		modules.consumeFuel(delta);

		Vertex nextVertex = path.getNextWaypoint().getVertex();

		Vector direction = (nextVertex - position); //enlever z
		Vector currentDirection = new Vector(Mathf.Cos(angle), Mathf.Sin(angle), 0);

		//Changes in angle
		if (direction.x() != 0.0f || direction.y() != 0.0f) { // if the destination is at the same position as the unit, no need to change angle
			float angleDiff = direction.angle(currentDirection);
			float angularSpeed = modules.getAngularSpeed(); //angular speed in radian/s

			if (Mathf.Abs(angleDiff) < angularSpeed * delta) {
				angle = Mathf.Acos(direction.normalized().x()) * Mathf.Sign(direction.normalized().y());
			}
			else if (angleDiff > 0) {
				angle -= angularSpeed * delta;
			}
			else {
				angle += angularSpeed * delta;
			}
		}

		//Changes in vertical angle
		float speedChange = 1.0f;
		if (direction.z() != 0.0f) {
			float theoreticalVerticalAngle = Mathf.Asin(direction.z() / ((nextVertex - position).norm()));

			//for simplicity, only brutal changes in vertical angle
			verticalAngle = Mathf.Clamp(theoreticalVerticalAngle, -modules.getVerticalAngleRange(), modules.getVerticalAngleRange());

			//Debug.Log(verticalAngle);

			speedChange -= Mathf.Sin(verticalAngle) * Mathf.Sign(verticalAngle) / 2; //simulate gravity

			//Debug.Log(speedChange);
		}
		else {
			verticalAngle = 0.0f;
		}

		//Speed and movement
		updateSpeed(delta);

		float distTravelled = currentSpeed * delta * speedChange;

		if (distTravelled * Constants.TOLERANCE_DISTANCE_FACTOR >= direction.norm()) {
			path.removeFirstWaypoint();
			position = nextVertex;

			if (path.size() == 0) {
				stop();
			}
		}
		else {
			position += (new Vector(Mathf.Cos(angle) * Mathf.Cos(verticalAngle), Mathf.Sin(angle) * Mathf.Cos(verticalAngle), Mathf.Sin(verticalAngle))) * distTravelled;
		}
	}

	private void updateSpeed(float delta) {
		
		if (currentSpeed < targetSpeed) {
			float acceleration = modules.getAcceleration(); //acceleration in m/s^2

			if (Mathf.Abs(currentSpeed - targetSpeed) <= acceleration * delta) {
				currentSpeed = targetSpeed;
			}
			else {
				currentSpeed += acceleration * delta;
			}
		}
		else {
			float deceleration = modules.getDeceleration(); //acceleration in m/s^2

			if (Mathf.Abs(currentSpeed - targetSpeed) <= deceleration * delta) {
				currentSpeed = targetSpeed;
			}
			else {
				currentSpeed -= deceleration * delta;
			}
		}
	}

	public void updateFireMissiles() {
		List<Tuple<Class, DeployedUnit>> toBeRemoved = new List<Tuple<Class, DeployedUnit>>();

		foreach (Tuple<Class, DeployedUnit> tuple in toBeFired) {
			if (GameController.fromID(tuple._2.getID()) == null || modules.tryToFire(tuple._1, tuple._2)) { //if fired or target has been destroyed
				toBeRemoved.Add(tuple);
			}
		}

		foreach (Tuple<Class, DeployedUnit> tuple in toBeRemoved) {
			toBeFired.Remove(tuple);
		}
	}

	public void updateLaunches() {
		//Get units to be prepared
		List<Tuple<Class, Loadout>> toBeRemoved = new List<Tuple<Class, Loadout>>();

		foreach (Tuple<Class, Loadout> tuple in toBeLaunched) {
			StoredUnit stored = modules.prepareLoading(tuple._1);
			if (stored != null) {
				toBeRemoved.Add(tuple);

				//assume the unit does not have any missile yet for now

				LoadoutData data = new LoadoutData(tuple._2);
				beingPrepared.Add(new Tuple<StoredUnit, LoadoutData>(stored, data));
			}
		}

		foreach (Tuple<Class, Loadout> tuple in toBeRemoved) {
			toBeLaunched.Remove(tuple);
		}

		//Load missiles into preparing units
		List<Tuple<StoredUnit, LoadoutData>> toBeRemoved2 = new List<Tuple<StoredUnit, LoadoutData>>();
		foreach (Tuple<StoredUnit, LoadoutData> tuple in beingPrepared) {
			if (tuple._2.isEmpty()) { //nothing else to load
				if (!(tuple._1.getStore() is LauncherModuleData)) {
					addReadyToLaunch(tuple._1);
				}

				toBeRemoved2.Add(tuple);
				readyForLaunch.Add(tuple._1);
			}
			else {
				
				foreach(KeyValuePair<Class, int> pair in tuple._2.getAmmo()) {
					if (pair.Value > 0) {
						int loaded = modules.loadInto(tuple._1, pair.Key, pair.Value);
						tuple._2.removeAmmo(pair.Key, loaded);
					}
				}
			}
		}

		foreach(Tuple<StoredUnit, LoadoutData> tuple in toBeRemoved2) {
			beingPrepared.Remove(tuple);
		}


		//Launch units
		List<StoredUnit> toBeRemoved3 = new List<StoredUnit>();
		foreach(StoredUnit unit in readyForLaunch) {
			if (modules.tryToLaunch(unit)) {
				toBeRemoved3.Add(unit);
			}
		}

		foreach(StoredUnit unit in toBeRemoved3) {
			readyForLaunch.Remove(unit);
		}
	}

	public void checkPathOutdated() {
		if (path == null) {
			return;
		}

		if (path.isOutdated()) {
			Requests.updatePath(this);
		}
	}

	//all verification relating to missiles
	public void checkMissile(float delta) {
		if (getClass().getClassType() != ClassType.Missile) {
			return;
		}

		//check if target still exists
		if (target == null) {
			Requests.destroyUnit(this);
			NetworkManager.addToLog("A " + getClass().getName() + " lost its target and crashed");
			return;
		}

		//check if arrived at target
		if ((position - target.position).norm() < currentSpeed * delta * Constants.TOLERANCE_DISTANCE_FACTOR) {
			//missile hits target
			Requests.destroyUnit(this);
			target.addDamage(modules.getDamage());

			NetworkManager.addToLog("A " + getClass().getName() + " hit a " + target.getClass().getName());
			return;
		}

		if (Random.value <= modules.getFailProbability(delta)) {
			//missile fails
			Requests.destroyUnit(this);

			NetworkManager.addToLog("A " + getClass().getName() + " has failed and crashed");
			return;
		}
	}

	public void checkLanding(float delta) {
		if (!isLanding) {
			return;
		}

		//no need to check if target is still valid, this is taken care of in GameController.Update()

		//check if arrived at target
		if ((position - target.position).norm() < currentSpeed * delta * Constants.TOLERANCE_DISTANCE_FACTOR) {
			//unit arrived at target
			Requests.storeUnit(this);
			target.addReadyToLaunch(GameController.storedFromID(this.getID()));

			NetworkManager.addToLog("A " + getClass().getName() + " has landed on a " + target.getClass().getName());
		}
	}

	public void checkAutoDef(float delta) {
		if (getClass().getClassType() != ClassType.Ship) { //Maybe add other types after
			return;
		}

		modules.autoDef(delta);
	}

	public void checkHull() {
		if (modules.isHullDead()) {
			Requests.destroyUnit(this);
			NetworkManager.addToLog("A " + getClass().getName() + " has been destroyed");
		}
	}

	public void addDamage(float amount) {
		modules.addDamage(amount);
	}

	//detection
	public Visibility getNewVisibility(Country country) {
		return newVisibility[country];
	}

	public void setNewVisibility(Country country, Visibility newCountryVisibility) {
		newVisibility[country] = newCountryVisibility;
	}

	public void resetVisibility() {
		Country country = getClass().getCountry();

		newVisibility = new Dictionary<Country, Visibility>();
		foreach(Country other in GameController.getCountries()) {
			if (country.getRelation(other) == Relation.Player) {
				newVisibility.Add(other, Visibility.Visible);
			}
			else {
				newVisibility.Add(other, Visibility.Invisible);
			}
		}
	}

	public void detectUnits() {
		modules.detectUnits();
	}

	public void switchVisibility() {
		visibility = newVisibility;
	}

	public Visibility getVisibility(Country country) {
		return visibility[country];
	}


	/*public bool isStacked() {
		return stackedParent != null;
	}

	public bool hasStackedChildren() {
		return stackedChildren.count() > 0;
	}

	public DeployedUnit getStackedParent() {
		return stackedParent;
	}

	public void setStackedParent(DeployedUnit newStackedParent) {
		

		if (stackedParent != null) {
			stackedParent.stackedChildren.remove(this);
		}

		if (newStackedParent != null) {

			newStackedParent.stackedChildren.add(this);

			foreach(DeployedUnit stackedChild in stackedChildren) {
				if (newStackedParent != stackedChild) {

					stackedChild.stackedParent = newStackedParent;
					newStackedParent.stackedChildren.add(stackedChild);
				}
			}

			stackedChildren.clear();
		}

		stackedParent = newStackedParent;
	}*/

	public void setMouseOver(bool val) {
		isMouseOver = val;
	}

	//Targeted units
	public bool hasAlreadyBeenTargeted(DeployedUnit missileTarget) {
		return alreadyTargeted.contains(missileTarget);
	}

	public void setTargeted(DeployedUnit missileTarget) {
		alreadyTargeted.add(missileTarget);
	}
}
