#pragma once

#include <xrpl/basics/CountedObject.h>
#include <xrpl/basics/LocalValue.h>
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STBase.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/json_get_or_throw.h>

namespace xrpl {

// Internal form:
// 1: If amount is zero, then value is zero and offset is -100
// 2: Otherwise:
//   legal offset range is -96 to +80 inclusive
//   value range is 10^15 to (10^16 - 1) inclusive
//  amount = value * [10 ^ offset]

// Wire form:
// High 8 bits are (offset+142), legal range is, 80 to 22 inclusive
// Low 56 bits are value, legal range is 10^15 to (10^16 - 1) inclusive
class STAmount final : public STBase, public CountedObject<STAmount>
{
public:
    using mantissa_type = std::uint64_t;
    using exponent_type = int;
    using rep = std::pair<mantissa_type, exponent_type>;

private:
    Asset mAsset;
    mantissa_type mValue;
    exponent_type mOffset;
    bool mIsNegative;

public:
    using value_type = STAmount;

    static int const cMinOffset = -96;
    static int const cMaxOffset = 80;

    // Maximum native value supported by the code
    constexpr static std::uint64_t cMinValue = 1'000'000'000'000'000ull;
    static_assert(isPowerOfTen(cMinValue));
    constexpr static std::uint64_t cMaxValue = cMinValue * 10 - 1;
    static_assert(cMaxValue == 9'999'999'999'999'999ull);
    constexpr static std::uint64_t cMaxNative = 9'000'000'000'000'000'000ull;

    // Max native value on network.
    constexpr static std::uint64_t cMaxNativeN = 100'000'000'000'000'000ull;
    constexpr static std::uint64_t cIssuedCurrency = 0x8'000'000'000'000'000ull;
    constexpr static std::uint64_t cPositive = 0x4'000'000'000'000'000ull;
    constexpr static std::uint64_t cMPToken = 0x2'000'000'000'000'000ull;
    constexpr static std::uint64_t cValueMask = ~(cPositive | cMPToken);

    static std::uint64_t const uRateOne;

    //--------------------------------------------------------------------------
    STAmount(SerialIter& sit, SField const& name);

    struct unchecked
    {
        explicit unchecked() = default;
    };

    // Do not call canonicalize
    template <AssetType A>
    STAmount(
        SField const& name,
        A const& asset,
        mantissa_type mantissa,
        exponent_type exponent,
        bool negative,
        unchecked);

    template <AssetType A>
    STAmount(
        A const& asset,
        mantissa_type mantissa,
        exponent_type exponent,
        bool negative,
        unchecked);

    // Call canonicalize
    template <AssetType A>
    STAmount(
        SField const& name,
        A const& asset,
        mantissa_type mantissa = 0,
        exponent_type exponent = 0,
        bool negative = false);

    STAmount(SField const& name, std::int64_t mantissa);

    STAmount(SField const& name, std::uint64_t mantissa = 0, bool negative = false);

    explicit STAmount(std::uint64_t mantissa = 0, bool negative = false);

    explicit STAmount(SField const& name, STAmount const& amt);

    template <AssetType A>
    STAmount(A const& asset, std::uint64_t mantissa = 0, int exponent = 0, bool negative = false)
        : mAsset(asset), mValue(mantissa), mOffset(exponent), mIsNegative(negative)
    {
        canonicalize();
    }

    // VFALCO Is this needed when we have the previous signature?
    template <AssetType A>
    STAmount(A const& asset, std::uint32_t mantissa, int exponent = 0, bool negative = false);

    template <AssetType A>
    STAmount(A const& asset, std::int64_t mantissa, int exponent = 0);

    template <AssetType A>
    STAmount(A const& asset, int mantissa, int exponent = 0);

    template <AssetType A>
    STAmount(A const& asset, Number const& number) : STAmount(fromNumber(asset, number))
    {
    }

    // Legacy support for new-style amounts
    STAmount(IOUAmount const& amount, Issue const& issue);
    STAmount(XRPAmount const& amount);
    STAmount(MPTAmount const& amount, MPTIssue const& mptIssue);
    operator Number() const;

    //--------------------------------------------------------------------------
    //
    // Observers
    //
    //--------------------------------------------------------------------------

    int
    exponent() const noexcept;

    bool
    integral() const noexcept;

    bool
    native() const noexcept;

    template <ValidIssueType TIss>
    constexpr bool
    holds() const noexcept;

    bool
    negative() const noexcept;

    std::uint64_t
    mantissa() const noexcept;

    Asset const&
    asset() const;

    template <ValidIssueType TIss>
    constexpr TIss const&
    get() const;

    Issue const&
    issue() const;

    // These three are deprecated
    Currency const&
    getCurrency() const;

    AccountID const&
    getIssuer() const;

    int
    signum() const noexcept;

    /** Returns a zero value with the same issuer and currency. */
    STAmount
    zeroed() const;

    void
    setJson(Json::Value&) const;

    STAmount const&
    value() const noexcept;

    //--------------------------------------------------------------------------
    //
    // Operators
    //
    //--------------------------------------------------------------------------

    explicit
    operator bool() const noexcept;

    STAmount&
    operator+=(STAmount const&);
    STAmount&
    operator-=(STAmount const&);

    STAmount& operator=(beast::Zero);

    STAmount&
    operator=(XRPAmount const& amount);

    STAmount&
    operator=(Number const&);

    //--------------------------------------------------------------------------
    //
    // Modification
    //
    //--------------------------------------------------------------------------

    void
    negate();

    void
    clear();

    // Zero while copying currency and issuer.
    void
    clear(Asset const& asset);

    void
    setIssuer(AccountID const& uIssuer);

    /** Set the Issue for this amount. */
    void
    setIssue(Asset const& asset);

    //--------------------------------------------------------------------------
    //
    // STBase
    //
    //--------------------------------------------------------------------------

    SerializedTypeID
    getSType() const override;

    std::string
    getFullText() const override;

    std::string
    getText() const override;

    Json::Value getJson(JsonOptions = JsonOptions::none) const override;

    void
    add(Serializer& s) const override;

    bool
    isEquivalent(STBase const& t) const override;

    bool
    isDefault() const override;

    XRPAmount
    xrp() const;
    IOUAmount
    iou() const;
    MPTAmount
    mpt() const;

private:
    template <AssetType A>
    static STAmount
    fromNumber(A const& asset, Number const& number);

    static std::unique_ptr<STAmount>
    construct(SerialIter&, SField const& name);

    void
    set(std::int64_t v);
    void
    canonicalize();

    STBase*
    copy(std::size_t n, void* buf) const override;
    STBase*
    move(std::size_t n, void* buf) override;

    STAmount&
    operator=(IOUAmount const& iou);

    friend class detail::STVar;

    friend STAmount
    operator+(STAmount const& v1, STAmount const& v2);
};

template <AssetType A>
STAmount::STAmount(
    SField const& name,
    A const& asset,
    mantissa_type mantissa,
    exponent_type exponent,
    bool negative,
    unchecked)
    : STBase(name), mAsset(asset), mValue(mantissa), mOffset(exponent), mIsNegative(negative)
{
}

template <AssetType A>
STAmount::STAmount(
    A const& asset,
    mantissa_type mantissa,
    exponent_type exponent,
    bool negative,
    unchecked)
    : mAsset(asset), mValue(mantissa), mOffset(exponent), mIsNegative(negative)
{
}

template <AssetType A>
STAmount::STAmount(
    SField const& name,
    A const& asset,
    std::uint64_t mantissa,
    int exponent,
    bool negative)
    : STBase(name), mAsset(asset), mValue(mantissa), mOffset(exponent), mIsNegative(negative)
{
    // mValue is uint64, but needs to fit in the range of int64
    if (Number::getMantissaScale() == MantissaRange::small)
    {
        XRPL_ASSERT(
            mValue <= std::numeric_limits<std::int64_t>::max(),
            "xrpl::STAmount::STAmount(SField, A, std::uint64_t, int, bool) : "
            "maximum mantissa input");
    }
    else
    {
        if (integral() && mValue > std::numeric_limits<std::int64_t>::max())
            throw std::overflow_error("STAmount mantissa is too large " + std::to_string(mantissa));
    }
    canonicalize();
}

template <AssetType A>
STAmount::STAmount(A const& asset, std::int64_t mantissa, int exponent)
    : mAsset(asset), mOffset(exponent)
{
    set(mantissa);
    canonicalize();
}

template <AssetType A>
STAmount::STAmount(A const& asset, std::uint32_t mantissa, int exponent, bool negative)
    : STAmount(asset, safe_cast<std::uint64_t>(mantissa), exponent, negative)
{
}

template <AssetType A>
STAmount::STAmount(A const& asset, int mantissa, int exponent)
    : STAmount(asset, safe_cast<std::int64_t>(mantissa), exponent)
{
}

// Legacy support for new-style amounts
inline STAmount::STAmount(IOUAmount const& amount, Issue const& issue)
    : mAsset(issue), mOffset(amount.exponent()), mIsNegative(amount < beast::zero)
{
    if (mIsNegative)
        mValue = unsafe_cast<std::uint64_t>(-amount.mantissa());
    else
        mValue = unsafe_cast<std::uint64_t>(amount.mantissa());

    canonicalize();
}

inline STAmount::STAmount(MPTAmount const& amount, MPTIssue const& mptIssue)
    : mAsset(mptIssue), mOffset(0), mIsNegative(amount < beast::zero)
{
    if (mIsNegative)
        mValue = unsafe_cast<std::uint64_t>(-amount.value());
    else
        mValue = unsafe_cast<std::uint64_t>(amount.value());

    canonicalize();
}

//------------------------------------------------------------------------------
//
// Creation
//
//------------------------------------------------------------------------------

// VFALCO TODO The parameter type should be Quality not uint64_t
STAmount
amountFromQuality(std::uint64_t rate);

STAmount
amountFromString(Asset const& asset, std::string const& amount);

STAmount
amountFromJson(SField const& name, Json::Value const& v);

bool
amountFromJsonNoThrow(STAmount& result, Json::Value const& jvSource);

// IOUAmount and XRPAmount define toSTAmount, defining this
// trivial conversion here makes writing generic code easier
inline STAmount const&
toSTAmount(STAmount const& a)
{
    return a;
}

//------------------------------------------------------------------------------
//
// Observers
//
//------------------------------------------------------------------------------

inline int
STAmount::exponent() const noexcept
{
    return mOffset;
}

inline bool
STAmount::integral() const noexcept
{
    return mAsset.integral();
}

inline bool
STAmount::native() const noexcept
{
    return mAsset.native();
}

template <ValidIssueType TIss>
constexpr bool
STAmount::holds() const noexcept
{
    return mAsset.holds<TIss>();
}

inline bool
STAmount::negative() const noexcept
{
    return mIsNegative;
}

inline std::uint64_t
STAmount::mantissa() const noexcept
{
    return mValue;
}

inline Asset const&
STAmount::asset() const
{
    return mAsset;
}

template <ValidIssueType TIss>
constexpr TIss const&
STAmount::get() const
{
    return mAsset.get<TIss>();
}

inline Issue const&
STAmount::issue() const
{
    return get<Issue>();
}

inline Currency const&
STAmount::getCurrency() const
{
    return mAsset.get<Issue>().currency;
}

inline AccountID const&
STAmount::getIssuer() const
{
    return mAsset.getIssuer();
}

inline int
STAmount::signum() const noexcept
{
    return mValue ? (mIsNegative ? -1 : 1) : 0;
}

inline STAmount
STAmount::zeroed() const
{
    return STAmount(mAsset);
}

inline STAmount::
operator bool() const noexcept
{
    return *this != beast::zero;
}

inline STAmount::
operator Number() const
{
    if (native())
        return xrp();
    if (mAsset.holds<MPTIssue>())
        return mpt();
    return iou();
}

inline STAmount&
STAmount::operator=(beast::Zero)
{
    clear();
    return *this;
}

inline STAmount&
STAmount::operator=(XRPAmount const& amount)
{
    *this = STAmount(amount);
    return *this;
}

template <AssetType A>
inline STAmount
STAmount::fromNumber(A const& a, Number const& number)
{
    bool const negative = number.mantissa() < 0;
    Number const working{negative ? -number : number};
    Asset asset{a};
    if (asset.integral())
    {
        std::uint64_t const intValue = static_cast<std::int64_t>(working);
        return STAmount{asset, intValue, 0, negative};
    }

    auto const [mantissa, exponent] = working.normalizeToRange(cMinValue, cMaxValue);

    return STAmount{asset, mantissa, exponent, negative};
}

inline void
STAmount::negate()
{
    if (*this != beast::zero)
        mIsNegative = !mIsNegative;
}

inline void
STAmount::clear()
{
    // The -100 is used to allow 0 to sort less than a small positive values
    // which have a negative exponent.
    mOffset = integral() ? 0 : -100;
    mValue = 0;
    mIsNegative = false;
}

inline void
STAmount::clear(Asset const& asset)
{
    setIssue(asset);
    clear();
}

inline void
STAmount::setIssuer(AccountID const& uIssuer)
{
    mAsset.get<Issue>().account = uIssuer;
}

inline STAmount const&
STAmount::value() const noexcept
{
    return *this;
}

inline bool
isLegalNet(STAmount const& value)
{
    return !value.native() || (value.mantissa() <= STAmount::cMaxNativeN);
}

//------------------------------------------------------------------------------
//
// Operators
//
//------------------------------------------------------------------------------

bool
operator==(STAmount const& lhs, STAmount const& rhs);
bool
operator<(STAmount const& lhs, STAmount const& rhs);

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

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

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

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

STAmount
operator-(STAmount const& value);

//------------------------------------------------------------------------------
//
// Arithmetic
//
//------------------------------------------------------------------------------

STAmount
operator+(STAmount const& v1, STAmount const& v2);
STAmount
operator-(STAmount const& v1, STAmount const& v2);

STAmount
divide(STAmount const& v1, STAmount const& v2, Asset const& asset);

STAmount
multiply(STAmount const& v1, STAmount const& v2, Asset const& asset);

// multiply rounding result in specified direction
STAmount
mulRound(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp);

// multiply following the rounding directions more precisely.
STAmount
mulRoundStrict(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp);

// divide rounding result in specified direction
STAmount
divRound(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp);

// divide following the rounding directions more precisely.
STAmount
divRoundStrict(STAmount const& v1, STAmount const& v2, Asset const& asset, bool roundUp);

// Someone is offering X for Y, what is the rate?
// Rate: smaller is better, the taker wants the most out: in/out
// VFALCO TODO Return a Quality object
std::uint64_t
getRate(STAmount const& offerOut, STAmount const& offerIn);

/** Round an arbitrary precision Amount to the precision of an STAmount that has
 * a given exponent.
 *
 * This is used to ensure that calculations involving IOU amounts do not collect
 * dust beyond the precision of the reference value.
 *
 * @param value The value to be rounded
 * @param scale An exponent value to establish the precision limit of
 *     `value`. Should be larger than `value.exponent()`.
 * @param rounding Optional Number rounding mode
 *
 */
[[nodiscard]] STAmount
roundToScale(
    STAmount const& value,
    std::int32_t scale,
    Number::rounding_mode rounding = Number::getround());

/** Round an arbitrary precision Number IN PLACE to the precision of a given
 * Asset.
 *
 * This is used to ensure that calculations do not collect dust for IOUs, or
 * fractional amounts for the integral types XRP and MPT.
 *
 * @param asset The relevant asset
 * @param value The lvalue to be rounded
 */
template <AssetType A>
void
roundToAsset(A const& asset, Number& value)
{
    value = STAmount{asset, value};
}

/** Round an arbitrary precision Number to the precision of a given Asset.
 *
 * This is used to ensure that calculations do not collect dust beyond specified
 * scale for IOUs, or fractional amounts for the integral types XRP and MPT.
 *
 * @param asset The relevant asset
 * @param value The value to be rounded
 * @param scale Only relevant to IOU assets. An exponent value to establish the
 *      precision limit of `value`. Should be larger than `value.exponent()`.
 * @param rounding Optional Number rounding mode
 */
template <AssetType A>
[[nodiscard]] Number
roundToAsset(
    A const& asset,
    Number const& value,
    std::int32_t scale,
    Number::rounding_mode rounding = Number::getround())
{
    NumberRoundModeGuard mg(rounding);
    STAmount const ret{asset, value};
    if (ret.integral())
        return ret;
    // Note that the ctor will round integral types (XRP, MPT) via canonicalize,
    // so no extra work is needed for those.
    return roundToScale(ret, scale);
}

//------------------------------------------------------------------------------

inline bool
isXRP(STAmount const& amount)
{
    return amount.native();
}

bool
canAdd(STAmount const& amt1, STAmount const& amt2);

bool
canSubtract(STAmount const& amt1, STAmount const& amt2);

}  // namespace xrpl

//------------------------------------------------------------------------------
namespace Json {
template <>
inline xrpl::STAmount
getOrThrow(Json::Value const& v, xrpl::SField const& field)
{
    using namespace xrpl;
    Json::StaticString const& key = field.getJsonName();
    if (!v.isMember(key))
        Throw<JsonMissingKeyError>(key);
    Json::Value const& inner = v[key];
    return amountFromJson(field, inner);
}
}  // namespace Json
