//------------------------------------------------------------------------------
/*
    This file is part of clio: https://github.com/XRPLF/clio
    Copyright (c) 2023, the clio developers.

    Permission to use, copy, modify, and distribute this software for any
    purpose with or without fee is hereby granted, provided that the above
    copyright notice and this permission notice appear in all copies.

    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================

/** @file */
#pragma once

#include "data/Types.hpp"
#include "rpc/JS.hpp"
#include "rpc/RPCHelpers.hpp"

#include <boost/json/conversion.hpp>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/jss.h>

#include <algorithm>
#include <cstdint>
#include <iterator>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <vector>

namespace rpc {

/**
 * @brief Represents an entry in the book_changes' changes array.
 */
struct BookChange {
    ripple::STAmount sideAVolume;
    ripple::STAmount sideBVolume;
    ripple::STAmount highRate;
    ripple::STAmount lowRate;
    ripple::STAmount openRate;
    ripple::STAmount closeRate;
    std::optional<ripple::uint256> domain;
};

/**
 * @brief Encapsulates the book_changes computations and transformations.
 */
class BookChanges final {
public:
    BookChanges() = delete;  // only accessed via static handle function

    /**
     * @brief Computes all book_changes for the given transactions.
     *
     * @param transactions The transactions to compute book changes for
     * @return Book changes
     */
    [[nodiscard]] static std::vector<BookChange>
    compute(std::vector<data::TransactionAndMetadata> const& transactions)
    {
        return HandlerImpl{}(transactions);
    }

private:
    class HandlerImpl final {
        std::map<std::string, BookChange> tally_;
        std::optional<uint32_t> offerCancel_;

    public:
        [[nodiscard]] std::vector<BookChange>
        operator()(std::vector<data::TransactionAndMetadata> const& transactions)
        {
            for (auto const& tx : transactions)
                handleBookChange(tx);

            // TODO: rewrite this with std::ranges when compilers catch up
            std::vector<BookChange> changes;
            std::transform(
                std::make_move_iterator(std::begin(tally_)),
                std::make_move_iterator(std::end(tally_)),
                std::back_inserter(changes),
                [](auto obj) { return obj.second; }
            );
            return changes;
        }

    private:
        void
        handleAffectedNode(ripple::STObject const& node)
        {
            auto const& metaType = node.getFName();
            auto const nodeType = node.getFieldU16(ripple::sfLedgerEntryType);

            // we only care about ripple::ltOFFER objects being modified or
            // deleted
            if (nodeType != ripple::ltOFFER || metaType == ripple::sfCreatedNode)
                return;

            // if either FF or PF are missing we can't compute
            // but generally these are cancelled rather than crossed
            // so skipping them is consistent
            if (!node.isFieldPresent(ripple::sfFinalFields) || !node.isFieldPresent(ripple::sfPreviousFields))
                return;

            auto const& finalFields = node.peekAtField(ripple::sfFinalFields).downcast<ripple::STObject>();
            auto const& previousFields = node.peekAtField(ripple::sfPreviousFields).downcast<ripple::STObject>();

            // defensive case that should never be hit
            if (!finalFields.isFieldPresent(ripple::sfTakerGets) || !finalFields.isFieldPresent(ripple::sfTakerPays) ||
                !previousFields.isFieldPresent(ripple::sfTakerGets) ||
                !previousFields.isFieldPresent(ripple::sfTakerPays))
                return;

            // filter out any offers deleted by explicit offer cancels
            if (metaType == ripple::sfDeletedNode && offerCancel_ &&
                finalFields.getFieldU32(ripple::sfSequence) == *offerCancel_)
                return;

            // compute the difference in gets and pays actually
            // affected onto the offer
            auto const deltaGets =
                finalFields.getFieldAmount(ripple::sfTakerGets) - previousFields.getFieldAmount(ripple::sfTakerGets);
            auto const deltaPays =
                finalFields.getFieldAmount(ripple::sfTakerPays) - previousFields.getFieldAmount(ripple::sfTakerPays);

            transformAndStore(deltaGets, deltaPays, finalFields[~ripple::sfDomainID]);
        }

        void
        transformAndStore(
            ripple::STAmount const& deltaGets,
            ripple::STAmount const& deltaPays,
            std::optional<ripple::uint256> const& domain
        )
        {
            auto const g = to_string(deltaGets.issue());
            auto const p = to_string(deltaPays.issue());

            auto const noswap = [&]() {
                if (isXRP(deltaGets))
                    return true;
                return isXRP(deltaPays) ? false : (g < p);
            }();

            auto first = noswap ? deltaGets : deltaPays;
            auto second = noswap ? deltaPays : deltaGets;

            // defensively programmed, should (probably) never happen
            if (second == beast::zero)
                return;

            auto const rate = divide(first, second, ripple::noIssue());

            if (first < beast::zero)
                first = -first;

            if (second < beast::zero)
                second = -second;

            auto const key = noswap ? (g + '|' + p) : (p + '|' + g);
            if (tally_.contains(key)) {
                auto& entry = tally_.at(key);

                entry.sideAVolume += first;
                entry.sideBVolume += second;

                if (entry.highRate < rate)
                    entry.highRate = rate;

                if (entry.lowRate > rate)
                    entry.lowRate = rate;

                entry.closeRate = rate;
                entry.domain = domain;
            } else {
                tally_[key] = {
                    .sideAVolume = first,
                    .sideBVolume = second,
                    .highRate = rate,
                    .lowRate = rate,
                    .openRate = rate,
                    .closeRate = rate,
                    .domain = domain,
                };
            }
        }

        void
        handleBookChange(data::TransactionAndMetadata const& blob)
        {
            auto const [tx, meta] = rpc::deserializeTxPlusMeta(blob);
            if (!tx || !meta || !tx->isFieldPresent(ripple::sfTransactionType))
                return;

            offerCancel_ = shouldCancelOffer(tx);
            for (auto const& node : meta->getFieldArray(ripple::sfAffectedNodes))
                handleAffectedNode(node);
        }

        static std::optional<uint32_t>
        shouldCancelOffer(std::shared_ptr<ripple::STTx const> const& tx)
        {
            switch (tx->getFieldU16(ripple::sfTransactionType)) {
                // in future if any other ways emerge to cancel an offer
                // this switch makes them easy to add
                case ripple::ttOFFER_CANCEL:
                case ripple::ttOFFER_CREATE:
                    if (tx->isFieldPresent(ripple::sfOfferSequence))
                        return tx->getFieldU32(ripple::sfOfferSequence);
                    [[fallthrough]];
                default:
                    return std::nullopt;
            }
        }
    };
};

/**
 * @brief Implementation of value_from for BookChange type.
 *
 * @param [out] jv The JSON value to populate
 * @param change The BookChange to serialize
 */
inline void
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, BookChange const& change)
{
    auto amountStr = [](ripple::STAmount const& amount) -> std::string {
        return isXRP(amount) ? to_string(amount.xrp()) : to_string(amount.iou());
    };

    auto currencyStr = [](ripple::STAmount const& amount) -> std::string {
        return isXRP(amount) ? "XRP_drops" : to_string(amount.issue());
    };

    jv = {
        {JS(currency_a), currencyStr(change.sideAVolume)},
        {JS(currency_b), currencyStr(change.sideBVolume)},
        {JS(volume_a), amountStr(change.sideAVolume)},
        {JS(volume_b), amountStr(change.sideBVolume)},
        {JS(high), to_string(change.highRate.iou())},
        {JS(low), to_string(change.lowRate.iou())},
        {JS(open), to_string(change.openRate.iou())},
        {JS(close), to_string(change.closeRate.iou())},
    };

    if (change.domain.has_value())
        jv.as_object()[JS(domain)] = ripple::to_string(*change.domain);
}

/**
 * @brief Computes all book changes for the given ledger header and transactions.
 *
 * @param lgrInfo The ledger header
 * @param transactions The vector of transactions with heir metadata
 * @return The book changes
 */
[[nodiscard]] boost::json::object
computeBookChanges(ripple::LedgerHeader const& lgrInfo, std::vector<data::TransactionAndMetadata> const& transactions);

}  // namespace rpc
