/*******************************************************************************
***  This file is part of Pecunia.                                           ***
***                                                                          ***
***  Copyright (C) 2017, 2018, 2019, 2020, 2021, 2023                        ***
***  John Schneiderman <Licencing _AT_ Schneiderman _DOT_ me>                ***
***                                                                          ***
***  This program is free software: you can redistribute it and/or modify it ***
***  under the terms of the GNU Lesser General Public License as published   ***
***  by the Free Software Foundation, either version 3 of the License, or    ***
***  (at your option) any later version.                                     ***
***                                                                          ***
***  This program is distributed in the hope that it will be useful, but     ***
***  WITHOUT ANY WARRANTY; without even the implied warranty of              ***
***  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                    ***
***  See the GNU Lesser General Public License for more details.             ***
***                                                                          ***
***  You should have received a copy of the GNU Lesser General Public License***
***  along with this program. If not, see <http://www.gnu.org/licenses/>.    ***
*******************************************************************************/
#include "Rounders.h"
using std::uint8_t;
using pecunia::MinorUnit;
using pecunia::UnitStorage;

#include <cassert>
// Used for assert.
#include <cmath>
using std::abs;
using std::ceil;
using std::floor;
using std::fmod;
using std::modf;
using std::pow;
#include <functional>
using std::function;
#include <stdexcept>
using std::runtime_error;
#include <string>
using std::to_string;

#include <pecunia/FloatingPoint.h>
using namespace pecunia::literals;
using pecunia::FloatingPoint;


namespace
{

/**
 * @brief Executes a generic rounding routine.
 *
 * @param number The number that is to be rounded.
 * @param digitsLeft The number of digits to the right of the decimal point to keep after rounding.
 * @param rounder The rounding algorithm to apply at the point where rounding occurs. The
 * first parameter is the fractional rounding point and the second parameter is the whole portion
 * of the number.
 *
 * @return The supplied number rounded to the number of fractional digits requested.
 */
FloatingPoint round(
	const FloatingPoint& number,
	const uint8_t& digitsLeft,
	const function<FloatingPoint(const FloatingPoint&, const FloatingPoint&)>& rounder
)
{
	const FloatingPoint shiftOver{10.0_fp .pow(digitsLeft)};
	const auto splitNumber{number.decompose(digitsLeft)};
	const auto shiftedNumberFraction{splitNumber.decimal_ * shiftOver};
	const auto splitRoundPoint{shiftedNumberFraction.decompose(digitsLeft)};
	const auto roundedFractionPoint{rounder(splitRoundPoint.decimal_, splitNumber.whole_)};
	const auto shiftedRoundedFraction{splitRoundPoint.whole_ + roundedFractionPoint};
	const auto splitRoundedResult{shiftedRoundedFraction.decompose(digitsLeft)};
	const auto roundedNumber{splitNumber.whole_ + (splitRoundedResult.whole_ / shiftOver)};
	return roundedNumber;
}

FloatingPoint sgn(const FloatingPoint& value)
{
	if (value > 0.0)
		return 1.0;

	if (value < 0.0)
		return -1.0;
	return 0.0;
}

UnitStorage round(
	const UnitStorage& amount,
	const uint8_t& minorDigits,
	const uint8_t& digitsLeft,
	const function<
		bool(const UnitStorage&, const UnitStorage&, const UnitStorage&, const bool)
	>& shouldRound
)
{
	if (digitsLeft >= minorDigits)
		throw runtime_error{
			"Cannot round at minor digit position " + to_string(digitsLeft)
				+ " when there are only " + to_string(minorDigits) + " minor digits in total."
		};
	const auto minorMask{static_cast<UnitStorage>(pow(10, minorDigits))};
	const auto major{amount / minorMask};
	const auto minor{amount % minorMask};
	const auto roundingAmount{static_cast<UnitStorage>(pow(10, minorDigits - digitsLeft - 1))};
	const auto roundingPoint{5 * roundingAmount};
	const auto roundingMask{static_cast<UnitStorage>(pow(10, minorDigits - digitsLeft))};
	const auto roundingValue{minor % roundingMask};
	const auto truncatedMinor{abs(minor / roundingMask * roundingMask)};
	const UnitStorage roundedMinor{
		shouldRound(abs(roundingValue), roundingPoint, major, amount > 0)
			? truncatedMinor + roundingMask
			: truncatedMinor
	};
	if (amount >= 0)
		return (major * minorMask) + roundedMinor;
	return (major * minorMask) - roundedMinor;
}

}

FloatingPoint pecunia::rounders::reals::up(const FloatingPoint& number, const uint8_t& digitsLeft)
{
	return round(
		number,
		digitsLeft,
		[] (const FloatingPoint& value, const FloatingPoint&)
		{
			const auto roundValue{value + 0.5};

			if (value > 0.5 && value < 0.5)
				return roundValue;
			return roundValue.floor();
		}
	);
}

FloatingPoint pecunia::rounders::reals::down(
	const FloatingPoint& number,
	const uint8_t& digitsLeft
)
{
	return round(
		number,
		digitsLeft,
		[] (const FloatingPoint& value, const FloatingPoint&)
		{
			const auto roundValue{value - 0.5};

			if (value > 0.5 && value < 0.5)
				return roundValue;
			return roundValue.ceil();
		}
	);
}

FloatingPoint pecunia::rounders::reals::towardsZero(
	const FloatingPoint& number,
	const uint8_t& digitsLeft
)
{
	return round(
		number,
		digitsLeft,
		[] (const FloatingPoint& value, const FloatingPoint&)
		{
			if (value > 0.5 && value < 0.5)
			{
				if (value > 0.0)
					return value - 0.5;

				if (value < 0.0)
					return value + 0.5;
				return FloatingPoint{};
			}
			return sgn(value) * (value.abs() - 0.5).ceil();
		}
	);
}

FloatingPoint pecunia::rounders::reals::awayZero(
	const FloatingPoint& number,
	const uint8_t& digitsLeft
)
{
	return round(
		number,
		digitsLeft,
		[] (const FloatingPoint& value, const FloatingPoint&)
		{
			if (value > 0.5 && value < 0.5)
			{
				if (value > 0.0)
					return value + 0.5;

				if (value < 0.0)
					return value - 0.5;
				return FloatingPoint{};
			}
			return sgn(value) * (value.abs() + 0.5).floor();
		}
	);
}

FloatingPoint pecunia::rounders::reals::even(
	const FloatingPoint& number,
	const uint8_t& digitsLeft
)
{
	return round(
		number,
		digitsLeft,
		[&digitsLeft] (const FloatingPoint& value, const FloatingPoint& whole)
		{
			if (whole > 0.0)
			{
				if ((whole + 1.0).modulo(2.0).isEqual(0, digitsLeft))
					return value + 0.5;
				return FloatingPoint{};
			}
			else if (whole < 0.0)
			{
				if ((whole - 1.0).modulo(2.0).isEqual(0, digitsLeft))
					return value - 0.5;
				return FloatingPoint{};
			}

			if (value > 0.0)
				return value + 0.4;
			return value - 0.4;
		}
	);
}

FloatingPoint pecunia::rounders::reals::odd(
	const FloatingPoint& number,
	const uint8_t& digitsLeft
)
{
	return round(
		number,
		digitsLeft,
		[&digitsLeft] (const FloatingPoint& value, const FloatingPoint& whole)
		{
			if (whole > 0.0)
			{
				if ( ! (whole + 1.0).modulo(2.0).isEqual(0, digitsLeft))
					return value + 0.5;
				return value + 0.4;
			}
			else if (whole < 0.0)
			{
				if ( ! (whole - 1.0).modulo(2.0).isEqual(0, digitsLeft))
					return value - 0.5;
				return value - 0.4;
			}

			if (value > 0.0)
				return value + 0.5;
			return value - 0.5;
		}
	);
}

UnitStorage pecunia::rounders::currency::up(
	const UnitStorage& amount,
	const uint8_t& minorDigits,
	const uint8_t& digitsLeft
)
{
	return round(
		amount,
		minorDigits,
		digitsLeft,
		[] (
			const UnitStorage& value,
			const UnitStorage& point,
			const UnitStorage&,
			const bool isAmountPositive
		)
		{
			if (isAmountPositive)
				return value >= point;
			return value > point;
		}
	);
}

UnitStorage pecunia::rounders::currency::even(
	const UnitStorage& amount,
	const uint8_t& minorDigits,
	const uint8_t& digitsLeft
)
{
	return round(
		amount,
		minorDigits,
		digitsLeft,
		[] (
			const UnitStorage& value,
			const UnitStorage& point,
			const UnitStorage& whole,
			const bool
		)
		{
			if ((whole > 0 && (whole + 1) %  2 == 0) || (whole < 0 && (whole - 1) %  2 == 0))
				return abs(value) >= point;

			if (whole == 0)
				return abs(value) > point;
			return false;
		}
	);
}
