#ifndef FIXED_POINT_NUMBER_HPP
#define FIXED_POINT_NUMBER_HPP

#include <cmath>
#include <cstdint>

#include <iostream>
#include <limits>
#include <type_traits>
#include <utility>

namespace osrm
{
namespace util
{

// implements an binary based fixed point number type
template <unsigned FractionalBitSize,
          bool use_64_bits = false,
          bool is_unsigned = false,
          bool truncate_results = false>
class FixedPointNumber
{
    static_assert(FractionalBitSize > 0, "FractionalBitSize must be greater than 0");
    static_assert(FractionalBitSize <= 32, "FractionalBitSize must at most 32");

    typename std::conditional<use_64_bits, int64_t, int32_t>::type m_fixed_point_state;
    constexpr static const decltype(m_fixed_point_state) PRECISION = 1 << FractionalBitSize;

    // state signage encapsulates whether the state should either represent a
    // signed or an unsigned floating point number
    using state_signage =
        typename std::conditional<is_unsigned,
                                  typename std::make_unsigned<decltype(m_fixed_point_state)>::type,
                                  decltype(m_fixed_point_state)>::type;

  public:
    FixedPointNumber() : m_fixed_point_state(0) {}

    // the type is either initialized with a floating point value or an
    // integral state. Anything else will throw at compile-time.
    template <class T>
    constexpr FixedPointNumber(const T &&input) noexcept
        : m_fixed_point_state(static_cast<decltype(m_fixed_point_state)>(
              std::round(std::forward<const T>(input) * PRECISION)))
    {
        static_assert(
            std::is_floating_point<T>::value || std::is_integral<T>::value,
            "FixedPointNumber needs to be initialized with floating point or integral value");
    }

    // get max value
    template <typename T,
              typename std::enable_if<std::is_floating_point<T>::value>::type * = nullptr>
    constexpr static auto max() noexcept -> T
    {
        return static_cast<T>(std::numeric_limits<state_signage>::max()) / PRECISION;
    }

    // get min value
    template <typename T,
              typename std::enable_if<std::is_floating_point<T>::value>::type * = nullptr>
    constexpr static auto min() noexcept -> T
    {
        return static_cast<T>(1) / PRECISION;
    }

    // get lowest value
    template <typename T,
              typename std::enable_if<std::is_floating_point<T>::value>::type * = nullptr>
    constexpr static auto lowest() noexcept -> T
    {
        return static_cast<T>(std::numeric_limits<state_signage>::min()) / PRECISION;
    }

    // cast to floating point type T, return value
    template <typename T,
              typename std::enable_if<std::is_floating_point<T>::value>::type * = nullptr>
    explicit operator const T() const noexcept
    {
        // casts to external type (signed or unsigned) and then to float
        return static_cast<T>(static_cast<state_signage>(m_fixed_point_state)) / PRECISION;
    }

    // warn about cast to integral type T, its disabled for good reason
    template <typename T, typename std::enable_if<std::is_integral<T>::value>::type * = nullptr>
    explicit operator T() const
    {
        static_assert(std::is_integral<T>::value,
                      "casts to integral types have been disabled on purpose");
    }

    // compare, ie. sort fixed-point numbers
    bool operator<(const FixedPointNumber &other) const noexcept
    {
        return m_fixed_point_state < other.m_fixed_point_state;
    }

    // equality, ie. sort fixed-point numbers
    bool operator==(const FixedPointNumber &other) const noexcept
    {
        return m_fixed_point_state == other.m_fixed_point_state;
    }

    bool operator!=(const FixedPointNumber &other) const { return !(*this == other); }
    bool operator>(const FixedPointNumber &other) const { return other < *this; }
    bool operator<=(const FixedPointNumber &other) const { return !(other < *this); }
    bool operator>=(const FixedPointNumber &other) const { return !(*this < other); }

    // arithmetic operators
    FixedPointNumber operator+(const FixedPointNumber &other) const noexcept
    {
        FixedPointNumber tmp = *this;
        tmp.m_fixed_point_state += other.m_fixed_point_state;
        return tmp;
    }

    FixedPointNumber &operator+=(const FixedPointNumber &other) noexcept
    {
        this->m_fixed_point_state += other.m_fixed_point_state;
        return *this;
    }

    FixedPointNumber operator-(const FixedPointNumber &other) const noexcept
    {
        FixedPointNumber tmp = *this;
        tmp.m_fixed_point_state -= other.m_fixed_point_state;
        return tmp;
    }

    FixedPointNumber &operator-=(const FixedPointNumber &other) noexcept
    {
        this->m_fixed_point_state -= other.m_fixed_point_state;
        return *this;
    }

    FixedPointNumber operator*(const FixedPointNumber &other) const noexcept
    {
        int64_t temp = this->m_fixed_point_state;
        temp *= other.m_fixed_point_state;

        // rounding!
        if (!truncate_results)
        {
            temp = temp + ((temp & 1 << (FractionalBitSize - 1)) << 1);
        }
        temp >>= FractionalBitSize;
        FixedPointNumber tmp;
        tmp.m_fixed_point_state = static_cast<decltype(m_fixed_point_state)>(temp);
        return tmp;
    }

    FixedPointNumber &operator*=(const FixedPointNumber &other) noexcept
    {
        int64_t temp = this->m_fixed_point_state;
        temp *= other.m_fixed_point_state;

        // rounding!
        if (!truncate_results)
        {
            temp = temp + ((temp & 1 << (FractionalBitSize - 1)) << 1);
        }
        temp >>= FractionalBitSize;
        this->m_fixed_point_state = static_cast<decltype(m_fixed_point_state)>(temp);
        return *this;
    }

    FixedPointNumber operator/(const FixedPointNumber &other) const noexcept
    {
        int64_t temp = this->m_fixed_point_state;
        temp <<= FractionalBitSize;
        temp /= static_cast<int64_t>(other.m_fixed_point_state);
        FixedPointNumber tmp;
        tmp.m_fixed_point_state = static_cast<decltype(m_fixed_point_state)>(temp);
        return tmp;
    }

    FixedPointNumber &operator/=(const FixedPointNumber &other) noexcept
    {
        int64_t temp = this->m_fixed_point_state;
        temp <<= FractionalBitSize;
        temp /= static_cast<int64_t>(other.m_fixed_point_state);
        FixedPointNumber tmp;
        this->m_fixed_point_state = static_cast<decltype(m_fixed_point_state)>(temp);
        return *this;
    }
};

static_assert(4 == sizeof(FixedPointNumber<1>), "FP19 has wrong size != 4");
}
}

#endif // FIXED_POINT_NUMBER_HPP