package star;

import java.util.Arrays;
import java.util.HashMap;

public class Weapon implements Cloneable {
	
	/*--------------------------------------------------------------*/
	/*----------------        Initialization        ----------------*/
	/*--------------------------------------------------------------*/
	
	public static Weapon makeWeapon(String s){
		s=s.replace('_', ' ');
		Weapon w=map.get(s.toLowerCase());
		if(w!=null){
			return w.copy();
		}
		w=new Weapon(s);
		assert(!map.containsKey(w.name)) : "Key collision: "+w.name;
		assert(!map.containsKey(w.name.toLowerCase())) : "Key collision: "+w.name+"\n"+s;
		assert(w.name2==null || !map.containsKey(w.name2)) : "Key collision: "+w.name2;
		assert(w.name2==null || !map.containsKey(w.name2.toLowerCase())) : "Key collision: "+w.name2;
		map.put(w.name.toLowerCase(), w);
		map.put(w.name2.toLowerCase(), w);
		return w;
	}
	
	private Weapon(String s){
		this(s.split("[;,]"));//should allow semi or comma delimiters
	}
	
	private Weapon(String[] args) {
		for(int i=0; i<args.length; i++){
			String arg=args[i];
			String[] split=arg.split("=");
			String a=split[0].toLowerCase();
			String b=split.length>1 ? split[1] : null;
			if(b!=null && b.equalsIgnoreCase("null")){b=null;}

			if(a.equals("name")){
				name=b.replace('_', ' ');
			}else if(a.equals("name2")){
				name2=b.replace('_', ' ');
			}else if(a.equals("type")){
				type=find(b.toLowerCase(), types);
			}else if(a.equals("fluxtype")){
				fluxType=find(b.toLowerCase(), fluxTypes);
			}else if(a.equals("damagetype")){
				damageType=find(b.toLowerCase(), damageTypes);
			}else if(a.equals("damage")){
				damage=Integer.parseInt(b);
			}else if(a.equals("fluxcost")){
				fluxCostPerBurstNominal=Integer.parseInt(b);
			}else if(a.equals("damage")){
				damage=Integer.parseInt(b);
			}else if(a.equals("empdamage")){
				empDamage=Float.parseFloat(b);
			}else if(a.equals("burst")){
				burst=Integer.parseInt(b);
				assert(burst>0);
			}else if(a.equals("bursttime") || a.equals("burstduration")){
				burstDuration=Float.parseFloat(b);
			}else if(a.equals("reload") || a.equals("reloadtime")){
				reloadTime=Float.parseFloat(b);
			}else if(a.equals("ammo") || a.equals("maxammo")){
				maxAmmo=Integer.parseInt(b);
			}else if(a.equals("regen") || a.equals("ammoregen")){
				ammoRegen=Float.parseFloat(b);
			}else if(a.equals("submunitions")){
				submunitions=Integer.parseInt(b);
			}else{
				assert(false) : "Unknown parameter "+args[i];
			}
		}
		
//		secondsPerBolt=burstDuration/burst;
		shieldDamagePerBolt=(type==BOLT ? damage : damage*0.01f)*shieldDamageMult();
		maxArmorDamagePerBolt=(type==BOLT ? damage : damage*0.01f)*armorDamageMult();
		maxHullDamagePerBolt=(type==BOLT ? damage : damage*0.01f);
		empDamagePerBolt=(type==BOLT ? empDamage : empDamage*0.01f);
		hardFluxDamagePerBolt=shieldDamagePerBolt*(fluxType==HARD ? 1 : 0);
		softFluxDamagePerBolt=shieldDamagePerBolt*(fluxType==SOFT ? 1 : 0);
		fluxCostPerBurstActual=(type==BOLT ? fluxCostPerBurstNominal : fluxCostPerBurstNominal*0.01f);
		
		damageForCalculation=(type==BOLT ? maxArmorDamagePerBolt : damage*0.50f*armorDamageMult());
		
		centiPerReload=Math.round(reloadTime*100);
		centiPerBurst=Math.round(burstDuration*100);
		centiPerRound=Math.round(burstDuration*100/burst);
//		assert(false) : "burstDuration="+burstDuration+",centiPerReload="+centiPerReload+",centiPerBurst="+centiPerBurst+",centiPerRound="+centiPerRound;
		regenPerCenti=ammoRegen*0.01f;
		
		reset();
		
		assert(reloadTime>0);
		assert(name!=null);
	}
	
	public Weapon copy(){
		try {
			Weapon w=(Weapon)this.clone();
			w.reset();
			return w;
		} catch (CloneNotSupportedException e) {
			throw new RuntimeException(e);
		}
	}
	
	/*--------------------------------------------------------------*/
	/*----------------       Outer Functions        ----------------*/
	/*--------------------------------------------------------------*/
	
	public boolean validate(){
		assert(type>=0 && type<types.length);
		assert(fluxType>=0 && fluxType<fluxTypes.length);
		assert(damageType>=0 && damageType<damageTypes.length);

		assert(fluxCostPerBurstNominal>=0);
		assert(damage>=0); //TODO: or EMP
		assert(damage>0); //for now
		assert(burst>=1 && burst<=999); //At least one shot per burst
		assert(burstDuration>=0 && burstDuration<99); //can be zero
		assert(reloadTime>0); //Must be a minimum of 0.01s
		assert(maxAmmo>=0); //non-negative; 0 means unlimited
		assert(ammoRegen>=0 && ammoRegen<999); //nonnegative and not crazy
		assert(submunitions>=1);
		assert(shieldDamagePerBolt>0); //true in vanilla
		assert(maxArmorDamagePerBolt>0); //true in vanilla
		assert(maxHullDamagePerBolt>0); //true in vanilla
		assert(hardFluxDamagePerBolt+softFluxDamagePerBolt>0); //true in vanilla
		assert(centiPerBurst>=0);
		assert(centiPerReload>0);
		assert(centiPerRound>0 || burstDuration==0);
//		assert(burst*centiPerRound==centiPerBurst);
		assert(roundsThisBurst<=burst);
		return true;
	}
	
	//Reinitialize mutables
	public void reset(){
		ammo=maxAmmo;
		timeToNextBurst=0;
		timeInBurst=0;
		timeToNextRound=0;
		roundsThisBurst=0;
		inBurst=false;
		assert(validate());
	}
	
	/**
	 * Use a weapon on the source ship against the target ship.
	 * This does everything necessary for the current time cycle.
	 * @param a Ship holding the weapon
	 * @param b Target ship
	 */
	void use(Ship a, Ship b){
		assert(validate());
		timeToNextBurst--;
		if(timeToNextBurst<=0 && (maxAmmo<=0 || ammo>=burst)){
			assert(timeInBurst==0);
			startBurst(a, b);
		}
		if(inBurst){
			continueBurst(a);
		}
		if(inBurst && roundsThisBurst>=burst){
			//This assertion ia not needed and causes problems when bursts don't have good integer divisors
//			assert(timeInBurst>=centiPerBurst) : timeInBurst+", "+centiPerBurst+", "+roundsThisBurst+", "+burst+"\n"+this+"\n";
			endBurst(a);
		}
		ammo=min(ammo+regenPerCenti, maxAmmo);
	}
	
	public String toString(){
		StringBuilder sb=new StringBuilder();
		sb.append(name2==null || useFullName ? name : name2).append('\t');
		sb.append("ammo="+ammo).append(' ');
		sb.append("ttNextBurst="+timeToNextBurst).append(' ');
		sb.append("tiBurst="+timeInBurst).append(' ');
		sb.append("ttNextRound="+timeToNextRound).append(' ');
		sb.append("damage="+damage).append(' ');
		sb.append("flux="+fluxCostPerBurstNominal);
		return sb.toString();
	}
	
	/*--------------------------------------------------------------*/
	/*----------------       Inner Functions        ----------------*/
	/*--------------------------------------------------------------*/
	
	/**
	 * Start a burst from ship a to b.
	 * It has already been determined that the weapon is ready to start firing and there is flux available.
	 * @param source Ship holding the weapon
	 * @param target Target ship
	 */
	private void startBurst(Ship a, Ship b){
		assert(timeInBurst==0);
		if(a.flux()+fluxCostPerBurstActual>a.maxFlux){return;}
//		assert(a.flux()+fluxCost<a.maxFlux) : this+"\n"+a;
		target=b;
		assert(timeToNextBurst<=0);
		assert(!inBurst);
		a.softFlux+=fluxCostPerBurstActual;
		timeInBurst=0;
		inBurst=true;
		//timeToNextBurst=centiPerBurst+centiPerReload;
		timeToNextBurst=centiPerReload;
	}
	
	private void continueBurst(Ship a){
		assert(inBurst);
		if(timeToNextRound<=0){
			fireBolt();
			timeToNextRound=centiPerRound;
		}else{
			timeToNextRound--;
		}
		timeInBurst++;
	}
	
	private void endBurst(Ship a){
		assert(inBurst);
		assert(roundsThisBurst==burst) : roundsThisBurst+", "+burst+", "+this;
		timeInBurst=0;
		timeToNextRound=0;
		roundsThisBurst=0;
		inBurst=false;
	}
	
	private void fireBolt(){
		if(maxAmmo>0){
			if(ammo<1){return;}
			ammo--;
		}
		roundsThisBurst++;
		if(verbose || printShots){
			Simulate.shotsThisTurn++;
			System.out.println("\nFiring "+name2+" at "+target.shipName);
			System.out.println("Before: "+this);
			System.out.println(target.shipName+": soft="+target.softFlux+", hard="+target.hardFlux+
					", armor="+target.armor+", hull="+target.hull+", shieldsUp="+target.shieldsUp);
		}
		final float shieldDamageEst=estimateShieldDamage();
		final float shieldRemainingEst=Math.round(target.maxFlux-(target.flux()+shieldDamageEst));
		
		final float armorDamageEst=estimateArmorDamage();
		final float armorRemainingEst=Math.round(target.armor-armorDamageEst);
		
		final float hullDamageEst=estimateHullDamage();
		final float hullRemainingEst=target.hull-hullDamageEst;
		
		if(target.shieldsUp && shieldRemainingEst<=0){//Prevent overload
			target.shieldsUp=false;
			if(target.canVent){target.venting=true;}
		}else if(target.hasShield && !target.shieldsUp && !target.venting && 
				shieldRemainingEst>0 && target.hull>0){//Can raise shields
				if(hullRemainingEst<=0){//Prevent ship destruction
					target.shieldsUp=true;
				}else if(armorRemainingEst<=0 && target.armor>0 && damageType==HE && preventArmorDestuction){
					target.shieldsUp=true;
				}else if(maxHullDamagePerBolt>=500 && damageType!=KINETIC && preventTorpedoes){
					target.shieldsUp=true;
				}
		}
		if(target.shieldsUp){
			hitShield();
		}else{
			hitArmor();
		}

		if(verbose || printShots){
			System.out.println("After:  "+this);
			System.out.println(target.shipName+": soft="+target.softFlux+", hard="+target.hardFlux+
					", armor="+target.armor+", hull="+target.hull+", shieldsUp="+target.shieldsUp);
		}
	}
	
	private void hitShield(){
		assert(target.shieldsUp);
		target.softFlux+=softFluxDamagePerBolt*target.fluxPerDamage;
		target.hardFlux+=hardFluxDamagePerBolt*target.fluxPerDamage;
		assert(target.flux()<target.maxFlux);
	}
	
	private void hitArmor(){
		if(empDamage>0){
			applyEmpDamage();
		}
		float armorForCalculation=max(target.armor+target.bonusArmor, target.strippedArmorFraction*target.maxArmor);
		float armorModifier=damageForCalculation/(damageForCalculation+armorForCalculation);
		armorModifier=max(target.minArmorModifier, armorModifier);
		float residualFraction=1f; //Amount of hull damage remaining after armor damage is dealt
		if(target.armor>0){//Reduce armor
			float modArmorDamage=maxArmorDamagePerBolt*armorModifier;
			float actualArmorDamage=min(modArmorDamage, target.armor);
			target.armor-=actualArmorDamage;
			residualFraction=(modArmorDamage-actualArmorDamage)/modArmorDamage;
		}
		float actualHullDamage=maxHullDamagePerBolt*residualFraction*armorModifier;
		target.hull-=actualHullDamage;
	}
	
	private void applyEmpDamage(){
		target.empDamageTaken+=empDamagePerBolt;
	}
	
	private float estimateShieldDamage(){
		return (softFluxDamagePerBolt+hardFluxDamagePerBolt)*target.fluxPerDamage;
	}
	
	private float estimateHullDamage(){
		float armorForCalculation=max(target.armor+target.bonusArmor, target.strippedArmorFraction*target.maxArmor);
		float armorModifier=damageForCalculation/(damageForCalculation+armorForCalculation);
		armorModifier=max(target.minArmorModifier, armorModifier);
		float residualFraction=1f; //Amount of hull damage remaining after armor damage is dealt
		if(target.armor>0){//Reduce armor
			float modArmorDamage=maxArmorDamagePerBolt*armorModifier;
			float actualArmorDamage=min(modArmorDamage, target.armor);
			//target.armor-=actualArmorDamage;
			residualFraction=(modArmorDamage-actualArmorDamage)/modArmorDamage;
		}
		float actualHullDamage=maxHullDamagePerBolt*residualFraction*armorModifier;
		return actualHullDamage;
	}
	
	private float estimateArmorDamage(){
		if(target.armor<=0){return 0;}
		float armorForCalculation=max(target.armor+target.bonusArmor, target.strippedArmorFraction*target.maxArmor);
		float armorModifier=damageForCalculation/(damageForCalculation+armorForCalculation);
		armorModifier=max(target.minArmorModifier, armorModifier);
		float modArmorDamage=maxArmorDamagePerBolt*armorModifier;
		float actualArmorDamage=(float)Math.ceil(min(modArmorDamage, target.armor));
		return actualArmorDamage;
	}
	
	/*--------------------------------------------------------------*/
	/*----------------           Helpers            ----------------*/
	/*--------------------------------------------------------------*/

	final float armorDamageMult(){
		return damageType==KINETIC ? 0.5f : damageType==HE ? 2f : 
			damageType==FRAG ? 0.25f : damageType==ENERGY ? 1 : 1;
	}
	final float shieldDamageMult(){
		return damageType==KINETIC ? 2 : damageType==HE ? 0.5f : 
			damageType==FRAG ? 0.25f : damageType==ENERGY ? 1 : 1;
	}
	
	/**
	 * Find a String in an array.
	 * @param a String to find.
	 * @param array Array of Strings.
	 * @return Index of element, or -1 if not found.
	 */
	public static final int find(String a, String[] array){
		for(int i=0; i<array.length; i++){
			if(a.equals(array[i])){return i;}
		}
		assert(false) : "Can't find "+a+" in "+Arrays.toString(array);
		return -1;
	}
	
	/**
	 * Parse this argument.  More liberal than Boolean.parseBoolean.
	 * Null, t, true, or 1 all yield true.
	 * Everything else, including the String "null", is false.
	 * @param s Argument to parse
	 * @return boolean form
	 */
	public static boolean parseBoolean(String s){
		if(s==null || s.length()<1){return true;}
		if(s.length()==1){
			char c=Character.toLowerCase(s.charAt(0));
			return c=='t' || c=='1';
		}
		if(s.equalsIgnoreCase("null") || s.equalsIgnoreCase("none")){return false;}
		return Boolean.parseBoolean(s);
	}
	
	public static final int min(int x, int y){return x<y ? x : y;}
	public static final int max(int x, int y){return x>y ? x : y;}
	public static final float min(float x, float y){return x<y ? x : y;}
	public static final float max(float x, float y){return x>y ? x : y;}
	public static final int roundUp(float x){return (int)Math.ceil(x);}
	
	/*--------------------------------------------------------------*/
	/*----------------           Fields             ----------------*/
	/*--------------------------------------------------------------*/
	
	/*--------------------------------------------------------------*/
	/*----------------     Initialized Fields       ----------------*/
	/*--------------------------------------------------------------*/

	String name;
	String name2;
	
	int type=BOLT; //bolt or beam
	int fluxType=HARD; //hard or soft
	int damageType=KINETIC; //kinetic, HE, frag, energy
	int fluxCostPerBurstNominal=-1;
	final float fluxCostPerBurstActual;//For beams this would be per centi
	int damage=-1; //per shot; for beams this would be dps
	float empDamage=0; //per shot; for beams this would be emp dps
	int burst=1; //shots per burst
	float burstDuration; //time per burst, in seconds
	float reloadTime=-1; //in seconds
	int maxAmmo=-1;
	float ammoRegen=0; //ammo added per second
	
	int rawBurstTime=0; //in 1/100s
	int rawReloadTime=0; //in 1/100s
	int rawFluxCost=-1; //per burst; for beams this would be per second
	
	int submunitions=1; //for MIRV / splatter / shotguns / etc
	
	
	
	/*--------------------------------------------------------------*/
	/*----------------        Derived Fields        ----------------*/
	/*--------------------------------------------------------------*/
	
//	final float secondsPerBolt; //seconds per bolt (i.e. bullet) while firing; derived.
	final float shieldDamagePerBolt;
	final float maxArmorDamagePerBolt;
	final float maxHullDamagePerBolt;
	final float hardFluxDamagePerBolt;
	final float softFluxDamagePerBolt;
	final float damageForCalculation;
	final float empDamagePerBolt;
	final int centiPerBurst; //in 1/100 second
	final int centiPerReload;
	final int centiPerRound;
	final float regenPerCenti;
	
	/*--------------------------------------------------------------*/
	/*----------------       Mutable Fields         ----------------*/
	/*--------------------------------------------------------------*/
	
	float ammo;
	int timeToNextBurst=0; //in centis
	int timeInBurst=0; //in centis
	int timeToNextRound=0;
	int roundsThisBurst=0;
	boolean inBurst=false;
	Ship target;
	
	/*--------------------------------------------------------------*/
	/*----------------          Constants           ----------------*/
	/*--------------------------------------------------------------*/
	
	public static final int BOLT=0, BEAM=1;
	public static final int HARD=0, SOFT=1;
	public static final int KINETIC=0, HE=1, FRAG=2, ENERGY=3;

	public static final String[] types={"bolt", "beam"};
	public static final String[] fluxTypes={"hard", "soft"};
	public static final String[] damageTypes={"kinetic", "he", "frag", "energy"};
	
	static final HashMap<String, Weapon> map=new HashMap<String, Weapon>();

	static boolean verbose=false;
	static boolean printShots=false;
	static boolean useFullName=false;
	static boolean preventArmorDestuction=false;
	static boolean preventTorpedoes=false;
	
}
