import { Idl, InstructionCoder } from "@project-serum/anchor";
import { ETHEREUM_KEY_LENGTH } from "../../utils";

export class WormholeInstructionCoder implements InstructionCoder {
  constructor(_: Idl) {}

  encode(ixName: string, ix: any): Buffer {
    switch (ixName) {
      case "initialize": {
        return encodeInitialize(ix);
      }
      case "postVaa": {
        return encodePostVaa(ix);
      }
      case "setFees": {
        return encodeSetFees(ix);
      }
      case "transferFees": {
        return encodeTransferFees(ix);
      }
      case "upgradeContract": {
        return encodeUpgradeContract(ix);
      }
      case "upgradeGuardianSet": {
        return encodeUpgradeGuardianSet(ix);
      }
      case "verifySignatures": {
        return encodeVerifySignatures(ix);
      }
      default: {
        throw new Error(`Invalid instruction: ${ixName}`);
      }
    }
  }

  encodeState(_ixName: string, _ix: any): Buffer {
    throw new Error("Wormhole program does not have state");
  }
}

/** Solitaire enum of existing the Core Bridge's instructions.
 *
 * https://github.com/certusone/wormhole/blob/main/solana/bridge/program/src/lib.rs#L92
 */
export enum WormholeInstruction {
  Initialize,
  PostMessage,
  PostVAA,
  SetFees,
  TransferFees,
  UpgradeContract,
  UpgradeGuardianSet,
  VerifySignatures,
  PostMessageUnreliable, // sounds useful
}

function encodeInitialize({
  guardianSetExpirationTime,
  fee,
  initialGuardians,
}: any): Buffer {
  if (typeof fee != "bigint") {
    fee = BigInt(fee);
  }
  const initialGuardiansLen = initialGuardians.length;
  const serialized = Buffer.alloc(
    16 + initialGuardiansLen * ETHEREUM_KEY_LENGTH
  );
  serialized.writeUInt32LE(guardianSetExpirationTime, 0);
  serialized.writeBigUInt64LE(fee, 4);
  serialized.writeUInt32LE(initialGuardiansLen, 12);
  for (let i = 0; i < initialGuardiansLen; ++i) {
    const key = initialGuardians.at(i)!;
    if (!Buffer.isBuffer(key)) {
      throw new Error("key must be Buffer");
    }
    serialized.write(key.toString("hex"), 16 + i * ETHEREUM_KEY_LENGTH, "hex");
  }
  return encodeWormholeInstructionData(
    WormholeInstruction.Initialize,
    serialized
  );
}

function encodeWormholeInstructionData(
  instructionType: WormholeInstruction,
  data?: Buffer
): Buffer {
  const instructionData = Buffer.alloc(
    1 + (data === undefined ? 0 : data.length)
  );
  instructionData.writeUInt8(instructionType, 0);
  if (data !== undefined) {
    instructionData.write(data.toString("hex"), 1, "hex");
  }
  return instructionData;
}

function encodePostVaa({
  version,
  guardianSetIndex,
  timestamp,
  nonce,
  emitterChain,
  emitterAddress,
  sequence,
  consistencyLevel,
  payload,
}: any) {
  if (!Buffer.isBuffer(emitterAddress)) {
    throw new Error("emitterAddress must be Buffer");
  }
  if (!Buffer.isBuffer(payload)) {
    throw new Error("payload must be Buffer");
  }
  if (typeof sequence != "bigint") {
    sequence = BigInt(sequence);
  }
  const serialized = Buffer.alloc(60 + payload.length);
  serialized.writeUInt8(version, 0);
  serialized.writeUInt32LE(guardianSetIndex, 1);
  serialized.writeUInt32LE(timestamp, 5);
  serialized.writeUInt32LE(nonce, 9);
  serialized.writeUInt16LE(emitterChain, 13);
  serialized.write(emitterAddress.toString("hex"), 15, "hex");
  serialized.writeBigUInt64LE(sequence, 47);
  serialized.writeUInt8(consistencyLevel, 55);
  serialized.writeUInt32LE(payload.length, 56);
  serialized.write(payload.toString("hex"), 60, "hex");
  return encodeWormholeInstructionData(WormholeInstruction.PostVAA, serialized);
}

function encodeSetFees({}: any) {
  return encodeWormholeInstructionData(WormholeInstruction.SetFees);
}

function encodeTransferFees({}: any) {
  return encodeWormholeInstructionData(WormholeInstruction.TransferFees);
}

function encodeUpgradeContract({}: any) {
  return encodeWormholeInstructionData(WormholeInstruction.UpgradeContract);
}

function encodeUpgradeGuardianSet({}: any) {
  return encodeWormholeInstructionData(WormholeInstruction.UpgradeGuardianSet);
}

function encodeVerifySignatures({ signatureStatus }: any) {
  return encodeWormholeInstructionData(
    WormholeInstruction.VerifySignatures,
    Buffer.from(signatureStatus, "hex")
  );
}
