import {
  BIGINT_0,
  EthereumJSErrorWithoutCode,
  TypeOutput,
  bytesToHex,
  concatBytes,
  hexToBytes,
  intToBytes,
  toType,
} from '@ethereumjs/util'
import { EventEmitter } from 'eventemitter3'

import { crc32 } from './crc.ts'
import { eipsDict } from './eips.ts'
import { Hardfork } from './enums.ts'
import { hardforksDict } from './hardforks.ts'

import type { BigIntLike, PrefixedHexString } from '@ethereumjs/util'
import type { ConsensusAlgorithm, ConsensusType } from './enums.ts'
import type {
  BootstrapNodeConfig,
  BpoSchedule,
  CasperConfig,
  ChainConfig,
  CliqueConfig,
  CommonEvent,
  CommonOpts,
  CustomCrypto,
  EthashConfig,
  GenesisBlockConfig,
  HardforkByOpts,
  HardforkConfig,
  HardforkTransitionConfig,
  ParamsConfig,
  ParamsDict,
} from './types.ts'

/**
 * Common class to access chain and hardfork parameters and to provide
 * a unified and shared view on the network and hardfork state.
 *
 * Use the {@link createCustomCommon} constructor for creating simple
 * custom chain {@link Common} objects (more complete custom chain setups
 * can be created via the main constructor).
 *
 * Use the {@link createCommonFromGethGenesis} constructor for creating
 * a Common object from a Geth genesis file.
 */
export class Common {
  readonly DEFAULT_HARDFORK: string | Hardfork

  protected _chainParams: ChainConfig
  protected _hardfork: string | Hardfork
  protected _eips: number[] = []
  protected _params: ParamsDict

  public readonly customCrypto: CustomCrypto

  protected _paramsCache: ParamsConfig = {}
  protected _activatedEIPsCache: number[] = []

  protected HARDFORK_CHANGES: [string, HardforkConfig][]

  public events: EventEmitter<CommonEvent>

  constructor(opts: CommonOpts) {
    this.events = new EventEmitter<CommonEvent>()

    this._chainParams = JSON.parse(JSON.stringify(opts.chain)) // copy
    this.DEFAULT_HARDFORK = this._chainParams.defaultHardfork ?? Hardfork.Prague
    // Assign hardfork changes in the sequence of the applied hardforks
    this.HARDFORK_CHANGES = this.hardforks().map((hf) => [
      hf.name,
      // Allow to even override an existing hardfork specification
      (this._chainParams.customHardforks && this._chainParams.customHardforks[hf.name]) ??
        hardforksDict[hf.name],
    ])
    this._hardfork = this.DEFAULT_HARDFORK
    this._params = opts.params ? JSON.parse(JSON.stringify(opts.params)) : {} // copy

    if (opts.hardfork !== undefined) {
      this.setHardfork(opts.hardfork)
    }
    if (opts.eips) {
      this.setEIPs(opts.eips)
    }
    this.customCrypto = opts.customCrypto ?? {}

    if (Object.keys(this._paramsCache).length === 0) {
      this._buildParamsCache()
      this._buildActivatedEIPsCache()
    }
  }

  /**
   * Update the internal Common EIP params set. Existing values
   * will get preserved unless there is a new value for a parameter
   * provided with params.
   *
   * Example Format:
   *
   * ```ts
   * {
   *   1559: {
   *     initialBaseFee: 1000000000,
   *   }
   * }
   * ```
   *
   * @param params
   */
  updateParams(params: ParamsDict) {
    for (const [eip, paramsConfig] of Object.entries(params)) {
      if (!(eip in this._params)) {
        this._params[eip] = JSON.parse(JSON.stringify(paramsConfig)) // copy
      } else {
        this._params[eip] = JSON.parse(JSON.stringify({ ...this._params[eip], ...params[eip] })) // copy
      }
    }

    this._buildParamsCache()
  }

  /**
   * Fully resets the internal Common EIP params set with the values provided.
   *
   * Example Format:
   *
   * ```ts
   * {
   *   1559: {
   *     initialBaseFee: 1000000000,
   *   }
   * }
   * ```
   *
   * @param params
   */
  resetParams(params: ParamsDict) {
    this._params = JSON.parse(JSON.stringify(params)) // copy
    this._buildParamsCache()
  }

  /**
   * Sets the hardfork to get params for
   * @param hardfork String identifier (e.g. 'byzantium') or {@link Hardfork} enum
   */
  setHardfork(hardfork: string | Hardfork): void {
    let existing = false
    for (const hfChanges of this.HARDFORK_CHANGES) {
      if (hfChanges[0] === hardfork) {
        if (this._hardfork !== hardfork) {
          this._hardfork = hardfork
          this._buildParamsCache()
          this._buildActivatedEIPsCache()
          this.events.emit('hardforkChanged', hardfork)
        }
        existing = true
      }
    }
    if (!existing) {
      throw EthereumJSErrorWithoutCode(`Hardfork with name ${hardfork} not supported`)
    }
  }

  /**
   * Returns the hardfork either based on block number (older HFs) or
   * timestamp (Shanghai upwards).
   *
   * @param opts Block number or timestamp
   * @returns The name of the HF
   */
  getHardforkBy(opts: HardforkByOpts): string {
    const blockNumber: bigint | undefined = toType(opts.blockNumber, TypeOutput.BigInt)
    const timestamp: bigint | undefined = toType(opts.timestamp, TypeOutput.BigInt)

    // Filter out hardforks with no block number, no timestamp (i.e. unapplied hardforks)
    const hfs = this.hardforks().filter((hf) => hf.block !== null || hf.timestamp !== undefined)

    // Find the first hardfork that has a block number greater than `blockNumber`
    // If timestamp is not provided, it also skips timestamps hardforks to continue
    // discovering/checking number hardforks.
    let hfIndex = hfs.findIndex(
      (hf) =>
        (blockNumber !== undefined && hf.block !== null && BigInt(hf.block) > blockNumber) ||
        (timestamp !== undefined && hf.timestamp !== undefined && BigInt(hf.timestamp) > timestamp),
    )

    if (hfIndex === -1) {
      // all hardforks apply, set hfIndex to the last one as that's the candidate
      hfIndex = hfs.length
    } else if (hfIndex === 0) {
      // cannot have a case where a block number is before all applied hardforks
      // since the chain has to start with a hardfork
      throw Error('Must have at least one hardfork at block 0')
    }

    // If timestamp is not provided, we need to rollback to the last hf with block
    if (timestamp === undefined) {
      const stepBack = hfs
        .slice(0, hfIndex)
        .reverse()
        .findIndex((hf) => hf.block !== null)
      hfIndex = hfIndex - stepBack
    }
    // Move hfIndex one back to arrive at candidate hardfork
    hfIndex = hfIndex - 1

    const hfStartIndex = hfIndex
    // Move the hfIndex to the end of the hardforks that might be scheduled on the same block/timestamp
    // This won't anyway be the case with Merge hfs
    for (; hfIndex < hfs.length - 1; hfIndex++) {
      // break out if hfIndex + 1 is not scheduled at hfIndex
      if (
        hfs[hfIndex].block !== hfs[hfIndex + 1].block ||
        hfs[hfIndex].timestamp !== hfs[hfIndex + 1].timestamp
      ) {
        break
      }
    }

    if (timestamp !== undefined) {
      const minTimeStamp = hfs
        .slice(0, hfStartIndex)
        .reduce(
          (acc: number, hf: HardforkTransitionConfig) => Math.max(Number(hf.timestamp ?? '0'), acc),
          0,
        )
      if (minTimeStamp > timestamp) {
        throw Error(`Maximum HF determined by timestamp is lower than the block number HF`)
      }

      const maxTimeStamp = hfs
        .slice(hfIndex + 1)
        .reduce(
          (acc: number, hf: HardforkTransitionConfig) =>
            Math.min(Number(hf.timestamp ?? timestamp), acc),
          Number(timestamp),
        )

      if (maxTimeStamp < timestamp) {
        throw Error(`Maximum HF determined by block number is lower than timestamp HF`)
      }
    }
    const hardfork = hfs[hfIndex]
    return hardfork.name
  }

  /**
   * Sets a new hardfork either based on block number (older HFs) or
   * timestamp (Shanghai upwards).
   *
   * @param opts Block number or timestamp
   * @returns The name of the HF set
   */
  setHardforkBy(opts: HardforkByOpts): string {
    const hardfork = this.getHardforkBy(opts)
    this.setHardfork(hardfork)
    return hardfork
  }

  /**
   * Internal helper function, returns the params for the given hardfork for the chain set
   * @param hardfork Hardfork name
   * @returns Dictionary with hardfork params or null if hardfork not on chain
   */
  protected _getHardfork(hardfork: string | Hardfork): HardforkTransitionConfig | null {
    const hfs = this.hardforks()
    for (const hf of hfs) {
      if (hf['name'] === hardfork) return hf
    }
    return null
  }

  /**
   * Sets the active EIPs
   * @param eips
   */
  setEIPs(eips: number[] = []) {
    for (const eip of eips) {
      if (!(eip in eipsDict)) {
        throw EthereumJSErrorWithoutCode(`${eip} not supported`)
      }
      const minHF = this.gteHardfork(eipsDict[eip]['minimumHardfork'])
      if (!minHF) {
        throw EthereumJSErrorWithoutCode(
          `${eip} cannot be activated on hardfork ${this.hardfork()}, minimumHardfork: ${minHF}`,
        )
      }
    }
    this._eips = eips
    this._buildParamsCache()
    this._buildActivatedEIPsCache()

    for (const eip of eips) {
      if (eipsDict[eip].requiredEIPs !== undefined) {
        for (const elem of eipsDict[eip].requiredEIPs!) {
          if (!(eips.includes(elem) || this.isActivatedEIP(elem))) {
            throw EthereumJSErrorWithoutCode(
              `${eip} requires EIP ${elem}, but is not included in the EIP list`,
            )
          }
        }
      }
    }
  }

  /**
   * Internal helper for _buildParamsCache()
   */
  protected _mergeWithParamsCache(params: ParamsConfig) {
    for (const [key, value] of Object.entries(params)) {
      this._paramsCache[key] = value
    }
  }

  /**
   * Build up a cache for all parameter values for the current HF and all activated EIPs
   */
  protected _buildParamsCache() {
    this._paramsCache = {}

    // Iterate through all hardforks up to hardfork set
    const hardfork = this.hardfork()
    for (const hfChanges of this.HARDFORK_CHANGES) {
      // EIP-referencing HF config (e.g. for berlin)
      if ('eips' in hfChanges[1]) {
        const hfEIPs = hfChanges[1].eips ?? []
        for (const eip of hfEIPs) {
          if (this._params[eip] !== undefined && this._params[eip] !== null) {
            this._mergeWithParamsCache(this._params[eip])
          }
        }
      }
      // Hardfork-scoped params (e.g. for bpo1, bpo2)
      // override the baseline EIP values when present
      const hfScopedParams = this._params[hfChanges[0]]
      if (hfScopedParams !== undefined && hfScopedParams !== null) {
        this._mergeWithParamsCache(hfScopedParams)
      }
      // Parameter-inlining HF config (e.g. for istanbul or custom blobSchedule)
      if (hfChanges[1].params !== undefined && hfChanges[1].params !== null) {
        this._mergeWithParamsCache(hfChanges[1].params)
      }
      if (hfChanges[0] === hardfork) break
    }

    // Iterate through all additionally activated EIPs
    for (const eip of this._eips) {
      if (this._params[eip] !== undefined && this._params[eip] !== null) {
        this._mergeWithParamsCache(this._params[eip])
      }
    }
  }

  /**
   * Builds the cache of EIPs activated either via hardforks or constructor `eips`.
   */
  protected _buildActivatedEIPsCache() {
    this._activatedEIPsCache = []

    for (const [name, hf] of this.HARDFORK_CHANGES) {
      if (this.gteHardfork(name) && 'eips' in hf) {
        this._activatedEIPsCache = this._activatedEIPsCache.concat(hf.eips ?? [])
      }
    }
    this._activatedEIPsCache = this._activatedEIPsCache.concat(this._eips)
  }

  /**
   * Returns a parameter for the current chain setup
   *
   * If the parameter is present in an EIP, the EIP always takes precedence.
   * Otherwise the parameter is taken from the latest applied HF with
   * a change on the respective parameter.
   *
   * @param name Parameter name (e.g. 'minGasLimit')
   * @returns The value requested (throws if not found)
   */
  param(name: string): bigint {
    // TODO: consider the case that different active EIPs
    // can change the same parameter
    if (!(name in this._paramsCache)) {
      throw EthereumJSErrorWithoutCode(`Missing parameter value for ${name}`)
    }
    const value = this._paramsCache[name]
    return BigInt(value ?? 0)
  }

  /**
   * Returns the parameter corresponding to a hardfork
   * @param name Parameter name (e.g. 'minGasLimit')
   * @param hardfork Hardfork name
   * @returns The value requested (throws if not found)
   */
  paramByHardfork(name: string, hardfork: string | Hardfork): bigint {
    let value
    for (const hfChanges of this.HARDFORK_CHANGES) {
      // EIP-referencing HF config (e.g. for berlin)
      if ('eips' in hfChanges[1]) {
        const hfEIPs = hfChanges[1]['eips']
        for (const eip of hfEIPs!) {
          const eipParams = this._params[eip]
          const eipValue = eipParams?.[name]
          if (eipValue !== undefined) {
            value = eipValue
          }
        }
        // Parameter-inlining HF config (e.g. for istanbul)
      } else {
        const hfValue = hfChanges[1].params?.[name]
        if (hfValue !== undefined) {
          value = hfValue
        }
      }
      if (hfChanges[0] === hardfork) break
    }
    if (value === undefined) {
      throw EthereumJSErrorWithoutCode(`Missing parameter value for ${name}`)
    }
    return BigInt(value ?? 0)
  }

  /**
   * Returns a parameter corresponding to an EIP
   * @param name Parameter name (e.g. 'minGasLimit' for 'gasConfig' topic)
   * @param eip Number of the EIP
   * @returns The value requested (throws if not found)
   */
  paramByEIP(name: string, eip: number): bigint | undefined {
    if (!(eip in eipsDict)) {
      throw EthereumJSErrorWithoutCode(`${eip} not supported`)
    }

    const eipParams = this._params[eip]
    if (eipParams?.[name] === undefined) {
      throw EthereumJSErrorWithoutCode(`Missing parameter value for ${name}`)
    }
    const value = eipParams![name]
    return BigInt(value ?? 0)
  }

  /**
   * Returns a parameter for the hardfork active on block number or
   * optional provided total difficulty (Merge HF)
   * @param name Parameter name
   * @param blockNumber Block number
   * @returns The value requested or `BigInt(0)` if not found
   */
  paramByBlock(name: string, blockNumber: BigIntLike, timestamp?: BigIntLike): bigint {
    const hardfork = this.getHardforkBy({ blockNumber, timestamp })
    return this.paramByHardfork(name, hardfork)
  }

  /**
   * Returns the blob gas schedule for the current hardfork
   * @returns The blob gas schedule
   */
  getBlobGasSchedule(): BpoSchedule {
    if (this.gteHardfork(Hardfork.Bpo1)) {
      return {
        targetBlobGasPerBlock: this.param('target') * this.param('blobGasPerBlob'),
        maxBlobGasPerBlock: this.param('max') * this.param('blobGasPerBlob'),
        blobGasPriceUpdateFraction: this.param('blobGasPriceUpdateFraction'),
      }
    }
    return {
      targetBlobGasPerBlock: this.param('targetBlobGasPerBlock'),
      maxBlobGasPerBlock: this.param('maxBlobGasPerBlock'),
      blobGasPriceUpdateFraction: this.param('blobGasPriceUpdateFraction'),
    }
  }

  /**
   * Checks if an EIP is activated by either being included in the EIPs
   * manually passed in with the {@link CommonOpts.eips} or in a
   * hardfork currently being active
   *
   * Note: this method only works for EIPs being supported
   * by the {@link CommonOpts.eips} constructor option
   * @param eip
   */
  isActivatedEIP(eip: number): boolean {
    if (this._activatedEIPsCache.includes(eip)) {
      return true
    }
    return false
  }

  /**
   * Checks if set or provided hardfork is active on block number
   * @param hardfork Hardfork name or null (for HF set)
   * @param blockNumber
   * @returns True if HF is active on block number
   */
  hardforkIsActiveOnBlock(hardfork: string | Hardfork | null, blockNumber: BigIntLike): boolean {
    blockNumber = toType(blockNumber, TypeOutput.BigInt)
    hardfork = hardfork ?? this._hardfork
    const hfBlock = this.hardforkBlock(hardfork)
    if (typeof hfBlock === 'bigint' && hfBlock !== BIGINT_0 && blockNumber >= hfBlock) {
      return true
    }
    return false
  }

  /**
   * Alias to hardforkIsActiveOnBlock when hardfork is set
   * @param blockNumber
   * @returns True if HF is active on block number
   */
  activeOnBlock(blockNumber: BigIntLike): boolean {
    return this.hardforkIsActiveOnBlock(null, blockNumber)
  }

  /**
   * Sequence based check if given or set HF1 is greater than or equal HF2
   * @param hardfork1 Hardfork name or null (if set)
   * @param hardfork2 Hardfork name
   * @returns True if HF1 gte HF2
   */
  hardforkGteHardfork(hardfork1: string | Hardfork | null, hardfork2: string | Hardfork): boolean {
    hardfork1 = hardfork1 ?? this._hardfork
    const hardforks = this.hardforks()

    let posHf1 = -1,
      posHf2 = -1
    let index = 0
    for (const hf of hardforks) {
      if (hf['name'] === hardfork1) posHf1 = index
      if (hf['name'] === hardfork2) posHf2 = index
      index += 1
    }
    return posHf1 >= posHf2 && posHf2 !== -1
  }

  /**
   * Alias to hardforkGteHardfork when hardfork is set
   * @param hardfork Hardfork name
   * @returns True if hardfork set is greater than hardfork provided
   */
  gteHardfork(hardfork: string | Hardfork): boolean {
    return this.hardforkGteHardfork(null, hardfork)
  }

  /**
   * Returns the hardfork change block for hardfork provided or set
   * @param hardfork Hardfork name, optional if HF set
   * @returns Block number or null if unscheduled
   */
  hardforkBlock(hardfork?: string | Hardfork): bigint | null {
    hardfork = hardfork ?? this._hardfork
    const block = this._getHardfork(hardfork)?.['block']
    if (block === undefined || block === null) {
      return null
    }
    return BigInt(block)
  }

  /**
   * Returns the timestamp at which a given hardfork is scheduled (if any).
   * @param hardfork Hardfork name, optional if HF set
   * @returns Timestamp or null if the hardfork is not timestamp-based
   */
  hardforkTimestamp(hardfork?: string | Hardfork): bigint | null {
    hardfork = hardfork ?? this._hardfork
    const timestamp = this._getHardfork(hardfork)?.['timestamp']
    if (timestamp === undefined || timestamp === null) {
      return null
    }
    return BigInt(timestamp)
  }

  /**
   * Returns the hardfork change block for eip
   * @param eip EIP number
   * @returns Block number or null if unscheduled
   */
  eipBlock(eip: number): bigint | null {
    for (const hfChanges of this.HARDFORK_CHANGES) {
      const hf = hfChanges[1]
      if ('eips' in hf) {
        // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
        if ((hf['eips'] as any).includes(eip)) {
          return this.hardforkBlock(hfChanges[0])
        }
      }
    }
    return null
  }

  /**
   * Returns the scheduled timestamp of the EIP (if scheduled and scheduled by timestamp)
   * @param eip EIP number
   * @returns Scheduled timestamp. If this EIP is unscheduled, or the EIP is scheduled by block number, then it returns `null`.
   */
  eipTimestamp(eip: number): bigint | null {
    for (const hfChanges of this.HARDFORK_CHANGES) {
      const hf = hfChanges[1]
      if ('eips' in hf) {
        // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
        if ((hf['eips'] as any).includes(eip)) {
          return this.hardforkTimestamp(hfChanges[0])
        }
      }
    }
    return null
  }

  /**
   * Returns the block number or timestamp at which the next hardfork will occur.
   * For pre-merge hardforks, returns the block number.
   * For post-merge hardforks, returns the timestamp.
   * Returns null if there is no next hardfork.
   * @param hardfork Hardfork name, optional if HF set
   * @returns Block number or timestamp, or null if not available
   */
  nextHardforkBlockOrTimestamp(hardfork?: string | Hardfork): bigint | null {
    const targetHardfork = hardfork ?? this._hardfork
    const hfs = this.hardforks()

    // Find the index of the target hardfork
    let targetHfIndex = hfs.findIndex((hf) => hf.name === targetHardfork)

    // Special handling for The Merge (Paris) hardfork
    if (targetHardfork === Hardfork.Paris) {
      // The Merge is determined by total difficulty, not block number
      // So we look at the previous hardfork's parameters instead
      targetHfIndex -= 1
    }

    // If we couldn't find a valid hardfork index, return null
    if (targetHfIndex < 0) {
      return null
    }

    // Get the current hardfork's block/timestamp
    const currentHf = hfs[targetHfIndex]
    const currentBlockOrTimestamp = currentHf.timestamp ?? currentHf.block
    if (currentBlockOrTimestamp === null || currentBlockOrTimestamp === undefined) {
      return null
    }

    // Find the next hardfork that has a different block/timestamp
    const nextHf = hfs.slice(targetHfIndex + 1).find((hf) => {
      const nextBlockOrTimestamp = hf.timestamp ?? hf.block
      return (
        nextBlockOrTimestamp !== null &&
        nextBlockOrTimestamp !== undefined &&
        nextBlockOrTimestamp !== currentBlockOrTimestamp
      )
    })
    // If no next hf found with valid block or timestamp return null
    if (nextHf === undefined) {
      return null
    }

    // Get the block/timestamp for the next hardfork
    const nextBlockOrTimestamp = nextHf.timestamp ?? nextHf.block
    if (nextBlockOrTimestamp === null || nextBlockOrTimestamp === undefined) {
      return null
    }

    return BigInt(nextBlockOrTimestamp)
  }

  /**
   * Internal helper function to calculate a fork hash
   * @param hardfork Hardfork name
   * @param genesisHash Genesis block hash of the chain
   * @returns Fork hash as hex string
   */
  protected _calcForkHash(hardfork: string | Hardfork, genesisHash: Uint8Array): PrefixedHexString {
    let hfBytes = new Uint8Array(0)
    let prevBlockOrTime = 0
    for (const hf of this.hardforks()) {
      const { block, timestamp, name } = hf
      // Timestamp to be used for timestamp based hfs even if we may bundle
      // block number with them retrospectively
      let blockOrTime = timestamp ?? block
      blockOrTime = blockOrTime !== null ? Number(blockOrTime) : null

      // Skip for chainstart (0), not applied HFs (null) and
      // when already applied on same blockOrTime HFs
      // and on the merge since forkhash doesn't change on merge hf
      if (
        typeof blockOrTime === 'number' &&
        blockOrTime !== 0 &&
        blockOrTime !== prevBlockOrTime &&
        name !== Hardfork.Paris
      ) {
        const hfBlockBytes = hexToBytes(`0x${blockOrTime.toString(16).padStart(16, '0')}`)
        hfBytes = concatBytes(hfBytes, hfBlockBytes)
        prevBlockOrTime = blockOrTime
      }

      if (hf.name === hardfork) break
    }
    const inputBytes = concatBytes(genesisHash, hfBytes)

    // CRC32 delivers result as signed (negative) 32-bit integer,
    // convert to hex string
    const forkhash = bytesToHex(intToBytes(crc32(inputBytes) >>> 0))
    return forkhash
  }

  /**
   * Returns an eth/64 compliant fork hash (EIP-2124)
   * @param hardfork Hardfork name, optional if HF set
   * @param genesisHash Genesis block hash of the network, optional if already defined and not needed to be calculated
   * @returns Fork hash as a hex string
   */
  forkHash(hardfork?: string | Hardfork, genesisHash?: Uint8Array): PrefixedHexString {
    hardfork = hardfork ?? this._hardfork
    const data = this._getHardfork(hardfork)
    if (data === null || (data?.block === null && data?.timestamp === undefined)) {
      const msg = 'No fork hash calculation possible for future hardfork'
      throw EthereumJSErrorWithoutCode(msg)
    }
    if (data?.forkHash !== null && data?.forkHash !== undefined) {
      return data.forkHash
    }
    if (!genesisHash)
      throw EthereumJSErrorWithoutCode('genesisHash required for forkHash calculation')
    return this._calcForkHash(hardfork, genesisHash)
  }

  /**
   *
   * @param forkHash Fork hash as a hex string
   * @returns Array with hardfork data (name, block, forkHash)
   */
  hardforkForForkHash(forkHash: string): HardforkTransitionConfig | null {
    const resArray = this.hardforks().filter((hf: HardforkTransitionConfig) => {
      return hf.forkHash === forkHash
    })
    return resArray.length >= 1 ? resArray[resArray.length - 1] : null
  }

  /**
   * Sets any missing forkHashes on this {@link Common} instance.
   * @param genesisHash The genesis block hash
   */
  setForkHashes(genesisHash: Uint8Array) {
    for (const hf of this.hardforks()) {
      const blockOrTime = hf.timestamp ?? hf.block
      if (
        (hf.forkHash === null || hf.forkHash === undefined) &&
        blockOrTime !== null &&
        blockOrTime !== undefined
      ) {
        hf.forkHash = this.forkHash(hf.name, genesisHash)
      }
    }
  }

  /**
   * Returns the Genesis parameters of the current chain
   * @returns Genesis dictionary
   */
  genesis(): GenesisBlockConfig {
    return this._chainParams.genesis
  }

  /**
   * Returns the hardfork definitions for the current chain.
   * @returns Array of hardfork transition configs
   */
  hardforks(): HardforkTransitionConfig[] {
    const hfs = this._chainParams.hardforks
    if (this._chainParams.customHardforks !== undefined) {
      // Add transition configs for custom hardforks that aren't already in the hardforks array
      const existingNames = new Set(hfs.map((hf) => hf.name))
      const customHfEntries = Object.keys(this._chainParams.customHardforks)
        .filter((name) => !existingNames.has(name))
        .map((name) => ({
          name,
          block: null, // Custom hardforks without explicit transition config default to null (inactive by block)
        }))
      return [...hfs, ...customHfEntries]
    }
    return hfs
  }

  /**
   * Returns bootstrap nodes for the current chain.
   * @returns Array of bootstrap node configs
   */
  bootstrapNodes(): BootstrapNodeConfig[] {
    return this._chainParams.bootstrapNodes
  }

  /**
   * Returns DNS networks for the current chain
   * @returns {String[]} Array of DNS ENR urls
   */
  dnsNetworks(): string[] {
    return this._chainParams.dnsNetworks!
  }

  /**
   * Returns the hardfork set
   * @returns Hardfork name
   */
  hardfork(): string | Hardfork {
    return this._hardfork
  }

  /**
   * Returns the Id of current chain
   * @returns chain Id
   */
  chainId(): bigint {
    return BigInt(this._chainParams.chainId)
  }

  /**
   * Returns the name of current chain
   * @returns chain name (lower case)
   */
  chainName(): string {
    return this._chainParams.name
  }

  /**
   * Returns the additionally activated EIPs
   * (by using the `eips` constructor option)
   * @returns List of EIPs
   */
  eips(): number[] {
    return this._eips
  }

  /**
   * Returns the consensus type of the network
   * Possible values: "pow"|"poa"|"pos"
   *
   * Note: This value can update along a Hardfork.
   */
  consensusType(): string | ConsensusType {
    const hardfork = this.hardfork()

    let value
    for (const hfChanges of this.HARDFORK_CHANGES) {
      if ('consensus' in hfChanges[1]) {
        value = (hfChanges[1] as any)['consensus']['type']
      }
      if (hfChanges[0] === hardfork) break
    }
    return value ?? this._chainParams['consensus']['type']
  }

  /**
   * Returns the concrete consensus implementation
   * algorithm or protocol for the network
   * e.g. "ethash" for "pow" consensus type,
   * "clique" for "poa" consensus type or
   * "casper" for "pos" consensus type.
   *
   * Note: This value can update along a Hardfork.
   */
  consensusAlgorithm(): string | ConsensusAlgorithm {
    const hardfork = this.hardfork()

    let value
    for (const hfChanges of this.HARDFORK_CHANGES) {
      if ('consensus' in hfChanges[1]) {
        value = hfChanges[1]['consensus']!['algorithm']
      }
      if (hfChanges[0] === hardfork) break
    }
    return value ?? (this._chainParams['consensus']['algorithm'] as ConsensusAlgorithm)
  }

  /**
   * Returns a dictionary with consensus configuration
   * parameters based on the consensus algorithm
   *
   * Expected returns (parameters must be present in
   * the respective chain JSON files):
   *
   * ethash: empty object
   * clique: period, epoch
   * casper: empty object
   *
   * Note: This value can update along a Hardfork.
   */
  consensusConfig(): { [key: string]: CliqueConfig | EthashConfig | CasperConfig } {
    const hardfork = this.hardfork()

    let value
    for (const hfChanges of this.HARDFORK_CHANGES) {
      if ('consensus' in hfChanges[1]) {
        // The config parameter is named after the respective consensus algorithm
        const config = hfChanges[1]
        const algorithm = config['consensus']!['algorithm']
        value = (config['consensus'] as any)[algorithm]
      }
      if (hfChanges[0] === hardfork) break
    }
    return (
      value ?? this._chainParams['consensus'][this.consensusAlgorithm() as ConsensusAlgorithm] ?? {}
    )
  }

  /**
   * Returns a deep copy of this {@link Common} instance.
   */
  copy(): Common {
    const copy = Object.assign(Object.create(Object.getPrototypeOf(this)), this)
    copy.events = new EventEmitter()
    return copy
  }
}
