import { BinaryTree } from '@ethereumjs/binarytree'
import { BinaryTreeAccessedStateType } from '@ethereumjs/common'
import { RLP } from '@ethereumjs/rlp'
import type { Address, BinaryTreeExecutionWitness, PrefixedHexString } from '@ethereumjs/util'
import {
  Account,
  BINARY_TREE_CODE_CHUNK_SIZE,
  BINARY_TREE_CODE_OFFSET,
  BINARY_TREE_NODE_WIDTH,
  BinaryTreeLeafType,
  EthereumJSErrorWithoutCode,
  KECCAK256_NULL,
  MapDB,
  bigIntToBytes,
  bytesToBigInt,
  bytesToHex,
  chunkifyBinaryTreeCode,
  createAddressFromString,
  createPartialAccount,
  createPartialAccountFromRLP,
  decodeBinaryTreeLeafBasicData,
  encodeBinaryTreeLeafBasicData,
  equalsBytes,
  generateBinaryTreeChunkSuffixes,
  generateBinaryTreeCodeStems,
  getBinaryTreeKeyForStorageSlot,
  getBinaryTreeStem,
  hexToBigInt,
  hexToBytes,
  padToEven,
  setLengthLeft,
  setLengthRight,
  short,
  unprefixedHexToBytes,
} from '@ethereumjs/util'
import { blake3 } from '@noble/hashes/blake3'
import debugDefault from 'debug'
import { keccak256 } from 'ethereum-cryptography/keccak.js'

import { OriginalStorageCache } from './cache/originalStorageCache.ts'
import { modifyAccountFields } from './util.ts'

import type {
  AccountFields,
  BinaryTreeAccessWitnessInterface,
  BinaryTreeAccessedStateWithAddress,
  GenesisState,
  StateManagerInterface,
  StorageDump,
  StoragePair,
  StorageRange,
} from '@ethereumjs/common'
import type { Debugger } from 'debug'
import type { Caches } from './cache/caches.ts'
import type { BinaryTreeState, StatefulBinaryTreeStateManagerOpts } from './types.ts'

const ZEROVALUE = '0x0000000000000000000000000000000000000000000000000000000000000000'
export class StatefulBinaryTreeStateManager implements StateManagerInterface {
  protected _debug: Debugger
  protected _caches?: Caches

  preStateRoot: Uint8Array
  originalStorageCache: OriginalStorageCache
  hashFunction: (input: Uint8Array) => Uint8Array

  protected _tree: BinaryTree

  protected _checkpointCount: number

  // Post-state provided from the executionWitness.
  // Should not update. Used for comparing our computed post-state with the canonical one.
  private _postState: BinaryTreeState = {}

  /**
   * StateManager is run in DEBUG mode (default: false)
   * Taken from DEBUG environment variable
   *
   * Safeguards on debug() calls are added for
   * performance reasons to avoid string literal evaluation
   * @hidden
   */
  protected readonly DEBUG: boolean = false

  private keccakFunction: Function

  constructor(opts: StatefulBinaryTreeStateManagerOpts) {
    // Skip DEBUG calls unless 'ethjs' included in environmental DEBUG variables
    // Additional window check is to prevent vite browser bundling (and potentially other) to break
    this.DEBUG =
      typeof window === 'undefined' ? (process?.env?.DEBUG?.includes('ethjs') ?? false) : false

    this._checkpointCount = 0

    if (opts.common?.isActivatedEIP(7864) === false) {
      throw EthereumJSErrorWithoutCode('EIP-7864 required for binary tree state management')
    }

    this.hashFunction = opts.hashFunction ?? blake3
    this._tree =
      opts.tree ??
      new BinaryTree({
        hashFunction: this.hashFunction,
        db: new MapDB<string, string | Uint8Array>(),
        useRootPersistence: false,
        cacheSize: 0,
      })
    this._debug = debugDefault('statemanager:binarytree')
    this.originalStorageCache = new OriginalStorageCache(this.getStorage.bind(this))
    this._caches = opts.caches
    this.keccakFunction = keccak256
    this.preStateRoot = new Uint8Array(32) // Initial state root is zeroes
  }

  /**
   * Gets the account associated with `address` or `undefined` if account does not exist
   * @param address - Address of the `account` to get
   */
  getAccount = async (address: Address): Promise<Account | undefined> => {
    const elem = this._caches?.account?.get(address)
    if (elem !== undefined) {
      return elem.accountRLP !== undefined
        ? createPartialAccountFromRLP(elem.accountRLP)
        : undefined
    }

    const stem = getBinaryTreeStem(this.hashFunction, address, 0)

    // First retrieve the account "header" values from the trie
    const accountValues = await this._tree.get(stem, [
      BinaryTreeLeafType.BasicData,
      BinaryTreeLeafType.CodeHash,
    ])

    let account
    if (accountValues[0] !== null && accountValues[0] !== undefined) {
      const basicData = decodeBinaryTreeLeafBasicData(accountValues[0]!)
      account = createPartialAccount({
        version: basicData.version,
        balance: basicData.balance,
        nonce: basicData.nonce,
        // Codehash is either untouched (i.e. null) or deleted (i.e. overwritten with zeros)
        codeHash:
          accountValues[1] === null ||
          accountValues[1] === undefined ||
          equalsBytes(accountValues[1], new Uint8Array(32))
            ? KECCAK256_NULL
            : accountValues[1],
        codeSize: basicData.codeSize,
        storageRoot: KECCAK256_NULL,
      })
    } else if (accountValues[1] === undefined || accountValues[1] === null) {
      // account does not exist if both basic fields and codehash are undefined
      if (this.DEBUG) {
        this._debug(`getAccount address=${address.toString()} from DB (non-existent)`)
      }
      this._caches?.account?.put(address, account)
    }

    if (this.DEBUG) {
      this._debug(`getAccount address=${address.toString()} stem=${short(stem)}`)
    }
    return account
  }

  public initBinaryTreeExecutionWitness(
    _blockNum: bigint,
    executionWitness?: BinaryTreeExecutionWitness | null,
  ) {
    if (executionWitness === null || executionWitness === undefined) {
      const errorMsg = `Invalid executionWitness=${executionWitness} for initBinaryTreeExecutionWitness`
      this._debug(errorMsg)
      throw Error(errorMsg)
    }

    this.preStateRoot = hexToBytes(executionWitness.parentStateRoot) // set prestate root

    // Populate the post-state from the executionWitness

    const postStateRaw = executionWitness.stateDiff.flatMap(({ stem, suffixDiffs }) => {
      const suffixDiffPairs = suffixDiffs.map(({ newValue, currentValue, suffix }) => {
        const key = `${stem}${padToEven(Number(suffix).toString(16))}` as PrefixedHexString
        // A postState value of null means there was no change from the preState.
        // In this implementation, we therefore replace null with the preState.
        const value = newValue ?? currentValue

        return {
          [key]: value,
        }
      })

      return suffixDiffPairs
    })

    const postState = postStateRaw.reduce((prevValue, currentValue) => {
      const acc = { ...prevValue, ...currentValue }
      return acc
    }, {})

    this._postState = postState

    this._debug(`initBinaryTreeExecutionWitness postState=${JSON.stringify(this._postState)}`)
  }

  /**
   * Saves an account into state under the provided `address`.
   * @param address - Address under which to store `account`
   * @param account - The account to store or undefined if to be deleted
   */
  putAccount = async (address: Address, account?: Account): Promise<void> => {
    if (this.DEBUG) {
      this._debug(
        `putAccount address=${address} nonce=${account?.nonce} balance=${
          account?.balance
        } contract=${account && account.isContract() ? 'yes' : 'no'} empty=${
          account && account.isEmpty() ? 'yes' : 'no'
        }`,
      )
    }
    if (this._caches?.account === undefined) {
      if (account !== undefined) {
        const stem = getBinaryTreeStem(this.hashFunction, address, 0)
        const basicDataBytes = encodeBinaryTreeLeafBasicData(account)
        await this._tree.put(
          stem,
          [BinaryTreeLeafType.BasicData, BinaryTreeLeafType.CodeHash],
          [basicDataBytes, account.codeHash],
        )
      } else {
        // Delete account
        await this.deleteAccount(address)
      }
    } else {
      if (account !== undefined) {
        this._caches?.account?.put(address, account, true)
      } else {
        this._caches?.account?.del(address)
      }
    }
  }

  /**
   * Deletes an account from state under the provided `address`.
   * @param address - Address of the account which should be deleted
   */
  deleteAccount = async (address: Address): Promise<void> => {
    if (this.DEBUG) {
      this._debug(`Delete account ${address}`)
    }

    this._caches?.deleteAccount(address)

    if (this._caches?.account === undefined) {
      const stem = getBinaryTreeStem(this.hashFunction, address)
      // Special instance where we delete the account and revert the trie value to untouched
      await this._tree.put(
        stem,
        [BinaryTreeLeafType.BasicData, BinaryTreeLeafType.CodeHash],
        [null, null],
      )
    }
  }

  modifyAccountFields = async (address: Address, accountFields: AccountFields): Promise<void> => {
    await modifyAccountFields(this, address, accountFields)
  }
  putCode = async (address: Address, value: Uint8Array): Promise<void> => {
    if (this.DEBUG) {
      this._debug(`putCode address=${address.toString()} value=${short(value)}`)
    }

    this._caches?.code?.put(address, value)

    const codeHash = keccak256(value)
    if (equalsBytes(codeHash, KECCAK256_NULL)) {
      // If the code hash is the null hash, no code has to be stored
      return
    }

    if ((await this.getAccount(address)) === undefined) {
      await this.putAccount(address, new Account())
    }
    if (this.DEBUG) {
      this._debug(`Update codeHash (-> ${short(codeHash)}) for account ${address}`)
    }

    const codeChunks = chunkifyBinaryTreeCode(value)
    const chunkStems = generateBinaryTreeCodeStems(codeChunks.length, address, this.hashFunction)

    const chunkSuffixes: number[] = generateBinaryTreeChunkSuffixes(codeChunks.length)
    // Put the code chunks corresponding to the first stem (up to 128 chunks)
    await this._tree.put(
      chunkStems[0],
      chunkSuffixes.slice(
        0,
        chunkSuffixes.length <= BINARY_TREE_CODE_OFFSET
          ? chunkSuffixes.length
          : BINARY_TREE_CODE_OFFSET,
      ),
      codeChunks.slice(
        0,
        codeChunks.length <= BINARY_TREE_CODE_OFFSET ? codeChunks.length : BINARY_TREE_CODE_OFFSET,
      ),
    )

    // Put additional chunks under additional stems as applicable
    for (let stem = 1; stem < chunkStems.length; stem++) {
      const sliceStart = BINARY_TREE_CODE_OFFSET + BINARY_TREE_NODE_WIDTH * (stem - 1)
      const sliceEnd =
        value.length <= BINARY_TREE_CODE_OFFSET + BINARY_TREE_NODE_WIDTH * stem
          ? value.length
          : BINARY_TREE_CODE_OFFSET + BINARY_TREE_NODE_WIDTH * stem
      await this._tree.put(
        chunkStems[stem],
        chunkSuffixes.slice(sliceStart, sliceEnd),
        codeChunks.slice(sliceStart, sliceEnd),
      )
    }
    await this.modifyAccountFields(address, {
      codeHash,
      codeSize: value.length,
    })
  }

  getCode = async (address: Address): Promise<Uint8Array> => {
    if (this.DEBUG) {
      this._debug(`getCode address=${address.toString()}`)
    }

    const elem = this._caches?.code?.get(address)
    if (elem !== undefined) {
      return elem.code ?? new Uint8Array(0)
    }

    const account = await this.getAccount(address)
    if (!account) {
      return new Uint8Array(0)
    }
    if (!account.isContract()) {
      return new Uint8Array(0)
    }

    // allocate the code
    const codeSize = account.codeSize

    const stems = generateBinaryTreeCodeStems(
      Math.ceil(codeSize / BINARY_TREE_CODE_CHUNK_SIZE),
      address,
      this.hashFunction,
    )
    const chunkSuffixes = generateBinaryTreeChunkSuffixes(
      Math.ceil(codeSize / BINARY_TREE_CODE_CHUNK_SIZE),
    )

    const chunksByStem = new Array(stems.length)
    // Retrieve the code chunks stored in the first leaf node
    chunksByStem[0] = await this._tree.get(
      stems[0],
      chunkSuffixes.slice(
        0,
        codeSize <= BINARY_TREE_CODE_OFFSET ? codeSize : BINARY_TREE_CODE_OFFSET,
      ),
    )

    // Retrieve code chunks on any additional stems
    for (let stem = 1; stem < stems.length; stem++) {
      const sliceStart = BINARY_TREE_CODE_OFFSET + BINARY_TREE_NODE_WIDTH * (stem - 1)
      const sliceEnd =
        codeSize <= BINARY_TREE_CODE_OFFSET + BINARY_TREE_NODE_WIDTH * stem
          ? codeSize
          : BINARY_TREE_CODE_OFFSET + BINARY_TREE_NODE_WIDTH * stem
      chunksByStem[stem] = await this._tree.get(
        stems[stem],
        chunkSuffixes.slice(sliceStart, sliceEnd),
      )
    }
    const chunks = chunksByStem.flat()
    const code = new Uint8Array(codeSize)
    // Insert code chunks into final array (skipping PUSHDATA overflow indicator byte)
    for (let x = 0; x < chunks.length; x++) {
      if (chunks[x] === undefined)
        throw EthereumJSErrorWithoutCode(`expected code chunk at index ${x}, got undefined`)

      let lastChunkByteIndex = BINARY_TREE_CODE_CHUNK_SIZE
      // Determine code ending byte (if we're on the last chunk)
      if (x === chunks.length - 1) {
        // On the last chunk, the slice either ends on a partial chunk (if codeSize doesn't exactly fit in full chunks), or a full chunk
        lastChunkByteIndex = codeSize % BINARY_TREE_CODE_CHUNK_SIZE || BINARY_TREE_CODE_CHUNK_SIZE
      }
      code.set(
        chunks[x]!.slice(1, lastChunkByteIndex + 1),
        code.byteOffset + x * BINARY_TREE_CODE_CHUNK_SIZE,
      )
    }
    this._caches?.code?.put(address, code)

    return code
  }

  getCodeSize = async (address: Address): Promise<number> => {
    const accountBytes = (
      await this._tree.get(getBinaryTreeStem(this.hashFunction, address), [
        BinaryTreeLeafType.BasicData,
      ])
    )[0]
    if (accountBytes === null) return 0
    return decodeBinaryTreeLeafBasicData(accountBytes).codeSize
  }
  getStorage = async (address: Address, key: Uint8Array): Promise<Uint8Array> => {
    if (key.length !== 32) {
      throw EthereumJSErrorWithoutCode('Storage key must be 32 bytes long')
    }
    const cachedValue = this._caches?.storage?.get(address, key)
    if (cachedValue !== undefined) {
      const decoded = RLP.decode(cachedValue ?? new Uint8Array(0)) as Uint8Array
      return decoded
    }

    const account = await this.getAccount(address)
    if (!account) {
      return new Uint8Array()
    }
    const storageKey = getBinaryTreeKeyForStorageSlot(
      address,
      bytesToBigInt(key),
      this.hashFunction,
    )
    const value = await this._tree.get(storageKey.slice(0, 31), [storageKey[31]])

    this._caches?.storage?.put(address, key, value[0] ?? hexToBytes('0x80'))
    const decoded = (value[0] ?? new Uint8Array(0)) as Uint8Array
    return setLengthLeft(decoded, 32)
  }

  putStorage = async (address: Address, key: Uint8Array, value: Uint8Array): Promise<void> => {
    this._caches?.storage?.put(address, key, RLP.encode(value))
    if (this._caches?.storage === undefined) {
      const storageKey = getBinaryTreeKeyForStorageSlot(
        address,
        bytesToBigInt(key),
        this.hashFunction,
      )
      await this._tree.put(storageKey.slice(0, 31), [storageKey[31]], [setLengthLeft(value, 32)])
    }
  }

  clearStorage = async (address: Address): Promise<void> => {
    this._caches?.storage?.clearStorage(address)
  }

  checkpoint = async (): Promise<void> => {
    this._tree.checkpoint()
    this._caches?.checkpoint()
    this._checkpointCount++
  }
  commit = async (): Promise<void> => {
    await this._tree.commit()
    this._caches?.commit()
    this._checkpointCount--

    if (this._checkpointCount === 0) {
      await this.flush()
      this.originalStorageCache.clear()
    }

    if (this.DEBUG) {
      this._debug(`state checkpoint committed`)
    }
  }
  revert = async (): Promise<void> => {
    await this._tree.revert()
    this._caches?.revert()

    this._checkpointCount--

    if (this._checkpointCount === 0) {
      await this.flush()
      this.originalStorageCache.clear()
    }
  }

  flush = async (): Promise<void> => {
    const codeItems = this._caches?.code?.flush() ?? []
    for (const item of codeItems) {
      const addr = createAddressFromString(`0x${item[0]}`)

      const code = item[1].code
      if (code === undefined) {
        continue
      }

      await this.putCode(addr, code)
    }

    const storageItems = this._caches?.storage?.flush() ?? []
    for (const item of storageItems) {
      const address = createAddressFromString(`0x${item[0]}`)
      const keyHex = item[1]
      const keyBytes = unprefixedHexToBytes(keyHex)
      const value = item[2]

      const decoded = RLP.decode(value ?? new Uint8Array(0)) as Uint8Array
      const account = await this.getAccount(address)
      if (account) {
        await this.putStorage(address, keyBytes, decoded)
      }
    }

    const accountItems = this._caches?.account?.flush() ?? []
    for (const item of accountItems) {
      const address = createAddressFromString(`0x${item[0]}`)
      const elem = item[1]
      if (elem.accountRLP === undefined) {
        await this.deleteAccount(address)
      } else {
        const account = createPartialAccountFromRLP(elem.accountRLP)
        await this.putAccount(address, account)
      }
    }
  }

  async getComputedValue(
    accessedState: BinaryTreeAccessedStateWithAddress,
  ): Promise<PrefixedHexString | null> {
    const { address, type } = accessedState

    switch (type) {
      case BinaryTreeAccessedStateType.BasicData: {
        if (this._caches === undefined) {
          const accountData = await this.getAccount(address)
          if (accountData === undefined) {
            return null
          }
          const basicDataBytes = encodeBinaryTreeLeafBasicData(accountData)
          return bytesToHex(basicDataBytes)
        } else {
          const encodedAccount = this._caches?.account?.get(address)?.accountRLP
          if (encodedAccount === undefined) {
            return null
          }
          const basicDataBytes = encodeBinaryTreeLeafBasicData(
            createPartialAccountFromRLP(encodedAccount),
          )
          return bytesToHex(basicDataBytes)
        }
      }

      case BinaryTreeAccessedStateType.CodeHash: {
        if (this._caches === undefined) {
          const accountData = await this.getAccount(address)
          if (accountData === undefined) {
            return null
          }

          return bytesToHex(accountData.codeHash)
        } else {
          const encodedAccount = this._caches?.account?.get(address)?.accountRLP
          if (encodedAccount === undefined) {
            return null
          }
          return bytesToHex(createPartialAccountFromRLP(encodedAccount).codeHash)
        }
      }

      case BinaryTreeAccessedStateType.Code: {
        const { codeOffset } = accessedState
        let code: Uint8Array | undefined | null = null
        if (this._caches === undefined) {
          code = await this.getCode(address)
          if (code === undefined) {
            return null
          }
        } else {
          code = this._caches?.code?.get(address)?.code
          if (code === undefined) {
            return null
          }
        }

        // we can only compare the actual code because to compare the first byte would
        // be very tricky and impossible in certain scenarios like when the previous code chunk
        // was not accessed and hence not even provided in the witness
        // We are left-padding with two zeroes to get a 32-byte length, but these bytes should not be considered reliable
        return bytesToHex(
          setLengthLeft(
            setLengthRight(
              code.slice(codeOffset, codeOffset + BINARY_TREE_CODE_CHUNK_SIZE),
              BINARY_TREE_CODE_CHUNK_SIZE,
            ),
            BINARY_TREE_CODE_CHUNK_SIZE + 1,
          ),
        )
      }

      case BinaryTreeAccessedStateType.Storage: {
        const { slot } = accessedState
        const key = setLengthLeft(bigIntToBytes(slot), 32)
        let storage: Uint8Array | undefined | null = null
        if (this._caches === undefined) {
          storage = await this.getStorage(address, key)
          if (storage === undefined) {
            return null
          }
        } else {
          storage = this._caches?.storage?.get(address, key)
        }
        if (storage === undefined) {
          return null
        }
        return bytesToHex(setLengthLeft(storage, 32))
      }
    }
  }

  // Verifies that the witness post-state matches the computed post-state
  async verifyBinaryTreePostState(
    accessWitness: BinaryTreeAccessWitnessInterface,
  ): Promise<boolean> {
    // track what all chunks were accessed so as to compare in the end if any chunks were missed
    // in access while comparing against the provided poststate in the execution witness
    const accessedChunks = new Map<string, boolean>()
    // switch to false if postVerify fails
    let postFailures = 0

    for (const accessedState of accessWitness?.accesses() ?? []) {
      const { address, type } = accessedState
      let extraMeta = ''
      if (accessedState.type === BinaryTreeAccessedStateType.Code) {
        extraMeta = `codeOffset=${accessedState.codeOffset}`
      } else if (accessedState.type === BinaryTreeAccessedStateType.Storage) {
        extraMeta = `slot=${accessedState.slot}`
      }

      const { chunkKey } = accessedState
      accessedChunks.set(chunkKey, true)
      let computedValue: PrefixedHexString | null | undefined =
        await this.getComputedValue(accessedState)
      if (computedValue === undefined) {
        this.DEBUG &&
          this._debug(
            `Missing computed value for address=${address} type=${type} ${extraMeta} chunkKey=${chunkKey}`,
          )
        postFailures++
        continue
      }

      let canonicalValue: PrefixedHexString | null | undefined = this._postState[chunkKey]

      if (canonicalValue === undefined) {
        this.DEBUG &&
          this._debug(
            `Block accesses missing from postState for address=${address} type=${type} ${extraMeta} chunkKey=${chunkKey}`,
          )
        postFailures++
        continue
      }

      // if the access type is code, then we can't match the first byte because since the computed value
      // doesn't has the first byte for push data since previous chunk code itself might not be available
      if (accessedState.type === BinaryTreeAccessedStateType.Code) {
        computedValue = computedValue !== null ? `0x${computedValue.slice(4)}` : null
        canonicalValue = canonicalValue !== null ? `0x${canonicalValue.slice(4)}` : null
      } else if (
        accessedState.type === BinaryTreeAccessedStateType.Storage &&
        canonicalValue === null &&
        computedValue === ZEROVALUE
      ) {
        canonicalValue = ZEROVALUE
      }

      this._debug(`computed ${computedValue} canonical ${canonicalValue}`)
      if (computedValue !== canonicalValue) {
        if (type === BinaryTreeAccessedStateType.BasicData) {
          this.DEBUG &&
            this._debug(
              `canonical value: `,
              canonicalValue === null
                ? null
                : decodeBinaryTreeLeafBasicData(hexToBytes(canonicalValue)),
            )
          this.DEBUG &&
            this._debug(
              `computed value: `,
              computedValue === null
                ? null
                : decodeBinaryTreeLeafBasicData(hexToBytes(computedValue)),
            )
        }
        this.DEBUG &&
          this._debug(
            `Block accesses mismatch address=${address} type=${type} ${extraMeta} chunkKey=${chunkKey}`,
          )
        this.DEBUG && this._debug(`expected=${canonicalValue}`)
        this.DEBUG && this._debug(`computed=${computedValue}`)
        postFailures++
      }
    }

    for (const canChunkKey of Object.keys(this._postState)) {
      if (accessedChunks.get(canChunkKey) === undefined) {
        this.DEBUG && this._debug(`Missing chunk access for canChunkKey=${canChunkKey}`)
        postFailures++
      }
    }

    const verifyPassed = postFailures === 0
    this.DEBUG &&
      this._debug(
        `verifyBinaryTreePostState verifyPassed=${verifyPassed} postFailures=${postFailures}`,
      )

    return verifyPassed
  }

  getStateRoot(): Promise<Uint8Array> {
    return Promise.resolve(this._tree.root())
  }

  setStateRoot(stateRoot: Uint8Array, clearCache?: boolean): Promise<void> {
    this._tree.root(stateRoot)
    clearCache === true && this.clearCaches()
    return Promise.resolve()
  }
  hasStateRoot(root: Uint8Array): Promise<boolean> {
    return this._tree.checkRoot(root)
  }
  dumpStorage?(_address: Address): Promise<StorageDump> {
    throw EthereumJSErrorWithoutCode('Method not implemented.')
  }
  dumpStorageRange?(_address: Address, _startKey: bigint, _limit: number): Promise<StorageRange> {
    throw EthereumJSErrorWithoutCode('Method not implemented.')
  }
  clearCaches(): void {
    this._caches?.clear()
  }
  shallowCopy(_downlevelCaches?: boolean): StateManagerInterface {
    throw EthereumJSErrorWithoutCode('Method not implemented.')
  }
  async checkChunkWitnessPresent(_address: Address, _codeOffset: number): Promise<boolean> {
    throw EthereumJSErrorWithoutCode('Method not implemented.')
  }
  async generateCanonicalGenesis(genesisState: GenesisState) {
    await this._tree.createRootNode()
    await this.checkpoint()
    for (const addressStr of Object.keys(genesisState) as PrefixedHexString[]) {
      const addrState = genesisState[addressStr]
      let nonce: PrefixedHexString | undefined
      let balance: PrefixedHexString | bigint
      let code: PrefixedHexString | undefined
      let storage: StoragePair[] | undefined = []
      if (Array.isArray(addrState)) {
        ;[balance, code, storage, nonce] = addrState
      } else {
        balance = hexToBigInt(addrState)
        nonce = '0x1'
        code = '0x'
      }
      const address = createAddressFromString(addressStr)
      await this.putAccount(address, new Account())
      const codeBuf = hexToBytes(code ?? '0x')

      const codeHash = this.keccakFunction(codeBuf)

      // Set contract storage
      if (storage !== undefined) {
        for (const [storageKey, valHex] of storage) {
          const val = hexToBytes(valHex)
          if (['0x', '0x00'].includes(bytesToHex(val))) {
            continue
          }
          const key = setLengthLeft(hexToBytes(storageKey), 32)
          await this.putStorage(address, key, val)
        }
      }
      // Put contract code
      await this.putCode(address, codeBuf)

      // Put account data
      const account = createPartialAccount({
        nonce,
        balance,
        codeHash,
        codeSize: codeBuf.byteLength,
      })

      await this.putAccount(address, account)
    }
    await this.commit()
    await this.flush()
  }
}
