package physics.unit;

import java.util.EnumSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;

// http://en.wikipedia.org/wiki/SI_prefix
/**
 * Quantities with an numerical factor, SI prefixes and unit
 * @author Daniel Weschke
 */
public class Quantity {
	
	/**
	 * Numerical Factor
	 */
	private double value;
	
	public static enum Prefix{
		/**
		 * yotta 10<sup>24</sup>
		 */
		YOTTA(1e24, "Y", 24),
		/**
		 * zetta 10<sup>21</sup>
		 */
		ZETTA(1e21, "Z", 21),
		/**
		 * exa 10<sup>18</sup>
		 */
		EXA(1e18, "E", 18),
		/**
		 * peta 10<sup>15</sup>
		 */
		PETA(1e15, "P", 15),
		/**
		 * tera 10<sup>12</sup>
		 */
		TERA(1e12, "T", 12),
		/**
		 * giga 10<sup>9</sup>
		 */
		GIGA(1e9, "G", 9),
		/**
		 * mega 10<sup>6</sup>
		 */
		MEGA(1e6, "M", 6),
		/**
		 * kilo 10<sup>3</sup>
		 */
		KILO(1e3, "K", 3),
		/**
		 * hecto 10<sup>2</sup>
		 */
		HECTO(1e2, "h", 2),
		/**
		 * deca 10<sup>1</sup>
		 */
		DECA(1e1, "da", 1),
		/**
		 * unus 10<sup>0</sup>
		 */
		UNUS(1e0, "", 0),
		/**
		 * deci 10<sup>-1</sup>
		 */
		DECI(1e-1, "deci", -1),
		/**
		 * centi 10<sup>-2</sup>
		 */
		CENTI(1e-2, "c", -2),
		/**
		 * milli 10<sup>-3</sup>
		 */
		MILLI(1e-3, "m", -3),
		/**
		 * micro 10<sup>-6</sup>
		 */
		MICRO(1e-6, "µ", -6),
		/**
		 * nano 10<sup>-9</sup>
		 */
		NANO(1e-9, "n", -9),
		/**
		 * pico 10<sup>-12</sup>
		 */
		PICO(1e-12, "p", -12),
		/**
		 * femto 10<sup>-15</sup>
		 */
		FEMTO(1e-15, "f", -15),
		/**
		 * atto 10<sup>-18</sup>
		 */
		ATTO(1e-18, "a", -18),
		/**
		 * zepto 10<sup>-21</sup>
		 */
		ZEPTO(1e-21, "z", -21),
		/**
		 * yocto 10<sup>-24</sup>
		 */
		YOCTO(1e-24, "y", 24);
		/**
		 * Decimal factor
		 */
		private final double factor;
		private final String symbol;
		private final int orderOfMagnitude;
		private Prefix(double factor, String symbol, int orderOfMagnitude){
			this.factor = factor;
			this.symbol = symbol;
			this.orderOfMagnitude = orderOfMagnitude;
		}
		public static Prefix getInstanceFromOrderOfMagnitude(int orderOfMagnitude){
		    for (Prefix type : Prefix.values()) {
		        if(type.orderOfMagnitude == orderOfMagnitude)
		            return type;
		    }
			return null;
		}
	    public double getDecimal() {
	        return factor;
	    }
	    public String getSymbol() {
	        return symbol;
	    }
	    public int getOrderOfMagnitude() {
	        return orderOfMagnitude;
	    }
		public Prefix times(Prefix prefix) {
			return times(prefix, 0);
		}
		public Prefix times(Prefix prefix, int orderOfMagnitude) {
			int factor = getOrderOfMagnitude()+prefix.getOrderOfMagnitude()+orderOfMagnitude;
			return getInstanceFromOrderOfMagnitude(factor);
		}
	}
	private Prefix prefix;

    public interface UnitTypeInterface {
        String getDisplayableType();
    }
	public static enum UnitType implements UnitTypeInterface{
		Length("Length"),
		Area("Area"),
		Volume("Volume"),
		PlaneAngle("Plane angle"),
		SolidAngle("Solid angle"),
		Mass("Mass"),
		Density("Density"),
		Time("Time"),
		Frequency("Frequency"),
		Velocity("Velocity"),
		Flow("Flow"),
		Acceleration("Acceleration"),
		Force("Force"),
		/**
		 * Pressure or mechanical stress
		 */
		Pressure("Pressure"),
		/**
		 * Torque or moment of force
		 */
		Torque("Torque"),
		Energy("Energy"),
		/**
		 * Power or heat flow rate
		 */
		Power("Power"),
		Action("Action"),
		DynamicViscosity("Dynamic viscosity"),
		KinematicViscosity("Kinematic viscosity"),
		Current("Electric current"),
		Charge("Electric charge"),
		Dipole("Electric dipole"),
		/**
		 * Electromotive force, electric potential difference, Voltage
		 */
		Potential("Electric potential"),
		Resistance("Electrical resistance"),
		/**
		 * Capacitor's ability to store charge
		 */
		Capacitance("Capacitance"),
		MagneticFlux("Magnetic flux"),
		MagneticFluxDensity("Magnetic flux density"),
		Inductance("Inductance"),
		Temperature("Temperature"),
		InformationEntropy("Information entropy"),
		LuminousIntensity("Luminous intensity"),
		Luminance("Luminance"),
		LuminousFlux("Luminous flux"),
		Illuminance("Illuminance"),
		/**
		 * Radiation - source activity
		 */
		Radioactivity("Radioactivity"),
		//RadiationExposure("Radiation - exposure"),
		RadiationAbsorbedDose("Radiation - absorbed dose"),
		RadiationEquivalentDose("Radiation - equivalent dose"),
		Rational("Rational"),
		;
		
        private final String type;

        private UnitType(final String type) {
            this.type = type;
        }

        public String getDisplayableType() {
            return type;
        }
	}
	public static enum Unit implements UnitTypeInterface {
		m("m", "metre", UnitType.Length),
		sm("m²", "square metre", 2, UnitType.Area),  // TODO: m^2
		cm("m³", "cubic metre", 3, UnitType.Volume), // TODO: m^3
		deg("°", "degree", UnitType.PlaneAngle),
		arcmin("′", "arcminute", UnitType.PlaneAngle),
		arcsec("″", "arcsecond", UnitType.PlaneAngle),
		rad("rad", "radian", UnitType.PlaneAngle),
		grad("grad", "gradian", UnitType.PlaneAngle),
		s("s", "second", UnitType.Time),
		min("min", "minute", UnitType.Time),
		d("d", "day", UnitType.Time),
		wk("wk", "week", UnitType.Time),
		mo("m", "month", UnitType.Time),
		a("a", "year", UnitType.Time),
		kgpcm("kg/m³", "kilogram per cubic metre", UnitType.Density), // TODO: k*g/m^3
		Hz("Hz", "hertz", UnitType.Frequency),
		rpm("rpm", "revolutions per minute", UnitType.Frequency),
		meps("m/s", "metre per second", UnitType.Velocity), // TODO: m/s
		cmps("m³/s", "cubic metre per second", UnitType.Flow), // TODO: m^3/s
		mpss("m/s²", "metre per second squared", UnitType.Acceleration), // TODO: m/s^2
		N("N", "Newton", UnitType.Force),
		g("g", "gram", UnitType.Mass),
		kg("kg", "kilogram", UnitType.Mass), // TODO: k*g
		Pa("Pa", "pascal", UnitType.Pressure),
		bar("bar", "bar", UnitType.Pressure),
		Nm("Nm", "newton metre", UnitType.Torque),  // TODO: N*m
		J("J", "joule", UnitType.Energy),
		W("W", "watt", UnitType.Power),
		Pas("Pas", "pascal second", UnitType.DynamicViscosity),   // TODO: Pa*s
		A("A", "ampere", UnitType.Current),
		C("C", "coulomb", UnitType.Charge),
		Cm("Cm", "coulomb metre", UnitType.Dipole),  // TODO: C*m
		V("V", "volt", UnitType.Dipole),
		Ω("Ω", "ohm", UnitType.Resistance),
		F("F", "farad", UnitType.Capacitance),
		Wb("Wb", "weber", UnitType.MagneticFlux),
		T("T", "tesla", UnitType.MagneticFluxDensity),
		G("G", "gauß", UnitType.MagneticFluxDensity),
		H("H", "henry", UnitType.Inductance),
		K("K", "kelvin", UnitType.Temperature),
		degC("°C", "degree Celsius", UnitType.Temperature),
		degF("°F", "degree Fahrenheit", UnitType.Temperature),
		bit("bit", "bit", UnitType.InformationEntropy),
		B("B", "byte", UnitType.InformationEntropy),
		cd("cd", "candela", UnitType.LuminousIntensity),
		lm("lm", "lumen", UnitType.LuminousFlux),
		lx("lx", "lux", UnitType.Illuminance),
		Bq("Bq", "becquerel", UnitType.Radioactivity),
		Gy("Gy", "gray", UnitType.RadiationAbsorbedDose),
		Sv("Sv", "sievert", UnitType.RadiationEquivalentDose),
		per("/", "per", UnitType.Rational),
		;
		
		private final String label;
		private final String name;
		private final int dimension;
		private final UnitType type;
		private Unit(String label, String name, UnitType type){
			this.label = label;
			this.name = name;
			this.dimension = 1;
			this.type = type;
		}
		private Unit(String label, String name, int dimension, UnitType type){
			this.label = label;
			this.name = name;
			this.dimension = dimension;
			this.type = type;
		}
        public String getLabel() {
            return label;
        }
        public String getName() {
            return name;
        }
        public int getDimension() {
            return dimension;
        }
        public String getDisplayableType() {
            return type.getDisplayableType();
        }
		@Override
		public String toString() {
			return super.toString();
		}
	};
	final static Set<Unit> Pas = EnumSet.of(Unit.Pa, Unit.s);
	final Set<Unit> meps = EnumSet.of(Unit.m, Unit.per, Unit.s);
	private Set<Unit> units = new LinkedHashSet<Unit>();

    public Quantity() {
        super();
    }

	public Quantity(double value){
		this.value = value;
		this.prefix = Prefix.UNUS;
		this.units = null;
	}

	public Quantity(double value, Unit unit){
		this.value = value;
		this.prefix = Prefix.UNUS;
		this.units = EnumSet.of(unit);
	}

	public Quantity(double value, Prefix prefix, Unit unit){
		this.value = value;
		this.prefix = prefix;
		this.units = EnumSet.of(unit);
	}
	
	public Quantity(double value, Set<Unit> unit) {
		this.value = value;
		this.prefix = Prefix.UNUS;
		this.units = unit;
	}

	public Quantity(double value, Prefix prefix, Set<Unit> unit) {
		this.value = value;
		this.prefix = prefix;
		this.units = unit;
	}

	public double getValue(){
//		if(prefix == Prefix.Unus) return numericalFactor;
		return value*prefix.getDecimal();
	}
	
	public Quantity plus(Quantity add){
//		System.out.println(equalType(add));
		if(!equalType(add)) throw new IllegalArgumentException("Quantity has to be of same type");
//		System.out.println(units.equals(add.units));
//		System.out.println(units.containsAll(add.units));
//		if(!units.equals(add.units)) throw new IllegalArgumentException("No conversion yet");
		if(!units.containsAll(add.units))
			throw new IllegalArgumentException("No conversion of "+units+" and "+add.units+" yet");
		return new Quantity(value+add.value*add.prefix.getDecimal()/prefix.getDecimal(), prefix, units);
	}
	
	public Quantity minus(Quantity subtract){
		if(!equalType(subtract)) throw new IllegalArgumentException("Quantity has to be of same type");
		if(!units.containsAll(subtract.units))
			throw new IllegalArgumentException("No conversion of "+units+" and "+subtract.units+" yet");
		return new Quantity(value-subtract.value*subtract.prefix.getDecimal()/prefix.getDecimal(), prefix, units);
	}
	
	public Quantity times(Quantity multiplicate){
		if(multiplicate.units != null && multiplicate.units.iterator().next()!=null)
			throw new IllegalArgumentException("No conversion of "+units+" and "+multiplicate.units+" yet");
		double valueProduct = value*multiplicate.value;
		int translation = 0;
		int orderOfMagnitude = (int) Math.log10(valueProduct);
		if(orderOfMagnitude>=3 || orderOfMagnitude<=3) {
			translation = ((int) orderOfMagnitude/3)*3;
		}
		Prefix prefixProduct = prefix.times(multiplicate.prefix,translation);
		if(prefixProduct == null){
			prefixProduct = Prefix.UNUS;
			translation = -prefix.orderOfMagnitude;
		}
		return new Quantity(valueProduct/Math.pow(10, translation),
				prefixProduct, units);
	}
	
	public Quantity conversion(){
		// TODO: http://en.wikipedia.org/wiki/Conversion_of_units
		return new Quantity();
	}
	
	public boolean equalType(Quantity quantity){
		boolean other = false;
		Iterator<Unit> thisUnitIterator, otherUnitIterator;
		Unit thisUnit, otherUnit;
		
		thisUnitIterator = units.iterator();
		while(thisUnitIterator.hasNext()){
			thisUnit = thisUnitIterator.next();
			other = false;
			if(quantity.units!=null){
				otherUnitIterator = quantity.units.iterator();
				while(otherUnitIterator.hasNext()){
					otherUnit = otherUnitIterator.next();
		            if(thisUnit.type.equals(otherUnit.type))
			            other = true;
	//	            System.out.println(" " + thisUnit.type + " " + otherUnit.type + " " + thisUnit.type.equals(otherUnit.type));
				}
			}
//			System.out.println("  " + other);
			if(!other) return other;
		}
		otherUnitIterator = quantity.units.iterator();
		while(otherUnitIterator.hasNext()){
			otherUnit = otherUnitIterator.next();
			other = false;
			thisUnitIterator = units.iterator();
			while(thisUnitIterator.hasNext()){
				thisUnit = thisUnitIterator.next();
	            if(otherUnit.type.equals(thisUnit.type))
		            other = true;
//	            System.out.println(" " + otherUnit.type + " " + thisUnit.type + " " + otherUnit.type.equals(thisUnit.type));
			}
//			System.out.println("  " + other);
			if(!other) return other;
		}
//		System.out.println(result);
        return other;
	}
	
	public void printValue(){
		System.out.println(value*prefix.getDecimal());
	}
	
	@Override
	public String toString() {
		String result = value + " ";
		if(prefix != Prefix.UNUS) result += prefix.getSymbol();
		if(units!=null)for(Unit u : units)
			result += u.toString();
		return result;
	}
	
	public void print(){
		System.out.print(this.toString());
	}
	
	public void println(){
		System.out.println(this.toString());
	}

    private static void printUnit(Unit unit) {
        printType(unit);
        System.out.print(" - ");
        System.out.print(unit.getLabel());
    }

    private static void printType(UnitTypeInterface displayable) {
        System.out.print(displayable.getDisplayableType());
    }
	
	public static void main(String[] args) {
        System.out.println("All unit types");
        for(UnitType type : UnitType.values()) {
            printType(type);
            System.out.println();
        }
        System.out.println();
        System.out.println("All units");
        for(Unit unit : Unit.values()) {
            printUnit(unit);
            System.out.println();
        };
        System.out.println();
		Quantity quantity1 = new Quantity(2.1e11,Unit.Pa);
		Quantity quantity2 = new Quantity(70,Prefix.GIGA,Unit.Pa);
		Quantity quantity12 = quantity1.plus(quantity2);
		Quantity quantity21 = quantity2.plus(quantity1);
        quantity1.println();
        System.out.println(quantity2);
        System.out.println(quantity1 + " + " + quantity2 + " = " + quantity12);
        System.out.println(quantity2 + " + " + quantity1 + " = " + quantity21);
        System.out.println(quantity12 + " == " + quantity21 + " : " + (quantity12==quantity21));
        System.out.println(quantity12.getValue() + " == " + quantity21.getValue() + " : " + (quantity12.getValue()==quantity21.getValue()));
        System.out.println(quantity12 + " equals " + quantity21 + " : " + (quantity12.equals(quantity21)));

        System.out.println(quantity1 + " - " + quantity2 + " = " + quantity1.minus(quantity2));
//        System.out.println(quantity1 + " * " + quantity2 + " = " + quantity1.times(quantity2));
//        System.out.println(quantity1.times(quantity2) + " * " + quantity1 + " = " + quantity1.times(quantity2).times(quantity1));
		Quantity quantity1b = new Quantity(70);
        System.out.println(quantity1 + " * " + quantity1b + " = " + quantity1.times(quantity1b));
//        System.out.println(quantity1 + " + " + quantity1b + " = " + quantity1.plus(quantity1b));
        
		Quantity quantity3 = new Quantity(1.5,Quantity.Pas);
		Quantity quantity4 = new Quantity(.5,EnumSet.of(Unit.s, Unit.Pa));
//		quantity4 = new Quantity(.5,EnumSet.of(Unit.s, Unit.Pa, Unit.m));
		quantity3.println();
		quantity4.println();
		Quantity quantity34 = quantity3.plus(quantity4);
		quantity34.println();
		
		
	}

}
