package kont2015.unit1;

import java.util.function.BinaryOperator;

/**
 * A class representing a double value in a certain Unit.
 * Instances are immutable, i.e. cannot be modified once created.
 * @author hal
 *
 */
public class Value implements Comparable<Value> {

	private final Unit unit;
	private final double value;
	
	/**
	 * Creates a new value with the provided Unit and (double) value.
	 * @param unit the Unit of the new Value
	 * @param value the double value of the new Value
	 */
	public Value(Unit unit, double value) {
		this.unit = unit;
		this.value = value;
	}

	@Override
	public String toString() {
		return getValue() + unit.toString();
	}
	
	/**
	 * Returns this Value's Unit
	 * @return this Value's Unit
	 */
	public Unit getUnit() {
		return unit;
	}

	/**
	 * Returns this Value's double value
	 * @return this Value's double value
	 */
	public double getValue() {
		return value;
	}
	
	/**
	 * Returns a Value instance from the provided String.
	 * The format is a double (as parsed by Double.valueOf) followed by a Unit symbol (as parsed by Unit.valueOf), e.g. "1.0m" or "2.5km".
	 * @param s
	 * @return the String parses as a Value instance
	 */
	public static Value valueOf(String s) {
		int pos = s.length();
		while (Character.isAlphabetic(s.charAt(pos - 1))) {
			pos--;
		}
		return new Value(Unit.valueOf(s.substring(pos)), Double.valueOf(s.substring(0, pos)));
	}
	
	/**
	 * Computes the sum of this and other (both Value objects).
	 * The Unit of the returned Value is the common base Unit of this and other's Units.
	 * Hence, both Value object's double values are properly converted before adding. 
	 * @param other the other Value
	 * @return a new Value object representing the sum of this and other
	 */
	public Value add(Value other) {
		Unit base = this.unit.findCommonBaseUnit(other.unit);
		double sum = this.unit.convert(value, base) + other.unit.convert(other.value, base);
		return new Value(base, sum);
	}

	/**
	 * Computes a new value that is the combination of this Value's double value and the provided double.
	 * The double values are combined using the provided BinaryOperator.
	 * The Unit of the returned Value is the this Value's Unit.
	 * @param other the double value to combine with this
	 * @return a new Value object representing the sum of this and other
	 */
	public Value compute(BinaryOperator<Double> op, double other) {
		double result = op.apply(this.value, other);
		return new Value(this.unit, result);
	}	

	/**
	 * Computes the product of this Value and other (a double) using the compute method.
	 * @param other the other factor
	 * @return the product of this Value's double value and other (also a double)
	 */
	public Value mult(double other) {
		return compute((v1,  v2) -> v1 * v2, other);
	}
	
	/**
	 * Compares this Value with other according the the requirements of Comparable.
	 * Note that this Value and other may have different Units.
	 * @throws IllegalArgumentException if this and other don't have a common base Unit.
	 */
	@Override
	public int compareTo(Value other) {
		Unit base = this.unit.findCommonBaseUnit(other.unit);
		if (base == null) {
			throw new IllegalArgumentException("Cannot compare " + this + " with " + other);
		}
		double d1 = this.unit.convert(value, base), d2 = other.unit.convert(other.value, base);
		if (d1 < d2) {
			return -1;
		} else if (d1 > d2) {
			return 1;
		} else {
			return 0;
		}
	}
}