import { AnchorProvider, BN, Instruction, Program, Wallet } from "@coral-xyz/anchor";
import {
    AccountInfo,
    AddressLookupTableAccount,
    ComputeBudgetProgram,
    Connection,
    Keypair,
    MessageV0,
    PublicKey,
    SystemProgram,
    SYSVAR_RENT_PUBKEY,
    Transaction,
    TransactionInstruction,
    TransactionMessage,
    TransactionSignature,
    VersionedTransaction
} from "@solana/web3.js";
import { BasketsIDL, IDL } from "./basketsIDL";
import {
    ADDITIONAL_FEE,
    ADDITIONAL_UNITS,
    CreateBasketParams,
    CREATE_FEE_ACCOUNT,
    BasketStateChainData,
    BASKETS_PROGRAM_PDA,
    CURVE_DATA_ADDRESS,
    RebalanceInfo,
    REBALANCE_FEE_ACCOUNT,
    Side,
    SWAP_FEE_ACCOUNT,
    TokenSettings,
    TOKEN_LIST_ADDRESS,
    BASKETS_PROGRAM_ID,
    JupSwapData,
    JUP_AGGREGATOR,
    SimpleEditParams,
    FilterType,
    FilterTime,
    SortBy,
    WeightType,
    WeightTime,
    SimpleCreateParams,
    TransactionToSend,
} from "./config";
import {
    calculateRebalanceAmounts,
    getOraclePrices,
    rawOraclePrices,
    sendSignedTransactions,
    signTransactionsWithWallet,
    signVersionedTransactions,
    validateCreateBasketParams
} from "./utils";
import { TOKEN_PROGRAM_ID, createAssociatedTokenAccountInstruction, getAssociatedTokenAddressSync } from "./splTokenHelpers";
import axios from "axios";
import { MD5 } from "crypto-js";
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import { Metaplex } from "@metaplex-foundation/js";
import { buildClaimTokensFromSellStateIxs, buildCloseBasketIx, buildCreateBasketIx, buildEditBasketIx, buildEditManagetIx, buildSellBasketIx, buildSellBasketToSingleTokenIx, buildSetMetadataIx, buildUpdateCurrentWeightsIx, getSwbFeedsUpdateIxs } from "./instructionsBuilder";

export class Basket {
    public ownAddress: PublicKey;
    public data: BasketStateChainData;

    constructor(
        ownAddress: PublicKey,
        basketData: BasketStateChainData,
    ) {
        this.ownAddress = ownAddress;
        this.data = basketData;
    }

    static loadFromRawData(
        program: Program<BasketsIDL>,
        rawData: {
            pubkey: PublicKey;
            account: AccountInfo<Buffer>;
        }
    ): Basket {
        return new Basket(
            rawData.pubkey,
            program.coder.accounts.decode("fundState", rawData.account.data)
        )
    }

    static async loadFromPubkey(
        program: Program<BasketsIDL>,
        basketState: PublicKey
    ): Promise<Basket> {
        let basketData = await program.account.fundState.fetch(basketState, "confirmed");
        return new Basket(
            basketState,
            //@ts-ignore
            basketData,
        );
    }

    static async getCompositionAndPrice(
        connection: Connection,
        pubkey: PublicKey,
        getPrice: boolean = false,
    ): Promise<any> {
        let provider = new AnchorProvider(
            connection,
            new NodeWallet(Keypair.generate()),
            {
                skipPreflight: true,
                preflightCommitment: "recent",
                commitment: "processed",
            }
        );
        let program = new Program<BasketsIDL>(IDL, provider);
        let accounts = [pubkey, TOKEN_LIST_ADDRESS];
        let accountsInfo = await connection.getMultipleAccountsInfo(accounts, "confirmed");
        //@ts-ignore
        let basketData = await program.coder.accounts.decode("fundState", accountsInfo[0]?.data);
        //@ts-ignore
        let tokenList = await program.coder.accounts.decode("tokenList", accountsInfo[1]?.data);
        let supply = parseInt(basketData.supplyOutstanding.toString()) / 10 ** 6;
        let mint = basketData.fundToken.toBase58();
        let tokenIds = basketData.currentCompToken
            .slice(0, parseInt(basketData.numOfTokens.toString())).map((x: any) => parseInt(x.toString()));
        let tokenAmounts = basketData.currentCompAmount
            .slice(0, parseInt(basketData.numOfTokens.toString()))
            .map((x: any, id: any) => 
                parseInt(x.toString()) / 10 ** tokenList.list[tokenIds[id]].decimals
            );
        
        let tokenMints = tokenIds.map((x: any) => tokenList.list[x].tokenMint.toBase58());
        
        let price = undefined;
        let tvl = undefined;
        if (getPrice) {
            let oraclePrices = await rawOraclePrices(connection, tokenList.list.slice(0, parseInt(tokenList.numTokens.toString())));
            tvl = 0;
            for (let i=0; i<tokenAmounts.length; i++)
                tvl += oraclePrices[tokenIds[i]] * tokenAmounts[i];
            price = tvl / supply;
        }
        let result = {
            price: price,
            supply: supply,
            tvl: tvl,
            mint: mint,
            composition: tokenAmounts.map((_: any, id: any) => {
                return {
                    mint: tokenMints[id],
                    amount: tokenAmounts[id],
                }
            })
        }
        return result;
    }

    static async create(
        program: Program<BasketsIDL>,
        connection: Connection,
        wallet: Wallet,
        tokenList: TokenSettings[],
        lookups: AddressLookupTableAccount[],
        basketParams: SimpleCreateParams,
        lamports: number = ADDITIONAL_FEE,
    ): Promise<Basket> {
        let createData = await buildCreateBasketIx(program, tokenList, basketParams);

        let blockhash = (await connection.getLatestBlockhash("confirmed")).blockhash;
    
        let V0Create = new VersionedTransaction(
            new TransactionMessage({
                payerKey: wallet.publicKey,
                recentBlockhash: blockhash,
                instructions: [
                    createData,
                    ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS}),
                    ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
                ]
            }).compileToV0Message(lookups)
        );

        let signedTransactions = await signVersionedTransactions(
            wallet,
            [V0Create]
        );

        let resultCreate = await sendSignedTransactions(
            connection,
            signedTransactions,
            1
        );
        console.log("Create Tx:", resultCreate[0]);

        return await Basket
            .loadFromPubkey(
                program,
                createData.keys[2].pubkey,
            );
    }

    async update(program: Program<BasketsIDL>) {
        //@ts-ignore
        this.data = await program.account.fundState.fetch(this.ownAddress, "confirmed");
    }

    async editManager(
        program: Program<BasketsIDL>,
        connection: Connection,
        wallet: Wallet,
        newManager: PublicKey,
        lamports: number = ADDITIONAL_FEE,
    ): Promise<TransactionSignature> {
        let editManager = new Transaction();
        editManager.instructions = [
            await buildEditManagetIx(program, this, newManager),
            ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS}),
            ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
        ];
    
        let signedTransactions = await signTransactionsWithWallet(
            connection,
            wallet,
            [{ transaction: editManager, signers: [] }]
        );

        let resultEdit = await sendSignedTransactions(
            connection,
            [signedTransactions[0]],
            1
        );

        return resultEdit[0]; 
    }

    async edit(
        program: Program<BasketsIDL>,
        connection: Connection,
        wallet: Wallet,
        tokenList: TokenSettings[],
        lookups: AddressLookupTableAccount[],
        basketParams: SimpleEditParams,
        lamports: number = ADDITIONAL_FEE,
    ): Promise<TransactionSignature> {
        let blockhash = (await connection.getLatestBlockhash("confirmed")).blockhash;
    
        let V0Create = new VersionedTransaction(
            new TransactionMessage({
                payerKey: wallet.publicKey,
                recentBlockhash: blockhash,
                instructions: [
                    await buildEditBasketIx(program, tokenList, this, basketParams),
                    ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS}),
                    ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
                ]
            }).compileToV0Message(lookups)
        );

        let signedTransactions = await signVersionedTransactions(
            wallet,
            [V0Create]
        );

        let resultEdit = await sendSignedTransactions(
            connection,
            signedTransactions,
            1
        );
        
        return resultEdit[0];
    }

    async setMetaData(
        program: Program<BasketsIDL>,
        wallet: Wallet,
        symbol: string,
        name: string,
        uri: string,
        lamports: number = ADDITIONAL_FEE,
    ): Promise<TransactionSignature> {
        let transaction = new Transaction();
        transaction.instructions = [
            await buildSetMetadataIx(program, this, {symbol: symbol, name: name, uri: uri}),
            ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS}),
            ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
        ]
        let signedTransactions = await signTransactionsWithWallet(
            program.provider.connection,
            wallet,
            [{transaction: transaction, signers: []}]
        );
        return (await sendSignedTransactions(
            program.provider.connection,
            signedTransactions,
            1
        ))[0];
    }

    async close(
        program: Program<BasketsIDL>,
        wallet: Wallet,
        lamports: number = ADDITIONAL_FEE,
    ): Promise<TransactionSignature> {
        let transaction = new Transaction();
        transaction.instructions = [
            await buildCloseBasketIx(program, this),
            ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS}),
            ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
        ];
        let signedTransactions = await signTransactionsWithWallet(
            program.provider.connection,
            wallet,
            [{transaction: transaction, signers: []}]
        );
        return (await sendSignedTransactions(
            program.provider.connection,
            signedTransactions,
            1
        ))[0];
    }

    getSwbFeeds(
        tokenList: TokenSettings[],
    ): PublicKey[] {
        let oracles: PublicKey[] = [];
        for (let i = 0; i < this.data.numOfTokens.toNumber(); i++)
            if (tokenList[this.data.currentCompToken[i].toNumber()].oracleType == "SwbOnDemand")
                oracles.push(new PublicKey(tokenList[this.data.currentCompToken[i].toNumber()].oracleAccount))
        return oracles;
    }
    
    async rebalanceFromUsdcTransactionData(
        program: Program<BasketsIDL>,
        wallet: Wallet,
        pda: PublicKey,
        basketState: PublicKey,
        tokenList: TokenSettings[],
        rebalanceFeeAccount: PublicKey,
        jupSwapData: JupSwapData,
        lamports: number = ADDITIONAL_FEE,
    ): Promise<{
        payerKey: PublicKey,
        instructions: TransactionInstruction[],
        lookupTables: AddressLookupTableAccount[]
    }> {
        let tokenId = jupSwapData.toTokenId;
        let ix = (jupSwapData.type == "Simple") ?
            await program.methods
                .rebalanceBuy(
                    jupSwapData.toTokenId,
                    new BN(jupSwapData.fromAmount),
                    jupSwapData.dataLength,
                    Array.from(jupSwapData.data),
                )
                .accounts({
                    signer: wallet.publicKey,
                    fundState: basketState,
                    tokenList: TOKEN_LIST_ADDRESS,
                    oracleSol: new PublicKey(tokenList[1].oracleAccount),
                    oracleToken: new PublicKey(tokenList[tokenId].oracleAccount),
                    oracleUsdc: new PublicKey(tokenList[0].oracleAccount),
                    pdaAccount: pda,
                    pdaTokenAccount: new PublicKey(tokenList[tokenId].pdaTokenAccount),
                    pdaUsdcAccount: new PublicKey(tokenList[0].pdaTokenAccount),
                    rebalanceFeeAccount: rebalanceFeeAccount,
                    tokenProgram: TOKEN_PROGRAM_ID,
                })
                .remainingAccounts(jupSwapData.accounts)
                .instruction() :
            await program.methods
                .rebalanceBuyTransitive(
                    jupSwapData.toTokenId,
                    new BN(jupSwapData.fromAmount),
                    jupSwapData.firstIxEnd,
                    jupSwapData.dataLength,
                    jupSwapData.firstIxAccounts,
                    Array.from(jupSwapData.data),
                )
                .accounts({
                    signer: wallet.publicKey,
                    fundState: basketState,
                    tokenList: TOKEN_LIST_ADDRESS,
                    oracleSol: new PublicKey(tokenList[1].oracleAccount),
                    oracleToken: new PublicKey(tokenList[tokenId].oracleAccount),
                    oracleUsdc: new PublicKey(tokenList[0].oracleAccount),
                    pdaAccount: pda,
                    pdaTokenAccount: new PublicKey(tokenList[tokenId].pdaTokenAccount),
                    pdaMidAccount: new PublicKey(jupSwapData.midTokenPda),
                    pdaUsdcAccount: new PublicKey(tokenList[0].pdaTokenAccount),
                    rebalanceFeeAccount: rebalanceFeeAccount,
                    tokenProgram: TOKEN_PROGRAM_ID,
                })
                .remainingAccounts(jupSwapData.accounts)
                .instruction();
        let rawData = {
            payerKey: wallet.publicKey,
            instructions: [
                ix,
                ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS}),
                ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
            ],
            lookupTables: jupSwapData.lookupTableAccounts
        };
        return rawData;
    }
    
    async rebalanceToUsdcTransactionData(
        program: Program<BasketsIDL>,
        wallet: Wallet,
        pda: PublicKey,
        basketState: PublicKey,
        tokenList: TokenSettings[],
        rebalanceFeeAccount: PublicKey,
        jupSwapData: JupSwapData,
        lamports: number = ADDITIONAL_FEE,
    ): Promise<TransactionToSend> {
        let tokenId = jupSwapData.fromTokenId;
        let ix = (jupSwapData.type == "Simple") ?
            await program.methods
                .rebalanceSell(
                    tokenId,
                    new BN(jupSwapData.fromAmount),
                    jupSwapData.dataLength,
                    Array.from(jupSwapData.data),
                )
                .accounts({
                    signer: wallet.publicKey,
                    fundState: basketState,
                    tokenList: TOKEN_LIST_ADDRESS,
                    oracleSol: new PublicKey(tokenList[1].oracleAccount),
                    oracleToken: new PublicKey(tokenList[tokenId].oracleAccount),
                    oracleUsdc: new PublicKey(tokenList[0].oracleAccount),
                    pdaAccount: pda,
                    pdaTokenAccount: new PublicKey(tokenList[tokenId].pdaTokenAccount),
                    pdaUsdcAccount: new PublicKey(tokenList[0].pdaTokenAccount),
                    rebalanceFeeAccount: rebalanceFeeAccount,
                    tokenProgram: TOKEN_PROGRAM_ID,
                })
                .remainingAccounts(jupSwapData.accounts)
                .instruction() :
            await program.methods
                .rebalanceSellTransitive(
                    tokenId,
                    new BN(jupSwapData.fromAmount),
                    jupSwapData.firstIxEnd,
                    jupSwapData.dataLength,
                    jupSwapData.firstIxAccounts,
                    Array.from(jupSwapData.data),
                )
                .accounts({
                    signer: wallet.publicKey,
                    fundState: basketState,
                    tokenList: TOKEN_LIST_ADDRESS,
                    oracleSol: new PublicKey(tokenList[1].oracleAccount),
                    oracleToken: new PublicKey(tokenList[tokenId].oracleAccount),
                    oracleUsdc: new PublicKey(tokenList[0].oracleAccount),
                    pdaAccount: pda,
                    pdaTokenAccount: new PublicKey(tokenList[tokenId].pdaTokenAccount),
                    pdaMidAccount: new PublicKey(jupSwapData.midTokenPda),
                    pdaUsdcAccount: new PublicKey(tokenList[0].pdaTokenAccount),
                    rebalanceFeeAccount: rebalanceFeeAccount,
                    tokenProgram: TOKEN_PROGRAM_ID,
                })
                .remainingAccounts(jupSwapData.accounts)
                .instruction();
        let rawData = {
            payerKey: wallet.publicKey,
            instructions: [
                ix,
                ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS}),
                ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
            ],
            lookupTables: jupSwapData.lookupTableAccounts
        };
        return rawData;
    }

    getRebalanceInfo(
        program: Program<BasketsIDL>,
        tokenList: TokenSettings[],
        oraclePriceData: number[],
        timestamp: number,
        forceRebalance: boolean = false,
    ): RebalanceInfo[] {
        let basketStateData = this.data;
        let currentCompToken: BN[] = basketStateData.currentCompToken;
        let currentCompAmount: BN[] = basketStateData.currentCompAmount;
        let targetWeight: BN[] = basketStateData.targetWeight;
        let numTokens = parseInt(basketStateData.numOfTokens.toString());
        let rebalanceThreshold = basketStateData.rebalanceThreshold;
        let weightSum = basketStateData.weightSum;
        let rebalanceInterval = parseInt(basketStateData.rebalanceInterval.toString());
        let lastRebalanceTime = basketStateData.lastRebalanceTime; 
        return calculateRebalanceAmounts(
            program,
            numTokens,
            timestamp,
            Array.from(lastRebalanceTime, x => parseInt(x.toString())),
            rebalanceInterval,
            Array.from(currentCompToken, x => parseInt(x.toString())),
            Array.from(currentCompAmount, x => parseInt(x.toString())),
            Array.from(targetWeight, x => parseInt(x.toString())),
            parseInt(weightSum.toString()),
            tokenList,
            parseInt(rebalanceThreshold.toString()),
            oraclePriceData,
            forceRebalance,
        );
    }

    async rebalanceFrom(
        program: Program<BasketsIDL>,
        wallet: Wallet,
        tokenList: TokenSettings[],
        jupSwapDatas: JupSwapData[],
        rebalanceInfos: RebalanceInfo[],
        lookups: AddressLookupTableAccount[],
        lamports: number = ADDITIONAL_FEE,
    ): Promise<TransactionToSend[]> {
        let ix = await buildUpdateCurrentWeightsIx(program, this, tokenList);
        let transactionsData: {
            payerKey: PublicKey,
            instructions: TransactionInstruction[],
            lookupTables: AddressLookupTableAccount[]
        }[] = [];
        for (let i = rebalanceInfos.length - 1; i >= 0; i--) {
            if (rebalanceInfos[i].side == Side.To)
                continue;
            let rebalanceData = jupSwapDatas[i];
            if (!rebalanceData)
                continue;
            let txData = await this.rebalanceFromUsdcTransactionData(
                program,
                wallet,
                BASKETS_PROGRAM_PDA,
                this.ownAddress,
                tokenList,
                REBALANCE_FEE_ACCOUNT,
                rebalanceData,
                lamports,
            ).catch((e) => { console.log(e.message); return null;});
            if (!txData)
                continue;
            transactionsData.push(txData);
        }
        for (let i = 0; i < transactionsData.length; i++) {
            transactionsData[i].instructions = [ix, ...transactionsData[i].instructions];
            transactionsData[i].lookupTables = [...lookups, ...transactionsData[i].lookupTables];
        }
        return transactionsData;
    }

    async rebalanceTo(
        program: Program<BasketsIDL>,
        wallet: Wallet,
        tokenList: TokenSettings[],
        jupSwapDatas: JupSwapData[],
        rebalanceInfos: RebalanceInfo[],
        lookups: AddressLookupTableAccount[],
        lamports: number = ADDITIONAL_FEE,
    ): Promise<{
        payerKey: PublicKey,
        instructions: TransactionInstruction[],
        lookupTables: AddressLookupTableAccount[]
    }[]> {
        let ix = await buildUpdateCurrentWeightsIx(program, this, tokenList);
        let transactionsData: {
            payerKey: PublicKey,
            instructions: TransactionInstruction[],
            lookupTables: AddressLookupTableAccount[]
        }[] = [];
        for (let i = rebalanceInfos.length - 1; i >= 0; i--) {
            if (rebalanceInfos[i].side == Side.From)
                continue;
            let rebalanceData = jupSwapDatas[i];
            if (!rebalanceData)
                continue;
            let txData = await this.rebalanceToUsdcTransactionData(
                program,
                wallet,
                BASKETS_PROGRAM_PDA,
                this.ownAddress,
                tokenList,
                REBALANCE_FEE_ACCOUNT,
                rebalanceData,
                lamports,
            ).catch((e) => { console.log(e.message); return null;});
            if (!txData)
                continue;
            transactionsData.push(txData);
        }
        for (let i = 0; i < transactionsData.length; i++) {
            transactionsData[i].instructions = [ix, ...transactionsData[i].instructions];
            transactionsData[i].lookupTables = [...lookups, ...transactionsData[i].lookupTables];
        }
        return transactionsData;
    }

    // async rebalanceSingle(
    //     program: Program<BasketsIDL>,
    //     wallet: Wallet,
    //     tokenList: TokenSettings[],
    //     jupSwapData: JupSwapData,
    //     rebalanceInfo: RebalanceInfo,
    //     lamports: number,
    //     updateOracles: boolean, /// NEED to implement
    // ): Promise<TransactionSignature> {
    //     let prepareIx = await buildUpdateCurrentWeightsIx(program, this, tokenList);
    //     let txData = (rebalanceInfo.side == Side.To) ?
    //         await this.rebalanceToUsdcTransactionData(
    //             program,
    //             wallet,
    //             BASKETS_PROGRAM_PDA,
    //             this.ownAddress,
    //             tokenList,
    //             REBALANCE_FEE_ACCOUNT,
    //             jupSwapData,
    //             lamports,
    //         ) : 
    //         await this.rebalanceFromUsdcTransactionData(
    //             program,
    //             wallet,
    //             BASKETS_PROGRAM_PDA,
    //             this.ownAddress,
    //             tokenList,
    //             REBALANCE_FEE_ACCOUNT,
    //             jupSwapData,
    //             lamports,
    //         );
    //     txData.instructions = [prepareIx, ...txData.instructions];
    //     const lookupTableAccount1 = (
    //         await program.provider.connection.getAddressLookupTable(BASKETS_LOOKUP_TABLE_1)
    //     ).value;
    //     const lookupTableAccount2 = (
    //         await program.provider.connection.getAddressLookupTable(BASKETS_LOOKUP_TABLE_2)
    //     ).value;
    //       //@ts-ignore
    //     txData.lookupTables.push(lookupTableAccount1);
    //     //@ts-ignore
    //     txData.lookupTables.push(lookupTableAccount2);
    //     let blockhash = (await program.provider.connection.getLatestBlockhash()).blockhash;
    //     let signedTransactions = await signVersionedTransactions(
    //         wallet,
    //         [new VersionedTransaction(
    //             new TransactionMessage({
    //                 payerKey: txData.payerKey,
    //                 recentBlockhash: blockhash,
    //                 instructions: txData.instructions,
    //             }).compileToV0Message(txData.lookupTables)
    //         )]
    //     ).catch(e => { console.log(e); });
    //     let txs = await sendSignedTransactions(
    //         program.provider.connection, //@ts-ignore
    //         signedTransactions,
    //         1
    //     ).catch((e) => {console.log(e); return [""]});
    //     return txs[0];
    // }

    // async sellBasketToSingleToken(
    //     program: Program<BasketsIDL>,
    //     wallet: Wallet,
    //     tokenList: TokenSettings[],
    //     withdrawToken: PublicKey,
    //     amount: number,
    //     lamports: number,
    //     updateOracles: boolean, /// NEED to implement
    // ): Promise<TransactionSignature> {
    //     let transaction = new Transaction();
    //     transaction.instructions = [
    //         await buildSellBasketToSingleTokenIx(program, tokenList, wallet.publicKey, this, withdrawToken,  amount),
    //         ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS}),
    //         ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
    //     ];
    //     let signedTransactions = await signTransactionsWithWallet(
    //         program.provider.connection,
    //         wallet,
    //         [{transaction: transaction, signers:[]}]
    //     );
    //     let txs = await sendSignedTransactions(
    //         program.provider.connection,
    //         signedTransactions,
    //         1,
    //     );
    //     return txs[0];
    // }

    async computeOutputAmountWithSingleToken(
        oraclePrices: number[],
        tokenList: TokenSettings[],
        withdrawToken: TokenSettings,
        burnAmount: number,
    ): Promise<number> {

        let tokenIndex = -1;
        for (let i = 0; i < this.data.currentCompToken.length; i++)
            if (parseInt(this.data.currentCompToken[i].toString()) == withdrawToken.id) {
                tokenIndex = i; break;
            }
        if (tokenIndex == -1)
            throw new Error("Token not in composition");
      
        let withdrawTokenPrice = oraclePrices[withdrawToken.id];
        
        let withdrawWorth = 0;
        let basketWorth = 0;
        for (let i = 0; i < parseInt(this.data.numOfTokens.toString()); i++) {
            let tokenPrice = oraclePrices[parseInt(this.data.currentCompToken[i].toString())];
            basketWorth += tokenPrice * parseInt(this.data.currentCompAmount[i].toString())
                / 10 ** tokenList[parseInt(this.data.currentCompToken[i].toString())].decimals;
            let allocation = (
                parseInt(this.data.currentCompAmount[i].toString()) * burnAmount * 10 ** 6 /
                parseInt(this.data.supplyOutstanding.toString())
            );
            withdrawWorth += tokenPrice * allocation
                / 10 ** tokenList[parseInt(this.data.currentCompToken[i].toString())].decimals;
        }

        let targetValueBefore = basketWorth * parseInt(this.data.targetWeight[tokenIndex].toString())
            / parseInt(this.data.weightSum.toString());
        
        let valueBefore = withdrawTokenPrice * parseInt(this.data.currentCompAmount[tokenIndex].toString())
            / 10 ** withdrawToken.decimals
        let valueToRebalance = 0;
        if (valueBefore <= targetValueBefore)
            valueToRebalance = withdrawWorth; else  {
                let valueToTgtWeight = (valueBefore - targetValueBefore) *
                    parseInt(this.data.weightSum.toString()) /
                    (parseInt(this.data.weightSum.toString()) - parseInt(this.data.targetWeight[tokenIndex].toString()))
               
                if (valueToTgtWeight < withdrawWorth) {
                    valueToRebalance = withdrawWorth - valueToTgtWeight;
                }
        }
        
        let tokenAmount = 0;
        tokenAmount += (withdrawWorth - valueToRebalance) / withdrawTokenPrice;
        let penalty = valueToRebalance * 300 / 10000;
        tokenAmount += (valueToRebalance - penalty) / withdrawTokenPrice;

        if (tokenAmount > parseInt(this.data.currentCompAmount[tokenIndex].toString()) / 10 ** withdrawToken.decimals)
            tokenAmount = parseInt(this.data.currentCompAmount[tokenIndex].toString()) / 10 ** withdrawToken.decimals;
        
        return tokenAmount;
    }

    async sell(
        program: Program<BasketsIDL>,
        wallet: Wallet,
        amount: number,
        rebalance: number,
        lamports: number = ADDITIONAL_FEE,
    ): Promise<Basket> {
        let sellBasketData = await buildSellBasketIx(program, wallet.publicKey, this, amount, rebalance);
        let transaction = new Transaction();
        transaction.instructions = [
            sellBasketData,
            ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS}),
            ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
        ];
        let signedTransactions = await signTransactionsWithWallet(
            program.provider.connection,
            wallet,
            [{transaction: transaction, signers: []}]
        );
        await sendSignedTransactions(
            program.provider.connection,
            signedTransactions,
            1,
        );
    
        return await Basket.loadFromPubkey(
            program,
            sellBasketData.keys[3].pubkey,
        );
    }

    async claimTokens(
        program: Program<BasketsIDL>,
        wallet: Wallet,
        tokenList: TokenSettings[],
        lamports: number = ADDITIONAL_FEE,
    ): Promise<TransactionSignature[]> {
        await this.update(program);
        let basketOwner = this.data.manager;
        let connection = program.provider.connection;
        let transactions: Transaction[] = [];
        let claimTokensIxs = await buildClaimTokensFromSellStateIxs(program, tokenList, wallet.publicKey, this);
        let ixId = 0;
        for (let i = 0; i < parseInt(this.data.numOfTokens.toString()); i++) {
            if (parseInt(this.data.currentCompAmount[i].toString()) == 0 && i != 0)
                continue;
            let transaction = new Transaction();
            let tokenId = parseInt(this.data.currentCompToken[i].toString());
            let userTokenAccount = getAssociatedTokenAddressSync(
                new PublicKey(tokenList[tokenId].tokenMint),
                basketOwner,
                true,
            );
            let infoAta = await connection.getAccountInfo(userTokenAccount, "confirmed");
            if (!infoAta)
                transaction.add(
                    await createAssociatedTokenAccountInstruction(
                        wallet.publicKey,
                        userTokenAccount,
                        basketOwner,
                        new PublicKey(tokenList[tokenId].tokenMint),
                    )
                );
            transaction.instructions = [
                ...transaction.instructions,
                claimTokensIxs[ixId],
                ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS}),
                ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
            ]
            transactions.push(transaction);
            ixId = ixId + 1;
        }
        let signedTransactions = await signTransactionsWithWallet(
            connection,
            wallet,
            transactions.map(transaction => {
                return { transaction: transaction, signers: []}
            })
        );
        return await sendSignedTransactions(
            connection,
            signedTransactions,
        );
    }

    async removeDust(
        program: Program<BasketsIDL>,
        wallet: Wallet,
        tokenList: TokenSettings[],
        oraclePriceData: number[],
        lamports: number,
        updateOracles: boolean, /// NEED to implement
    ): Promise<TransactionSignature[]> {
        let preTransactions: Transaction[] = [];
        let swapTransactions: Transaction[] = [];
        for (let j = parseInt(this.data.numOfTokens.toString()) - 1; j > 0; j--) {
            let token = parseInt(this.data.currentCompToken[j].toString());
            let decimals = tokenList[token].decimals;
            let amount = parseInt(this.data.currentCompAmount[j].toString());
            let weight = parseInt(this.data.targetWeight[j].toString());
            let price = oraclePriceData[token];
            let value = (amount / 10 ** decimals) * price;
            if (value > 0.005 || weight != 0)
                continue;
            let transaction = new Transaction();
            let instructions = [];
            let fromTokenAccount = getAssociatedTokenAddressSync(
                new PublicKey(tokenList[0].tokenMint),
                wallet.publicKey,
                true,
            );
            let toTokenAccount = getAssociatedTokenAddressSync(
                new PublicKey(tokenList[token].tokenMint),
                wallet.publicKey,
                true,
            );
            const info = await program.provider.connection.getAccountInfo(toTokenAccount, "confirmed");
            if (!info) {
                instructions.push(
                    createAssociatedTokenAccountInstruction(
                        wallet.publicKey,
                        toTokenAccount,
                        wallet.publicKey,
                        new PublicKey(tokenList[token].tokenMint),
                    )
                );
            }
            let swapFeeAccount = getAssociatedTokenAddressSync(
                new PublicKey(tokenList[token].tokenMint),
                SWAP_FEE_ACCOUNT,
            );
            const info2 =  await program.provider.connection.getAccountInfo(swapFeeAccount, "confirmed");
            if (!info2) {
                instructions.push(
                    createAssociatedTokenAccountInstruction(
                        wallet.publicKey,
                        swapFeeAccount,
                        SWAP_FEE_ACCOUNT,
                        new PublicKey(tokenList[token].tokenMint),
                    )
                );
            }
            let hostFeeAccount = getAssociatedTokenAddressSync(
                new PublicKey(tokenList[token].tokenMint),
                this.data.hostPubkey,
                true,
            );
            const info3 =  await program.provider.connection.getAccountInfo(hostFeeAccount, "confirmed");
            if (!info3) {
                instructions.push(
                    createAssociatedTokenAccountInstruction(
                        wallet.publicKey,
                        hostFeeAccount,
                        this.data.hostPubkey,
                        new PublicKey(tokenList[token].tokenMint),
                    )
                );
            }
            let managerFeeAccount = getAssociatedTokenAddressSync(
                new PublicKey(tokenList[token].tokenMint),
                this.data.manager,
                true,
            );
            const info4 =  await program.provider.connection.getAccountInfo(managerFeeAccount, "confirmed");
            if (!info4 && managerFeeAccount.toBase58() != hostFeeAccount.toBase58()) {
                instructions.push(
                    createAssociatedTokenAccountInstruction(
                        wallet.publicKey,
                        managerFeeAccount,
                        this.data.manager,
                        new PublicKey(tokenList[token].tokenMint),
                    )
                );
            }
            if (instructions.length > 0) {
                transaction.add(...instructions);
                transaction = transaction.add(
                    ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS})
                ).add(
                    ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
                );
                preTransactions.push(transaction);
            }
            let usdcAmount = Math.max(Math.floor(1.1 * value * 10 ** 6), 50)
            swapTransactions.push(
                new Transaction()
                    .add(await program.methods.liquidityProvision(
                            new BN(0),
                            new BN(token),
                            new BN(usdcAmount),
                            new BN(amount),
                        )
                    .accounts({
                        buyer: program.provider.publicKey,
                        fundState: this.ownAddress,
                        pdaAccount: BASKETS_PROGRAM_PDA,
                        pdaFromTokenAccount: new PublicKey(tokenList[0].pdaTokenAccount),
                        buyerFromTokenAccount: fromTokenAccount,
                        pdaToTokenAccount: new PublicKey(tokenList[token].pdaTokenAccount),
                        buyerToTokenAccount: toTokenAccount,
                        oracleSol: new PublicKey(tokenList[1].oracleAccount),
                        swapFeeAccount: getAssociatedTokenAddressSync(
                            new PublicKey(tokenList[token].tokenMint),
                            SWAP_FEE_ACCOUNT,
                        ),
                        hostFeeAccount: getAssociatedTokenAddressSync(
                            new PublicKey(tokenList[token].tokenMint),
                            this.data.hostPubkey,
                            true,
                        ),
                        managerFeeAccount: getAssociatedTokenAddressSync(
                            new PublicKey(tokenList[token].tokenMint),
                            this.data.manager,
                            true,
                        ),
                        tokenList: TOKEN_LIST_ADDRESS,
                        curveData: CURVE_DATA_ADDRESS,
                        tokenProgram: TOKEN_PROGRAM_ID
                    })
                    .remainingAccounts(this.data.currentCompToken.map(x => {
                        return {
                            pubkey: new PublicKey(tokenList[parseInt(x.toString())].oracleAccount),
                            isSigner: false,
                            isWritable: false,
                        }
                    }))
                    .instruction()
                )
                .add(
                    ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS})
                ).add(
                    ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
                )
            );
        }
        let transactions = [...preTransactions, ...swapTransactions];
        let signedTransactions = await signTransactionsWithWallet(
            program.provider.connection,
            wallet,
            transactions.map(transaction => {
                return { transaction: transaction, signers: []}
            })
        );
        let _ = await sendSignedTransactions(
            program.provider.connection,
            signedTransactions.slice(0, preTransactions.length),
        );
        return await sendSignedTransactions(
            program.provider.connection,
            signedTransactions.slice(preTransactions.length, signedTransactions.length),
            swapTransactions.length,
        );
    }

    async liquidityProvision(
        program: Program<BasketsIDL>,
        wallet: Wallet,
        tokenList: TokenSettings[],
        fromToken: number,
        toToken: number,
        fromAmount: number,
        lamports: number = ADDITIONAL_FEE,
    ): Promise<TransactionSignature[]> {
        let preTransactions: Transaction[] = [];
        let swapTransactions: Transaction[] = [];
        
        let transaction = new Transaction();
        let instructions = [];
        let fromTokenAccount = getAssociatedTokenAddressSync(
            new PublicKey(tokenList[fromToken].tokenMint),
            wallet.publicKey,
            true,
        );
        let toTokenAccount = getAssociatedTokenAddressSync(
            new PublicKey(tokenList[toToken].tokenMint),
            wallet.publicKey,
            true,
        );
        const info = await program.provider.connection.getAccountInfo(toTokenAccount, "confirmed");
        if (!info) {
            instructions.push(
                createAssociatedTokenAccountInstruction(
                    wallet.publicKey,
                    toTokenAccount,
                    wallet.publicKey,
                    new PublicKey(tokenList[toToken].tokenMint),
                )
            );
        }
        let swapFeeAccount = getAssociatedTokenAddressSync(
            new PublicKey(tokenList[toToken].tokenMint),
            SWAP_FEE_ACCOUNT,
        );
        const info2 =  await program.provider.connection.getAccountInfo(swapFeeAccount, "confirmed");
        if (!info2) {
            instructions.push(
                createAssociatedTokenAccountInstruction(
                    wallet.publicKey,
                    swapFeeAccount,
                    SWAP_FEE_ACCOUNT,
                    new PublicKey(tokenList[toToken].tokenMint),
                )
            );
        }
        let hostFeeAccount = getAssociatedTokenAddressSync(
            new PublicKey(tokenList[toToken].tokenMint),
            this.data.hostPubkey,
            true,
        );
        const info3 =  await program.provider.connection.getAccountInfo(hostFeeAccount, "confirmed");
        if (!info3) {
            instructions.push(
                createAssociatedTokenAccountInstruction(
                    wallet.publicKey,
                    hostFeeAccount,
                    this.data.hostPubkey,
                    new PublicKey(tokenList[toToken].tokenMint),
                )
            );
        }
        let managerFeeAccount = getAssociatedTokenAddressSync(
            new PublicKey(tokenList[toToken].tokenMint),
            this.data.manager,
            true,
        );
        const info4 =  await program.provider.connection.getAccountInfo(managerFeeAccount, "confirmed");
        if (!info4 && managerFeeAccount.toBase58() != hostFeeAccount.toBase58()) {
            instructions.push(
                createAssociatedTokenAccountInstruction(
                    wallet.publicKey,
                    managerFeeAccount,
                    this.data.manager,
                    new PublicKey(tokenList[toToken].tokenMint),
                )
            );
        }
        if (instructions.length > 0) {
            transaction.add(...instructions);
            transaction = transaction.add(
                ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS})
            ).add(
                ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
            );
            preTransactions.push(transaction);
        }
        swapTransactions.push(
            new Transaction()
                .add(await program.methods.liquidityProvision(
                        new BN(fromToken),
                        new BN(toToken),
                        new BN(Math.floor(fromAmount * 10 ** tokenList[fromToken].decimals)),
                        new BN(0),
                    )
                .accounts({
                    buyer: program.provider.publicKey,
                    fundState: this.ownAddress,
                    pdaAccount: BASKETS_PROGRAM_PDA,
                    pdaFromTokenAccount: new PublicKey(tokenList[fromToken].pdaTokenAccount),
                    buyerFromTokenAccount: fromTokenAccount,
                    pdaToTokenAccount: new PublicKey(tokenList[toToken].pdaTokenAccount),
                    buyerToTokenAccount: toTokenAccount,
                    oracleSol: new PublicKey(tokenList[1].oracleAccount),
                    swapFeeAccount: getAssociatedTokenAddressSync(
                        new PublicKey(tokenList[toToken].tokenMint),
                        SWAP_FEE_ACCOUNT,
                    ),
                    hostFeeAccount: getAssociatedTokenAddressSync(
                        new PublicKey(tokenList[toToken].tokenMint),
                        this.data.hostPubkey,
                        true,
                    ),
                    managerFeeAccount: getAssociatedTokenAddressSync(
                        new PublicKey(tokenList[toToken].tokenMint),
                        this.data.manager,
                        true,
                    ),
                    tokenList: TOKEN_LIST_ADDRESS,
                    curveData: CURVE_DATA_ADDRESS,
                    tokenProgram: TOKEN_PROGRAM_ID
                })
                .remainingAccounts(this.data.currentCompToken
                        .slice(0, parseInt(this.data.numOfTokens.toString())).map(x => {
                    return {
                        pubkey: new PublicKey(tokenList[parseInt(x.toString())].oracleAccount),
                        isSigner: false,
                        isWritable: false,
                    }
                }))
                .instruction()
            )
            .add(
                ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS})
            ).add(
                ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports})
            )
        );

        let transactions = [...preTransactions, ...swapTransactions];
        let signedTransactions = await signTransactionsWithWallet(
            program.provider.connection,
            wallet,
            transactions.map(transaction => {
                return { transaction: transaction, signers: []}
            })
        );
        let _ = await sendSignedTransactions(
            program.provider.connection,
            signedTransactions.slice(0, preTransactions.length),
        );
        return await sendSignedTransactions(
            program.provider.connection,
            signedTransactions.slice(preTransactions.length, signedTransactions.length),
            swapTransactions.length,
        );
    }
}
