import { ethers, BaseContract, BigNumber, Signer, ContractTransaction } from 'ethers';
import {
  UFOMarketplaceConfig,
  NFTInfo,
  UAPClaimInfo,
  BucketInfo,
  WeaponInfo,
  LootBuySellInfo,
  PurchaseType,
} from './types';
import {
  SuperGalaticFactory,
  SuperGalaticFactory__factory,
  ERC20Upgradeable,
  ERC20Upgradeable__factory,
  ERC721Upgradeable,
  ERC721Upgradeable__factory,
  ERC20__factory,
  ERC20,
  UfoMarketplace,
  UfoMarketplace__factory,
  ERC721EnumerableUpgradeable,
  ERC721EnumerableUpgradeable__factory,
} from './wrapper';
import { BigNumber as DecimalBigNumber } from 'bignumber.js';

import { GENSIS_NFT_USDT_PRICE, NETWORK_ID, USER_CURRENCY } from './constants/constants';

const exponent = new DecimalBigNumber('10').pow(18);
const usdtExponent = new DecimalBigNumber('10').pow(6);
const MAX_INT = BigNumber.from('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
export class ufoMarketplaceSDK {
  private signer: Signer;
  private config: UFOMarketplaceConfig;
  private galaticFactory: SuperGalaticFactory;
  private plasma: ERC20Upgradeable | null;
  private usdtToken: ERC20 | null;
  private wETHToken: ERC20;
  private netId: number;
  constructor(_signer: Signer, _config: UFOMarketplaceConfig, _netID: string) {
    this.signer = _signer;
    this.config = _config;
    this.galaticFactory = new SuperGalaticFactory__factory(_signer).attach(_config.ufoSuperGalaticFactory);
    let netId = parseInt(_netID);
    this.netId = netId;
    switch (netId) {
      case NETWORK_ID.ETHEREUM:
      case NETWORK_ID.SEPOLIA:
        if (_config.ufoConfig.plasmaTokenOnEth != null && _config.ufoConfig.plasmaTokenOnEth != undefined) {
          this.plasma = new ERC20Upgradeable__factory(this.signer).attach(_config.ufoConfig.plasmaTokenOnEth);
        }
        this.wETHToken = new ERC20__factory(this.signer).attach(_config.ufoConfig.WETHOnEth);
        break;
      case NETWORK_ID.BEAM:
      case NETWORK_ID.BEAM_TESTNET:
        this.plasma = new ERC20Upgradeable__factory(this.signer).attach(_config.ufoConfig.plasmaTokenOnBeamTestNet);
        this.wETHToken = new ERC20__factory(this.signer).attach(_config.ufoConfig.WETHOnBeamTestNet);
        this.usdtToken = new ERC20__factory(this.signer).attach(_config.ufoConfig.usdtTokenOnBeamTestNet);
        break;
    }
  }

  public async sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  public async nftHasApprovalToMarketplace(contractAddr: string, nftId: string): Promise<boolean> {
    let nftContract: ERC721Upgradeable = new ERC721Upgradeable__factory(this.signer).attach(contractAddr);
    const _temp = new DecimalBigNumber(nftId);
    if (_temp.isNegative()) {
      throw new Error(`${nftId} is a negative amount`);
    }
    return (await nftContract.getApproved(nftId)) == this.config.ufoMarketplace;
  }

  public async approveNftToMarketplace(contractAddr: string, nftId: string): Promise<ContractTransaction> {
    let nftContract: ERC721Upgradeable = new ERC721Upgradeable__factory(this.signer).attach(contractAddr);
    const _temp = new DecimalBigNumber(nftId);
    if (_temp.isNegative()) {
      throw new Error(`${nftId} is a negative amount`);
    }
    return nftContract.approve(this.config.ufoMarketplace, nftId);
  }

  public async wETHAllowance(amount: string): Promise<boolean> {
    const addr = await this.signer.getAddress();
    const allowance = await this.wETHToken.allowance(addr, this.config.ufoMarketplace);
    const _temp = new DecimalBigNumber(amount);
    return new DecimalBigNumber(allowance.toString()).gte(_temp);
  }

  public async wETHBalance(): Promise<string> {
    const addr = await this.signer.getAddress();
    let balance = await this.wETHToken.balanceOf(addr);
    const balStr = ethers.utils.formatEther(balance);
    return balStr;
  }

  public async getNFTCurrentSupply(contractAddr: string): Promise<number> {
    let nftContract: ERC721EnumerableUpgradeable = ERC721EnumerableUpgradeable__factory.connect(
      contractAddr,
      this.signer
    );
    return (await nftContract.totalSupply()).toNumber();
  }

  public async revealNft(): Promise<ContractTransaction> {
    return await this.galaticFactory.registerForMint();
  }

  public async test(): Promise<void> {
    console.log("========1========");
    await this.sleep(5000);
    console.log("=========2=======");
  }

  public async batchMintGalaticNFT(
    categoryIds: [number],
    amounts: [number],
    type: PurchaseType,
    merkleProof: string[]
  ): Promise<ContractTransaction> {
    console.log('======', categoryIds, amounts, type, merkleProof);
    switch (type) {
      case PurchaseType.PLASMA:
        return this.batchMintGalaticNFTByPlasma(categoryIds, amounts, merkleProof);
      case PurchaseType.BEAM:
        return this.batchMintGalaticNFTByBeam(categoryIds, amounts, merkleProof);
      case PurchaseType.USDT:
        return this.batchMintGalaticNFTByUsdt(categoryIds, amounts, merkleProof);
    }
  }

  private async batchMintGalaticNFTByPlasma(
    categoryIds: [number],
    amounts: [number],
    merkleProof: string[]
  ): Promise<ContractTransaction> {
    return this.galaticFactory.mintBatchSuperGalatic(categoryIds, amounts, merkleProof!);
  }

  private async batchMintGalaticNFTByBeam(
    categoryIds: [number],
    amounts: [number],
    merkleProof: string[]
  ): Promise<ContractTransaction> {
    let totalCount = amounts.reduce((acc, curr) => {
      return acc + curr;
    }, 0);
    await this.sleep(5000);
    let nftPriceInBeam = await this.galaticFactory.beamAmountPerNft();

    let nativeTokenAmount = BigNumber.from(totalCount).mul(nftPriceInBeam).mul("250").toString();
    return this.galaticFactory.mintWithBeam(categoryIds, amounts, merkleProof, {
      value: nativeTokenAmount,
      gasLimit: BigNumber.from('2500000'),
    });
  }

  private async batchMintGalaticNFTByUsdt(
    categoryIds: [number],
    amounts: [number],
    merkleProof: string[]
  ): Promise<ContractTransaction> {
    // Estimate gas for the transaction
    // const gasEstimate = await this.galaticFactory.estimateGas.mintSuperGalaticByUSDT(categoryIds, amounts, merkleProof);

    // console.log("===============gas============", gasEstimate.toString());
    // // Execute the transaction with the estimated gas
    await this.sleep(5000);
    return this.galaticFactory.mintWithUSDT(categoryIds, amounts, merkleProof, {
      gasLimit: BigNumber.from('2500000'), //gasEstimate.mul(120).div(100) // Add 20% buffer to gas estimate
    });
  }

  public async plasmaAmountPerNFT(): Promise<string> {
    let plasmaAmount = await this.galaticFactory.plasmaAmountPerNFT();
    return new DecimalBigNumber(plasmaAmount.toString()).div(exponent).toString();
  }

  //wETH
  public async getWETHAllowanceOfNFTFactory(): Promise<string> {
    if (this.wETHToken == null || this.wETHToken == undefined)
      throw new Error('check the wETHToken contract of correct network');
    let allowance = await this.wETHToken.allowance(await this.signer.getAddress(), this.galaticFactory.address);
    let ret = new DecimalBigNumber(allowance.toString()).div(exponent).toString();
    return ret;
  }

  public async getWETHAllowanceOfMarketplace(): Promise<string> {
    if (this.wETHToken == null || this.wETHToken == undefined)
      throw new Error('check the wETHToken contract of correct network');
    let allowance = await this.wETHToken.allowance(await this.signer.getAddress(), this.config.ufoMarketplace);
    let ret = new DecimalBigNumber(allowance.toString()).div(exponent).toString();
    return ret;
  }

  private getAddressOfPriceToken(token_unit): string {
    console.log('getAddressOfPriceToken', this.netId);
    switch (this.netId) {
      case NETWORK_ID.BEAM_TESTNET:
        if (token_unit == USER_CURRENCY['WETH']) {
          return this.config.ufoConfig.WETHOnBeamTestNet;
        } else if (token_unit == USER_CURRENCY['UFO']) {
          return this.config.ufoConfig.ufoTokenOnBeamTestNet;
        } else {
          return this.config.ufoConfig.usdtTokenOnBeamTestNet;
        }
      case NETWORK_ID.BEAM:
        if (token_unit == USER_CURRENCY['WETH']) {
          return this.config.ufoConfig.WETHOnBeamTestNet;
        } else if (token_unit == USER_CURRENCY['UFO']) {
          return this.config.ufoConfig.ufoTokenOnBeamTestNet;
        } else {
          return this.config.ufoConfig.usdtTokenOnBeamTestNet;
        }
      default:
        return '000000';
    }
  }

  public async approveToken(price_unit: string, spender: string, amount: string): Promise<ContractTransaction> {
    let address: string = this.getAddressOfPriceToken(price_unit);
    let token: ERC20 = new ERC20__factory(this.signer).attach(address);
    return token.approve(spender, amount);
  }

  public async hasApprovalToken(price_unit: string, spnder: string, amount: string): Promise<boolean> {
    let address: string = this.getAddressOfPriceToken(price_unit);
    let token: ERC20 = new ERC20__factory(this.signer).attach(address);
    let allowance = await token.allowance(await this.signer.getAddress(), spnder);
    console.log('allowance======', allowance);
    let ret = new DecimalBigNumber(allowance.toString()).gte(new DecimalBigNumber(amount));
    return ret;
  }

  public async approveWETHToMarketplace(): Promise<ContractTransaction> {
    return this.wETHToken.approve(this.config.ufoMarketplace, MAX_INT);
  }

  public async approveWETHToNFTFactory(amount: BigNumber): Promise<ContractTransaction> {
    if (amount) {
      return this.wETHToken.approve(this.config.ufoSuperGalaticFactory, amount);
    } else {
      return this.wETHToken.approve(this.config.ufoSuperGalaticFactory, MAX_INT);
    }
  }

  public async hasWETHApprovalOfMarketplace(amount: string): Promise<boolean> {
    const _temp = new DecimalBigNumber(amount);
    if (_temp.isNegative()) {
      throw new Error(`${amount} is a negative amount`);
    }

    let allowance = await this.getWETHAllowanceOfMarketplace();

    return new DecimalBigNumber(allowance).gte(_temp);
  }

  //plasma function
  public async hasPlasmaApprovalOfNFTFactory(amount: string): Promise<boolean> {
    const _temp = new DecimalBigNumber(amount);
    if (_temp.isNegative()) {
      throw new Error(`${amount} is a negative amount`);
    }

    let allowance = await this.getPlasmaAllowanceOfNFTFactory();

    return new DecimalBigNumber(allowance).gte(_temp);
  }

  public async approveUsdtToNFTFactory(price: number): Promise<ContractTransaction> {
    return this.usdtToken.approve(this.galaticFactory.address, usdtExponent.multipliedBy(price).toFixed());
  }

  public async approvePlasmaToNFTFactory(price: number): Promise<ContractTransaction> {
    return this.plasma.approve(this.galaticFactory.address, exponent.multipliedBy(price).toFixed());
  }

  public async approvePlasmaToMarketplace(): Promise<ContractTransaction> {
    return this.plasma.approve(this.galaticFactory.address, MAX_INT);
  }

  private async getPlasmaAllowanceOfNFTFactory(): Promise<string> {
    if (this.plasma == null || this.plasma == undefined)
      throw new Error('check the Plasma contract of correct network');
    let allowance = await this.plasma.allowance(await this.signer.getAddress(), this.galaticFactory.address);
    let ret = new DecimalBigNumber(allowance.toString()).div(exponent).toString();
    return ret;
  }

  public async getNftAddresses(): Promise<string[]> {
    let res: string[] = [];
    for (let i = 0; i < 10; i++) {
      let addr = await this.galaticFactory.nftContracts(i);
      res.push(addr);
    }
    return res;
  }

  /**
   * buy and sell weapon
   * buy and sell type depends on buySellType variable of LootBuySellInfo structre
   */
  public async buySellWeaponNFT(
    v: BigNumber,
    r: string,
    s: string,
    data: LootBuySellInfo
  ): Promise<ContractTransaction> {
    const marketplaceContract = new UfoMarketplace__factory(this.signer).attach(this.config.ufoMarketplace);
    return marketplaceContract.buySellLootBoxes(v, r, s, data);
  }

  /**
   * this function is called by buyer to purchase the
   */
  public async purchaseLoot(quantity: BigNumber, tokenType: BigNumber): Promise<ContractTransaction> {
    return this.galaticFactory.purchaseLootbox(quantity, tokenType);
  }

  /**
   * this function is called by sender to send some loot as gift
   * this function is depricated because this function only transfer the loot box to receiver
   */
  // public async sendLootasGift(quantity: BigNumber, receiver: string): Promise<ContractTransaction> {
  //   const marketplaceContract = new UfoMarketplace__factory(this.signer).attach(this.config.ufoMarketplace);
  //   return marketplaceContract.giftLootBoxes(quantity, receiver);
  // }

  /**
   * this function is called by buyer to purchase the
   */
  public async purchaseLootAndSendGift(
    quantity: BigNumber,
    giftReceiver: string,
    tokenType: BigNumber
  ): Promise<ContractTransaction> {
    return this.galaticFactory.purchaseLootboxAndSendGift(quantity, giftReceiver, tokenType);
  }

  /**
   * this function is called by opener who open the loot
   */
  public async openLoot(v: BigNumber, r: string, s: string, data: WeaponInfo): Promise<ContractTransaction> {
    return this.galaticFactory.openLootBoxBySignature(v, r, s, data);
  }

  /**
   * update the weapon by weapon owner
   */
  public async updateWeapon(weaponId: BigNumber): Promise<ContractTransaction> {
    return this.galaticFactory.updateWeapon(weaponId);
  }

  /**
   * this function is called by buyer on fixed sell item
   */
  public async buyNFT(v: BigNumber, r: string, s: string, data: NFTInfo): Promise<ContractTransaction> {
    const marketplaceContract = new UfoMarketplace__factory(this.signer).attach(this.config.ufoMarketplace);
    return marketplaceContract.buySellItem(v, r, s, data);
  }

  /**
   * this function is called by seller on auction sell. seller accept one bid in many bids
   */
  public async sellNFT(v: BigNumber, r: string, s: string, data: NFTInfo): Promise<ContractTransaction> {
    const marketplaceContract = new UfoMarketplace__factory(this.signer).attach(this.config.ufoMarketplace);
    return marketplaceContract.buySellItem(v, r, s, data);
  }

  public async claimUAP(v: BigNumber, r: string, s: string, data: UAPClaimInfo): Promise<ContractTransaction> {
    const marketplaceContract = new UfoMarketplace__factory(this.signer).attach(this.config.ufoMarketplace);
    return marketplaceContract.claimUAP(v, r, s, data);
  }

  public async purchaseCartItems(v: BigNumber, r: string, s: string, data: BucketInfo): Promise<ContractTransaction> {
    const marketplaceContract = new UfoMarketplace__factory(this.signer).attach(this.config.ufoMarketplace);
    return marketplaceContract.buyCartItems(v, r, s, data);
  }
}
