import { AnchorProvider, BN, Program, Wallet } from "@coral-xyz/anchor";
import { AccountInfo, AddressLookupTableAccount, AddressLookupTableState, Connection, GetProgramAccountsFilter, GetProgramAccountsResponse, Keypair, MessageV0, PublicKey, SystemProgram, Transaction, TransactionInstruction, TransactionSignature, VersionedTransaction } from "@solana/web3.js"
import { BasketsIDL, IDL } from "./basketsIDL";
import { BPS_DIVIDER, COMBINED_TOKENS_IN_A_BASKET, CreateBasketParams, FilterType, BasketError, BASKETS_PROGRAM_ID, BASKETS_PROGRAM_PDA, RebalanceInfo, Rule, Side, TokenSettings, TOKEN_LIST_ADDRESS, TOKEN_STATS_ADDRESS, WeightType, REBALANCE_FEE_ACCOUNT, JupSwapData} from "./config";
import { parsePriceData } from '@pythnetwork/client';
import axios from "axios";
import { Basket } from "./basketState";
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import { AccountLayout, NATIVE_MINT, TOKEN_PROGRAM_ID, createAssociatedTokenAccountInstruction, createSyncNativeInstruction, getAssociatedTokenAddressSync } from "./splTokenHelpers";

export function delay(ms: number) {
    return new Promise( resolve => setTimeout(resolve, ms) );
}

export async function rawOraclePrices(
    connection: Connection,
    tokenList: any,
): Promise<number[]> {
    let oraclePricesData = await connection
        .getMultipleAccountsInfo(tokenList.map((x: any) => x.oracleAccount), "confirmed");
    let oraclePrices = [];
    for(let i=0; i<oraclePricesData.length; i++){
        if(oraclePricesData[i] == null) { oraclePrices.push(0); continue; }
        if (tokenList[i].oracleType == 0) //@ts-ignore
            oraclePrices.push(parsePriceData(oraclePricesData[i].data).aggregate.price); else
        if (tokenList[i].oracleType == 1) {
            let price = parseInt(new BN(//@ts-ignore
                oraclePricesData[i].data.subarray(
                    tokenList[i].oracleIndex * 8 + 9,
                    tokenList[i].oracleIndex * 8 + 17
                ),
                10,
                "le"
            ).toString()) / 10 ** 12;
            price = price * (10000 - tokenList[i].oracleConfidencePct) / 10000;
            oraclePrices.push(price);
        } else
        if (tokenList[i].oracleType == 2) {
            let total_lamports = parseInt(new BN(//@ts-ignore
            oraclePricesData[i].data.subarray(
                1+3*32+1+5*32,
                1+3*32+1+5*32+8
            ),
            10,
            "le"
            ).toString());
            let pool_token_supply = parseInt(new BN(//@ts-ignore
                oraclePricesData.data.subarray(
                    1+3*32+1+5*32+8,
                    1+3*32+1+5*32+16
                ),
                10,
                "le"
            ).toString());
            oraclePrices.push(total_lamports / pool_token_supply)
        } else 
        if (tokenList[i].oracleType == 3) {
            //@ts-ignore
            let data: Buffer = oraclePricesData[i].data;
            let price = data.readBigInt64LE(73);
            let exp = data.readInt32LE(89);
            oraclePrices.push(Number(price) * 10 ** exp);
        } else
        if (tokenList[i].oracleType == 4) {
            //@ts-ignore
            let priceObj = oraclePricesData[i].data.subarray(2264, 2392);
            let price = parseInt(new BN(priceObj.subarray(32, 48), 10, "le").toString());
            oraclePrices.push(price / 10 ** 18);
        } else {
            oraclePrices.push(0);
        }
    }
    for (let i = 0; i < oraclePrices.length; i++)
        if (tokenList[i].oracleType == 2)
            oraclePrices[i] *= oraclePrices[1];
    return oraclePrices;
}

export async function getOraclePrices(
    program: Program<BasketsIDL>,
    tokenList: TokenSettings[]
): Promise<number[]> {
    let oraclePricesData = (
        await program.provider.connection
            .getMultipleAccountsInfo(
                tokenList.map(x => new PublicKey(x.oracleAccount)),
                "confirmed"
            )
    );
    let oraclePrices = [];
    for(let i=0; i<oraclePricesData.length; i++){
        if(oraclePricesData[i] == null){
            oraclePrices.push(0);
        }
        else {
            if (tokenList[i].oracleType == "Pyth")
                //@ts-ignore
                oraclePrices.push(parsePriceData(oraclePricesData[i].data).aggregate.price); else
            if (tokenList[i].oracleType == "Switchboard") {
                let price = parseInt(new BN(//@ts-ignore
                    oraclePricesData[i].data.subarray(
                        tokenList[i].oracleIndex * 8 + 9,
                        tokenList[i].oracleIndex * 8 + 17
                    ),
                    10,
                    "le"
                ).toString()) / 10 ** 12;
                price = price * (10000 - tokenList[i].oracleConfidencePct) / 10000;
                oraclePrices.push(price);
            } else 
            if (tokenList[i].oracleType == "LST") {
                let total_lamports = parseInt(new BN( //@ts-ignore
                    oraclePricesData[i].data.subarray(
                        1+3*32+1+5*32,
                        1+3*32+1+5*32+8
                    ),
                    10,
                    "le"
                ).toString());
                let pool_token_supply = parseInt(new BN(//@ts-ignore
                    oraclePricesData[i].data.subarray(
                        1+3*32+1+5*32+8,
                        1+3*32+1+5*32+16
                    ),
                    10,
                    "le"
                ).toString());
                oraclePrices.push(total_lamports / pool_token_supply);
            } else 
            if (tokenList[i].oracleType == "PythSponsored") {
                //@ts-ignore
                let data: Buffer = oraclePricesData[i].data;
                let price = data.readBigInt64LE(73);
                let exp = data.readInt32LE(89);
                oraclePrices.push(Number(price) * 10 ** exp);
            }  else
            if (tokenList[i].oracleType == "SwbOnDemand") {
                //@ts-ignore
                let priceObj = oraclePricesData[i].data.subarray(2264, 2392);
                let price = parseInt(new BN(priceObj.subarray(32, 48), 10, "le").toString());
                oraclePrices.push(price / 10 ** 18);
            } else {
                oraclePrices.push(0);
            }
        }
    }
    for (let i = 0; i < oraclePrices.length; i++)
        if (tokenList[i].oracleType == "LST")
            oraclePrices[i] *= oraclePrices[1];
    return oraclePrices;
}

export async function getFilteredProgramAccounts(
    connection: Connection,
    filters: GetProgramAccountsFilter[]
): Promise<GetProgramAccountsResponse> {
    return await connection
        .getProgramAccounts(
            BASKETS_PROGRAM_ID,
            {
                commitment: "confirmed",
                filters,
                encoding: 'base64'
            }
        )
}

export async function signVersionedTransactions(
    wallet: Wallet,
    transactions: VersionedTransaction[],
): Promise<VersionedTransaction[]> {
    let txs: VersionedTransaction[] = [];
    // @ts-ignore
    txs = await wallet.signAllTransactions(transactions).catch((e) => {
        console.log("Couldn't sign transactions: " + e.message); return null;
    });
    if (!txs) {
        try { // @ts-ignore
            transactions.map(tx => tx.sign([wallet.payer]))
            txs = transactions;
        }
        catch (e: any) {
            console.log("Couldn't sign transactions: " + e.message);
        }
    }
    return txs;
}

export async function signTransactionsWithWallet(
    connection: Connection,
    wallet: Wallet,
    transactionsData: {
        transaction: Transaction,
        signers: Keypair[],
    }[]
): Promise<Transaction[]> {
    if (transactionsData.length == 0) return [];
    let { blockhash } = await connection.getLatestBlockhash("confirmed");
    for (let i = 0; i < transactionsData.length; i++) {
        transactionsData[i].transaction.feePayer = wallet.publicKey;
        transactionsData[i].transaction.recentBlockhash = blockhash;
        if (transactionsData[i].signers.length > 0)
            transactionsData[i].transaction.partialSign(
                ...transactionsData[i].signers
            );
    }
    return await wallet.signAllTransactions(
        transactionsData.map(data => data.transaction)
    ).catch((e) => { throw new BasketError("Couldn't sign transactions: " + e.message)});
}

export async function sendSignedTransaction(
    connection: Connection,
    transaction: Transaction|VersionedTransaction,
    retries: number = 2,
    delayMs: number = 400,
): Promise<TransactionSignature> {
    let serialized = transaction.serialize();
    let txId = await connection.sendRawTransaction(
        serialized, {skipPreflight: true, preflightCommitment: "processed", maxRetries: 3},
    ).catch((e) => { throw new BasketError("Couldn't send transaction: "  + e.message) });
    connection.sendRawTransaction(
        serialized, {skipPreflight: false, preflightCommitment: "processed", maxRetries: 3},
    ).catch((e) => console.log(txId + " : " + e.message));
    for (let numDelay = 1; numDelay < retries; numDelay++)
        delay(delayMs * numDelay).then(() => {
            connection.
            sendRawTransaction(serialized, {skipPreflight: true}).catch(() => {});
        })
    return txId;
}

export async function confirmTransaction(
    connection: Connection,
    txId: TransactionSignature,
    timeout: number = 30,
): Promise<boolean> {
    let result = undefined;
    for (let _ = 0; _ < timeout && result == undefined; _++) {
        await delay(1000);
        await connection
            .getTransaction(txId, {commitment: "confirmed", maxSupportedTransactionVersion: 1})
            .catch((e) => { throw new BasketError("Couldn't confirm transaction", txId); })
            .then(response => {
                if (!response)
                    return;
                if (response.meta && response.meta.err)
                    result = false;
                else
                    result = true;
            })
    }
    if (result == undefined)
        return false;
    return result;
}

export async function sendSignedTransactions(
    connection: Connection,
    transactions: (Transaction|VersionedTransaction)[],
    confirmFirstN: number = 0,
): Promise<TransactionSignature[]> {
    if (transactions.length == 0) return [];
    if (!transactions)
        return ["SignatureError"];
    let txs: TransactionSignature[] = [];

    for (let i = 0; i < confirmFirstN; i++) {
        let txId = await sendSignedTransaction(connection, transactions[i]);
        await confirmTransaction(connection, txId).catch((e) => {
            console.log(txId, e.message, "Couldn't confirm");
            return true;
        })
        txs.push(txId);
    }
    
    let remainingTxs = await Promise.all(
        transactions
            .slice(confirmFirstN, transactions.length)
            .map(transaction => sendSignedTransaction(connection, transaction).catch((e) => "Error"))
    );

    // await Promise.all(remainingTxs.map(tx => confirmTransaction(connection, tx))).catch(() => {});
    
    return [...txs, ...remainingTxs];
}

export const getAddressLookupTableAccounts = async (
    connection: Connection,
    keys: string[]
): Promise<AddressLookupTableAccount[]> => {
    const addressLookupTableAccountInfos =
      await connection.getMultipleAccountsInfo(
        keys.map((key) => new PublicKey(key))
      );
    return addressLookupTableAccountInfos.reduce((acc, accountInfo, index) => {
      const addressLookupTableAddress = keys[index];
      if (accountInfo) {
        let state: AddressLookupTableState = AddressLookupTableAccount.deserialize(accountInfo.data);
        const addressLookupTableAccount = new AddressLookupTableAccount({
          key: new PublicKey(addressLookupTableAddress),
          state: state,
        });
        acc.push(addressLookupTableAccount);
      }
  
      return acc;
    }, new Array<AddressLookupTableAccount>());
};

export async function buildTxFromQuoteResponse(
    quoteResponse: any,
    rebalanceInfo: RebalanceInfo,
    connection: Connection,
    jupAPIkey: string,
): Promise<JupSwapData> {
    const txData = (await axios.post(
        jupAPIkey + "swap-instructions",
        JSON.stringify({
            quoteResponse,
            userPublicKey: BASKETS_PROGRAM_PDA.toBase58(),
            // feeAccount: REBALANCE_FEE_ACCOUNT.toBase58(),
            wrapAndUnwrapSol: false,
        }),
        { headers: { 'Content-Type': 'application/json' } }
    )).data;
    let { addressLookupTableAddresses, swapInstruction } = txData;

    let from = getAssociatedTokenAddressSync(
        new PublicKey(quoteResponse.inputMint),
        BASKETS_PROGRAM_PDA,
        true,
    );
    let to = getAssociatedTokenAddressSync(
        new PublicKey(quoteResponse.outputMint),
        BASKETS_PROGRAM_PDA,
        true,
    );
    for (let i = 0; i < swapInstruction.accounts.length; i++) {
        if (swapInstruction.accounts[i].pubkey == BASKETS_PROGRAM_PDA.toBase58())
            swapInstruction.accounts[i].isSigner = false;
        if (swapInstruction.accounts[i].pubkey == from.toBase58())
            swapInstruction.accounts[i].pubkey = rebalanceInfo.tokenAccountFrom;
        if (swapInstruction.accounts[i].pubkey == to.toBase58())
            swapInstruction.accounts[i].pubkey = rebalanceInfo.tokenAccountTo;
    }
   swapInstruction.accounts = swapInstruction.accounts.map((x: any) => { return { ...x, pubkey: new PublicKey(x.pubkey)}});
    let data: Buffer = Buffer.from(swapInstruction.data, "base64");
    let dataLength = data.length - 19;
    let fromAmount = parseInt(new BN(data.slice(dataLength, dataLength + 8),"le").toString());
    let toAmount = parseInt(new BN(data.slice(dataLength + 8, dataLength + 16),"le").toString());
    let slippageBps = new BN(dataLength + 16, dataLength + 18, "le").toNumber();
    let feeBps = new BN(data.slice(dataLength + 18, dataLength + 19), "le").toNumber();
    data = Buffer.concat([data.slice(0, dataLength), Buffer.alloc(128 - dataLength)])

    let lookupTableAccounts = await getAddressLookupTableAccounts(connection, addressLookupTableAddresses);
    return {
        type: "Simple",
        programId: new PublicKey(swapInstruction.programId),
        accounts: swapInstruction.accounts,
        firstIxEnd: dataLength,
        firstIxAccounts: swapInstruction.accounts.length,
        dataLength: dataLength,
        data: data,
        fromTokenId: rebalanceInfo.side == Side.To ? rebalanceInfo.tokenId : 0,
        toTokenId: rebalanceInfo.side == Side.From ? rebalanceInfo.tokenId : 0,
        midTokenPda: "",
        fromAmount: fromAmount,
        toAmount: toAmount,
        slippageBps: slippageBps,
        feeBps: feeBps,
        lookupTableAccounts: lookupTableAccounts,
    };
}

export async function findRoute(
    rebalanceInfo: any,
    jupAPIkey: string,
    slippage: number,
    midToken: string,
    inputAmountOverwrite: number
): Promise<any> {
    let res: {
        inputAmount: number,
        outputAmount: number,
        quotes: any[],
    } = {
        inputAmount: 0,
        outputAmount: 0,
        quotes: []
    };

    try {
        let quoteResponse = (await axios.get(
            jupAPIkey + "quote" +
            "?inputMint=" + rebalanceInfo.mintFrom +
            "&outputMint=" +  rebalanceInfo.mintTo +
            "&amount=" + inputAmountOverwrite +
            "&slippageBps=" + slippage +
            // "&platformFeeBps=" + 5 +
            "&onlyDirectRoutes=true"
        )).data;
        res.inputAmount = inputAmountOverwrite;
        res.outputAmount = parseInt(quoteResponse.outAmount);
        res.quotes = [quoteResponse];
    } catch {}
    
    try {
        let route1 = (await axios.get(
            jupAPIkey + "quote" +
            "?inputMint=" + rebalanceInfo.mintFrom +
            "&outputMint=" +  midToken +
            "&amount=" + inputAmountOverwrite +
            "&slippageBps=" + slippage +
            // "&platformFeeBps=" + 5 +
            "&onlyDirectRoutes=true"
        )).data;

        let route2 = (await axios.get(
            jupAPIkey + "quote" +
            "?inputMint=" + midToken +
            "&outputMint=" +  rebalanceInfo.mintTo +
            "&amount=" + route1.outAmount +
            "&slippageBps=" + slippage +
            // "&platformFeeBps=" + 5 +
            "&onlyDirectRoutes=true"
        )).data;

        if (res.outputAmount <= parseInt(route2.outAmount)) {
            res.inputAmount = inputAmountOverwrite;
            res.outputAmount = parseInt(route2.outAmount);
            res.quotes = [route1, route2];
        }
    } catch {}

    return res;
}


export async function generateJupSwapInstruction(
    rebalanceInfo: RebalanceInfo,
    slippage: number,
    connection: Connection,
    fromPriceWithDecimals: number,
    toPriceWithDecimals: number,
    midTokenMint: string,
    midTokenPda: string,
    jupAPIkey: string,
): Promise<JupSwapData> {

    let l = 1, r = rebalanceInfo.amountFrom, m = 0;

    let res = await findRoute(
        rebalanceInfo,
        jupAPIkey,
        slippage,
        midTokenMint,
        rebalanceInfo.amountFrom
    );
    let inputValue = rebalanceInfo.amountFrom * fromPriceWithDecimals;
    let outputValue = res.outputAmount * toPriceWithDecimals;
    if (inputValue * (10000 - slippage) / 10000 > outputValue && "https://quote-api.jup.ag/v6/" != jupAPIkey) {
        for (let it = 0; it < 3; it++) {
            m = (l+r) >> 1;
            let check = await findRoute(
                rebalanceInfo,
                jupAPIkey,
                slippage,
                midTokenMint,
                m
            );
            let inputValue = m * fromPriceWithDecimals;
            let outputValue = check.outputAmount * toPriceWithDecimals;
            if (inputValue * (10000 - slippage) / 10000 >= outputValue)
                r = m; else { l = m; res = check; }
            if ((r-l) * fromPriceWithDecimals <= 5) break; // No point in iterating within 2USD
        }
    }

    if (res.outputAmount == 0)
        throw new Error("Couldn't find quote within slippage");

    if (res.quotes.length == 2) {
        let data = await Promise.all([
            await buildTxFromQuoteResponse(
                res.quotes[0],
                { ...rebalanceInfo, tokenAccountTo: midTokenPda },
                connection,
                jupAPIkey
            ),
            await buildTxFromQuoteResponse(
                res.quotes[1],
                { ...rebalanceInfo, tokenAccountFrom: midTokenPda },
                connection,
                jupAPIkey
            )
        ]);

        let combined = {
            type: "Transitive",
            programId: new PublicKey(data[0].programId),
            accounts: [...data[0].accounts, ...data[1].accounts],
            firstIxEnd: data[0].dataLength,
            dataLength: data[0].dataLength + data[1].dataLength,
            firstIxAccounts: data[0].accounts.length,
            data: Buffer.concat([
                data[0].data.slice(0, data[0].dataLength),
                data[1].data.slice(0, data[1].dataLength),
                Buffer.alloc(128 - data[0].dataLength - data[1].dataLength)
            ]),
            fromTokenId: rebalanceInfo.side == Side.To ? rebalanceInfo.tokenId : 0,
            toTokenId: rebalanceInfo.side == Side.From ? rebalanceInfo.tokenId : 0,
            midTokenPda: midTokenPda,
            fromAmount: data[0].fromAmount,
            toAmount: data[1].toAmount,
            slippageBps: data[1].slippageBps,
            feeBps: data[1].feeBps,
            lookupTableAccounts: [...data[0].lookupTableAccounts, ...data[1].lookupTableAccounts],
        }

        return combined;
    }

    let data = await buildTxFromQuoteResponse(
        res.quotes[0],
        rebalanceInfo,
        connection,
        jupAPIkey
    )

    return data;
}

export async function generateJupTxData(
    signer: PublicKey,
    mintFrom: string,
    mintTo: string,
    amountFrom: number,
    maxAllowedAccounts: number,
    slippage: number,
    fromPriceWithDecimals: number,
    toPriceWithDecimals: number,
    jupAPIkey: string,
): Promise<any> {
    // Helper function to get a quote from Jupiter API
    const getQuote = async (amount: number) => {
        let requestURL = `${jupAPIkey}quote?inputMint=${mintFrom}&outputMint=${mintTo}&amount=${amount}&slippageBps=${slippage + 50}`;
        if (maxAllowedAccounts != 64)
            requestURL += `&maxAccounts=${maxAllowedAccounts}`
        const response = await axios.get(requestURL).catch(e => {
            console.log("Jup quote error");
            console.log(e.message);
            console.log("--------");
            return ({ data: null })
        });
        return response.data;
    };
    let swapValue = 0;

    // Get initial quote
    let res = await getQuote(amountFrom);
    if (!res) return null;

    const inputValue = amountFrom * fromPriceWithDecimals;
    const outputValue = res.outAmount * toPriceWithDecimals;

    console.log("##### Checking Routes");
    console.log(mintFrom, mintTo, (amountFrom * fromPriceWithDecimals).toFixed(2), (res.outAmount * toPriceWithDecimals).toFixed(2), (outputValue / inputValue).toFixed(6));

    // Check if the swap meets the slippage requirement
    if (inputValue * (10000 - slippage) / 10000 > outputValue) {
        // Binary search for the optimal amount
        let l = 1, r = amountFrom;
        for (let it = 0; it < 5; it++) {
            if ((r - l) * fromPriceWithDecimals <= 1) break; // Stop if the difference is less than $1
            const m = Math.floor((l + r) / 2);
            const check = await getQuote(m);
            if (!check) {
                r = m;
                continue;
            }
            const checkInputValue = m * fromPriceWithDecimals;
            const checkOutputValue = check.outAmount * toPriceWithDecimals;
            console.log(checkInputValue.toFixed(2), checkOutputValue.toFixed(2), (checkOutputValue / checkInputValue).toFixed(6));
            if (checkInputValue * (10000 - slippage) / 10000 > checkOutputValue) {
                r = m;
            } else {
                l = m;
                res = check;
                swapValue = checkInputValue;
                break;
            }
        }
    } else swapValue = inputValue;

    if (!res) return null;

    // Get swap instructions from Jupiter API
    const txData = await axios.post(
        `${jupAPIkey}swap-instructions`,
        {
            quoteResponse: res,
            userPublicKey: signer.toBase58(),
            wrapAndUnwrapSol: false,
        },
        { headers: { 'Content-Type': 'application/json' } }
    ).then(response => response.data);

    return { ...txData, tokenAmount: res.inAmount, swapValue: swapValue, res };
}

export function calculateRebalanceAmounts(
    program: Program<BasketsIDL>,
    numTokens: number,
    timestamp: number,
    lastRebalanceTime: number[],
    rebalanceInterval: number,
    currentCompToken: number[],
    currentCompAmount: number[],
    targetWeights: number[],
    weightSum: number,
    tokenList: TokenSettings[],
    rebalanceThreshold: number,
    oraclePriceData: number[],
    forceRebalance: boolean,
): RebalanceInfo[] {
    let currentValues: number[] = [];
    let basketWorth: number = 0;
    for(let i=0; i<numTokens; i++){
        let price = oraclePriceData[currentCompToken[i]];
        let tokenAmount = currentCompAmount[i] / 10 ** tokenList[currentCompToken[i]].decimals;
        let tokenValue = price * tokenAmount;
        currentValues.push(tokenValue);
        basketWorth += tokenValue;
    }
    let rebalanceInfos: RebalanceInfo[] = [];
    if (basketWorth == 0)
        return rebalanceInfos;
    for(let i = 1; i < numTokens; i++) {
        let currentPercentage = (basketWorth > 0) ? currentValues[i] / basketWorth : 0;
        let targetPercentage = (weightSum > 0) ? targetWeights[i] / weightSum : 0;
        if (lastRebalanceTime[i] + rebalanceInterval > timestamp && (!forceRebalance))
            continue;
        if (currentPercentage > targetPercentage * (1 + rebalanceThreshold / 10000) || forceRebalance )
            rebalanceInfos.push({
                tokenId: currentCompToken[i],
                tokenAccountFrom: tokenList[currentCompToken[i]].pdaTokenAccount,
                mintFrom: tokenList[currentCompToken[i]].tokenMint,
                oracleFrom: tokenList[currentCompToken[i]].oracleAccount,
                tokenAccountTo: tokenList[0].pdaTokenAccount,
                mintTo: tokenList[0].tokenMint,
                oracleTo: tokenList[0].oracleAccount,
                amountFrom: Math.floor(
                    currentCompAmount[i] * (1 - targetPercentage / currentPercentage)
                ),
                decimals: tokenList[currentCompToken[i]].decimals,
                volume: currentValues[i] - targetPercentage * basketWorth,
                side: Side.To,
            })
        if (currentPercentage < targetPercentage * (1 - rebalanceThreshold / 10000) || forceRebalance)
            rebalanceInfos.push({
                tokenId: currentCompToken[i],
                tokenAccountTo: tokenList[currentCompToken[i]].pdaTokenAccount,
                mintTo: tokenList[currentCompToken[i]].tokenMint,
                oracleTo: tokenList[currentCompToken[i]].oracleAccount,
                tokenAccountFrom: tokenList[0].pdaTokenAccount,
                mintFrom: tokenList[0].tokenMint,
                oracleFrom: tokenList[0].oracleAccount,
                amountFrom: Math.floor(
                    (targetPercentage * basketWorth - currentValues[i]) *
                    10 ** tokenList[0].decimals
                ),
                decimals: tokenList[0].decimals,
                volume: (targetPercentage * basketWorth - currentValues[i]),
                side: Side.From
            })
    }
    return rebalanceInfos.filter(x => x.volume > 0.005);
}

export function stringToAscii(
    coingeckoId: string,
): Array<number> {
    let coingeckoIdAscii = [];
    for(let i=0; i<coingeckoId.length; i++)
    coingeckoIdAscii.push(coingeckoId[i].charCodeAt(0));
    while (coingeckoIdAscii.length != 30)
    coingeckoIdAscii.push(0);
    return coingeckoIdAscii;
}

export function asciiToString(
    coingeckoIdAscii: number[],
): string {
    let coingeckoId: string = "";
    for(let i=0; i<coingeckoIdAscii.length; i++)
        if(coingeckoIdAscii[i] != 0)
            coingeckoId += String.fromCharCode(coingeckoIdAscii[i]).toString();
    return coingeckoId;
}

export async function fetchTokenList(
    program: Program<BasketsIDL>,
): Promise<TokenSettings[]> {
    let solanaTokenList = (await axios.get(
        "https://cache.symmetry.fi/tokenlist.json"
    )).data;
    let tokenMap: any = {};
    for (let i = 0; i < solanaTokenList.length; i++)
        tokenMap[solanaTokenList[i].address] = {
            symbol: solanaTokenList[i].symbol,
            name: solanaTokenList[i].name,
            decimals: solanaTokenList[i].decimals,
        }
    let state = await program.account.tokenList.fetch(TOKEN_LIST_ADDRESS, "confirmed");
    let numTokens = state.numTokens.toNumber();
    let tokens = [];
    for (let i = 0; i < numTokens; i++) {
        //@ts-ignore
        let tokenSettings = state.list[i];
        tokens.push({
            id: i,
            symbol: tokenMap[tokenSettings.tokenMint.toBase58()]? tokenMap[tokenSettings.tokenMint.toBase58()].symbol : undefined,
            name: tokenMap[tokenSettings.tokenMint.toBase58()] ? tokenMap[tokenSettings.tokenMint.toBase58()].name : undefined,
            tokenMint: tokenSettings.tokenMint.toBase58(),
            decimals: tokenSettings.decimals,
            coingeckoId: asciiToString(tokenSettings.coingeckoId),
            pdaTokenAccount: tokenSettings.pdaTokenAccount.toBase58(),
            oracleType: ["Pyth", "Switchboard", "LST", "PythSponsored", "SwbOnDemand"][tokenSettings.oracleType],
            oracleAccount: tokenSettings.oracleAccount.toBase58(),
            oracleIndex: tokenSettings.oracleIndex,
            oracleConfidencePct: tokenSettings.oracleConfidencePct,
            fixedConfidenceBps: tokenSettings.fixedConfidenceBps,
            tokenSwapFeeBeforeTwBps: tokenSettings.tokenSwapFeeBeforeTwBps,
            tokenSwapFeeAfterTwBps: tokenSettings.tokenSwapFeeAfterTwBps,
            isLive: tokenSettings.isLive == 0 ? false : true,
            lpOn: tokenSettings.lpOn == 0 ? false : true,
            useCurveData: tokenSettings.useCurveData == 0 ? false : true,
            additionalData: tokenSettings.additionalData,
        });
    }
    return tokens;
}

export function getCurrentComposition(
    basket: Basket,
    tokenList: TokenSettings[],
    oraclePriceData: number[]
): any {
    let basketWorth = 0;
    let currentComp = [];
    for (let i = 0; i < basket.data.numOfTokens.toNumber(); i++) {
        let token = basket.data.currentCompToken[i].toNumber();
        let amount = parseInt(basket.data.currentCompAmount[i].toString()) / 10 ** tokenList[token].decimals;
        basketWorth += amount * oraclePriceData[token];
        currentComp.push({
            mintAddress: tokenList[token].tokenMint,
            coingeckoId: tokenList[token].coingeckoId,
            symbol: tokenList[token].symbol,
            symmetryTokenId: token,
            lockedAmount: amount,
            oraclePrice: oraclePriceData[token],
            usdValue: amount * oraclePriceData[token],
            currentWeight: 0,
            targetWeight: Number(basket.data.targetWeight[i].toNumber() * 100 / basket.data.weightSum.toNumber()),
            tokenData: tokenList[token],
        })
    }
    for (let i = 0; i < basket.data.numOfTokens.toNumber(); i++)
        currentComp[i].currentWeight = currentComp[i].usdValue * 100 / basketWorth;
    let symbol = asciiToString(basket.data.symbol.slice(0, basket.data.symbolLength));
    let name = asciiToString(basket.data.name.slice(0, basket.data.nameLength));
    let uri = asciiToString(basket.data.uri.slice(0, basket.data.uriLength));
    let basketInfo = {
        symbol: symbol,
        name: name,
        uri: uri,
        currentSupply: basket.data.supplyOutstanding.toNumber() / 10 ** 6,
        basketTokenMint: basket.data.fundToken.toBase58(),
        basketWorth: basketWorth,
        rawPrice: 10 ** 6 * basketWorth / basket.data.supplyOutstanding.toNumber(),
        manager: basket.data.manager.toBase58(),
        managerFee: basket.data.managerFee.toNumber() / 100,
        activelyManaged: basket.data.activelyManaged.toNumber(),
        host: basket.data.hostPubkey.toBase58(),
        hstFee: basket.data.hostFee.toNumber(),
        currentComposition: currentComp,
        rules: basket.data.rules.slice(0, basket.data.numOfRules.toNumber()).map(rule => {
            return {
                totalWeight: rule.totalWeight.toNumber(),
                filterBy: rule.filterBy.toNumber(),
                filterDays: rule.filterDays.toNumber(),
                sortBy: rule.sortBy.toNumber(),
                weightBy: rule.weightBy.toNumber(),
                weightDays: rule.weightDays.toNumber(),
                weightExpo: rule.weightExpo.toNumber() / 100,
                fixedAsset: tokenList[rule.fixedAsset.toNumber()],
                ruleAssets: rule.ruleAssets
                    .slice(0, rule.numAssets.toNumber())
                    .map(x => tokenList[x.toNumber()])
            }
        })
    };
    return basketInfo;
}

export async function validateCreateBasketParams(createBasketParams: CreateBasketParams, tokenList: TokenSettings[], basketState?: PublicKey, skipNameCheck: boolean = false) {
    if (createBasketParams.name.length > 60)
        throw new BasketError("Basket Name should be at most 60 characters");
    if (createBasketParams.symbol.length > 10)
        throw new BasketError("Basket Symbol should be at most 10 characters");
    if (createBasketParams.uri.length > 300)
        throw new BasketError("Basket MetadataURI should be at most 300 characters");

    if (createBasketParams.refilterInterval < 24 * 60 * 60)
        throw new BasketError("Minimum refilter interval should be 24 hours");
    if (createBasketParams.reweightInterval < 60 * 60)
        throw new BasketError("Minimum reweight interval should be 1 hour");
    if (createBasketParams.rebalanceInterval <  60 * 60)
        throw new BasketError("Minimum rebalance interval should be 1 hour");

    if (createBasketParams.rebalanceThreshold > BPS_DIVIDER)
        throw new BasketError("Maximum rebalance threshold should be 10000 bps");
    if (createBasketParams.rebalanceSlippage > BPS_DIVIDER)
        throw new BasketError("Maximum rebalance slippage should be 10000 bps");
    if (createBasketParams.lpOffsetThreshold > BPS_DIVIDER)
        throw new BasketError("Maximum lp offset threshold should be 10000 bps");
    if (createBasketParams.managerFee > BPS_DIVIDER)
        throw new BasketError("Maximum manager fee should be 10000 bps");
    if (createBasketParams.hostPlatformFee > BPS_DIVIDER)
        throw new BasketError("Maximum host platform fee should be 10000 bps");

    let totalAssets = 1;
    for (let i = 0; i < createBasketParams.rules.length; i++) {
        if (createBasketParams.rules[i].totalWeight <= 0)
            throw new BasketError("Total weight of each rule should be positive");
        if (createBasketParams.rules[i].totalWeight > 1000)
            throw new BasketError("Maximum total weight of each roule should be 1000");
        if (createBasketParams.rules[i].weightExpo < 0)
                throw new BasketError("Rule weight expo should be positive");
        if (createBasketParams.rules[i].weightExpo > 100 && createBasketParams.rules[i].weightBy != WeightType.Performance)
            throw new BasketError("Maxiumum rule weight expo should be 100");
        if (createBasketParams.rules[i].weightExpo > 1000)
            throw new BasketError("Maxiumum rule weight expo for performance should be 1000");
        if (createBasketParams.rules[i].weightBy > 3 || createBasketParams.rules[i].filterBy > 3)
            throw new BasketError("FilterBy and WeightBy should be in range 0..3");
        if (createBasketParams.rules[i].weightDays > 5 || createBasketParams.rules[i].filterDays > 5)
            throw new BasketError("FilterDays and WeightDays should be in range 0..5");
        if (createBasketParams.rules[i].sortBy > 1)
            throw new BasketError("SortBy should be in range 0..1");
        if (createBasketParams.rules[i].filterBy == FilterType.Fixed) {
            if (tokenList.find(token => token.id == createBasketParams.rules[i].fixedAsset)?.isLive == false)
                throw new BasketError("One of the fixed assets is not currently supported.");
            if ((createBasketParams.assetPool.find(x => x == createBasketParams.rules[i].fixedAsset)) == undefined)
                throw new BasketError("One of the fixed assets is not present in the asset pool.");
            totalAssets += 1;
            continue;
        } else
        totalAssets += createBasketParams.rules[i].numAssets;
    }

    createBasketParams.assetPool = createBasketParams.assetPool.sort();
    if (createBasketParams.assetPool[0] != 0 ||
            createBasketParams.assetPool.length != new Set(createBasketParams.assetPool).size)
                throw new BasketError("Asset pool should contain USDC and shouldn't contain repeating tokens");
    
    if (createBasketParams.assetPool.find(x => tokenList.find(token => token.id == x)?.isLive == false))
        throw new BasketError("One of the tokens in the asset pool is not currently supported.");

    if (totalAssets > COMBINED_TOKENS_IN_A_BASKET)
        throw new BasketError("Maximum allowed number of assets in a basket is " + COMBINED_TOKENS_IN_A_BASKET);

    if (skipNameCheck) return;

    let check = await axios.post(
        "https://api.symmetry.fi/v1/funds-name-setter",
        JSON.stringify({
            command: "check_exists",
            name: createBasketParams.name,
            symbol: createBasketParams.symbol,
            description: createBasketParams.uri,
        })
    );
    
    if (check.data.status == "fail" && check.data.msg) 
        throw new BasketError(check.data.msg);
    
    if (check.data.status != "ok")
        throw new BasketError("Validation failed");
    
    let basketStatePubkey = basketState ? basketState.toBase58() : "";
    if (
        (check.data.name_owner != null && check.data.name_owner != basketStatePubkey) ||
        (check.data.symbol_owner != null && check.data.symbol_owner != basketStatePubkey)
    )
        throw new BasketError("Basket with given name or symbol already exists.");
    
}

export async function tryMetadata(parsed: any)  {
    let metadata = undefined;
    try {
      if (parsed.uri) {
        let req = await fetch(parsed.uri);
        let json = await req.json();
        metadata = json;
      }
    } catch {}
    return metadata;
  }

async function getBasketTokenMint(
    connection: Connection,
    basket: PublicKey
): Promise<PublicKey> {
    let provider = new AnchorProvider(
        connection,
        new NodeWallet(Keypair.generate()),
        {commitment: "processed"}
    );
    let program = new Program<BasketsIDL>(IDL, provider);
    let basketObj = await Basket.loadFromPubkey(program, basket);
    return basketObj.data.fundToken;
}

async function initATAForUserOrWrapSolIxs(
    connection: Connection,
    user: PublicKey,
    tokenMint: PublicKey,
    lamports: number = 0
): Promise<TransactionInstruction[]> {
    let ixs = [];
    let ata = getAssociatedTokenAddressSync(
        tokenMint,
        user,
    );
    let infoAta = await connection.getMultipleAccountsInfo([ata]);
    if (!infoAta[0]) ixs.push(
        createAssociatedTokenAccountInstruction(
            user, ata, user, tokenMint,
        )
    );
    if (tokenMint.toBase58() == NATIVE_MINT.toBase58()) {
        //@ts-ignore
        let info: AccountInfo<Buffer> = infoAta[0];
        let toDeposit = lamports;
        if (info) {
            let parsedInfo = AccountLayout.decode(info.data);
            toDeposit -= parseInt(parsedInfo.amount.toString());
        }
        if (toDeposit > 0) {
            ixs.push(
                SystemProgram.transfer({
                    fromPubkey: user,
                    toPubkey: ata,
                    lamports: toDeposit
                }),
            );
            ixs.push(
                createSyncNativeInstruction(ata, TOKEN_PROGRAM_ID)
            );
        }
    }
    return ixs;
}
