import {struct, u8 } from '@coral-xyz/borsh';
import { Wallet } from "@coral-xyz/anchor/dist/cjs/provider";
import { TOKEN_PROGRAM_ID, createAssociatedTokenAccountInstruction, createCloseAccountInstruction, getAssociatedTokenAddressSync } from "@solana/spl-token";
import { Connection, PublicKey, SystemProgram, Transaction, TransactionInstruction, } from "@solana/web3.js";
import { TokenSettings, RouteData } from "./types";

export async function applyBlockHashAndPartialSign(
    connection: Connection, wallet: Wallet,
    preTransaction:Transaction, transaction:Transaction, postTransaction:Transaction,
    preSigners:Array<any>, signers:Array<any>
  ) {
    let { blockhash } = await connection.getRecentBlockhash('processed');
    let result: any = [];
    let txIndex = 0;
  
    if (preTransaction.instructions.length > 0) {
      txIndex = 1;
      preTransaction.recentBlockhash = blockhash;
      preTransaction.feePayer = wallet.publicKey;
      preTransaction.setSigners(wallet.publicKey, ...preSigners.map((s) => s.publicKey))
      if (preSigners.length > 0)
        preTransaction.partialSign(...preSigners);
      result.push(preTransaction);
    }
  
    transaction.recentBlockhash = blockhash;
    transaction.feePayer = wallet.publicKey;
    transaction.setSigners(wallet.publicKey, ...signers.map((s) => s.publicKey))
    if (signers.length > 0)
      transaction.partialSign(...signers);
    result.push(transaction);
   
    if (postTransaction.instructions.length > 0) {
      postTransaction.recentBlockhash = blockhash;
      postTransaction.feePayer = wallet.publicKey;
      postTransaction.setSigners(wallet.publicKey, ...signers.map((s) => s.publicKey))
      if (signers.length > 0)
        postTransaction.partialSign(...signers);
      result.push(postTransaction);
    }
  
    return {
      txIndex: txIndex,
      transactions: result
    }
  }

export async function sendAndConfirmTransaction(connection: Connection, serialized: any) {
    let tries = 0;
    let signature: any = null;
    let signatureResponse: any = null;

    while (true) {
        connection.sendRawTransaction(serialized, {skipPreflight: true}).catch((e) => {
            return null;
        }).then(
            result => { if (!signature) signature = result; }
        );

        if (signature) {
            if (tries % 2 == 0) {
                let response = await connection.getTransaction(
                    signature, { commitment: "confirmed" }
                ).catch(
                    () => { return "Error"; }
                )
                if (response) {
                    signatureResponse = response;
                    break;
                }
            }
        }

        tries = tries + 1;
        if (tries == 30)
            break;
        await delay(1000);
    }

    return {
        signature: signature,
        response: signatureResponse
    }
}

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

export function createSyncNativeInstruction(
  nativeAccount: PublicKey
): TransactionInstruction {
  const dataLayout = struct([
    u8("instruction"),
  ]);
  const data = Buffer.alloc(dataLayout.span);
  dataLayout.encode(
    {
      instruction: 17, // SyncNative instruction
    },
    data
  );

  const keys = [{ pubkey: nativeAccount, isSigner: false, isWritable: true }];
  return new TransactionInstruction({
    keys,
    programId: TOKEN_PROGRAM_ID,
    data,
  });
}

export async function createAssociatedTokenAccount(
  account: string | undefined | null,
  owner: PublicKey,
  mintAddress: string,
  connection: Connection,
  transaction: Transaction,
  atas: string[] = []
) {
  let publicKey
  if (account) {
    publicKey = new PublicKey(account)
  }

  const mint = new PublicKey(mintAddress)
  const ata = getAssociatedTokenAddressSync(mint, owner)

  const infoAta = await connection.getAccountInfo(ata);

  if ((!publicKey || !ata.equals(publicKey)) && !atas.includes(ata.toBase58()) && !infoAta) {
    transaction.add(
      createAssociatedTokenAccountInstruction(
        owner,
        ata,
        owner,
        mint,
      )
    )
    atas.push(ata.toBase58())
  }

  return ata
}

export async function prepareAccounts(
  wallet: Wallet,
  connection: Connection,
  tokenListData: TokenSettings[],
  routeData: RouteData,
  fromTokenAccount: PublicKey|undefined,
  toTokenAccount: PublicKey|undefined,
): Promise<{
  preTransaction: Transaction,
  postTransaction: Transaction,
  fromTokenAccount: PublicKey,
  toTokenAccount: PublicKey,
}> {
  let preTransaction = new Transaction();
  let postTransaction = new Transaction();
  if (fromTokenAccount == undefined || fromTokenAccount.equals(wallet.publicKey)){
      if (tokenListData[routeData.fromTokenId].tokenMint === "So11111111111111111111111111111111111111112") {
          fromTokenAccount = await createAssociatedTokenAccount(fromTokenAccount?.toBase58(), wallet.publicKey, tokenListData[routeData.fromTokenId].tokenMint, connection, preTransaction);
          preTransaction.add(
            SystemProgram.transfer({
              fromPubkey: wallet.publicKey,
              toPubkey: fromTokenAccount,
              lamports: routeData.fromAmount * 1e9 + 1e7, // 1 sol
            })
          );
          preTransaction.add(
            createSyncNativeInstruction(fromTokenAccount)
          );
          postTransaction.add(
              createCloseAccountInstruction(
                  fromTokenAccount,
                  wallet.publicKey,
                  wallet.publicKey,
              )
          )
      } else
        fromTokenAccount = await createAssociatedTokenAccount(fromTokenAccount?.toBase58(), wallet.publicKey, tokenListData[routeData.fromTokenId].tokenMint, connection, preTransaction)
  }
  if (toTokenAccount == undefined || toTokenAccount.equals(wallet.publicKey)){
      if (tokenListData[routeData.toTokenId].tokenMint ===  "So11111111111111111111111111111111111111112") {
          toTokenAccount = await createAssociatedTokenAccount(toTokenAccount?.toBase58(), wallet.publicKey, tokenListData[routeData.toTokenId].tokenMint, connection, preTransaction)
          postTransaction.add(
            createCloseAccountInstruction(
              toTokenAccount,
              wallet.publicKey,
              wallet.publicKey,
            )
          )
        } else
        toTokenAccount = await createAssociatedTokenAccount(toTokenAccount?.toBase58(), wallet.publicKey, tokenListData[routeData.toTokenId].tokenMint, connection, preTransaction)
  }
  
  return {
      preTransaction: preTransaction,
      postTransaction: postTransaction,
      fromTokenAccount: fromTokenAccount,
      toTokenAccount: toTokenAccount,
  };
}
