/* Lecture code 16.1
 *
 * Demonstrating how to use the C++ template system to perform dimensional
 * analysis at compile-time.
 */

#include <iostream>
#include <string>
using namespace std;

/* A class representing a value with a KMS unit associated with it.  The integer
 * arguments to the template indicate the powers of the KMS units.  For example,
 * a DimensionType<1, 0, 0> has unit kg, while a DimensionType<0, 1, -2> is in
 * meters per second squared.
 */
template <int kg, int m, int s> class DimensionType
{
public:
	/* Constructor sets up the stored value. */
	explicit DimensionType(double val = 0.0) : value(val) {}
	
	/* Retrieve stored value. */
	double getValue() const
	{
		return value;
	}

	/* We can add two DimensionTypes only if they have the same units.  See the
	 * handout on operator overloading for an explanation of the return type.
	 * Also note that because we did not explictly mention which type of
	 * DimensionType we accept and return, it defaults to using a DimensionType
	 * with the same units as the current type.
	 */
	const DimensionType operator+ (const DimensionType& other) const
	{
		return DimensionType(value + other.getValue());
	}
	const DimensionType operator- (const DimensionType& other) const
	{
		return DimensionType(value - other.getValue());
	}

	/* Multiplication and division of DimensionTypes is always well-defined and
	 * results in a DimensionType whose units are either the sum or the difference
	 * of the units of this DimensionType and the argument.  Note that this function
	 * is templatized over the units of the other DimensionType arguments so that
	 * we can multiply or divide with any units.
	 */
	template <int kg2, int m2, int s2>
		const DimensionType<kg + kg2, m + m2, s + s2>
		operator* (const DimensionType<kg2, m2, s2>& other) const
	{
		return DimensionType<kg + kg2, m + m2, s + s2>(value * other.getValue());
	}
	template <int kg2, int m2, int s2>
		const DimensionType<kg - kg2, m - m2, s - s2>
		operator/ (const DimensionType<kg2, m2, s2>& other) const
	{
		return DimensionType<kg - kg2, m - m2, s - s2>(value * other.getValue());
	}

	/* As an exercise, try overloading these operators for DimensionType:
	 * += -= *= /=
	 *
	 * For major C++ street cred, try implementing these operators by using the
	 * CRTP!
	 */

private:
	double value;
};

/* For simplicity's sake, typedef a few of these types. */
typedef DimensionType<1, 0, 0> kilogramT;
typedef DimensionType<0, 1, 0> meterT;
typedef DimensionType<0, 0, 1> secondT;
typedef DimensionType<0, 1, -1> velocityT;
typedef DimensionType<0, 1, -2> accelerationT;
typedef DimensionType<0, 0, -1> hertzT;
typedef DimensionType<1, 1, -2> newtonT;
typedef DimensionType<1, 2, -2> jouleT;
typedef DimensionType<1, 2, -3> wattT;

/* General stream insertion function for DimensionTypes just writes out the
 * dimension associated with each unit.
 */
template <int kg, int m, int s>
ostream& operator<< (ostream& out, const DimensionType<kg, m, s>& toPrint)
{
	out << toPrint.getValue();
	if(kg)
		out << "kg^" << kg;
	if(m)
		out << "m^" << m;
	if(s)
		out << "s^" << s;
	return out;
}

/* We also have a few special cases where we can print out the real symbol for
 * some complex types like Hz, N, J, and W.
 */
ostream& operator<< (ostream& out, const hertzT toPrint)
{
	return out << toPrint.getValue() << "Hz";
}
ostream& operator<< (ostream& out, const newtonT toPrint)
{
	return out << toPrint.getValue() << "N";
}
ostream& operator<< (ostream& out, const jouleT toPrint)
{
	return out << toPrint.getValue() << "J";
}
ostream& operator<< (ostream& out, const wattT toPrint)
{
	return out << toPrint.getValue() << "W";
}

/* Physics problem: How much power does it take to move 10kg of mass
 * vertically at 2m/s in Earth gravity?
 *
 * Answer: The weight of the object is 10kg * 9.8m/s^2, and we need to
 * move this force 2 meters per second, so the total answer should be
 *
 * 10kg * 9.8m/s^2 * 2m / s = ?
 */
int main()
{
	kilogramT mass(10.0); // 10.0 kilograms
	accelerationT acceleration(9.8); // 9.8m/s acceleration due to gravity.
	velocityT velocity(2); // 2m/s

	cout << mass << endl;
	cout << mass * acceleration << endl;
	cout << mass * acceleration * velocity << endl; // <-- our answer
	
	return 0;
}