import {
  AnchorProvider,
  BorshAccountsCoder,
  Program,
  utils
} from '@project-serum/anchor';
import { SignerWallet } from '@saberhq/solana-contrib';
import type { Connection } from '@solana/web3.js';
import { Keypair, PublicKey } from '@solana/web3.js';

import type { AccountData } from '../../types';
import type {
  BLAZE_PROGRAM,
  BlazeData,
  EntrantsData,
  EntrantsDataRaw,
  IdentifierData
} from './constants';
import {
  BLAZE_ADDRESS,
  BLAZE_IDL,
  BLAZE_PREFIX,
  BLAZE_PROJECT_ID_OFFSET,
  ENTRANTS_PREFIX
} from './constants';
import { findIdentifierId } from './pda';

const getProgram = (connection: Connection) => {
  const provider = new AnchorProvider(
    connection,
    new SignerWallet(Keypair.generate()),
    {}
  );
  return new Program<BLAZE_PROGRAM>(BLAZE_IDL, BLAZE_ADDRESS, provider);
};

export const getBlaze = async (
  connection: Connection,
  blazeId: PublicKey
): Promise<AccountData<BlazeData>> => {
  const program = getProgram(connection);

  const parsed = await program.account.blaze.fetch(blazeId);
  return {
    parsed,
    pubkey: blazeId
  };
};

export const getBlazes = async (
  connection: Connection,
  blazeIds: PublicKey[]
): Promise<AccountData<BlazeData>[]> => {
  const program = getProgram(connection);

  const blazes = (await program.account.blaze.fetchMultiple(
    blazeIds
  )) as BlazeData[];
  return blazes.map((tm, i) => ({
    parsed: tm,
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    pubkey: blazeIds[i]!
  }));
};

export const getAllBlazes = async (
  connection: Connection
): Promise<AccountData<BlazeData>[]> => {
  if (!connection) return [];
  const programAccounts = await connection.getProgramAccounts(BLAZE_ADDRESS, {
    filters: [
      {
        memcmp: {
          offset: 0,
          bytes: utils.bytes.bs58.encode(
            BorshAccountsCoder.accountDiscriminator(BLAZE_PREFIX)
          )
        }
      }
    ]
  });
  const blazeDatas: AccountData<BlazeData>[] = [];
  const coder = new BorshAccountsCoder(BLAZE_IDL);
  programAccounts.forEach(account => {
    try {
      const blazeData: BlazeData = coder.decode(
        BLAZE_PREFIX,
        account.account.data
      );
      if (blazeData) {
        blazeDatas.push({
          ...account,
          parsed: blazeData
        });
      }
      // eslint-disable-next-line no-empty
    } catch (e) {}
  });
  return blazeDatas.sort((a, b) =>
    a.pubkey.toBase58().localeCompare(b.pubkey.toBase58())
  );
};

export const getIdentifier = async (
  connection: Connection
): Promise<AccountData<IdentifierData>> => {
  const program = getProgram(connection);
  const [identifierId] = await findIdentifierId();
  const parsed = await program.account.identifier.fetch(identifierId);
  return {
    parsed,
    pubkey: identifierId
  };
};

export const getBlazesByProjectId = async (
  connection: Connection,
  projectId: PublicKey
): Promise<AccountData<BlazeData>[]> => {
  if (!connection) return [];
  const programAccounts = await connection.getProgramAccounts(BLAZE_ADDRESS, {
    filters: [
      {
        memcmp: {
          offset: 0,
          bytes: utils.bytes.bs58.encode(
            BorshAccountsCoder.accountDiscriminator(BLAZE_PREFIX)
          )
        }
      },
      {
        memcmp: {
          offset: BLAZE_PROJECT_ID_OFFSET,
          bytes: projectId.toBase58()
        }
      }
    ]
  });
  const blazeDatas: AccountData<BlazeData>[] = [];
  const coder = new BorshAccountsCoder(BLAZE_IDL);
  programAccounts.forEach(account => {
    try {
      const blazeData: BlazeData = coder.decode(
        BLAZE_PREFIX,
        account.account.data
      );
      if (blazeData) {
        blazeDatas.push({
          ...account,
          parsed: blazeData
        });
      }
      // eslint-disable-next-line no-empty
    } catch (e) {}
  });
  return blazeDatas.sort((a, b) =>
    a.pubkey.toBase58().localeCompare(b.pubkey.toBase58())
  );
};

export const getAllEntrants = async (
  connection: Connection
): Promise<AccountData<EntrantsData>[]> => {
  if (!connection) return [];
  const programAccounts = await connection.getProgramAccounts(BLAZE_ADDRESS, {
    filters: [
      {
        memcmp: {
          offset: 0,
          bytes: utils.bytes.bs58.encode(
            BorshAccountsCoder.accountDiscriminator(ENTRANTS_PREFIX)
          )
        }
      }
    ]
  });
  const entrantsDatas: AccountData<EntrantsData>[] = [];
  const coder = new BorshAccountsCoder(BLAZE_IDL);

  programAccounts.forEach(account => {
    try {
      const entrantsDataRaw: EntrantsDataRaw = coder.decode(
        ENTRANTS_PREFIX,
        account.account.data
      );
      if (entrantsDataRaw) {
        const entrants: PublicKey[] = [];
        const entrantsFieldData = account.account.data.subarray(8 + 4 + 4);
        for (let i = 0; i < entrantsDataRaw.total; i++) {
          entrants.push(
            new PublicKey(entrantsFieldData.slice(i * 32, (i + 1) * 32))
          );
        }
        entrantsDatas.push({
          ...account,
          parsed: { ...entrantsDataRaw, entrants }
        });
      }
      // eslint-disable-next-line no-empty
    } catch (e) {}
  });
  return entrantsDatas.sort((a, b) =>
    a.pubkey.toBase58().localeCompare(b.pubkey.toBase58())
  );
};

export const getEntrants = async (
  connection: Connection,
  entrantsId: PublicKey
): Promise<AccountData<EntrantsData> | null | undefined> => {
  if (!connection) return null;
  const programAccounts = await connection.getProgramAccounts(BLAZE_ADDRESS, {
    filters: [
      {
        memcmp: {
          offset: 0,
          bytes: utils.bytes.bs58.encode(
            BorshAccountsCoder.accountDiscriminator(ENTRANTS_PREFIX)
          )
        }
      }
    ]
  });
  const coder = new BorshAccountsCoder(BLAZE_IDL);

  const account = programAccounts.find(
    account => account.pubkey.toBase58() === entrantsId.toBase58()
  );
  if (!account) return null;
  try {
    const entrantsDataRaw: EntrantsDataRaw = coder.decode(
      ENTRANTS_PREFIX,
      account.account.data
    );
    if (entrantsDataRaw) {
      const entrants: PublicKey[] = [];
      const entrantsFieldData = account.account.data.subarray(8 + 4 + 4);
      for (let i = 0; i < entrantsDataRaw.total; i++) {
        entrants.push(
          new PublicKey(entrantsFieldData.slice(i * 32, (i + 1) * 32))
        );
      }
      return {
        ...account,
        parsed: { ...entrantsDataRaw, entrants }
      };
    }
    // eslint-disable-next-line no-empty
  } catch (e) {}
};
