using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Assertions;

public class UnitModules {
	private int parent;
	private List<Tuple<Module, ModuleData>> modules;

	public UnitModules(int parent, Class unitClass, bool constructModules = true) {
		Assert.IsNotNull(unitClass);
		this.parent = parent;

		modules = new List<Tuple<Module, ModuleData>>();

		if (constructModules) {
			foreach(ModulePlacement modulePlacement in unitClass.getModulePlacements()) {
				ModuleData data = modulePlacement.module.getData(parent, modulePlacement.angleOffset, modulePlacement.position);

				if (modulePlacement.twin != -1) {
					modules[modulePlacement.twin]._2.addHiddenTwin(data);
				}

				modules.Add(new Tuple<Module, ModuleData>(modulePlacement.module, data));
			}
		}
	}

	//SHOULD ONLY BE CALLED BY NETWORK MANAGER, WHEN RECEIVING A CREATE MODULE DATA COMMAND
	public void addModuleData(ModuleData data) {
		modules.Add(new Tuple<Module, ModuleData>(data.getTemplate(), data));
	}

	public List<Tuple<Module, ModuleData>> getModules() {
		return modules;
	}

	public void addReadyToLaunch(StoredUnit stored) {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is LauncherModule) {
				Requests.setStore(stored, (LauncherModuleData)module._2);
				return;
			}
		}
	}

	public void store(StoredUnit stored) {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is LauncherModule || module._1 is StorageModule) {
				Requests.setStore(stored, (ContainerModuleData)module._2);
				return;
			}
		}
	}

	public void addToStorage(StoredUnit stored) {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is StorageModule) {
				Requests.setStore(stored, (StorageModuleData)module._2);
				return;
			}
		}
	}

	/*public int getNumberStored(Class unitClass, bool immediate = false) {
		int total = 0;

		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._2 is LauncherModuleData && module._2.isOperationnal()) {
				LauncherModuleData launcherData = (LauncherModuleData)module._2;

				total += launcherData.getNumberReadyToLaunch(unitClass);
			}
			else if (!immediate && module._2 is StorageModuleData && module._2.isOperationnal()) {
				StorageModuleData storageData = (StorageModuleData)module._2;

				total += storageData.getNumberStored(unitClass);
			}
		}

		return total;
	}*/

	public int getNumberReadyToLaunch(Class unitClass) {
		int total = 0;

		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._2 is LauncherModuleData && module._2.isOperationnal()) {
				LauncherModuleData launcherData = (LauncherModuleData)module._2;

				total += launcherData.getNumberIdleStored(unitClass);
			}
		}

		return total;
	}

	public int getNumberIdleStored(Class unitClass) {
		int total = 0;

		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._2 is ContainerModuleData && module._2.isOperationnal()) {
				ContainerModuleData container = (ContainerModuleData)module._2;

				total += container.getNumberIdleStored(unitClass);
			}
		}

		return total;
	}

	/*public bool canLaunch(Class unitClass, int number, Loadout loadout = null) {
		
		if (getNumberStored(unitClass, false) < number) {
			return false; //not enough vehicle to launch
		}

		if (loadout == null) {
			return true;
		}

		//Check loadout ammo
		foreach(KeyValuePair<Class, int> pair in loadout.getAmmo()) {
			if (getNumberStored(pair.Key) < pair.Value * number) {
				return false; //not enough ammo for each vehicle
			}
		}

		return true;
	}*/

	public bool canBeLandedOn(DeployedUnit unit) {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._2 is LauncherModuleData && module._2.isOperationnal()) {
				LauncherModule launcher = (LauncherModule)module._1;

				if (launcher.canLaunch(unit.getClass())) {
					return true;
				}
			}
		}

		return false;
	}

	public bool isFiring(Class unitClass) {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._2 is LauncherModuleData && module._2.isOperationnal() && ((LauncherModuleData)module._2).isFiring(unitClass)) {
				return true;
			}
		}

		return false;
	}

	public StoredUnit getIdleReadyToLaunch(Class unitClass) {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._2 is LauncherModuleData && module._2.isOperationnal()) {
				StoredUnit loadingUnit = ((LauncherModuleData)module._2).getIdleStored(unitClass);
				if (loadingUnit != null) {
					return loadingUnit;
				}
			}
		}

		return null;
	}

	public StoredUnit getIdleStored(Class unitClass) {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._2 is ContainerModuleData && module._2.isOperationnal()) {
				StoredUnit loadingUnit = ((ContainerModuleData)module._2).getIdleStored(unitClass);
				if (loadingUnit != null) {
					return loadingUnit;
				}
			}
		}

		return null;
	}

	public LauncherModuleData getLauncherModule(Class toBeLaunched) {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._2 is LauncherModuleData && module._2.isOperationnal() && ((LauncherModule)module._1).canLaunch(toBeLaunched)) {
				return (LauncherModuleData)module._2;
			}
		}

		return null;
	}

	public ContainerModuleData getContainerModule(Class toBeStored) {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._2 is ContainerModuleData && module._2.isOperationnal() && ((ContainerModule)module._1).canStore(toBeStored)) {
				return (ContainerModuleData)module._2;
			}
		}

		return null;
	}

	public float getMaxSpeed() {
		int num = 0;
		float currentMaxSpeed = 0.0f;

		int totalPropellers = 0;
		int functionningPropellers = 0;
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is EngineModule && module._2.isOperationnal()) {
				if (getFuelAmount(((EngineModule)module._1).getFuelType()) > 0.0f) {
					num ++;
					currentMaxSpeed += ((EngineModule)module._1).getMaxSpeed() / num;
				}
			}
			else if (module._1 is PropellerModule) {
				if (module._2.isOperationnal()) {
					functionningPropellers++;
				}
				totalPropellers ++;
			}
		}

		if (GameController.unitFromID(parent).getClass().getClassType() == ClassType.Ship) {
			if (totalPropellers == 0) {
				currentMaxSpeed = 0.0f;
			}
			else {
				currentMaxSpeed *= Mathf.Sqrt(functionningPropellers / totalPropellers);
			}
		}

		return currentMaxSpeed;
	}

	public float getMinSpeed() {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is EngineModule && module._2.isOperationnal()) {
				if (getFuelAmount(((EngineModule)module._1).getFuelType()) > 0.0f) { //maybe change later
					return ((EngineModule)module._1).getMinSpeed();
				}
			}
		}

		return 0.0f;
	}

	public float getAcceleration() {
		if (!canBeSteered()) {
			return 0.0f;
		}

		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is EngineModule && module._2.isOperationnal()) {
				if (getFuelAmount(((EngineModule)module._1).getFuelType()) > 0.0f) {
					return ((EngineModule)module._1).getAcceleration();
				}
			}
		}

		return 0.0f;
	}

	public float getDeceleration() {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is EngineModule) {
				return ((EngineModule)module._1).getAcceleration();
			}
		}

		return 0.0f;
	}

	public float getAngularSpeed() {
		if (!canBeSteered()) {
			return 0.0f;
		}


		float angularSpeed = 0.0f;
		int totalPropellers = 0;
		int functionningPropellers = 0;
		int functionningRudders = 0;
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is EngineModule && module._2.isOperationnal()) {
				if (getFuelAmount(((EngineModule)module._1).getFuelType()) > 0.0f) {
					angularSpeed = ((EngineModule)module._1).getAngularSpeed();
				}
			}
			else if (module._1 is PropellerModule) {
				if (module._2.isOperationnal()) {
					functionningPropellers++;
				}
				totalPropellers ++;
			}
			else if (module._1 is RudderModule && module._2.isOperationnal()) {
				functionningRudders ++;
			}
		}

		if (GameController.unitFromID(parent).getClass().getClassType() == ClassType.Ship) {
			if (totalPropellers == 0 || functionningRudders == 0) {
				return 0.0f;
			}
			else {
				return angularSpeed * functionningPropellers / totalPropellers;
			}
		}

		return angularSpeed;
	}

	public float getVerticalAngleRange() {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is EngineModule && module._2.isOperationnal()) {
				if (getFuelAmount(((EngineModule)module._1).getFuelType()) > 0.0f) {
					return ((EngineModule)module._1).getVerticalAngleRange();
				}
			}
		}

		return 0.0f;
	}

	public float getCruiseAltitude() {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is EngineModule && module._2.isOperationnal()) {
				if (getFuelAmount(((EngineModule)module._1).getFuelType()) > 0.0f) {
					return ((EngineModule)module._1).getCruiseAltitude();
				}
			}
		}

		return 0.0f;
	}

	public float getMinAltitude() {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is EngineModule && module._2.isOperationnal()) {
				if (getFuelAmount(((EngineModule)module._1).getFuelType()) > 0.0f) {
					return ((EngineModule)module._1).getMinAltitude();
				}
			}
		}

		return 0.0f;
	}

	public float getMaxAltitude() {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is EngineModule && module._2.isOperationnal()) {
				if (getFuelAmount(((EngineModule)module._1).getFuelType()) > 0.0f) {
					return ((EngineModule)module._1).getMaxAltitude();
				}
			}
		}

		return 0.0f;
	}

	public MissileTechnique getMissileTechnique() {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is GuidanceModule && module._2.isOperationnal()) {
				return ((GuidanceModule)module._1).getMissileTechnique();
			}
		}

		return MissileTechnique.SeaSkimming;
	}

	public List<Tuple<float, float>> getLandingPattern() {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is CommandModule && module._2.isOperationnal()) {
				return ((CommandModule)module._1).getLandingPattern();
			}
		}

		return new List<Tuple<float, float>>();
	}

	public float getFailProbability(float delta) {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is GuidanceModule && module._2.isOperationnal()) {
				return ((GuidanceModule)module._1).getFailProbability(delta);
			}
		}

		return 1.0f;
	}

	public float getDamage() {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is WarheadModule && module._2.isOperationnal()) {
				return ((WarheadModule)module._1).getDamage();
			}
		}

		return 0.0f;
	}

	public float getCIWSRange() {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is CIWSModule && module._2.isOperationnal()) {
				return ((CIWSModule)module._1).getMaxRange();
			}
		}

		return 0.0f;
	}

	public float getFuelAmount(FuelType fuelType) {
		float sum = 0.0f;

		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is FuelTankModule && module._2.isOperationnal() && ((FuelTankModule)module._1).getFuelType() == fuelType) {
				sum += ((FuelTankModuleData)module._2).getFuelLeft();
			}
		}

		return sum;
	}
		
	public void addDamage(float amount) {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (!module._2.isAHiddenTwin()) {
				module._2.addDamage(amount);
			}
		}
	}

	public bool canBeCommanded() {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is CommandModule && module._2.isOperationnal() && ((CommandModule)module._1).getCanCommand()) {
				return true;
			}
		}

		return false;
	}

	public bool canBeSteered() {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is CommandModule && module._2.isOperationnal() && ((CommandModule)module._1).getCanSteer()) {
				return true;
			}
			else if (module._1 is GuidanceModule && module._2.isOperationnal()) {
				return true;
			}
		}

		return false;
	}

	public bool isHullDead() {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is HullModule && !module._2.isOperationnal()) {
				return true;
			}
		}

		return false;
	}

	public bool hasRadarOn() {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is ActiveModule && module._2.isOperationnal()) {
				return true;
			}
		}

		return false;
	}

	public int availableMissileControlSlots() {
		int total = 0;

		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is MissileControlModule && module._2.isOperationnal()) {
				total += ((MissileControlModuleData)module._2).getRemainingSlots();
			}
		}

		return total;
	}

	public void addControlledMissile(Unit missile) {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is MissileControlModule && module._2.isOperationnal()) {
				
				MissileControlModuleData control = ((MissileControlModuleData)module._2);
				if (control.getRemainingSlots() > 0) {
					Requests.addToMissileControl(control, missile);
					return;
				}
			}
		}
	}

	public bool hasAvailableIlluminatingSlots(DeployedUnit target) {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is IlluminatorModule && module._2.isOperationnal()) {
				if (((IlluminatorModuleData)module._2).getRemainingSlots() > 0 && ((IlluminatorModuleData)module._2).isInRangeAndAngle(target)) {
					return true;
				}
			}
		}

		return false;
	}

	public void illuminateTarget(DeployedUnit target) {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is IlluminatorModule && module._2.isOperationnal()) {
				if (((IlluminatorModuleData)module._2).getRemainingSlots() > 0 && ((IlluminatorModuleData)module._2).isInRangeAndAngle(target)) {
					Requests.addToIlluminated((IlluminatorModuleData)module._2, target);
					return;
				}
			}
		}
	}

	public bool hasFuelLeft() {
		Set<FuelType> fuelTypes = new Set<FuelType>();
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is EngineModule && module._2.isOperationnal()) {
				fuelTypes.add(((EngineModule)module._1).getFuelType());
			}
		}

		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is FuelTankModule && module._2.isOperationnal()) {
				if (((FuelTankModuleData)module._2).getFuelLeft() > 0 && fuelTypes.contains(((FuelTankModule)module._1).getFuelType())) {
					return true;
				}
			}
		}

		return false;
	}

	public FuelType getMainFuelType() {
		Dictionary<FuelType, int> dic = new Dictionary<FuelType, int>();

		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is EngineModule && module._2.isOperationnal()) {
				EngineModule engine = (EngineModule)module._1;

				if (dic.ContainsKey(engine.getFuelType())) {
					dic[engine.getFuelType()] ++;
				}
				else {
					dic[engine.getFuelType()] = 1;
				}
			}
		}

		int max = 0;
		FuelType type = FuelType.None;
		foreach(KeyValuePair<FuelType, int> pair in dic) {
			if (pair.Value > max) {
				max = pair.Value;
				type = pair.Key;
			}
		}

		return type;
	}

	public float getFuelLeft(FuelType fuelType) {
		float total = 0.0f;

		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is FuelTankModule && module._2.isOperationnal() && ((FuelTankModule)module._1).getFuelType() == fuelType) {
				total += ((FuelTankModuleData)module._2).getFuelLeft();
			}
		}

		return total;
	}

	public float getConsumption(FuelType fuelType, float currentSpeed) {
		float maxSpeed = getMaxSpeed();
		float ratio = currentSpeed / maxSpeed;

		float total = 0.0f;

		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is EngineModule && module._2.isOperationnal()) {
				total += ((EngineModule)module._1).getFuelConsumption(ratio);
			}
		}

		return total;
	}

	public void consumeFuel(float delta, float currentSpeed) {
		float maxSpeed = getMaxSpeed();
		float ratio = currentSpeed / maxSpeed;

		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is EngineModule && module._2.isOperationnal()) {
				EngineModule engine = (EngineModule)module._1;
				removeFuel(engine.getFuelType(), engine.getFuelConsumption(ratio) * delta);
			}
		}
	}

	private void removeFuel(FuelType fuelType, float amount) {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is FuelTankModule && module._2.isOperationnal() && ((FuelTankModule)module._1).getFuelType() == fuelType) {
				float leftInTank = ((FuelTankModuleData)module._2).getFuelLeft();

				if (leftInTank < amount) {
					amount -= leftInTank;
					((FuelTankModuleData)module._2).setFuelLeft(0.0f);
				}
				else {
					((FuelTankModuleData)module._2).setFuelLeft(leftInTank - amount);
					return;
				}
			}
		}
	}

	public void setEMCON(bool value) {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is ActiveModule) {
				module._2.setDisabled(!value);
			}
		}
	}

	public void detectUnits() {
		//When detect units is called, then the unit is deployed
		DeployedUnit parentUnit = GameController.fromID(parent);

		foreach(Tuple<Module, ModuleData> module in modules) {
			if ((module._1 is DetectionModule) && module._2.isOperationnal()) {

				foreach (DeployedUnit unit in GameController.getDeployedUnits()) {
					if (((DetectionModule)module._1).canDetect(unit.getClass().getClassType()) && unit.getNewVisibility(parentUnit.getClass().getCountry()) != Visibility.Visible) { //useless to detect if already visible
						Vertex position = parentUnit.getPosition();
						GameVertex gamePosition = Converter.metersToGameCoo(position);

						Vector3 direction = (unit.getPosition() - position).normalized().getValue();
						float distance = (unit.getPosition() - position).norm();

						DetectionModule mod = ((DetectionModule)module._1);

						if (((DetectionModuleData)module._2).isInRangeAndAngle(unit)) {
							if (!Physics.Raycast(new Vector3(gamePosition.getVector().x, gamePosition.getVector().y, position.z()), direction, Converter.metersToGameCoo(distance), (int)Mathf.Pow(2, 2))) {
								float val = 0.0f;

								if (mod is ActiveModule) {
									val = Mathf.Pow(1 - distance / mod.getMaxRange(), 0.3f) * mod.getAccuracy(); //nice formula :)
								}
								else if (mod is PasssiveModule && unit.hasRadarOn()) {
									val = Mathf.Pow(1 - distance / mod.getMaxRange(), 0.3f) * mod.getAccuracy() * 3; //nice formula :)
								}

								if (val > 0.95) {
									unit.setNewVisibility(parentUnit.getClass().getCountry(), Visibility.Visible);
								}
								else if (val > 0.9 && unit.getNewVisibility(parentUnit.getClass().getCountry()) == Visibility.Invisible) {
									unit.setNewVisibility(parentUnit.getClass().getCountry(), Visibility.Unknown);
								}
							}
						}
					}
				}
			}
		}
	}

	public void autoDef(float delta) {
		//When autoDef is called, then the unit is deployed
		DeployedUnit parentUnit = GameController.fromID(parent);

		float CIWSRange = getCIWSRange();

		foreach(DeployedUnit missile in GameController.getDeployedUnits()) {
			float distance = (parentUnit.getPosition()- missile.getPosition()).norm();
			if (missile.getClass().getClassType() == ClassType.Missile 
				&& missile.getClass().getCountry().getRelation(parentUnit.getClass().getCountry()) == Relation.Ennemy 
				&& !parentUnit.hasAlreadyBeenTargeted(missile)
				&& missile.getVisibility(parentUnit.getClass().getCountry()) != Visibility.Invisible) {
				bool hasFiredMissiles = false;

				if (distance < Constants.MAX_DISTANCE_MISSILE_AUTO_DEF) { //first, try to fire missiles
					hasFiredMissiles = autoDefMissilesFire(parentUnit, missile);
				}

				if (!hasFiredMissiles && distance < CIWSRange) {
					CIWSFire(parentUnit, missile);
				}
			}
		}
	}

	public bool autoDefMissilesFire(DeployedUnit parentUnit, DeployedUnit target) {
		foreach(Class antiMissile in parentUnit.getClass().getMissileAutoDef()) {

			if (parentUnit.canFire(antiMissile, parentUnit.getAntimissileSalvo(antiMissile), target)) {
				parentUnit.fire(antiMissile, parentUnit.getAntimissileSalvo(antiMissile), target);
				parentUnit.setTargeted(target);

				return true;
			}
		}

		return false;
	}

	public void CIWSFire(DeployedUnit parentUnit, DeployedUnit target) {
		foreach(Tuple<Module, ModuleData> module in modules) {
			if (module._1 is CIWSModule && module._2.isOperationnal()) {
				CIWSModuleData data = (CIWSModuleData)module._2;
				if (data.canFire(target)) {
					data.beginFire(target);
					parentUnit.setTargeted(target);
				}
			}
		}
	}
}
