// these helpers functions were copied verbatim from ethers

import type {
  TransactionRequest,
  PreparedTransactionRequest,
  BlockParams,
  TransactionResponseParams,
  TransactionReceiptParams,
  LogParams,
  JsonRpcTransactionRequest,
  AuthorizationLike,
  SignatureLike,
  Authorization,
} from "ethers";

import {
  accessListify,
  assert,
  assertArgument,
  authorizationify,
  getAddress,
  getBigInt,
  getCreateAddress,
  getNumber,
  hexlify,
  isHexString,
  Signature,
  toQuantity,
} from "ethers";
import { HardhatEthersError } from "./errors";

export type FormatFunc = (value: any) => any;

export function copyRequest(
  req: TransactionRequest
): PreparedTransactionRequest {
  const result: any = {};

  // These could be addresses, ENS names or Addressables
  if (req.to !== null && req.to !== undefined) {
    result.to = req.to;
  }
  if (req.from !== null && req.from !== undefined) {
    result.from = req.from;
  }

  if (req.data !== null && req.data !== undefined) {
    result.data = hexlify(req.data);
  }

  const bigIntKeys =
    "chainId,gasLimit,gasPrice,maxFeePerGas,maxPriorityFeePerGas,value".split(
      /,/
    );
  for (const key of bigIntKeys) {
    if (
      !(key in req) ||
      (req as any)[key] === null ||
      (req as any)[key] === undefined
    ) {
      continue;
    }
    result[key] = getBigInt((req as any)[key], `request.${key}`);
  }

  const numberKeys = "type,nonce".split(/,/);
  for (const key of numberKeys) {
    if (
      !(key in req) ||
      (req as any)[key] === null ||
      (req as any)[key] === undefined
    ) {
      continue;
    }
    result[key] = getNumber((req as any)[key], `request.${key}`);
  }

  if (req.accessList !== null && req.accessList !== undefined) {
    result.accessList = accessListify(req.accessList);
  }

  if (req.authorizationList !== null && req.authorizationList !== undefined) {
    result.authorizationList = req.authorizationList;
  }

  if ("blockTag" in req) {
    result.blockTag = req.blockTag;
  }

  if ("enableCcipRead" in req) {
    result.enableCcipReadEnabled = Boolean(req.enableCcipRead);
  }

  if ("customData" in req) {
    result.customData = req.customData;
  }

  return result;
}

export async function resolveProperties<T>(value: {
  [P in keyof T]: T[P] | Promise<T[P]>;
}): Promise<T> {
  const keys = Object.keys(value);
  const results = await Promise.all(
    keys.map((k) => Promise.resolve(value[k as keyof T]))
  );
  return results.reduce((accum: any, v, index) => {
    accum[keys[index]] = v;
    return accum;
  }, {} as { [P in keyof T]: T[P] });
}

export function formatBlock(value: any): BlockParams {
  const result = _formatBlock(value);
  result.transactions = value.transactions.map(
    (tx: string | TransactionResponseParams) => {
      if (typeof tx === "string") {
        return tx;
      }
      return formatTransactionResponse(tx);
    }
  );
  return result;
}

const _formatBlock = object({
  hash: allowNull(formatHash),
  parentHash: formatHash,
  number: getNumber,

  timestamp: getNumber,
  nonce: allowNull(formatData),
  difficulty: getBigInt,

  gasLimit: getBigInt,
  gasUsed: getBigInt,

  miner: allowNull(getAddress),
  extraData: formatData,

  baseFeePerGas: allowNull(getBigInt),
});

function object(
  format: Record<string, FormatFunc>,
  altNames?: Record<string, string[]>
): FormatFunc {
  return (value: any) => {
    const result: any = {};
    // eslint-disable-next-line guard-for-in
    for (const key in format) {
      let srcKey = key;
      if (altNames !== undefined && key in altNames && !(srcKey in value)) {
        for (const altKey of altNames[key]) {
          if (altKey in value) {
            srcKey = altKey;
            break;
          }
        }
      }

      try {
        const nv = format[key](value[srcKey]);
        if (nv !== undefined) {
          result[key] = nv;
        }
      } catch (error) {
        const message = error instanceof Error ? error.message : "not-an-error";
        assert(
          false,
          `invalid value for value.${key} (${message})`,
          "BAD_DATA",
          { value }
        );
      }
    }
    return result;
  };
}

function allowNull(format: FormatFunc, nullValue?: any): FormatFunc {
  return function (value: any) {
    // eslint-disable-next-line eqeqeq
    if (value === null || value === undefined) {
      return nullValue;
    }
    return format(value);
  };
}

function formatHash(value: any): string {
  assertArgument(isHexString(value, 32), "invalid hash", "value", value);
  return value;
}

function formatData(value: string): string {
  assertArgument(isHexString(value, true), "invalid data", "value", value);
  return value;
}

export function formatTransactionResponse(
  value: any
): TransactionResponseParams {
  // Some clients (TestRPC) do strange things like return 0x0 for the
  // 0 address; correct this to be a real address
  // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
  if (value.to && getBigInt(value.to) === 0n) {
    value.to = "0x0000000000000000000000000000000000000000";
  }

  const result = object(
    {
      hash: formatHash,

      type: (v: any) => {
        // eslint-disable-next-line eqeqeq
        if (v === "0x" || v == null) {
          return 0;
        }
        return getNumber(v);
      },
      accessList: allowNull(accessListify, null),
      authorizationList: allowNull(convertToTxAuthorizationList, null),

      blockHash: allowNull(formatHash, null),
      blockNumber: allowNull(getNumber, null),
      transactionIndex: allowNull(getNumber, null),

      from: getAddress,

      // either (gasPrice) or (maxPriorityFeePerGas + maxFeePerGas) must be set
      gasPrice: allowNull(getBigInt),
      maxPriorityFeePerGas: allowNull(getBigInt),
      maxFeePerGas: allowNull(getBigInt),

      gasLimit: getBigInt,
      to: allowNull(getAddress, null),
      value: getBigInt,
      nonce: getNumber,
      data: formatData,

      creates: allowNull(getAddress, null),

      chainId: allowNull(getBigInt, null),
    },
    {
      data: ["input"],
      gasLimit: ["gas"],
    }
  )(value);

  // If to and creates are empty, populate the creates from the value
  // eslint-disable-next-line eqeqeq
  if (result.to == null && result.creates == null) {
    result.creates = getCreateAddress(result);
  }

  // @TODO: Check fee data

  // Add an access list to supported transaction types
  // eslint-disable-next-line eqeqeq
  if ((value.type === 1 || value.type === 2) && value.accessList == null) {
    result.accessList = [];
  }

  // eslint-disable-next-line eqeqeq
  if (value.type === 4 && value.authorizationList == null) {
    result.authorizationList = [];
  }

  // Compute the signature
  // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
  if (value.signature) {
    result.signature = Signature.from(value.signature);
  } else {
    result.signature = Signature.from(value);
  }

  // Some backends omit ChainId on legacy transactions, but we can compute it
  // eslint-disable-next-line eqeqeq
  if (result.chainId == null) {
    const chainId = result.signature.legacyChainId;
    // eslint-disable-next-line eqeqeq
    if (chainId != null) {
      result.chainId = chainId;
    }
  }

  // 0x0000... should actually be null
  // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
  if (result.blockHash && getBigInt(result.blockHash) === 0n) {
    result.blockHash = null;
  }

  return result;
}

function arrayOf(format: FormatFunc): FormatFunc {
  return (array: any) => {
    if (!Array.isArray(array)) {
      throw new HardhatEthersError("not an array");
    }
    return array.map((i) => format(i));
  };
}

const _formatReceiptLog = object(
  {
    transactionIndex: getNumber,
    blockNumber: getNumber,
    transactionHash: formatHash,
    address: getAddress,
    topics: arrayOf(formatHash),
    data: formatData,
    index: getNumber,
    blockHash: formatHash,
  },
  {
    index: ["logIndex"],
  }
);

const _formatTransactionReceipt = object(
  {
    to: allowNull(getAddress, null),
    from: allowNull(getAddress, null),
    contractAddress: allowNull(getAddress, null),
    // should be allowNull(hash), but broken-EIP-658 support is handled in receipt
    index: getNumber,
    root: allowNull(hexlify),
    gasUsed: getBigInt,
    logsBloom: allowNull(formatData),
    blockHash: formatHash,
    hash: formatHash,
    logs: arrayOf(formatReceiptLog),
    blockNumber: getNumber,
    cumulativeGasUsed: getBigInt,
    effectiveGasPrice: allowNull(getBigInt),
    status: allowNull(getNumber),
    type: allowNull(getNumber, 0),
  },
  {
    effectiveGasPrice: ["gasPrice"],
    hash: ["transactionHash"],
    index: ["transactionIndex"],
  }
);

export function formatTransactionReceipt(value: any): TransactionReceiptParams {
  return _formatTransactionReceipt(value);
}

export function formatReceiptLog(value: any): LogParams {
  return _formatReceiptLog(value);
}

function formatBoolean(value: any): boolean {
  switch (value) {
    case true:
    case "true":
      return true;
    case false:
    case "false":
      return false;
  }
  assertArgument(
    false,
    `invalid boolean; ${JSON.stringify(value)}`,
    "value",
    value
  );
}

const _formatLog = object(
  {
    address: getAddress,
    blockHash: formatHash,
    blockNumber: getNumber,
    data: formatData,
    index: getNumber,
    removed: formatBoolean,
    topics: arrayOf(formatHash),
    transactionHash: formatHash,
    transactionIndex: getNumber,
  },
  {
    index: ["logIndex"],
  }
);

export function formatLog(value: any): LogParams {
  return _formatLog(value);
}

export function getRpcTransaction(
  tx: TransactionRequest
): JsonRpcTransactionRequest {
  const result: JsonRpcTransactionRequest = {};

  // JSON-RPC now requires numeric values to be "quantity" values
  [
    "chainId",
    "gasLimit",
    "gasPrice",
    "type",
    "maxFeePerGas",
    "maxPriorityFeePerGas",
    "nonce",
    "value",
  ].forEach((key) => {
    if ((tx as any)[key] === null || (tx as any)[key] === undefined) {
      return;
    }
    let dstKey = key;
    if (key === "gasLimit") {
      dstKey = "gas";
    }
    (result as any)[dstKey] = toQuantity(
      getBigInt((tx as any)[key], `tx.${key}`)
    );
  });

  // Make sure addresses and data are lowercase
  ["from", "to", "data"].forEach((key) => {
    if ((tx as any)[key] === null || (tx as any)[key] === undefined) {
      return;
    }
    (result as any)[key] = hexlify((tx as any)[key]);
  });

  // Normalize the access list object
  if (tx.accessList !== null && tx.accessList !== undefined) {
    result.accessList = accessListify(tx.accessList);
  }

  // Normalize the authorization list
  if (tx.authorizationList !== null && tx.authorizationList !== undefined) {
    result.authorizationList = authorizationListify(tx.authorizationList);
  }

  return result;
}

function authorizationListify(authorizationList: AuthorizationLike[]) {
  return authorizationList.map((el) => {
    const auth = authorizationify(el);

    return {
      address: auth.address,
      nonce: toQuantity(auth.nonce),
      chainId: toQuantity(auth.chainId),
      r: auth.signature.r,
      s: auth.signature.s,
      yParity: toQuantity(auth.signature.yParity),
    };
  });
}

function convertToTxAuthorizationList(
  authorizationList: Array<
    {
      address: string;
      nonce: string;
      chainId: string;
    } & SignatureLike
  >
): Authorization[] {
  const authorizations: Authorization[] = [];

  for (const auth of authorizationList) {
    authorizations.push({
      address: auth.address,
      nonce: getBigInt(auth.nonce),
      chainId: getBigInt(auth.chainId),
      signature: Signature.from(auth),
    });
  }

  return authorizations;
}
