import {
  BYTES_PER_FIELD_ELEMENT,
  BYTES_PER_LOGS_BLOOM,
  CELLS_PER_EXT_BLOB,
  CONSOLIDATION_REQUEST_TYPE,
  DEPOSIT_REQUEST_TYPE,
  FIELD_ELEMENTS_PER_BLOB,
  ForkName,
  ForkSeq,
  WITHDRAWAL_REQUEST_TYPE,
} from "@lodestar/params";
import {
  BlobsBundle,
  ExecutionPayload,
  ExecutionRequests,
  Root,
  Wei,
  bellatrix,
  capella,
  deneb,
  electra,
  gloas,
  ssz,
} from "@lodestar/types";
import {BlobAndProof} from "@lodestar/types/deneb";
import {BlobAndProofV2} from "@lodestar/types/fulu";
import {
  ExecutionPayloadStatus,
  ExecutionRequestType,
  PayloadAttributes,
  VersionedHashes,
  isExecutionRequestType,
} from "./interface.js";
import {WithdrawalV1} from "./payloadIdCache.js";
import {
  DATA,
  QUANTITY,
  bytesToData,
  dataIntoBytes,
  dataToBytes,
  numToQuantity,
  quantityToBigint,
  quantityToNum,
} from "./utils.js";

export type EngineApiRpcParamTypes = {
  /**
   * 1. Object - Instance of ExecutionPayload
   */
  engine_newPayloadV1: [ExecutionPayloadRpc];
  engine_newPayloadV2: [ExecutionPayloadRpc];
  engine_newPayloadV3: [ExecutionPayloadRpc, VersionedHashesRpc, DATA];
  engine_newPayloadV4: [ExecutionPayloadRpc, VersionedHashesRpc, DATA, ExecutionRequestsRpc];
  engine_newPayloadV5: [ExecutionPayloadRpc, VersionedHashesRpc, DATA, ExecutionRequestsRpc];
  /**
   * 1. Object - Payload validity status with respect to the consensus rules:
   *   - blockHash: DATA, 32 Bytes - block hash value of the payload
   *   - status: String: VALID|INVALID - result of the payload validation with respect to the proof-of-stake consensus rules
   */
  engine_forkchoiceUpdatedV1: [
    forkChoiceData: {headBlockHash: DATA; safeBlockHash: DATA; finalizedBlockHash: DATA},
    payloadAttributes?: PayloadAttributesRpc,
  ];
  engine_forkchoiceUpdatedV2: [
    forkChoiceData: {headBlockHash: DATA; safeBlockHash: DATA; finalizedBlockHash: DATA},
    payloadAttributes?: PayloadAttributesRpc,
  ];
  engine_forkchoiceUpdatedV3: [
    forkChoiceData: {headBlockHash: DATA; safeBlockHash: DATA; finalizedBlockHash: DATA},
    payloadAttributes?: PayloadAttributesRpc,
  ];
  engine_forkchoiceUpdatedV4: [
    forkChoiceData: {headBlockHash: DATA; safeBlockHash: DATA; finalizedBlockHash: DATA},
    payloadAttributes?: PayloadAttributesRpc,
  ];
  /**
   * 1. payloadId: QUANTITY, 64 Bits - Identifier of the payload building process
   */
  engine_getPayloadV1: [QUANTITY];
  engine_getPayloadV2: [QUANTITY];
  engine_getPayloadV3: [QUANTITY];
  engine_getPayloadV4: [QUANTITY];
  engine_getPayloadV5: [QUANTITY];
  engine_getPayloadV6: [QUANTITY];

  /**
   * 1. Array of DATA - Array of block_hash field values of the ExecutionPayload structure
   *  */
  engine_getPayloadBodiesByHashV1: DATA[][];

  /**
   *  1. start: QUANTITY, 64 bits - Starting block number
   *  2. count: QUANTITY, 64 bits - Number of blocks to return
   */
  engine_getPayloadBodiesByRangeV1: [start: QUANTITY, count: QUANTITY];

  /**
   * Object - Instance of ClientVersion
   */
  engine_getClientVersionV1: [ClientVersionRpc];

  engine_getBlobsV1: [DATA[]];
  engine_getBlobsV2: [DATA[]];
};

export type PayloadStatus = {
  status: ExecutionPayloadStatus;
  latestValidHash: DATA | null;
  validationError: string | null;
};

export type EngineApiRpcReturnTypes = {
  /**
   * Object - Response object:
   * - status: String - the result of the payload execution:
   */
  engine_newPayloadV1: PayloadStatus;
  engine_newPayloadV2: PayloadStatus;
  engine_newPayloadV3: PayloadStatus;
  engine_newPayloadV4: PayloadStatus;
  engine_newPayloadV5: PayloadStatus;
  engine_forkchoiceUpdatedV1: {
    payloadStatus: PayloadStatus;
    payloadId: QUANTITY | null;
  };
  engine_forkchoiceUpdatedV2: {
    payloadStatus: PayloadStatus;
    payloadId: QUANTITY | null;
  };
  engine_forkchoiceUpdatedV3: {
    payloadStatus: PayloadStatus;
    payloadId: QUANTITY | null;
  };
  engine_forkchoiceUpdatedV4: {
    payloadStatus: PayloadStatus;
    payloadId: QUANTITY | null;
  };
  /**
   * payloadId | Error: QUANTITY, 64 Bits - Identifier of the payload building process
   */
  engine_getPayloadV1: ExecutionPayloadRpc;
  engine_getPayloadV2: ExecutionPayloadResponse;
  engine_getPayloadV3: ExecutionPayloadResponse;
  engine_getPayloadV4: ExecutionPayloadResponse;
  engine_getPayloadV5: ExecutionPayloadResponse;
  engine_getPayloadV6: ExecutionPayloadResponse;

  engine_getPayloadBodiesByHashV1: (ExecutionPayloadBodyRpc | null)[];

  engine_getPayloadBodiesByRangeV1: (ExecutionPayloadBodyRpc | null)[];

  engine_getClientVersionV1: ClientVersionRpc[];

  engine_getBlobsV1: (BlobAndProofRpc | null)[];
  engine_getBlobsV2: BlobAndProofV2Rpc[] | null;
};

type ExecutionPayloadRpcWithValue = {
  executionPayload: ExecutionPayloadRpc;
  // even though CL tracks this as executionPayloadValue, EL returns this as blockValue
  blockValue: QUANTITY;
  blobsBundle?: BlobsBundleRpc;
  executionRequests?: ExecutionRequestsRpc;
  shouldOverrideBuilder?: boolean;
};
type ExecutionPayloadResponse = ExecutionPayloadRpcWithValue;

export type ExecutionPayloadBodyRpc = {
  transactions: DATA[];
  withdrawals: WithdrawalV1[] | null | undefined;
};

export type ExecutionPayloadBody = {
  transactions: bellatrix.Transaction[];
  withdrawals: capella.Withdrawals | null;
};

export type ExecutionPayloadRpc = {
  parentHash: DATA; // 32 bytes
  feeRecipient: DATA; // 20 bytes
  stateRoot: DATA; // 32 bytes
  receiptsRoot: DATA; // 32 bytes
  logsBloom: DATA; // 256 bytes
  prevRandao: DATA; // 32 bytes
  blockNumber: QUANTITY;
  gasLimit: QUANTITY;
  gasUsed: QUANTITY;
  timestamp: QUANTITY;
  extraData: DATA; // 0 to 32 bytes
  baseFeePerGas: QUANTITY;
  blockHash: DATA; // 32 bytes
  transactions: DATA[];
  withdrawals?: WithdrawalRpc[]; // Capella hardfork
  blobGasUsed?: QUANTITY; // DENEB
  excessBlobGas?: QUANTITY; // DENEB
  blockAccessList?: DATA; // GLOAS:EIP-7928
  slotNumber?: QUANTITY; // GLOAS:EIP-7843
};

export type WithdrawalRpc = {
  index: QUANTITY;
  validatorIndex: QUANTITY;
  address: DATA;
  amount: QUANTITY;
};

/**
 * ExecutionRequestsRpc only holds at most 3 elements and no repeated type:
 * - ssz'ed DepositRequests
 * - ssz'ed WithdrawalRequests
 * - ssz'ed ConsolidationRequests
 */
export type ExecutionRequestsRpc = (DepositRequestsRpc | WithdrawalRequestsRpc | ConsolidationRequestsRpc)[];

export type DepositRequestsRpc = DATA;
export type WithdrawalRequestsRpc = DATA;
export type ConsolidationRequestsRpc = DATA;

export type BlobAndProofRpc = {
  blob: DATA;
  proof: DATA;
};

export type BlobAndProofV2Rpc = {
  blob: DATA;
  proofs: DATA[];
};

const BLOB_BYTES = BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB;
const PROOF_BYTES = 48;

export const BLOB_AND_PROOF_V2_RPC_BYTES = BLOB_BYTES + PROOF_BYTES * CELLS_PER_EXT_BLOB;

export type VersionedHashesRpc = DATA[];

export type PayloadAttributesRpc = {
  /** QUANTITY, 64 Bits - value for the timestamp field of the new payload */
  timestamp: QUANTITY;
  /** DATA, 32 Bytes - value for the prevRandao field of the new payload */
  prevRandao: DATA;
  /** DATA, 20 Bytes - suggested value for the coinbase field of the new payload */
  suggestedFeeRecipient: DATA;
  withdrawals?: WithdrawalRpc[];
  /** DATA, 32 Bytes - value for the parentBeaconBlockRoot to be used for building block */
  parentBeaconBlockRoot?: DATA;
  /** QUANTITY, 64 Bits - value for the slot number field of the new payload (EIP-7843) */
  slotNumber?: QUANTITY;
};

export type ClientVersionRpc = {
  /** ClientCode */
  code: string;
  /** string, Human-readable name of the client */
  name: string;
  /** string, the version string of the current implementation */
  version: string;
  /** DATA, 4 bytes - first four bytes of the latest commit hash of this build  */
  commit: DATA;
};

export interface BlobsBundleRpc {
  commitments: DATA[]; // each 48 bytes
  blobs: DATA[]; // each 4096 * 32 = 131072 bytes
  proofs: DATA[]; // some ELs could also provide proofs, each 48 bytes
}

export function serializeExecutionPayload(fork: ForkName, data: ExecutionPayload): ExecutionPayloadRpc {
  const payload: ExecutionPayloadRpc = {
    parentHash: bytesToData(data.parentHash),
    feeRecipient: bytesToData(data.feeRecipient),
    stateRoot: bytesToData(data.stateRoot),
    receiptsRoot: bytesToData(data.receiptsRoot),
    logsBloom: bytesToData(data.logsBloom),
    prevRandao: bytesToData(data.prevRandao),
    blockNumber: numToQuantity(data.blockNumber),
    gasLimit: numToQuantity(data.gasLimit),
    gasUsed: numToQuantity(data.gasUsed),
    timestamp: numToQuantity(data.timestamp),
    extraData: bytesToData(data.extraData),
    baseFeePerGas: numToQuantity(data.baseFeePerGas),
    blockHash: bytesToData(data.blockHash),
    transactions: data.transactions.map((tran) => bytesToData(tran)),
  };

  // Capella adds withdrawals to the ExecutionPayload
  if (ForkSeq[fork] >= ForkSeq.capella) {
    const {withdrawals} = data as capella.ExecutionPayload;
    payload.withdrawals = withdrawals.map(serializeWithdrawal);
  }

  // DENEB adds blobGasUsed & excessBlobGas to the ExecutionPayload
  if (ForkSeq[fork] >= ForkSeq.deneb) {
    const {blobGasUsed, excessBlobGas} = data as deneb.ExecutionPayload;
    payload.blobGasUsed = numToQuantity(blobGasUsed);
    payload.excessBlobGas = numToQuantity(excessBlobGas);
  }

  // No changes in Electra

  if (ForkSeq[fork] >= ForkSeq.gloas) {
    const {blockAccessList, slotNumber} = data as gloas.ExecutionPayload;
    payload.blockAccessList = bytesToData(blockAccessList);
    payload.slotNumber = numToQuantity(slotNumber);
  }

  return payload;
}

export function serializeVersionedHashes(vHashes: VersionedHashes): VersionedHashesRpc {
  return vHashes.map(bytesToData);
}

export function hasPayloadValue(
  response: ExecutionPayloadResponse | ExecutionPayloadRpc
): response is ExecutionPayloadRpcWithValue {
  return (response as ExecutionPayloadRpcWithValue).blockValue !== undefined;
}

export function parseExecutionPayload(
  fork: ForkName,
  response: ExecutionPayloadResponse | ExecutionPayloadRpc
): {
  executionPayload: ExecutionPayload;
  executionPayloadValue: Wei;
  blobsBundle?: BlobsBundle;
  executionRequests?: ExecutionRequests;
  shouldOverrideBuilder?: boolean;
} {
  let data: ExecutionPayloadRpc;
  let executionPayloadValue: Wei;
  let blobsBundle: BlobsBundle | undefined;
  let executionRequests: ExecutionRequests | undefined;
  let shouldOverrideBuilder: boolean;

  if (hasPayloadValue(response)) {
    executionPayloadValue = quantityToBigint(response.blockValue);
    data = response.executionPayload;
    blobsBundle = response.blobsBundle ? parseBlobsBundle(response.blobsBundle) : undefined;
    executionRequests = response.executionRequests
      ? deserializeExecutionRequests(response.executionRequests)
      : undefined;
    shouldOverrideBuilder = response.shouldOverrideBuilder ?? false;
  } else {
    data = response;
    // Just set it to zero as default
    executionPayloadValue = BigInt(0);
    blobsBundle = undefined;
    executionRequests = undefined;
    shouldOverrideBuilder = false;
  }

  const executionPayload = {
    parentHash: dataToBytes(data.parentHash, 32),
    feeRecipient: dataToBytes(data.feeRecipient, 20),
    stateRoot: dataToBytes(data.stateRoot, 32),
    receiptsRoot: dataToBytes(data.receiptsRoot, 32),
    logsBloom: dataToBytes(data.logsBloom, BYTES_PER_LOGS_BLOOM),
    prevRandao: dataToBytes(data.prevRandao, 32),
    blockNumber: quantityToNum(data.blockNumber),
    gasLimit: quantityToNum(data.gasLimit),
    gasUsed: quantityToNum(data.gasUsed),
    timestamp: quantityToNum(data.timestamp),
    extraData: dataToBytes(data.extraData, null),
    baseFeePerGas: quantityToBigint(data.baseFeePerGas),
    blockHash: dataToBytes(data.blockHash, 32),
    transactions: data.transactions.map((tran) => dataToBytes(tran, null)),
  };

  if (ForkSeq[fork] >= ForkSeq.capella) {
    const {withdrawals} = data;
    // Geth can also reply with null
    if (withdrawals == null) {
      throw Error(
        `withdrawals missing for ${fork} >= capella executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}`
      );
    }
    (executionPayload as capella.ExecutionPayload).withdrawals = withdrawals.map((w) => deserializeWithdrawal(w));
  }

  // DENEB adds excessBlobGas to the ExecutionPayload
  if (ForkSeq[fork] >= ForkSeq.deneb) {
    const {blobGasUsed, excessBlobGas} = data;

    if (blobGasUsed == null) {
      throw Error(
        `blobGasUsed missing for ${fork} >= deneb executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}`
      );
    }
    if (excessBlobGas == null) {
      throw Error(
        `excessBlobGas missing for ${fork} >= deneb executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}`
      );
    }

    (executionPayload as deneb.ExecutionPayload).blobGasUsed = quantityToBigint(blobGasUsed);
    (executionPayload as deneb.ExecutionPayload).excessBlobGas = quantityToBigint(excessBlobGas);
  }

  // No changes in Electra

  if (ForkSeq[fork] >= ForkSeq.gloas) {
    const {blockAccessList, slotNumber} = data;
    if (blockAccessList == null) {
      throw Error(
        `blockAccessList missing for ${fork} >= gloas executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}`
      );
    }
    if (slotNumber == null) {
      throw Error(
        `slotNumber missing for ${fork} >= gloas executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}`
      );
    }
    (executionPayload as gloas.ExecutionPayload).blockAccessList = dataToBytes(blockAccessList, null);
    (executionPayload as gloas.ExecutionPayload).slotNumber = quantityToNum(slotNumber);
  }

  return {executionPayload, executionPayloadValue, blobsBundle, executionRequests, shouldOverrideBuilder};
}

export function serializePayloadAttributes(data: PayloadAttributes): PayloadAttributesRpc {
  return {
    timestamp: numToQuantity(data.timestamp),
    prevRandao: bytesToData(data.prevRandao),
    suggestedFeeRecipient: data.suggestedFeeRecipient,
    withdrawals: data.withdrawals?.map(serializeWithdrawal),
    parentBeaconBlockRoot: data.parentBeaconBlockRoot ? bytesToData(data.parentBeaconBlockRoot) : undefined,
    slotNumber: data.slotNumber !== undefined ? numToQuantity(data.slotNumber) : undefined,
  };
}

export function serializeBeaconBlockRoot(data: Root): DATA {
  return bytesToData(data);
}

export function deserializePayloadAttributes(data: PayloadAttributesRpc): PayloadAttributes {
  return {
    timestamp: quantityToNum(data.timestamp),
    prevRandao: dataToBytes(data.prevRandao, 32),
    // DATA is anyway a hex string, so we can just track it as a hex string to
    // avoid any conversions
    suggestedFeeRecipient: data.suggestedFeeRecipient,
    withdrawals: data.withdrawals?.map((withdrawal) => deserializeWithdrawal(withdrawal)),
    parentBeaconBlockRoot: data.parentBeaconBlockRoot ? dataToBytes(data.parentBeaconBlockRoot, 32) : undefined,
    slotNumber: data.slotNumber !== undefined ? quantityToNum(data.slotNumber) : undefined,
  };
}

export function parseBlobsBundle(data: BlobsBundleRpc): BlobsBundle {
  return {
    // As of Nov 17th 2022 according to Dan's tests Geth returns null if no blobs in block
    commitments: (data.commitments ?? []).map((kzg) => dataToBytes(kzg, 48)),
    blobs: (data.blobs ?? []).map((blob) => dataToBytes(blob, BLOB_BYTES)),
    proofs: (data.proofs ?? []).map((kzg) => dataToBytes(kzg, PROOF_BYTES)),
  };
}

export function serializeBlobsBundle(data: BlobsBundle): BlobsBundleRpc {
  return {
    commitments: data.commitments.map((kzg) => bytesToData(kzg)),
    blobs: data.blobs.map((blob) => bytesToData(blob)),
    proofs: data.proofs.map((proof) => bytesToData(proof)),
  };
}

export function serializeWithdrawal(withdrawal: capella.Withdrawal): WithdrawalRpc {
  return {
    index: numToQuantity(withdrawal.index),
    validatorIndex: numToQuantity(withdrawal.validatorIndex),
    address: bytesToData(withdrawal.address),
    // Both CL and EL now deal in Gwei, just little-endian to big-endian conversion required
    amount: numToQuantity(withdrawal.amount),
  };
}

export function deserializeWithdrawal(serialized: WithdrawalRpc): capella.Withdrawal {
  return {
    index: quantityToNum(serialized.index),
    validatorIndex: quantityToNum(serialized.validatorIndex),
    address: dataToBytes(serialized.address, 20),
    // Both CL and EL now deal in Gwei, just big-endian to little-endian conversion required
    amount: quantityToBigint(serialized.amount),
  } as capella.Withdrawal;
}

/**
 * Prepend a single-byte requestType to requestsBytes
 */
function prefixRequests(requestsBytes: Uint8Array, requestType: ExecutionRequestType): Uint8Array {
  const prefixedRequests = new Uint8Array(1 + requestsBytes.length);
  prefixedRequests[0] = requestType;
  prefixedRequests.set(requestsBytes, 1);

  return prefixedRequests;
}

function serializeDepositRequests(depositRequests: electra.DepositRequests): DepositRequestsRpc {
  const requestsBytes = ssz.electra.DepositRequests.serialize(depositRequests);
  return bytesToData(prefixRequests(requestsBytes, DEPOSIT_REQUEST_TYPE));
}

function deserializeDepositRequests(serialized: DepositRequestsRpc): electra.DepositRequests {
  return ssz.electra.DepositRequests.deserialize(dataToBytes(serialized, null));
}

function serializeWithdrawalRequests(withdrawalRequests: electra.WithdrawalRequests): WithdrawalRequestsRpc {
  const requestsBytes = ssz.electra.WithdrawalRequests.serialize(withdrawalRequests);
  return bytesToData(prefixRequests(requestsBytes, WITHDRAWAL_REQUEST_TYPE));
}

function deserializeWithdrawalRequests(serialized: WithdrawalRequestsRpc): electra.WithdrawalRequests {
  return ssz.electra.WithdrawalRequests.deserialize(dataToBytes(serialized, null));
}

function serializeConsolidationRequests(
  consolidationRequests: electra.ConsolidationRequests
): ConsolidationRequestsRpc {
  const requestsBytes = ssz.electra.ConsolidationRequests.serialize(consolidationRequests);
  return bytesToData(prefixRequests(requestsBytes, CONSOLIDATION_REQUEST_TYPE));
}

function deserializeConsolidationRequests(serialized: ConsolidationRequestsRpc): electra.ConsolidationRequests {
  return ssz.electra.ConsolidationRequests.deserialize(dataToBytes(serialized, null));
}

/**
 * This is identical to get_execution_requests_list in
 * https://github.com/ethereum/consensus-specs/blob/v1.5.0-alpha.8/specs/electra/beacon-chain.md#new-get_execution_requests_list
 */
export function serializeExecutionRequests(executionRequests: ExecutionRequests): ExecutionRequestsRpc {
  const {deposits, withdrawals, consolidations} = executionRequests;
  const result = [];

  if (deposits.length !== 0) {
    result.push(serializeDepositRequests(deposits));
  }

  if (withdrawals.length !== 0) {
    result.push(serializeWithdrawalRequests(withdrawals));
  }

  if (consolidations.length !== 0) {
    result.push(serializeConsolidationRequests(consolidations));
  }

  return result;
}

export function deserializeExecutionRequests(serialized: ExecutionRequestsRpc): ExecutionRequests {
  const result: ExecutionRequests = {
    deposits: [],
    withdrawals: [],
    consolidations: [],
  };

  if (serialized.length === 0) {
    return result;
  }

  let prevRequestType: ExecutionRequestType | undefined;

  for (let prefixedRequests of serialized) {
    // Slice out 0x so it is easier to extract request type
    if (prefixedRequests.startsWith("0x")) {
      prefixedRequests = prefixedRequests.slice(2);
    }

    const currentRequestType = parseInt(prefixedRequests.substring(0, 2), 16);

    if (!isExecutionRequestType(currentRequestType)) {
      throw Error(`Invalid request type currentRequestType=${prefixedRequests.substring(0, 2)}`);
    }

    const requests = prefixedRequests.slice(2);

    if (requests.length === 0) {
      throw Error(
        `Request with empty data must be excluded from execution requests currentRequestType=${currentRequestType}`
      );
    }

    if (prevRequestType !== undefined && prevRequestType >= currentRequestType) {
      throw Error(
        `Current request type must be larger than previous request type prevRequestType=${prevRequestType} currentRequestType=${currentRequestType}`
      );
    }

    switch (currentRequestType) {
      case DEPOSIT_REQUEST_TYPE: {
        result.deposits = deserializeDepositRequests(requests);
        break;
      }
      case WITHDRAWAL_REQUEST_TYPE: {
        result.withdrawals = deserializeWithdrawalRequests(requests);
        break;
      }
      case CONSOLIDATION_REQUEST_TYPE: {
        result.consolidations = deserializeConsolidationRequests(requests);
        break;
      }
    }
    prevRequestType = currentRequestType;
  }

  return result;
}

export function deserializeExecutionPayloadBody(data: ExecutionPayloadBodyRpc | null): ExecutionPayloadBody | null {
  return data
    ? {
        transactions: data.transactions.map((tran) => dataToBytes(tran, null)),
        withdrawals: data.withdrawals ? data.withdrawals.map(deserializeWithdrawal) : null,
      }
    : null;
}

export function serializeExecutionPayloadBody(data: ExecutionPayloadBody | null): ExecutionPayloadBodyRpc | null {
  return data
    ? {
        transactions: data.transactions.map((tran) => bytesToData(tran)),
        withdrawals: data.withdrawals ? data.withdrawals.map(serializeWithdrawal) : null,
      }
    : null;
}

export function deserializeBlobAndProofs(data: BlobAndProofRpc | null): BlobAndProof | null {
  return data
    ? {
        blob: dataToBytes(data.blob, BLOB_BYTES),
        proof: dataToBytes(data.proof, PROOF_BYTES),
      }
    : null;
}

export function deserializeBlobAndProofsV2(data: BlobAndProofV2Rpc): BlobAndProofV2 {
  return {
    blob: dataToBytes(data.blob, BLOB_BYTES),
    proofs: data.proofs.map((proof) => dataToBytes(proof, PROOF_BYTES)),
  };
}

/**
 * The same to deserializeBlobAndProofsV2 but using preallocated buffers since BlobAndProofV2Rpc is fixed size
 */
export function deserializeBlobAndProofsV2IntoBytes(data: BlobAndProofV2Rpc, buffer: Uint8Array): BlobAndProofV2 {
  if (buffer.length !== BLOB_AND_PROOF_V2_RPC_BYTES) {
    throw Error(
      `Invalid buffer length ${buffer.length}, expected ${BLOB_AND_PROOF_V2_RPC_BYTES} to hold BlobAndProofV2Rpc`
    );
  }

  // https://github.com/ethereum/execution-apis/blob/main/src/engine/osaka.md#blobandproofv2
  // proofs MUST contain exactly CELLS_PER_EXT_BLOB cell proofs.
  if (data.proofs.length !== CELLS_PER_EXT_BLOB) {
    throw Error(`Invalid proofs length ${data.proofs.length}, expected ${CELLS_PER_EXT_BLOB}`);
  }

  const blob = dataIntoBytes(data.blob, buffer.subarray(0, BLOB_BYTES));
  const proofs: Uint8Array[] = [];
  for (let i = 0; i < CELLS_PER_EXT_BLOB; i++) {
    const proof = dataIntoBytes(
      data.proofs[i],
      buffer.subarray(BLOB_BYTES + i * PROOF_BYTES, BLOB_BYTES + (i + 1) * PROOF_BYTES)
    );
    if (proof.length !== PROOF_BYTES) {
      throw Error(`Invalid proof length ${proof.length}, expected ${PROOF_BYTES}`);
    }
    proofs.push(proof);
  }

  return {
    blob,
    proofs,
  };
}

export function assertReqSizeLimit(blockHashesReqCount: number, count: number): void {
  if (blockHashesReqCount > count) {
    throw new Error(`Requested blocks must not be > ${count}`);
  }
  return;
}
