#pragma once

#if defined(GMP)
    #include "boost/multiprecision/gmp.hpp"
using Rational = boost::multiprecision::mpq_rational;
using BigInt   = boost::multiprecision::mpz_int;
#else
    #include "boost/multiprecision/cpp_int.hpp"
using Rational = boost::multiprecision::cpp_rational;
using BigInt   = boost::multiprecision::cpp_int;
#endif

#include <cstdint>
#include <iostream>

namespace zx {

    /*
 * Representation of fractions as multiples of pi
 * Rationals can only have values in the half-open interval (-1,1],
 * corresponding to the interval (-pi, pi]
 */
    class PiRational {
        // void normalize();
    public:
        PiRational():
            frac(){};
        explicit PiRational(int64_t num, int64_t denom):
            frac(num, denom) {
            modPi();
        }
        explicit PiRational(const BigInt& num, const BigInt& denom):
            frac(num, denom) {
            modPi();
        }
        explicit PiRational(int64_t num):
            frac(num, 1) {
            modPi();
        }
        explicit PiRational(double val);

        PiRational& operator+=(const PiRational& rhs);
        PiRational& operator+=(int64_t rhs);

        PiRational& operator-=(const PiRational& rhs);
        PiRational& operator-=(int64_t rhs);

        PiRational& operator*=(const PiRational& rhs);
        PiRational& operator*=(int64_t rhs);

        PiRational& operator/=(const PiRational& rhs);
        PiRational& operator/=(int64_t rhs);

        // double to_double() const;
        [[nodiscard]] bool isInteger() const {
            return boost::multiprecision::denominator(frac) == 1;
        }
        [[nodiscard]] bool isZero() const {
            return boost::multiprecision::numerator(frac) == 0;
        }
        [[nodiscard]] BigInt getDenom() const {
            return boost::multiprecision::denominator(frac);
        }

        [[nodiscard]] BigInt getNum() const {
            return boost::multiprecision::numerator(frac);
        }

        [[nodiscard]] double toDouble() const;

        [[nodiscard]] double toDoubleDivPi() const {
            return frac.convert_to<double>();
        }

        [[nodiscard]] bool isClose(double x, double tolerance) const {
            return std::abs(toDouble() - x) < tolerance;
        }

        [[nodiscard]] bool isCloseDivPi(double x, double tolerance) const {
            return std::abs(toDoubleDivPi() - x) < tolerance;
        }

    private:
        Rational frac;

        void modPi();

        void setNum(const BigInt& num) {
            boost::multiprecision::numerator(frac) = num;
        }

        void setDenom(const BigInt& denom) {
            boost::multiprecision::denominator(frac) = denom;
        }
    };

    inline PiRational operator-(const PiRational& rhs) {
        return PiRational(-rhs.getNum(), rhs.getDenom());
    }
    inline PiRational operator+(PiRational lhs, const PiRational& rhs) {
        lhs += rhs;
        return lhs;
    }
    inline PiRational operator+(PiRational lhs, const int64_t rhs) {
        lhs += rhs;
        return lhs;
    }
    inline PiRational operator+(const int64_t lhs, PiRational rhs) {
        rhs += lhs;
        return rhs;
    }

    inline PiRational operator-(PiRational lhs, const PiRational& rhs) {
        lhs -= rhs;
        return lhs;
    }
    inline PiRational operator-(PiRational lhs, const int64_t rhs) {
        lhs -= rhs;
        return lhs;
    }
    inline PiRational operator-(const int64_t lhs, PiRational rhs) {
        rhs -= lhs;
        return rhs;
    }

    inline PiRational operator*(PiRational lhs, const PiRational& rhs) {
        lhs *= rhs;
        return lhs;
    }
    inline PiRational operator*(PiRational lhs, const int64_t rhs) {
        lhs *= rhs;
        return lhs;
    }
    inline PiRational operator*(const int64_t lhs, PiRational rhs) {
        rhs *= lhs;
        return rhs;
    }

    inline PiRational operator/(PiRational lhs, const PiRational& rhs) {
        lhs /= rhs;
        return lhs;
    }
    inline PiRational operator/(PiRational lhs, const int64_t rhs) {
        lhs /= rhs;
        return lhs;
    }
    inline PiRational operator/(const int64_t lhs, PiRational rhs) {
        rhs /= lhs;
        return rhs;
    }

    inline bool operator<(const PiRational& lhs, const PiRational& rhs) {
        return lhs.getNum() * rhs.getDenom() < rhs.getNum() * lhs.getDenom();
    }

    inline bool operator<(const PiRational& lhs, int64_t rhs) {
        return lhs.getNum() < rhs * lhs.getDenom();
    }

    inline bool operator<(int64_t lhs, const PiRational& rhs) {
        return lhs * rhs.getDenom() < rhs.getNum();
    }

    inline bool operator<=(const PiRational& lhs, const PiRational& rhs) {
        return lhs.getNum() * rhs.getDenom() <= rhs.getNum() * lhs.getDenom();
    }

    inline bool operator<=(const PiRational& lhs, int64_t rhs) {
        return lhs.getNum() <= rhs * lhs.getDenom();
    }

    inline bool operator<=(int64_t lhs, const PiRational& rhs) {
        return lhs * rhs.getDenom() <= rhs.getNum();
    }

    inline bool operator>(const PiRational& lhs, const PiRational& rhs) {
        return rhs < lhs;
    }

    inline bool operator>(const PiRational& lhs, int64_t rhs) {
        return rhs < lhs;
    }

    inline bool operator>(int64_t lhs, const PiRational& rhs) {
        return rhs < lhs;
    }

    inline bool operator>=(const PiRational& lhs, const PiRational& rhs) {
        return rhs <= lhs;
    }

    inline bool operator>=(const PiRational& lhs, int64_t rhs) {
        return rhs <= lhs;
    }

    inline bool operator>=(int64_t lhs, const PiRational& rhs) {
        return rhs <= lhs;
    }

    inline bool operator==(const PiRational& lhs, const PiRational& rhs) {
        return lhs.getNum() == rhs.getNum() && lhs.getDenom() == rhs.getDenom();
    }

    inline bool operator==(const PiRational& lhs, int64_t rhs) {
        return lhs.getNum() == rhs && lhs.getDenom() == 1;
    }

    inline bool operator==(int64_t lhs, const PiRational& rhs) {
        return rhs == lhs;
    }

    inline bool operator!=(const PiRational& lhs, const PiRational& rhs) {
        return !(lhs == rhs);
    }

    inline bool operator!=(const PiRational& lhs, int64_t rhs) {
        return !(lhs == rhs);
    }

    inline bool operator!=(int64_t lhs, const PiRational& rhs) {
        return !(lhs == rhs);
    }

    inline std::ostream& operator<<(std::ostream& os, const zx::PiRational& rhs) {
        os << rhs.getNum() << "/" << rhs.getDenom();
        return os;
    }

} // namespace zx
