import { PublicKey } from "@solana/web3.js";
import { CURVE_DATA_ADDRESS, CurveChainData, FUNDS_PROGRAM_ID, FUNDS_PROGRAM_PDA, FundStateChainData, OraclePrice, RouteData, SWAP_FEE_ACCOUNT, TOKEN_LIST_ADDRESS, TokenPriceData, TokenSettings } from "./types";

export const BPS_DIVIDER = 10000;
export const WEIGHT_MULTIPLIER = 10000;

export function mul_div(a: number, b: number, c: number): number {
    if (c == 0) return 0;
    return Math.floor(a * b / c);
}

export function amount_to_usd_value(amount: number, decimals: number, price: number): number {
   return mul_div(amount, price, 10 ** decimals)
}

export function usd_value_to_amount(worth: number, decimals:number, price:number): number {
    return mul_div(worth, 10 ** decimals, price)
}

export function compute_value_of_sold_token(
    amount: number,
    token_settings: TokenSettings,
    price: OraclePrice,
    start_amount: number,
    target_amount: number,
    curve_data: TokenPriceData
): number {
    let current_amount = start_amount;
    let curve_offset = (start_amount > target_amount) ? (start_amount - target_amount) : 0;
    let current_output_value = 0;
    let amount_left = amount;
    let current_price = price.sell_price;
    let total_fees = 0;

    for (let step = 0; step <= 10; step++) {
        let step_amount = (step < 10) ? curve_data.amount[step] : amount_left;
        if (step < 10 && curve_data.price[step] < current_price) {
            if (token_settings.useCurveData == true)
                { current_price = curve_data.price[step]; }
        }
        if (step == 10) { curve_offset = 0; }
        if (step_amount <= curve_offset) {
            curve_offset -= curve_data.amount[step];
            continue;
        }
        let amount_in_interval = step_amount - curve_offset;
        curve_offset = 0;
        if (amount_in_interval > amount_left) { amount_in_interval = amount_left };
        let amount_before_tw = amount_in_interval;
        if (current_amount >= target_amount)
            { amount_before_tw = 0; } else
        if (current_amount + amount_in_interval >= target_amount)
            { amount_before_tw -= current_amount + amount_in_interval - target_amount; }
        let amount_after_tw = amount_in_interval - amount_before_tw;
        let value_before_tw = amount_to_usd_value(
            amount_before_tw,
            token_settings.decimals,
            current_price
        );
        let value_after_tw = amount_to_usd_value(
            amount_after_tw,
            token_settings.decimals,
            current_price
        );
        let fees =
            mul_div(value_before_tw, token_settings.tokenSwapFeeBeforeTwBps, BPS_DIVIDER) +
            mul_div(value_after_tw, token_settings.tokenSwapFeeAfterTwBps, BPS_DIVIDER);
        total_fees += fees;
        current_output_value += value_before_tw + value_after_tw - fees;
        amount_left -= amount_in_interval;
        current_amount += amount_in_interval;
        if (amount_left == 0) { break; }
    };
    let fee_bps_x100 = mul_div(total_fees, BPS_DIVIDER * 100, total_fees + current_output_value);

    return current_output_value;
}

export function compute_amount_of_bought_token(
    value: number,
    token_settings: TokenSettings,
    price: OraclePrice,
    start_amount: number,
    target_amount: number,
    curve_data: TokenPriceData,
): number {
    let current_amount = start_amount;
    let curve_offset = (start_amount < target_amount) ? (target_amount - start_amount) : 0;
    let current_output_amount = 0;
    let value_left = value;
    let current_price = price.buy_price;
    let total_fees = 0;

    for (let step = 0; step <= 10; step++){
        let step_amount = (step < 10) ? curve_data.amount[step]: usd_value_to_amount(value_left * 2, token_settings.decimals, current_price);
        if (step < 10 && curve_data.price[step] > current_price) {
            if (token_settings.useCurveData == true) { current_price = curve_data.price[step]; };
        }
        if (step == 10) { curve_offset = 0; }
        if (step_amount <= curve_offset) {
            curve_offset -= curve_data.amount[step];
            continue;
        }
        let amount_in_interval = step_amount - curve_offset;
        curve_offset = 0;

        let value_in_interval = amount_to_usd_value(amount_in_interval, token_settings.decimals, current_price);
        if (value_in_interval > value_left) {
            value_in_interval = value_left;
            amount_in_interval = usd_value_to_amount(value_in_interval, token_settings.decimals, current_price);
        }

        let value_before_tw = value_in_interval;
        if (current_amount <= target_amount)
            { value_before_tw = 0; } else
        if (current_amount <= target_amount + amount_in_interval)
            { value_before_tw -= amount_to_usd_value(target_amount + amount_in_interval - current_amount, token_settings.decimals, current_price)}
        let value_after_tw = value_in_interval - value_before_tw;

        let fees =
            mul_div(value_before_tw, token_settings.tokenSwapFeeBeforeTwBps, BPS_DIVIDER) +
            mul_div(value_after_tw, token_settings.tokenSwapFeeAfterTwBps, BPS_DIVIDER);
        
        let amount_bought = usd_value_to_amount(value_in_interval - fees, token_settings.decimals, current_price);
        
        current_output_amount += amount_bought;
        value_left -= value_in_interval;
        total_fees += fees;
        if (amount_bought > current_amount)
            { current_amount = 0; } else { current_amount -= amount_bought; }
        if (value_left == 0) { break; }
    };
    let fee_bps_x100 = mul_div(total_fees, BPS_DIVIDER * 100, value);

    return current_output_amount
}

export function computeOutputAmount(
    from_token_id: number,
    to_token_id: number,
    from_amount: number,
    fundState: FundStateChainData,
    oracle_prices: OraclePrice[],
    curve_data: CurveChainData,
    token_list: {list: TokenSettings[]},
) {
    fundState.currentCompToken = fundState.currentCompToken.map(x => x.toNumber());
    fundState.currentCompAmount = fundState.currentCompAmount.map(x => x.toNumber());
    fundState.targetWeight = fundState.targetWeight.map(x => x.toNumber());
    fundState.weightSum = fundState.weightSum.toNumber();
    fundState.numOfTokens = fundState.numOfTokens.toNumber();
    let from_token_settings = token_list.list[from_token_id];
    let to_token_settings = token_list.list[to_token_id];
    let from_token_index = fundState.currentCompToken.findIndex((x: any) => x == from_token_id);
    let to_token_index = fundState.currentCompToken.findIndex((x: any) => x == to_token_id);
    if (!from_token_settings.isLive || !to_token_settings.isLive)
        return { toAmount: undefined, feePct: undefined }
    if (from_token_index == -1 || to_token_index == -1)
        return { toAmount: undefined, feePct: undefined }

    let fund_worth = 0;
    for (let i=0;i<fundState.numOfTokens; i++) {
        let token = fundState.currentCompToken[i];
        let token_settings = token_list.list[token];
        let token_price = oracle_prices[token];
        fund_worth += amount_to_usd_value(
            fundState.currentCompAmount[i],
            token_settings.decimals,
            token_price.avg_price
        );
    }

    let from_token_price = oracle_prices[from_token_id];
    let to_token_price = oracle_prices[to_token_id];

    let from_token_target_amount = usd_value_to_amount(
        mul_div(fundState.targetWeight[from_token_index], fund_worth, fundState.weightSum),
        from_token_settings.decimals,
        from_token_price.avg_price
    );

    let to_token_target_amount = usd_value_to_amount(
        mul_div(fundState.targetWeight[to_token_index], fund_worth, fundState.weightSum),
        to_token_settings.decimals,
        to_token_price.avg_price,
    );

    let value = compute_value_of_sold_token(
        from_amount,
        from_token_settings,
        from_token_price,
        fundState.currentCompAmount[from_token_index],
        from_token_target_amount,
        curve_data.sell[from_token_id],
    );

    let to_amount = compute_amount_of_bought_token(
        value,
        to_token_settings,
        to_token_price,
        fundState.currentCompAmount[to_token_index],
        to_token_target_amount,
        curve_data.buy[to_token_id],
    );

    let amount_without_fees = usd_value_to_amount(
        amount_to_usd_value(
            from_amount,
            from_token_settings.decimals,
            from_token_price.sell_price
        ),
        to_token_settings.decimals,
        to_token_price.buy_price
    );

    let fair_amount = usd_value_to_amount(
        amount_to_usd_value(
            from_amount,
            from_token_settings.decimals,
            from_token_price.avg_price
        ),
        to_token_settings.decimals,
        to_token_price.avg_price
    );

    if (amount_without_fees > fundState.currentCompAmount[to_token_index]) {
        amount_without_fees = fundState.currentCompAmount[to_token_index];
    }

    if (to_amount > amount_without_fees) {
        to_amount = amount_without_fees
    }

    let total_fees = amount_without_fees - to_amount;

    let symmetry_bps = token_list.list[0].additionalData[60];
    let symmetry_fee = mul_div(total_fees, symmetry_bps, 100);

    let host_bps = token_list.list[0].additionalData[61];
    let host_fee = mul_div(total_fees, host_bps, 100);

    let manager_bps = token_list.list[0].additionalData[62];
    let manager_fee = mul_div(total_fees, manager_bps, 100);

    let fund_fee = total_fees - symmetry_fee - host_fee - manager_fee;

    let confidence_bps = mul_div(
        fair_amount - amount_without_fees,
        BPS_DIVIDER * 100,
        fair_amount
    );
    let fee_bps = mul_div(
        amount_without_fees - to_amount,
        BPS_DIVIDER * 100,
        fair_amount
    );

    let from_token_worth_before_swap = amount_to_usd_value(
        fundState.currentCompAmount[from_token_index],
        from_token_settings.decimals,
        from_token_price.avg_price
    );
    let to_token_worth_before_swap = amount_to_usd_value(
        fundState.currentCompAmount[to_token_index],
        to_token_settings.decimals,
        to_token_price.avg_price
    );

    let toTokenAfter = fundState.currentCompAmount[to_token_index] - (amount_without_fees - fund_fee);
    let fromTokenAfter = fundState.currentCompAmount[from_token_index] + from_amount;

    let from_token_worth_after_swap = amount_to_usd_value(
        fromTokenAfter,
        from_token_settings.decimals,
        from_token_price.avg_price
    );
    let to_token_worth_after_swap= amount_to_usd_value(
        toTokenAfter,
        to_token_settings.decimals,
        to_token_price.avg_price
    );

    let from_old_weight = mul_div(
        from_token_worth_before_swap,
        WEIGHT_MULTIPLIER,
        fund_worth
    );
    let to_old_weight = mul_div(
        to_token_worth_before_swap,
        WEIGHT_MULTIPLIER,
        fund_worth
    );

    fund_worth = fund_worth - from_token_worth_before_swap;
    fund_worth = fund_worth - to_token_worth_before_swap;
    fund_worth = fund_worth + from_token_worth_after_swap;
    fund_worth = fund_worth + to_token_worth_after_swap;

    let from_new_weight = mul_div(
        from_token_worth_after_swap,
        WEIGHT_MULTIPLIER,
        fund_worth
    );
    let to_new_weight = mul_div(
        to_token_worth_after_swap,
        WEIGHT_MULTIPLIER,
        fund_worth
    );

    let allowed_offset = fundState.rebalanceThreshold.toNumber() * fundState.lpOffsetThreshold.toNumber();

    let allowed_from_target_weight = mul_div(
        fundState.targetWeight[from_token_index],
        BPS_DIVIDER * BPS_DIVIDER + allowed_offset,
        BPS_DIVIDER * BPS_DIVIDER
    );
    let allowed_to_target_weight = mul_div(
        fundState.targetWeight[to_token_index],
        BPS_DIVIDER * BPS_DIVIDER - allowed_offset,
        BPS_DIVIDER * BPS_DIVIDER
    );
    if (allowed_from_target_weight > WEIGHT_MULTIPLIER) {
        allowed_from_target_weight = WEIGHT_MULTIPLIER;
    }

    let removing_dust = (from_token_id == 0  &&
        fundState.targetWeight[to_token_index] == 0);
        
    if (from_new_weight > allowed_from_target_weight && (!removing_dust))
        return { toAmount: undefined, feePct: undefined }

    if (to_new_weight < allowed_to_target_weight)
        return { toAmount: undefined, feePct: undefined }

    return {
        toAmount: Math.floor(to_amount),
        feePct: fee_bps/100,
    }
}

export function checkForLiquidity(
    tokenListData: TokenSettings[],
    curveChainData: CurveChainData,
    fundAddress: PublicKey,
    fund: FundStateChainData,
    oraclePrices: OraclePrice[],
    tokenFrom: number,
    tokenTo: number,
    fromAmount: number,
): RouteData|undefined {
    if (tokenFrom == undefined || tokenTo == undefined)
        throw new Error("From or To tokens are not defined");
    let initialAmount = fromAmount;
    let {toAmount, feePct} = computeOutputAmount(
        tokenFrom,
        tokenTo,
        fromAmount,
        Object.assign({},fund),
        oraclePrices,
        Object.assign({}, curveChainData),
        {list: tokenListData}
    )
    if (toAmount == undefined || feePct == undefined) return undefined;
    let oracleAccounts = [];
    for (let i = 0; i < fund.numOfTokens.toNumber(); i++)
        oracleAccounts.push({
            pubkey: new PublicKey(
                tokenListData[fund.currentCompToken[i].toNumber()].oracleAccount
            ),
            isSigner: false,
            isWritable: false,
        })
    return {
        fromAmount: initialAmount / (10 ** tokenListData[tokenFrom].decimals),
        toAmount: toAmount / (10 ** tokenListData[tokenTo].decimals),
        fromTokenId: tokenFrom,
        toTokenId: tokenTo,
        feePct: feePct,
        priceImpactPct: feePct,
        swapAccounts: {
            program: FUNDS_PROGRAM_ID,
            fundState: fundAddress,
            authority: FUNDS_PROGRAM_PDA,
            source: new PublicKey(tokenListData[tokenFrom].pdaTokenAccount),
            destination:  new PublicKey(tokenListData[tokenTo].pdaTokenAccount),
            fees: {
                smfWallet: SWAP_FEE_ACCOUNT,
                hostWallet: fund.hostPubkey,
                managerWallet: fund.manager,
                feeTokenMint: new PublicKey(tokenListData[tokenTo].tokenMint)
            },
            tokenList: TOKEN_LIST_ADDRESS,
            curveData: CURVE_DATA_ADDRESS,
            remainingAccounts: oracleAccounts,
        }
    }
}

export function loadRouteData(
    tokenListData: TokenSettings[],
    curveChainData: CurveChainData,
    funds: {pubkey: PublicKey, fund: FundStateChainData}[],
    oraclePrices: OraclePrice[],
    tokenFrom: number,
    tokenTo: number,
    fromAmount: number,
): RouteData {
    if (tokenFrom == undefined || tokenTo == undefined)
        throw new Error("From or To tokens are not defined");
    let fromTokenAmount = fromAmount * 10 ** tokenListData[tokenFrom].decimals;
    let bestRouteData: RouteData = {
        fromAmount: 0,
        toAmount: 0,
        fromTokenId: 0,
        toTokenId: 0,
        feePct: 0,
        priceImpactPct: 0,
        swapAccounts: {
            program: FUNDS_PROGRAM_ID,
            fundState: PublicKey.default,
            authority: FUNDS_PROGRAM_PDA,
            source: PublicKey.default,
            destination:  PublicKey.default,
            fees: {
                smfWallet: SWAP_FEE_ACCOUNT,
                hostWallet: PublicKey.default,
                managerWallet: PublicKey.default,
                feeTokenMint: PublicKey.default
            },
            tokenList: TOKEN_LIST_ADDRESS,
            curveData: CURVE_DATA_ADDRESS,
            remainingAccounts: [],
        }
    };
    for (let i = 0; i < funds.length; i++) {
        let routeData = checkForLiquidity(
            tokenListData,
            curveChainData,
            funds[i].pubkey,
            funds[i].fund,
            oraclePrices,
            tokenFrom,
            tokenTo,
            fromTokenAmount,
        );
        if(!routeData) continue;
        if (routeData.toAmount > bestRouteData.toAmount)
            bestRouteData = routeData;
    }
    return bestRouteData;
}
