import invariant from "tiny-invariant";
import { ONE, TradeType, ZERO } from "../constants";
import { sortedInsert } from "../utils";
import { ETHER } from "./currency";
import { CurrencyAmount } from "./fractions/currencyAmount";
import { Fraction } from "./fractions/fraction";
import { Percent } from "./fractions/percent";
import { Price } from "./fractions/price";
import { TokenAmount } from "./fractions/tokenAmount";
import { Route } from "./route";
import { currencyEquals, Token, WETH } from "./token";
/**
 * Returns the percent difference between the mid price and the execution price, i.e. price impact.
 * @param midPrice mid price before the trade
 * @param inputAmount the input amount of the trade
 * @param outputAmount the output amount of the trade
 */
function computePriceImpact(midPrice, inputAmount, outputAmount) {
    const exactQuote = midPrice.raw.multiply(inputAmount.raw);
    // calculate slippage := (exactQuote - outputAmount) / exactQuote
    const slippage = exactQuote.subtract(outputAmount.raw).divide(exactQuote);
    return new Percent(slippage.numerator, slippage.denominator);
}
// comparator function that allows sorting trades by their output amounts, in decreasing order, and then input amounts
// in increasing order. i.e. the best trades have the most outputs for the least inputs and are sorted first
export function inputOutputComparator(a, b) {
    // must have same input and output token for comparison
    invariant(currencyEquals(a.inputAmount.currency, b.inputAmount.currency), "INPUT_CURRENCY");
    invariant(currencyEquals(a.outputAmount.currency, b.outputAmount.currency), "OUTPUT_CURRENCY");
    if (a.outputAmount.equalTo(b.outputAmount)) {
        if (a.inputAmount.equalTo(b.inputAmount)) {
            return 0;
        }
        // trade A requires less input than trade B, so A should come first
        if (a.inputAmount.lessThan(b.inputAmount)) {
            return -1;
        }
        else {
            return 1;
        }
    }
    else {
        // tradeA has less output than trade B, so should come second
        if (a.outputAmount.lessThan(b.outputAmount)) {
            return 1;
        }
        else {
            return -1;
        }
    }
}
// extension of the input output comparator that also considers other dimensions of the trade in ranking them
export function tradeComparator(a, b) {
    const ioComp = inputOutputComparator(a, b);
    if (ioComp !== 0) {
        return ioComp;
    }
    // consider lowest slippage next, since these are less likely to fail
    if (a.priceImpact.lessThan(b.priceImpact)) {
        return -1;
    }
    else if (a.priceImpact.greaterThan(b.priceImpact)) {
        return 1;
    }
    // finally consider the number of hops since each hop costs gas
    return a.route.path.length - b.route.path.length;
}
/**
 * Given a currency amount and a chain ID, returns the equivalent representation as the token amount.
 * In other words, if the currency is ETHER, returns the WETH token amount for the given chain. Otherwise, returns
 * the input currency amount.
 */
function wrappedAmount(currencyAmount, chainId) {
    if (currencyAmount instanceof TokenAmount)
        return currencyAmount;
    if (currencyAmount.currency === ETHER)
        return new TokenAmount(WETH[chainId], currencyAmount.raw);
    invariant(false, "CURRENCY");
}
function wrappedCurrency(currency, chainId) {
    if (currency instanceof Token)
        return currency;
    if (currency === ETHER)
        return WETH[chainId];
    invariant(false, "CURRENCY");
}
/**
 * Represents a trade executed against a list of pairs.
 * Does not account for slippage, i.e. trades that front run this trade and move the price.
 */
export class Trade {
    constructor(route, amount, tradeType) {
        const amounts = new Array(route.path.length);
        const nextPairs = new Array(route.pairs.length);
        if (tradeType === TradeType.EXACT_INPUT) {
            invariant(currencyEquals(amount.currency, route.input), "INPUT");
            amounts[0] = wrappedAmount(amount, route.chainId);
            for (let i = 0; i < route.path.length - 1; i++) {
                const pair = route.pairs[i];
                const [outputAmount, nextPair] = pair.getOutputAmount(amounts[i]);
                amounts[i + 1] = outputAmount;
                nextPairs[i] = nextPair;
            }
        }
        else {
            invariant(currencyEquals(amount.currency, route.output), "OUTPUT");
            amounts[amounts.length - 1] = wrappedAmount(amount, route.chainId);
            for (let i = route.path.length - 1; i > 0; i--) {
                const pair = route.pairs[i - 1];
                const [inputAmount, nextPair] = pair.getInputAmount(amounts[i]);
                amounts[i - 1] = inputAmount;
                nextPairs[i - 1] = nextPair;
            }
        }
        this.route = route;
        this.tradeType = tradeType;
        this.inputAmount =
            tradeType === TradeType.EXACT_INPUT
                ? amount
                : route.input === ETHER
                    ? CurrencyAmount.ether(amounts[0].raw)
                    : amounts[0];
        this.outputAmount =
            tradeType === TradeType.EXACT_OUTPUT
                ? amount
                : route.output === ETHER
                    ? CurrencyAmount.ether(amounts[amounts.length - 1].raw)
                    : amounts[amounts.length - 1];
        this.executionPrice = new Price(this.inputAmount.currency, this.outputAmount.currency, this.inputAmount.raw, this.outputAmount.raw);
        this.nextMidPrice = Price.fromRoute(new Route(nextPairs, route.input));
        this.priceImpact = computePriceImpact(route.midPrice, this.inputAmount, this.outputAmount);
    }
    /**
     * Constructs an exact in trade with the given amount in and route
     * @param route route of the exact in trade
     * @param amountIn the amount being passed in
     */
    static exactIn(route, amountIn) {
        return new Trade(route, amountIn, TradeType.EXACT_INPUT);
    }
    /**
     * Constructs an exact out trade with the given amount out and route
     * @param route route of the exact out trade
     * @param amountOut the amount returned by the trade
     */
    static exactOut(route, amountOut) {
        return new Trade(route, amountOut, TradeType.EXACT_OUTPUT);
    }
    /**
     * Get the minimum amount that must be received from this trade for the given slippage tolerance
     * @param slippageTolerance tolerance of unfavorable slippage from the execution price of this trade
     */
    minimumAmountOut(slippageTolerance) {
        invariant(!slippageTolerance.lessThan(ZERO), "SLIPPAGE_TOLERANCE");
        if (this.tradeType === TradeType.EXACT_OUTPUT) {
            return this.outputAmount;
        }
        else {
            const slippageAdjustedAmountOut = new Fraction(ONE)
                .add(slippageTolerance)
                .invert()
                .multiply(this.outputAmount.raw).quotient;
            return this.outputAmount instanceof TokenAmount
                ? new TokenAmount(this.outputAmount.token, slippageAdjustedAmountOut)
                : CurrencyAmount.ether(slippageAdjustedAmountOut);
        }
    }
    /**
     * Get the maximum amount in that can be spent via this trade for the given slippage tolerance
     * @param slippageTolerance tolerance of unfavorable slippage from the execution price of this trade
     */
    maximumAmountIn(slippageTolerance) {
        invariant(!slippageTolerance.lessThan(ZERO), "SLIPPAGE_TOLERANCE");
        if (this.tradeType === TradeType.EXACT_INPUT) {
            return this.inputAmount;
        }
        else {
            const slippageAdjustedAmountIn = new Fraction(ONE).add(slippageTolerance).multiply(this.inputAmount.raw).quotient;
            return this.inputAmount instanceof TokenAmount
                ? new TokenAmount(this.inputAmount.token, slippageAdjustedAmountIn)
                : CurrencyAmount.ether(slippageAdjustedAmountIn);
        }
    }
    /**
     * Given a list of pairs, and a fixed amount in, returns the top `maxNumResults` trades that go from an input token
     * amount to an output token, making at most `maxHops` hops.
     * Note this does not consider aggregation, as routes are linear. It's possible a better route exists by splitting
     * the amount in among multiple routes.
     * @param pairs the pairs to consider in finding the best trade
     * @param currencyAmountIn exact amount of input currency to spend
     * @param currencyOut the desired currency out
     * @param maxNumResults maximum number of results to return
     * @param maxHops maximum number of hops a returned trade can make, e.g. 1 hop goes through a single pair
     * @param currentPairs used in recursion; the current list of pairs
     * @param originalAmountIn used in recursion; the original value of the currencyAmountIn parameter
     * @param bestTrades used in recursion; the current list of best trades
     */
    static bestTradeExactIn(pairs, currencyAmountIn, currencyOut, { maxNumResults = 3, maxHops = 3 } = {}, 
    // used in recursion.
    currentPairs = [], originalAmountIn = currencyAmountIn, bestTrades = []) {
        invariant(pairs.length > 0, "PAIRS");
        invariant(maxHops > 0, "MAX_HOPS");
        invariant(originalAmountIn === currencyAmountIn || currentPairs.length > 0, "INVALID_RECURSION");
        const chainId = currencyAmountIn instanceof TokenAmount
            ? currencyAmountIn.token.chainId
            : currencyOut instanceof Token
                ? currencyOut.chainId
                : undefined;
        invariant(chainId !== undefined, "CHAIN_ID");
        const amountIn = wrappedAmount(currencyAmountIn, chainId);
        const tokenOut = wrappedCurrency(currencyOut, chainId);
        for (let i = 0; i < pairs.length; i++) {
            const pair = pairs[i];
            // pair irrelevant
            if (!pair.token0.equals(amountIn.token) && !pair.token1.equals(amountIn.token))
                continue;
            if (pair.reserve0.equalTo(ZERO) || pair.reserve1.equalTo(ZERO))
                continue;
            let amountOut;
            try {
                [amountOut] = pair.getOutputAmount(amountIn);
            }
            catch (error) {
                // input too low
                if (error.isInsufficientInputAmountError) {
                    continue;
                }
                throw error;
            }
            // we have arrived at the output token, so this is the final trade of one of the paths
            if (amountOut.token.equals(tokenOut)) {
                sortedInsert(bestTrades, new Trade(new Route([...currentPairs, pair], originalAmountIn.currency, currencyOut), originalAmountIn, TradeType.EXACT_INPUT), maxNumResults, tradeComparator);
            }
            else if (maxHops > 1 && pairs.length > 1) {
                const pairsExcludingThisPair = pairs.slice(0, i).concat(pairs.slice(i + 1, pairs.length));
                // otherwise, consider all the other paths that lead from this token as long as we have not exceeded maxHops
                Trade.bestTradeExactIn(pairsExcludingThisPair, amountOut, currencyOut, {
                    maxNumResults,
                    maxHops: maxHops - 1,
                }, [...currentPairs, pair], originalAmountIn, bestTrades);
            }
        }
        return bestTrades;
    }
    /**
     * similar to the above method but instead targets a fixed output amount
     * given a list of pairs, and a fixed amount out, returns the top `maxNumResults` trades that go from an input token
     * to an output token amount, making at most `maxHops` hops
     * note this does not consider aggregation, as routes are linear. it's possible a better route exists by splitting
     * the amount in among multiple routes.
     * @param pairs the pairs to consider in finding the best trade
     * @param currencyIn the currency to spend
     * @param currencyAmountOut the exact amount of currency out
     * @param maxNumResults maximum number of results to return
     * @param maxHops maximum number of hops a returned trade can make, e.g. 1 hop goes through a single pair
     * @param currentPairs used in recursion; the current list of pairs
     * @param originalAmountOut used in recursion; the original value of the currencyAmountOut parameter
     * @param bestTrades used in recursion; the current list of best trades
     */
    static bestTradeExactOut(pairs, currencyIn, currencyAmountOut, { maxNumResults = 3, maxHops = 3 } = {}, 
    // used in recursion.
    currentPairs = [], originalAmountOut = currencyAmountOut, bestTrades = []) {
        invariant(pairs.length > 0, "PAIRS");
        invariant(maxHops > 0, "MAX_HOPS");
        invariant(originalAmountOut === currencyAmountOut || currentPairs.length > 0, "INVALID_RECURSION");
        const chainId = currencyAmountOut instanceof TokenAmount
            ? currencyAmountOut.token.chainId
            : currencyIn instanceof Token
                ? currencyIn.chainId
                : undefined;
        invariant(chainId !== undefined, "CHAIN_ID");
        const amountOut = wrappedAmount(currencyAmountOut, chainId);
        const tokenIn = wrappedCurrency(currencyIn, chainId);
        for (let i = 0; i < pairs.length; i++) {
            const pair = pairs[i];
            // pair irrelevant
            if (!pair.token0.equals(amountOut.token) && !pair.token1.equals(amountOut.token))
                continue;
            if (pair.reserve0.equalTo(ZERO) || pair.reserve1.equalTo(ZERO))
                continue;
            let amountIn;
            try {
                [amountIn] = pair.getInputAmount(amountOut);
            }
            catch (error) {
                // not enough liquidity in this pair
                if (error.isInsufficientReservesError) {
                    continue;
                }
                throw error;
            }
            // we have arrived at the input token, so this is the first trade of one of the paths
            if (amountIn.token.equals(tokenIn)) {
                sortedInsert(bestTrades, new Trade(new Route([pair, ...currentPairs], currencyIn, originalAmountOut.currency), originalAmountOut, TradeType.EXACT_OUTPUT), maxNumResults, tradeComparator);
            }
            else if (maxHops > 1 && pairs.length > 1) {
                const pairsExcludingThisPair = pairs.slice(0, i).concat(pairs.slice(i + 1, pairs.length));
                // otherwise, consider all the other paths that arrive at this token as long as we have not exceeded maxHops
                Trade.bestTradeExactOut(pairsExcludingThisPair, currencyIn, amountIn, {
                    maxNumResults,
                    maxHops: maxHops - 1,
                }, [pair, ...currentPairs], originalAmountOut, bestTrades);
            }
        }
        return bestTrades;
    }
}
