/*******************************************************************************
***  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 "FloatingPoint.h"
using namespace pecunia::literals;
using std::string;
using std::to_string;
using pecunia::FloatingPoint;
using pecunia::FloatingPointBase;
using pecunia::FloatingPointParts;

#include <cmath>
using std::abs;
using std::ceil;
using std::fabs;
using std::floor;
using std::fmod;
using std::modf;
using std::pow;
#include <cfenv>
using std::feclearexcept;
using std::fetestexcept;
#include <stdexcept>
using std::domain_error;
using std::overflow_error;
using std::underflow_error;
#include <utility>
using std::move;


namespace
{

FloatingPoint testExceptions(const FloatingPointBase& value)
{
	int32_t exceptions{fetestexcept(FE_ALL_EXCEPT)};

	if (exceptions & FE_DIVBYZERO)
		throw domain_error{"The floating-point operation resulted in a divide by zero."};

	if (exceptions & FE_UNDERFLOW)
		throw underflow_error{
			"The floating-point operation resulted in a value that is too small to fit within "
				"the FloatingPoint type."
		};

	if (exceptions & FE_OVERFLOW)
		throw overflow_error{
			"The floating-point operation resulted in a value that is too large to fit within "
				"the FloatingPoint type."
		};

	if (exceptions & FE_INVALID)
		throw domain_error{
			"The floating-point operation resulted in a value that is not valid."
		};
	return FloatingPoint{value};
}

}

FloatingPoint::FloatingPoint() :
	FloatingPoint{static_cast<FloatingPointBase>(0)}
{}

FloatingPoint::FloatingPoint(const FloatingPointBase& value) :
	value_{value}
{}

FloatingPoint::FloatingPoint(const int32_t& value) :
	FloatingPoint{static_cast<FloatingPointBase>(value)}
{}

FloatingPoint::FloatingPoint(const int64_t& value) :
	FloatingPoint{static_cast<FloatingPointBase>(value)}
{}

FloatingPoint FloatingPoint::operator+(const FloatingPoint& other) const
{
	feclearexcept(FE_ALL_EXCEPT);
	return testExceptions(this->value_ + other.value_);
}

FloatingPoint FloatingPoint::operator-(const FloatingPoint& other) const
{
	feclearexcept(FE_ALL_EXCEPT);
	return testExceptions(this->value_ - other.value_);
}

FloatingPoint FloatingPoint::operator*(const FloatingPoint& other) const
{
	feclearexcept(FE_ALL_EXCEPT);
	return testExceptions(this->value_ * other.value_);
}

FloatingPoint FloatingPoint::operator/(const FloatingPoint& other) const
{
	feclearexcept(FE_ALL_EXCEPT);
	return testExceptions(this->value_ / other.value_);
}

bool FloatingPoint::operator<(const FloatingPoint& other) const
{
	return this->value_ < other.value_;
}

bool FloatingPoint::operator>(const FloatingPoint& other) const
{
	return this->value_ > other.value_;
}

bool FloatingPoint::operator<(const FloatingPointBase& other) const
{
	return this->value_ < other;
}

bool FloatingPoint::operator>(const FloatingPointBase& other) const
{
	return this->value_ > other;
}

FloatingPoint FloatingPoint::operator-() const
{
	feclearexcept(FE_ALL_EXCEPT);
	return testExceptions(-this->value_);
}

const FloatingPointBase& FloatingPoint::underlying() const noexcept
{
	return this->value_;
}

FloatingPointParts FloatingPoint::decompose(const uint8_t digits) const
{
	feclearexcept(FE_ALL_EXCEPT);
	FloatingPointBase whole;
	const auto fraction{testExceptions(modf(this->value_, &whole))};
	const auto shiftPrecision{10.0_fp .pow(digits)};
	const auto shiftedFractionWhole{fraction * shiftPrecision};
	const auto shiftedFraction{shiftedFractionWhole / shiftPrecision};
	return FloatingPointParts{whole, shiftedFraction};
}

FloatingPoint FloatingPoint::floor() const
{
	feclearexcept(FE_ALL_EXCEPT);
	return testExceptions(::floor(this->value_));
}

FloatingPoint FloatingPoint::ceil() const
{
	feclearexcept(FE_ALL_EXCEPT);
	return testExceptions(::ceil(this->value_));
}

FloatingPoint FloatingPoint::abs() const
{
	feclearexcept(FE_ALL_EXCEPT);
	return testExceptions(::abs(this->value_));
}

FloatingPoint FloatingPoint::modulo(const FloatingPoint& other) const
{
	feclearexcept(FE_ALL_EXCEPT);
	return testExceptions(fmod(this->value_, other.value_));
}

FloatingPoint FloatingPoint::pow(const int64_t& exp) const
{
	feclearexcept(FE_ALL_EXCEPT);
	return testExceptions(::pow(this->value_, exp));
}

FloatingPoint FloatingPoint::pow(const FloatingPoint& exp) const
{
	feclearexcept(FE_ALL_EXCEPT);
	return testExceptions(::pow(this->value_, exp.value_));
}

bool FloatingPoint::isEqual(const FloatingPoint& other, const uint8_t digits) const
{
	const auto difference{(*this - other).abs()};
	const auto shiftPrecision{FloatingPoint{10}.pow(digits)};
	const auto shiftedDifference{difference * shiftPrecision};
	return static_cast<uint64_t>(shiftedDifference.underlying()) == 0;
}

FloatingPoint FloatingPoint::reciprocal() const
{
	return FloatingPoint{1} / *this;
}

string FloatingPoint::toString() const // cppcheck-suppress unusedFunction
{
	return to_string(this->value_);
}

FloatingPointParts::FloatingPointParts() :
	FloatingPointParts{static_cast<FloatingPointBase>(0), static_cast<FloatingPointBase>(0)}
{}

FloatingPointParts::FloatingPointParts(
	const FloatingPointBase& whole,
	const FloatingPointBase& decimal
) :
	FloatingPointParts{FloatingPoint{whole}, FloatingPoint{decimal}}
{}

FloatingPointParts::FloatingPointParts(const FloatingPoint& whole, const FloatingPoint& decimal) :
	whole_{whole},
	decimal_{decimal}
{}

bool pecunia::operator<(const FloatingPointBase lhs, const FloatingPoint& rhs)
{
	return FloatingPoint{lhs} < rhs;
}

bool pecunia::operator>(const FloatingPointBase lhs, const FloatingPoint& rhs)
{
	return FloatingPoint{lhs} > rhs;
}

FloatingPoint pecunia::operator/(
	const FloatingPointBase numerator,
	const FloatingPoint& denominator
)
{
	return FloatingPoint{numerator} / denominator;
}

FloatingPoint pecunia::literals::operator""_fp(long double value)
{
	return FloatingPoint{static_cast<FloatingPointBase>(value)};
}

