"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.transitiveOrderbook = exports.Orderbook = exports.Offer = void 0;
const fraction_1 = require("./fraction");
class Offer {
    constructor(price, volume) {
        if (typeof volume == "number") {
            this.volume = new fraction_1.Fraction(volume, 1);
        }
        else {
            this.volume = volume;
        }
        this.price = price;
    }
    clone() {
        return new Offer(this.price.clone(), this.volume.clone());
    }
    static fromJSON(o) {
        return new Offer(fraction_1.Fraction.fromJSON(o.price), fraction_1.Fraction.fromJSON(o.volume));
    }
}
exports.Offer = Offer;
class Orderbook {
    constructor(baseToken, quoteToken, options = { fee: new fraction_1.Fraction(1, 1000) }) {
        this.baseToken = baseToken.toString();
        this.quoteToken = quoteToken.toString();
        if ("fee" in options) {
            this.remainingFractionAfterFee = new fraction_1.Fraction(1, 1).sub(options.fee);
        }
        else {
            this.remainingFractionAfterFee = options.remainingFractionAfterFee;
        }
        this.asks = new Map();
        this.bids = new Map();
    }
    getOffers() {
        const asks = Array.from(this.asks.values());
        const bids = Array.from(this.bids.values());
        asks.sort(sortOffersAscending);
        bids.sort(sortOffersDescending);
        return { bids, asks };
    }
    toJSON() {
        return {
            baseToken: this.baseToken,
            quoteToken: this.quoteToken,
            remainingFractionAfterFee: this.remainingFractionAfterFee,
            asks: offersToJSON(this.asks),
            bids: offersToJSON(this.bids),
        };
    }
    static fromJSON(o) {
        const remainingFractionAfterFee = fraction_1.Fraction.fromJSON(o.remainingFractionAfterFee);
        const result = new Orderbook(o.baseToken, o.quoteToken, {
            remainingFractionAfterFee,
        });
        result.asks = offersFromJSON(o.asks);
        result.bids = offersFromJSON(o.bids);
        return result;
    }
    pair() {
        return `${this.baseToken}/${this.quoteToken}`;
    }
    addBid(bid) {
        // For bids the effective price after fee becomes smaller
        const offer = new Offer(bid.price.mul(this.remainingFractionAfterFee), bid.volume.mul(this.remainingFractionAfterFee));
        addOffer(offer, this.bids);
    }
    addAsk(ask) {
        // For asks the effective price after fee becomes larger
        const offer = new Offer(ask.price.div(this.remainingFractionAfterFee), ask.volume.mul(this.remainingFractionAfterFee));
        addOffer(offer, this.asks);
    }
    /**
     * @returns the inverse of the current order book (e.g. ETH/DAI becomes DAI/ETH)
     * by switching bids/asks and recomputing price/volume to the new reference token.
     */
    inverted() {
        const result = new Orderbook(this.quoteToken, this.baseToken, {
            fee: this.fee(),
        });
        result.bids = invertPricePoints(this.asks, this.remainingFractionAfterFee);
        result.asks = invertPricePoints(this.bids, this.remainingFractionAfterFee.inverted());
        return result;
    }
    /**
     * In-place adds the given orderbook to the current one, combining all bids and asks at the same price point
     * @param orderbook - the orderbook to be added to this one
     */
    add(orderbook) {
        if (orderbook.pair() != this.pair()) {
            throw new Error(`Cannot add ${orderbook.pair()} orderbook to ${this.pair()} orderbook`);
        }
        orderbook.bids.forEach((bid) => {
            addOffer(bid, this.bids);
        });
        orderbook.asks.forEach((ask) => {
            addOffer(ask, this.asks);
        });
    }
    /**
     * @param amount - the amount of base tokens to be sold
     * @returns the price for which there are enough bids to fill the specified amount or undefined if there is not enough liquidity
     */
    priceToSellBaseToken(amount) {
        const bids = Array.from(this.bids.values());
        bids.sort(sortOffersDescending);
        const price_before_fee = priceToCoverAmount(new fraction_1.Fraction(amount, 1), bids);
        // Price to sell base token after fee will be lower
        return price_before_fee === null || price_before_fee === void 0 ? void 0 : price_before_fee.mul(this.remainingFractionAfterFee);
    }
    /**
     * @param amount - the amount of base tokens to be bought
     * @returns the price for which there are enough asks to fill the specified amount or undefined if there is not enough liquidity
     */
    priceToBuyBaseToken(amount) {
        const asks = Array.from(this.asks.values());
        asks.sort(sortOffersAscending);
        const price_before_fee = priceToCoverAmount(new fraction_1.Fraction(amount, 1), asks);
        // Price to buy base token after fee will be higher
        return price_before_fee === null || price_before_fee === void 0 ? void 0 : price_before_fee.div(this.remainingFractionAfterFee);
    }
    /**
     * Removes any overlapping bid/asks which could be matched in the current orderbook
     * @returns A new instance of the orderbook with no more overlapping orders.
     */
    reduced() {
        const result = new Orderbook(this.baseToken, this.quoteToken);
        const bids = Array.from(this.bids.values());
        bids.sort(sortOffersDescending);
        const asks = Array.from(this.asks.values());
        asks.sort(sortOffersAscending);
        const bid_iterator = bids.values();
        const ask_iterator = asks.values();
        let best_bid = bid_iterator.next();
        let best_ask = ask_iterator.next();
        while (!(best_bid.done || best_ask.done) &&
            !best_bid.value.price.lt(best_ask.value.price)) {
            // We have an overlapping bid/ask. Subtract the smaller from the larger and remove the smaller
            if (best_bid.value.volume.gt(best_ask.value.volume)) {
                best_bid.value = new Offer(best_bid.value.price, best_bid.value.volume.sub(best_ask.value.volume));
                best_ask = ask_iterator.next();
            }
            else {
                best_ask.value = new Offer(best_ask.value.price, best_ask.value.volume.sub(best_bid.value.volume));
                best_bid = bid_iterator.next();
                // In case the orders matched perfectly we will move ask as well
                if (best_ask.value.volume.isZero()) {
                    best_ask = ask_iterator.next();
                }
            }
        }
        //Add remaining bids/asks to result
        while (!best_ask.done) {
            addOffer(best_ask.value, result.asks);
            best_ask = ask_iterator.next();
        }
        while (!best_bid.done) {
            addOffer(best_bid.value, result.bids);
            best_bid = bid_iterator.next();
        }
        return result;
    }
    /**
     * Computes the transitive closure of this orderbook (e.g. ETH/DAI) with another one (e.g. DAI/USDC).
     * Throws if the orderbooks cannot be combined (baseToken is not equal to quoteToken)
     * @param orderbook - The orderbook for which the transitive closure will be computed
     * @returns A new instance of an orderbook representing the resulting closure.
     */
    transitiveClosure(orderbook) {
        if (orderbook.baseToken != this.quoteToken) {
            throw new Error(`Cannot compute transitive closure of ${this.pair()} orderbook and ${orderbook.pair()} orderbook`);
        }
        const ask_closure = this.transitiveAskClosure(orderbook);
        // Since bids are the asks of the inverted orderbook, computing transitive closure of bids is equivalent to
        // 1) inverting both orderbooks
        // 2) computing the transitive ask closure on the inverses
        // 3) re-inverting the result
        const bid_closure = orderbook
            .inverted()
            .transitiveAskClosure(this.inverted())
            .inverted();
        ask_closure.add(bid_closure);
        return ask_closure;
    }
    transitiveAskClosure(orderbook) {
        const result = new Orderbook(this.baseToken, orderbook.quoteToken, {
            fee: this.fee(),
        });
        // Create a copy here so original orders stay untouched
        const left_asks = Array.from(this.asks.values(), (o) => o.clone());
        const right_asks = Array.from(orderbook.asks.values(), (o) => o.clone());
        left_asks.sort(sortOffersAscending);
        right_asks.sort(sortOffersAscending);
        const left_iterator = left_asks.values();
        const right_iterator = right_asks.values();
        let right_next = right_iterator.next();
        let left_next = left_iterator.next();
        while (!(left_next.done || right_next.done)) {
            const right_offer = right_next.value;
            const left_offer = left_next.value;
            const price = left_offer.price.mul(right_offer.price);
            let volume;
            const right_offer_volume_in_left_offer_base_token = right_offer.volume.div(left_offer.price);
            if (left_offer.volume.gt(right_offer_volume_in_left_offer_base_token)) {
                volume = right_offer_volume_in_left_offer_base_token;
                left_offer.volume = left_offer.volume.sub(volume);
                right_next = right_iterator.next();
            }
            else {
                volume = left_offer.volume;
                right_offer.volume = right_offer.volume.sub(volume.mul(left_offer.price));
                left_next = left_iterator.next();
                // In case the orders matched perfectly we will move right as well
                if (right_offer.volume.isZero()) {
                    right_next = right_iterator.next();
                }
            }
            addOffer(new Offer(price, volume), result.asks);
        }
        return result;
    }
    fee() {
        return new fraction_1.Fraction(1, 1).sub(this.remainingFractionAfterFee);
    }
    clone() {
        const result = new Orderbook(this.baseToken, this.quoteToken, {
            fee: this.fee(),
        });
        this.bids.forEach((o) => addOffer(o, result.bids));
        this.asks.forEach((o) => addOffer(o, result.asks));
        return result;
    }
}
exports.Orderbook = Orderbook;
/**
 * Given a list of direct orderbooks this method returns the transitive orderbook
 * between two tokens by computing the transitive closure via a certain number of "hops".
 * @param direct_orderbooks - the map direct (non-transitive) orderbooks between tokens
 * @param base - the base token for which the transitive orderbook should be computed
 * @param quote - the quote token for which the transitive orderbook should be computed
 * @param hops - the number of intermediate tokens that should be considered when computing the transitive orderbook
 */
function transitiveOrderbook(direct_orderbooks, base, quote, hops) {
    const complete_orderbooks = new Map();
    direct_orderbooks.forEach((book) => {
        complete_orderbooks.set(book.pair(), book.clone());
        // If inverse pair doesn't exist we will create an empty one
        if (!direct_orderbooks.has(book.inverted().pair())) {
            const empty_book = new Orderbook(book.quoteToken, book.baseToken);
            complete_orderbooks.set(empty_book.pair(), empty_book);
        }
    });
    // Merge bid/ask orderbooks
    complete_orderbooks.forEach((book, pair) => {
        const inverse = book.inverted();
        const inverse_pair = inverse.pair();
        // Only update one of the two sides
        if (pair > inverse_pair) {
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            complete_orderbooks.get(inverse_pair).add(inverse);
            complete_orderbooks.set(pair, 
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            complete_orderbooks.get(inverse_pair).inverted());
        }
    });
    return transitiveOrderbookRecursive(complete_orderbooks, base, quote, hops, []);
}
exports.transitiveOrderbook = transitiveOrderbook;
function transitiveOrderbookRecursive(orderbooks, base, quote, hops, ignore) {
    const result = new Orderbook(base, quote);
    // Add the direct book if it exists
    const orderbook = orderbooks.get(result.pair());
    if (orderbook) {
        result.add(orderbook);
    }
    if (hops === 0) {
        return result;
    }
    // Check for each orderbook that starts with same baseToken, if there exists a connecting book.
    // If yes, build transitive closure
    orderbooks.forEach((book) => {
        if (book.baseToken === base &&
            !(book.quoteToken === quote) &&
            !ignore.includes(book.quoteToken)) {
            const otherBook = transitiveOrderbookRecursive(orderbooks, book.quoteToken, quote, hops - 1, ignore.concat(book.baseToken));
            const closure = book.transitiveClosure(otherBook);
            result.add(closure);
        }
    });
    return result;
}
function addOffer(offer, existingOffers) {
    const price = offer.price.toNumber();
    let current_offer_at_price;
    let current_volume_at_price = new fraction_1.Fraction(0, 1);
    if ((current_offer_at_price = existingOffers.get(price))) {
        current_volume_at_price = current_offer_at_price.volume;
    }
    existingOffers.set(price, new Offer(offer.price, offer.volume.add(current_volume_at_price)));
}
function sortOffersAscending(left, right) {
    if (left.price.gt(right.price)) {
        return 1;
    }
    else if (left.price.lt(right.price)) {
        return -1;
    }
    else {
        return 0;
    }
}
function sortOffersDescending(left, right) {
    return sortOffersAscending(left, right) * -1;
}
function priceToCoverAmount(amount, offers) {
    for (const offer of offers) {
        if (offer.volume.lt(amount)) {
            amount = amount.sub(offer.volume);
        }
        else {
            return offer.price;
        }
    }
    return undefined;
}
function invertPricePoints(prices, priceAdjustmentForFee) {
    return new Map(Array.from(prices.entries()).map(([, offer]) => {
        const inverted_price = offer.price.inverted();
        const price_before_fee = offer.price.mul(priceAdjustmentForFee);
        const inverted_volume = offer.volume.mul(price_before_fee);
        return [
            inverted_price.toNumber(),
            new Offer(inverted_price, inverted_volume),
        ];
    }));
}
function offersFromJSON(o) {
    const offers = new Map();
    for (const [key, value] of Object.entries(o)) {
        offers.set(key, Offer.fromJSON(value));
    }
    return offers;
}
function offersToJSON(offers) {
    const o = {};
    offers.forEach((value, key) => {
        o[key] = value;
    });
    return o;
}
