#pragma once

#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/XRPAmount.h>

#include <optional>

namespace xrpl {

struct AmountSpec
{
    explicit AmountSpec() = default;

    bool native;
    union
    {
        XRPAmount xrp;
        IOUAmount iou = {};
    };
    std::optional<AccountID> issuer;
    std::optional<Currency> currency;

    friend std::ostream&
    operator<<(std::ostream& stream, AmountSpec const& amt)
    {
        if (amt.native)
            stream << to_string(amt.xrp);
        else
            stream << to_string(amt.iou);
        if (amt.currency)
            stream << "/(" << *amt.currency << ")";
        if (amt.issuer)
            stream << "/" << *amt.issuer << "";
        return stream;
    }
};

struct EitherAmount
{
#ifndef NDEBUG
    bool native = false;
#endif

    union
    {
        IOUAmount iou = {};
        XRPAmount xrp;
    };

    EitherAmount() = default;

    explicit EitherAmount(IOUAmount const& a) : iou(a)
    {
    }

#if defined(__GNUC__) && !defined(__clang__)
#pragma GCC diagnostic push
    // ignore warning about half of iou amount being uninitialized
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
#endif
    explicit EitherAmount(XRPAmount const& a) : xrp(a)
    {
#ifndef NDEBUG
        native = true;
#endif
    }
#if defined(__GNUC__) && !defined(__clang__)
#pragma GCC diagnostic pop
#endif

    explicit EitherAmount(AmountSpec const& a)
    {
#ifndef NDEBUG
        native = a.native;
#endif
        if (a.native)
            xrp = a.xrp;
        else
            iou = a.iou;
    }

#ifndef NDEBUG
    friend std::ostream&
    operator<<(std::ostream& stream, EitherAmount const& amt)
    {
        if (amt.native)
            stream << to_string(amt.xrp);
        else
            stream << to_string(amt.iou);
        return stream;
    }
#endif
};

template <class T>
T&
get(EitherAmount& amt)
{
    static_assert(sizeof(T) == -1, "Must used specialized function");
    return T(0);
}

template <>
inline IOUAmount&
get<IOUAmount>(EitherAmount& amt)
{
    XRPL_ASSERT(!amt.native, "xrpl::get<IOUAmount>(EitherAmount&) : is not XRP");
    return amt.iou;
}

template <>
inline XRPAmount&
get<XRPAmount>(EitherAmount& amt)
{
    XRPL_ASSERT(amt.native, "xrpl::get<XRPAmount>(EitherAmount&) : is XRP");
    return amt.xrp;
}

template <class T>
T const&
get(EitherAmount const& amt)
{
    static_assert(sizeof(T) == -1, "Must used specialized function");
    return T(0);
}

template <>
inline IOUAmount const&
get<IOUAmount>(EitherAmount const& amt)
{
    XRPL_ASSERT(!amt.native, "xrpl::get<IOUAmount>(EitherAmount const&) : is not XRP");
    return amt.iou;
}

template <>
inline XRPAmount const&
get<XRPAmount>(EitherAmount const& amt)
{
    XRPL_ASSERT(amt.native, "xrpl::get<XRPAmount>(EitherAmount const&) : is XRP");
    return amt.xrp;
}

inline AmountSpec
toAmountSpec(STAmount const& amt)
{
    XRPL_ASSERT(
        amt.mantissa() < std::numeric_limits<std::int64_t>::max(),
        "xrpl::toAmountSpec(STAmount const&) : maximum mantissa");
    bool const isNeg = amt.negative();
    std::int64_t const sMant = isNeg ? -std::int64_t(amt.mantissa()) : amt.mantissa();
    AmountSpec result;

    result.native = isXRP(amt);
    if (result.native)
    {
        result.xrp = XRPAmount(sMant);
    }
    else
    {
        result.iou = IOUAmount(sMant, amt.exponent());
        result.issuer = amt.issue().account;
        result.currency = amt.issue().currency;
    }

    return result;
}

inline EitherAmount
toEitherAmount(STAmount const& amt)
{
    if (isXRP(amt))
        return EitherAmount{amt.xrp()};
    return EitherAmount{amt.iou()};
}

inline AmountSpec
toAmountSpec(EitherAmount const& ea, std::optional<Currency> const& c)
{
    AmountSpec r;
    r.native = (!c || isXRP(*c));
    r.currency = c;
    XRPL_ASSERT(
        ea.native == r.native,
        "xrpl::toAmountSpec(EitherAmount const&&, std::optional<Currency>) : "
        "matching native");
    if (r.native)
    {
        r.xrp = ea.xrp;
    }
    else
    {
        r.iou = ea.iou;
    }
    return r;
}

}  // namespace xrpl
