import * as abitype from 'abitype'
import type * as Abi from './Abi.js'
import * as AbiItem from './AbiItem.js'
import * as AbiParameters from './AbiParameters.js'
import * as Address from './Address.js'
import * as Bytes from './Bytes.js'
import * as Errors from './Errors.js'
import * as Hash from './Hash.js'
import * as Hex from './Hex.js'
import type * as internal from './internal/abiEvent.js'
import type * as AbiItem_internal from './internal/abiItem.js'
import * as Cursor from './internal/cursor.js'
import { prettyPrint } from './internal/errors.js'
import * as formatAbiItem from './internal/human-readable/formatAbiItem.js'
import type { Compute, IsNarrowable } from './internal/types.js'

/** Root type for an {@link ox#AbiItem.AbiItem} with an `event` type. */
export type AbiEvent = abitype.AbiEvent & {
  hash?: Hex.Hex | undefined
  overloads?: readonly AbiEvent[] | undefined
}

/**
 * Module-scope regex for matching an array suffix on an indexed event
 * parameter type (e.g. `uint256[]`, `bytes32[3]`). Hoisted out of
 * `decode` / `encode` so we don't recompile per call.
 *
 * @internal
 */
const arraySuffixRegex = /^(.*)\[(\d+)?\]$/

/**
 * Encodes a primitive (32-byte) indexed event topic directly to a
 * left-padded 32-byte hex word, skipping the `AbiParameters.encode`
 * → `prepareParameters` → `Hex.concat` round-trip used for general
 * parameters. Returns `undefined` if the parameter type isn't a
 * supported primitive.
 *
 * @internal
 */
function encodePrimitiveTopic(
  type: string,
  value: unknown,
): Hex.Hex | undefined {
  if (type === 'address') {
    Address.assert(value as Address.Address, { strict: false })
    return `0x000000000000000000000000${(value as string).slice(2).toLowerCase()}` as Hex.Hex
  }
  if (type === 'bool') {
    if (typeof value !== 'boolean') return undefined
    return value
      ? '0x0000000000000000000000000000000000000000000000000000000000000001'
      : '0x0000000000000000000000000000000000000000000000000000000000000000'
  }
  // `uintN` / `intN` (covers all 8-bit-aligned widths up to 256).
  if (type.startsWith('uint') || type.startsWith('int')) {
    const signed = type[0] === 'i'
    const sizeStr = signed ? type.slice(3) : type.slice(4)
    const size = sizeStr === '' ? 256 : Number(sizeStr)
    if (!Number.isFinite(size)) return undefined
    return Hex.fromNumber(value as number | bigint, { size: 32, signed })
  }
  // `bytesN` (fixed): right-pad to 32 bytes.
  if (type.startsWith('bytes') && type.length > 5) {
    if (typeof value !== 'string') return undefined
    return Hex.padRight(value as Hex.Hex, 32)
  }
  return undefined
}

/**
 * Decodes a primitive (32-byte) indexed event topic directly to a
 * primitive value, skipping the `AbiParameters.decode` round-trip.
 * Returns a `[value]` tuple on hit, or `undefined` on miss.
 *
 * @internal
 */
function decodePrimitiveTopic(
  type: string,
  topic: Hex.Hex,
  options: decode.Options = {},
): [value: unknown] | undefined {
  if (type === 'address') {
    // Trailing 20 bytes; topic is always 66 chars (`0x` + 64 hex).
    const address = `0x${topic.slice(26)}` as Address.Address
    return [
      options.checksumAddress === false ? address : Address.checksum(address),
    ]
  }
  if (type === 'bool') {
    // Last byte determines truthiness (Solidity left-pads booleans).
    return [topic.charCodeAt(65) === 49 /* '1' */]
  }
  if (type.startsWith('uint') || type.startsWith('int')) {
    const signed = type[0] === 'i'
    const sizeStr = signed ? type.slice(3) : type.slice(4)
    const size = sizeStr === '' ? 256 : Number(sizeStr)
    if (!Number.isFinite(size)) return undefined
    const big =
      size > 48
        ? Hex.toBigInt(topic, { signed })
        : Hex.toNumber(topic, { signed })
    return [big]
  }
  if (type.startsWith('bytes') && type.length > 5) {
    const sizeStr = type.slice(5)
    const size = Number(sizeStr)
    if (!Number.isFinite(size)) return undefined
    // Take leading `size` bytes of the 32-byte topic.
    return [`0x${topic.slice(2, 2 + size * 2)}` as Hex.Hex]
  }
  return undefined
}

/**
 * Extracts an {@link ox#AbiEvent.AbiEvent} item from an {@link ox#Abi.Abi}, given a name.
 *
 * @example
 * ```ts twoslash
 * import { Abi, AbiEvent } from 'ox'
 *
 * const abi = Abi.from([
 *   'event Foo(string)',
 *   'event Bar(uint256)'
 * ])
 *
 * type Foo = AbiEvent.FromAbi<typeof abi, 'Foo'>
 * //   ^?
 * ```
 */
export type FromAbi<
  abi extends Abi.Abi,
  name extends ExtractNames<abi>,
> = abitype.ExtractAbiEvent<abi, name>

/**
 * Extracts the names of all {@link ox#AbiError.AbiError} items in an {@link ox#Abi.Abi}.
 *
 * @example
 * ```ts twoslash
 * import { Abi, AbiEvent } from 'ox'
 *
 * const abi = Abi.from([
 *   'event Foo(string)',
 *   'event Bar(uint256)'
 * ])
 *
 * type names = AbiEvent.Name<typeof abi>
 * //   ^?
 * ```
 */
export type Name<abi extends Abi.Abi | readonly unknown[] = Abi.Abi> =
  abi extends Abi.Abi ? ExtractNames<abi> : string

export type ExtractNames<abi extends Abi.Abi> =
  abitype.ExtractAbiEventNames<abi>

/**
 * Asserts that the provided arguments match the decoded log arguments.
 *
 * @example
 * ```ts twoslash
 * import { AbiEvent } from 'ox'
 *
 * const abiEvent = AbiEvent.from(
 *   'event Transfer(address indexed from, address indexed to, uint256 value)'
 * )
 *
 * const args = AbiEvent.decode(abiEvent, {
 *   data: '0x0000000000000000000000000000000000000000000000000000000000000001',
 *   topics: [
 *     '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
 *     '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac',
 *     '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac'
 *   ]
 * })
 *
 * AbiEvent.assertArgs(abiEvent, args, {
 *   from: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ad',
 *   to: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac',
 *   value: 1n
 * })
 *
 * // @error: AbiEvent.ArgsMismatchError: Given arguments to not match the arguments decoded from the log.
 * // @error: Event: event Transfer(address indexed from, address indexed to, uint256 value)
 * // @error: Expected Arguments:
 * // @error:   from:   0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac
 * // @error:   to:     0xa5cc3c03994db5b0d9a5eedd10cabab0813678ad
 * // @error:   value:  1
 * // @error: Given Arguments:
 * // @error:   from:   0xa5cc3c03994db5b0d9a5eedd10cabab0813678ad
 * // @error:   to:     0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac
 * // @error:   value:  1
 * ```
 *
 * @param abiEvent - ABI Event to check.
 * @param args - Decoded arguments.
 * @param matchArgs - The arguments to check.
 */
export function assertArgs<const abiEvent extends AbiEvent>(
  abiEvent: abiEvent | AbiEvent,
  args: unknown,
  matchArgs: IsNarrowable<abiEvent, AbiEvent> extends true
    ? abiEvent['inputs'] extends readonly []
      ? never
      : internal.ParametersToPrimitiveTypes<
          abiEvent['inputs'],
          { EnableUnion: true; IndexedOnly: false; Required: false }
        >
    : unknown,
) {
  if (!args || !matchArgs)
    throw new ArgsMismatchError({
      abiEvent,
      expected: args,
      given: matchArgs,
    })

  function isEqual(
    input: abitype.AbiEventParameter,
    value: unknown,
    arg: unknown,
  ) {
    if (input.type === 'address')
      return Address.isEqual(value as Address.Address, arg as Address.Address)
    if (input.type === 'string')
      return (
        Hash.keccak256(Bytes.fromString(value as string), { as: 'Hex' }) === arg
      )
    if (input.type === 'bytes') {
      const hex =
        typeof value === 'string'
          ? (value as Hex.Hex)
          : Hex.fromBytes(value as Bytes.Bytes)
      return Hash.keccak256(hex, { as: 'Hex' }) === arg
    }
    return value === arg
  }

  if (Array.isArray(args) && Array.isArray(matchArgs)) {
    for (const [index, value] of matchArgs.entries()) {
      if (value === null || value === undefined) continue
      const input = abiEvent.inputs[index]
      if (!input)
        throw new InputNotFoundError({
          abiEvent,
          name: `${index}`,
        })
      const value_ = Array.isArray(value) ? value : [value]
      let equal = false
      for (const value of value_) {
        if (isEqual(input, value, args[index])) equal = true
      }
      if (!equal)
        throw new ArgsMismatchError({
          abiEvent,
          expected: args,
          given: matchArgs,
        })
    }
  }

  if (
    typeof args === 'object' &&
    !Array.isArray(args) &&
    typeof matchArgs === 'object' &&
    !Array.isArray(matchArgs)
  )
    for (const [key, value] of Object.entries(matchArgs)) {
      if (value === null || value === undefined) continue
      const input = abiEvent.inputs.find((input) => input.name === key)
      if (!input) throw new InputNotFoundError({ abiEvent, name: key })
      const value_ = Array.isArray(value) ? value : [value]
      let equal = false
      for (const value of value_) {
        if (isEqual(input, value, (args as Record<string, unknown>)[key]))
          equal = true
      }
      if (!equal)
        throw new ArgsMismatchError({
          abiEvent,
          expected: args,
          given: matchArgs,
        })
    }
}

export declare namespace assertArgs {
  type ErrorType =
    | Address.isEqual.ErrorType
    | Bytes.fromString.ErrorType
    | Hash.keccak256.ErrorType
    | ArgsMismatchError
    | Errors.GlobalErrorType
}

/**
 * ABI-Decodes the provided [Log Topics and Data](https://info.etherscan.com/what-is-event-logs/) according to the ABI Event's parameter types (`input`).
 *
 * :::tip
 *
 * This function is typically used to decode an [Event Log](https://info.etherscan.com/what-is-event-logs/) that may be returned from a Log Query (e.g. `eth_getLogs`) or Transaction Receipt.
 *
 * See the [End-to-end Example](#end-to-end).
 *
 * :::
 *
 * @example
 * ```ts twoslash
 * import { AbiEvent } from 'ox'
 *
 * const transfer = AbiEvent.from(
 *   'event Transfer(address indexed from, address indexed to, uint256 value)'
 * )
 *
 * const log = {
 *   // ...
 *   data: '0x0000000000000000000000000000000000000000000000000000000000000001',
 *   topics: [
 *     '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
 *     '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac',
 *     '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac'
 *   ]
 * } as const
 *
 * const decoded = AbiEvent.decode(transfer, log)
 * // @log: {
 * // @log:   from: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC',
 * // @log:   to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC',
 * // @log:   value: 1n
 * // @log: }
 * ```
 *
 * @example
 * ### ABI-shorthand
 *
 * You can also specify an entire ABI object and an event name as parameters to {@link ox#AbiEvent.(decode:function)}:
 *
 * ```ts twoslash
 * // @noErrors
 * import { Abi, AbiEvent } from 'ox'
 *
 * const abi = Abi.from([...])
 * const log = {
 *   // ...
 *   data: '0x0000000000000000000000000000000000000000000000000000000000000001',
 *   topics: [
 *     '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
 *     '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac',
 *     '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac',
 *   ],
 * } as const
 *
 * const decoded = AbiEvent.decode(
 *   abi, // [!code focus]
 *   'Transfer', // [!code focus]
 *   log
 * )
 * // @log: {
 * // @log:   from: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC',
 * // @log:   to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC',
 * // @log:   value: 1n
 * // @log: }
 * ```
 *
 * @example
 * ### End-to-end
 *
 * Below is an end-to-end example of using `AbiEvent.decode` to decode the topics of a `Transfer` event on the [Wagmi Mint Example contract](https://etherscan.io/address/0xfba3912ca04dd458c843e2ee08967fc04f3579c2).
 *
 * ```ts twoslash
 * import 'ox/window'
 * import { AbiEvent, Hex } from 'ox'
 *
 * // 1. Instantiate the `Transfer` ABI Event.
 * const transfer = AbiEvent.from(
 *   'event Transfer(address indexed from, address indexed to, uint256 value)'
 * )
 *
 * // 2. Encode the ABI Event into Event Topics.
 * const { topics } = AbiEvent.encode(transfer)
 *
 * // 3. Query for events matching the encoded Topics.
 * const logs = await window.ethereum!.request({
 *   method: 'eth_getLogs',
 *   params: [
 *     {
 *       address: '0xfba3912ca04dd458c843e2ee08967fc04f3579c2',
 *       fromBlock: Hex.fromNumber(19760235n),
 *       toBlock: Hex.fromNumber(19760240n),
 *       topics
 *     }
 *   ]
 * })
 *
 * // 4. Decode the Log. // [!code focus]
 * const decoded = AbiEvent.decode(transfer, logs[0]!) // [!code focus]
 * // @log: {
 * // @log:   from: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC',
 * // @log:   to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC',
 * // @log:   value: 603n
 * // @log: }
 * ```
 *
 * :::note
 *
 * For simplicity, the above example uses `window.ethereum.request`, but you can use any
 * type of JSON-RPC interface.
 *
 * :::
 *
 * @param abiEvent - The ABI Event to decode.
 * @param log - `topics` & `data` to decode.
 * @param options - Decoding options.
 * @returns The decoded event.
 */
export function decode<
  const abi extends Abi.Abi | readonly unknown[],
  name extends Name<abi>,
  const args extends AbiItem_internal.ExtractArgs<abi, name> | undefined =
    undefined,
  //
  abiEvent extends AbiEvent = AbiItem.fromAbi.ReturnType<
    abi,
    name,
    args,
    AbiEvent
  >,
  allNames = Name<abi>,
>(
  abi: abi | Abi.Abi | readonly unknown[],
  name: Hex.Hex | (name extends allNames ? name : never),
  log: decode.Log,
  options?: decode.Options,
): decode.ReturnType<abiEvent>
export function decode<const abiEvent extends AbiEvent>(
  abiEvent: abiEvent | AbiEvent,
  log: decode.Log,
  options?: decode.Options,
): decode.ReturnType<abiEvent>
// eslint-disable-next-line jsdoc-js/require-jsdoc
export function decode(
  ...parameters:
    | [
        abi: Abi.Abi | readonly unknown[],
        name: Hex.Hex | string,
        log: decode.Log,
        options?: decode.Options | undefined,
      ]
    | [
        abiEvent: AbiEvent,
        log: decode.Log,
        options?: decode.Options | undefined,
      ]
): decode.ReturnType {
  const [abiEvent, log, options] = (() => {
    if (Array.isArray(parameters[0])) {
      const [abi, name, log, options] = parameters as [
        Abi.Abi | readonly unknown[],
        Hex.Hex | string,
        decode.Log,
        decode.Options | undefined,
      ]
      return [fromAbi(abi, name), log, options] as const
    }
    const [abiEvent, log, options] = parameters as [
      AbiEvent,
      decode.Log,
      decode.Options | undefined,
    ]
    return [abiEvent, log, options] as const
  })()

  const { data, topics } = log

  const isAnonymous = abiEvent.anonymous === true
  const argTopics = isAnonymous ? [...topics] : topics.slice(1)

  if (!isAnonymous) {
    const selector_ = topics[0]
    const selector = getSelector(abiEvent)
    if (selector_ !== selector)
      throw new SelectorTopicMismatchError({
        abiEvent,
        actual: selector_,
        expected: selector,
      })
  }

  const { inputs } = abiEvent
  const isUnnamed = inputs?.every((x) => !('name' in x && x.name))

  let args: any = isUnnamed ? [] : {}

  // Single-pass partition: keep both the input and its original index so we
  // don't have to call `inputs.indexOf(...)` per non-indexed entry below
  // (which is O(n^2) on event width).
  const indexedInputs: abitype.AbiEventParameter[] = []
  const nonIndexedInputs: abitype.AbiEventParameter[] = []
  const nonIndexedOriginalIndex: number[] = []
  for (let i = 0; i < inputs.length; i++) {
    const input = inputs[i]!
    if ('indexed' in input && input.indexed) indexedInputs.push(input)
    else {
      nonIndexedInputs.push(input)
      nonIndexedOriginalIndex.push(i)
    }
  }

  // Decode topics (indexed args).
  for (let i = 0; i < indexedInputs.length; i++) {
    const param = indexedInputs[i]!
    const topic = argTopics[i]
    if (!topic)
      throw new TopicsMismatchError({
        abiEvent,
        param: param as abitype.AbiParameter & { indexed: boolean },
      })
    args[isUnnamed ? i : param.name || i] = (() => {
      const t = param.type
      if (
        t === 'string' ||
        t === 'bytes' ||
        t === 'tuple' ||
        arraySuffixRegex.test(t)
      )
        return topic
      // Fast path: 32-byte primitive (`address`, `bool`, `uintN`,
      // `intN`, `bytesN`). Reads the topic word directly without
      // allocating a `Cursor` or running `Bytes.fromHex`.
      const fast = decodePrimitiveTopic(t, topic, options)
      if (fast) return fast[0]
      const decoded = AbiParameters.decode([param], topic, options) || []
      return decoded[0]
    })()
  }

  // Decode data (non-indexed args).
  if (nonIndexedInputs.length > 0) {
    if (data && data !== '0x') {
      try {
        const decodedData = AbiParameters.decode(
          nonIndexedInputs,
          data,
          options,
        )
        if (decodedData) {
          if (isUnnamed) args = [...args, ...decodedData]
          else {
            for (let i = 0; i < nonIndexedInputs.length; i++) {
              const index = nonIndexedOriginalIndex[i]!
              args[nonIndexedInputs[i]!.name! || index] = decodedData[i]
            }
          }
        }
      } catch (err) {
        if (
          err instanceof AbiParameters.DataSizeTooSmallError ||
          err instanceof Cursor.PositionOutOfBoundsError
        )
          throw new DataMismatchError({
            abiEvent,
            data: data,
            parameters: nonIndexedInputs,
            size: Hex.size(data),
          })
        throw err
      }
    } else {
      throw new DataMismatchError({
        abiEvent,
        data: '0x',
        parameters: nonIndexedInputs,
        size: 0,
      })
    }
  }

  return Object.values(args).length > 0 ? args : undefined
}

export declare namespace decode {
  type Log = {
    data?: Hex.Hex | undefined
    topics: readonly Hex.Hex[]
  }

  type Options = {
    /**
     * Whether decoded addresses should be checksummed.
     *
     * @default true
     */
    checksumAddress?: boolean | undefined
  }

  type ReturnType<abiEvent extends AbiEvent = AbiEvent> =
    IsNarrowable<abiEvent, AbiEvent> extends true
      ? abiEvent['inputs'] extends readonly []
        ? undefined
        : internal.ParametersToPrimitiveTypes<
            abiEvent['inputs'],
            { EnableUnion: false; IndexedOnly: false; Required: true }
          >
      : unknown

  type ErrorType =
    | AbiParameters.decode.ErrorType
    | getSelector.ErrorType
    | DataMismatchError
    | SelectorTopicMismatchError
    | TopicsMismatchError
    | Errors.GlobalErrorType
}

/**
 * Extracts an {@link ox#AbiEvent.AbiEvent} from an {@link ox#Abi.Abi} and decodes its arguments from a Log.
 *
 * @example
 * ```ts twoslash
 * import { Abi, AbiEvent } from 'ox'
 *
 * const abi = Abi.from([
 *   'event Transfer(address indexed from, address indexed to, uint256 value)'
 * ])
 *
 * const decoded = AbiEvent.decodeLog(abi, {
 *   data: '0x0000000000000000000000000000000000000000000000000000000000000001',
 *   topics: [
 *     '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
 *     '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac',
 *     '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac'
 *   ]
 * })
 * // @log: {
 * // @log:   event: { name: 'Transfer', type: 'event', ... },
 * // @log:   args: {
 * // @log:     from: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC',
 * // @log:     to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC',
 * // @log:     value: 1n,
 * // @log:   },
 * // @log: }
 * ```
 *
 * @param abi - The ABI to extract an event from.
 * @param log - `topics` & `data` to decode.
 * @param options - Decoding options.
 * @returns The decoded event and arguments.
 */
export function decodeLog<const abi extends Abi.Abi | readonly unknown[]>(
  abi: abi | Abi.Abi | readonly unknown[],
  log: decodeLog.Log,
  options?: decodeLog.Options,
): decodeLog.ReturnType<decodeLog.ExtractEvent<abi>> {
  const selector = log.topics[0]
  if (!selector) throw new SelectorTopicNotFoundError()
  const abiEvent = fromAbi(abi, selector) as decodeLog.ExtractEvent<abi>
  return {
    event: abiEvent,
    args: decode(abiEvent, log, options),
  }
}

export declare namespace decodeLog {
  type ExtractEvent<abi extends Abi.Abi | readonly unknown[]> =
    AbiItem.fromAbi.ReturnType<abi, Name<abi>, undefined, AbiEvent>

  type Log = decode.Log

  type Options = decode.Options

  type ReturnType<abiEvent extends AbiEvent = AbiEvent> = {
    event: abiEvent
    args: decode.ReturnType<abiEvent>
  }

  type ErrorType =
    | decode.ErrorType
    | fromAbi.ErrorType
    | SelectorTopicNotFoundError
    | Errors.GlobalErrorType
}

/**
 * Extracts and decodes Logs that match an ABI Event in an ABI.
 *
 * @example
 * ```ts twoslash
 * import { Abi, AbiEvent } from 'ox'
 *
 * const abi = Abi.from([
 *   'event Transfer(address indexed from, address indexed to, uint256 value)'
 * ])
 *
 * const logs = AbiEvent.extractLogs(abi, [
 *   {
 *     data: '0x0000000000000000000000000000000000000000000000000000000000000001',
 *     topics: [
 *       '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
 *       '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac',
 *       '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac'
 *     ]
 *   }
 * ])
 * // @log: [{
 * // @log:   eventName: 'Transfer',
 * // @log:   args: {
 * // @log:     from: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC',
 * // @log:     to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC',
 * // @log:     value: 1n,
 * // @log:   },
 * // @log:   topics: [...],
 * // @log:   data: '0x...',
 * // @log: }]
 * ```
 *
 * @param abi - The ABI to extract events from.
 * @param logs - Logs to extract.
 * @param options - Extraction options.
 * @returns The extracted logs.
 */
export function extractLogs<
  const abi extends Abi.Abi | readonly unknown[],
  const logs extends readonly extractLogs.Log[],
  eventName extends
    | extractLogs.EventName<abi>
    | readonly extractLogs.EventName<abi>[]
    | undefined = undefined,
  strict extends boolean | undefined = true,
>(
  abi: abi | Abi.Abi | readonly unknown[],
  logs: logs | readonly extractLogs.Log[],
  options: extractLogs.Options<abi, eventName, strict> = {},
): extractLogs.ReturnType<
  extractLogs.ExtractEvent<abi, eventName>,
  logs[number],
  strict
>[] {
  const {
    args,
    checksumAddress,
    strict = true,
  } = options as extractLogs.Options
  const eventName = (() => {
    if (!options.eventName) return undefined
    if (Array.isArray(options.eventName)) return options.eventName
    return [options.eventName as string]
  })()
  const abiEvents = (abi as Abi.Abi).filter(
    (item): item is AbiEvent => item.type === 'event',
  )
  const out: extractLogs.ReturnType[] = []
  for (const log of logs) {
    const log_ = formatExtractLog(log)
    const selector = log_.topics[0]
    if (!selector) continue

    const items = abiEvents.filter((item) => getSelector(item) === selector)
    if (items.length === 0) continue

    let event:
      | {
          args: unknown
          event: AbiEvent
        }
      | undefined

    for (const item of items) {
      try {
        event = {
          args: decodeForExtract(item, log_, {
            checksumAddress,
            strict: true,
          }),
          event: item,
        }
        break
      } catch {}
    }

    if (!event && !strict) {
      const item = items[0]!
      try {
        event = {
          args: decodeForExtract(item, log_, {
            checksumAddress,
            strict: false,
          }),
          event: item,
        }
      } catch {
        const isUnnamed = item.inputs.some((x) => !('name' in x && x.name))
        event = {
          args: isUnnamed ? [] : {},
          event: item,
        }
      }
    }

    if (!event) continue
    if (eventName && !eventName.includes(event.event.name)) continue
    if (!includesArgs(event.event, event.args, args)) continue

    out.push({
      ...log_,
      args: event.args,
      eventName: event.event.name,
    })
  }
  return out as never
}

export declare namespace extractLogs {
  type EventName<abi extends Abi.Abi | readonly unknown[]> = Name<abi>

  type ExtractEvent<
    abi extends Abi.Abi | readonly unknown[],
    eventName extends EventName<abi> | readonly EventName<abi>[] | undefined =
      EventName<abi>,
  > = AbiItem.fromAbi.ReturnType<
    abi,
    eventName extends readonly (infer name)[]
      ? name & EventName<abi>
      : eventName extends EventName<abi>
        ? eventName
        : EventName<abi>,
    undefined,
    AbiEvent
  >

  type Args<abiEvent extends AbiEvent = AbiEvent> =
    IsNarrowable<abiEvent, AbiEvent> extends true
      ? abiEvent['inputs'] extends readonly []
        ? never
        : internal.ParametersToPrimitiveTypes<
            abiEvent['inputs'],
            { EnableUnion: true; IndexedOnly: false; Required: false }
          >
      : unknown

  type Log = decode.Log & {
    address?: Address.Address | undefined
    blockHash?: Hex.Hex | null | undefined
    blockNumber?: bigint | Hex.Hex | null | undefined
    blockTimestamp?: bigint | Hex.Hex | null | undefined
    logIndex?: number | Hex.Hex | null | undefined
    removed?: boolean | undefined
    transactionHash?: Hex.Hex | null | undefined
    transactionIndex?: number | Hex.Hex | null | undefined
  }

  type Options<
    abi extends Abi.Abi | readonly unknown[] = Abi.Abi,
    eventName extends EventName<abi> | readonly EventName<abi>[] | undefined =
      EventName<abi>,
    strict extends boolean | undefined = boolean | undefined,
    abiEvent extends AbiEvent = ExtractEvent<abi, eventName>,
  > = decode.Options & {
    /** Arguments to match against decoded event arguments. */
    args?: Args<abiEvent> | undefined
    /** Event name, or event names, to extract. */
    eventName?:
      | eventName
      | EventName<abi>
      | readonly EventName<abi>[]
      | undefined
    /**
     * Whether to strictly decode log topics and data.
     *
     * @default true
     */
    strict?: strict | boolean | undefined
  }

  type ReturnType<
    abiEvent extends AbiEvent = AbiEvent,
    log extends Log = Log,
    strict extends boolean | undefined = boolean | undefined,
  > = Compute<
    log & {
      args: IsNarrowable<abiEvent, AbiEvent> extends true
        ? abiEvent['inputs'] extends readonly []
          ? undefined
          : internal.ParametersToPrimitiveTypes<
              abiEvent['inputs'],
              {
                EnableUnion: false
                IndexedOnly: false
                Required: strict extends boolean ? strict : false
              }
            >
        : unknown
      eventName: abiEvent['name']
    }
  >

  type ErrorType =
    | AbiParameters.decode.ErrorType
    | decode.ErrorType
    | getSelector.ErrorType
    | Errors.GlobalErrorType
}

function decodeForExtract(
  abiEvent: AbiEvent,
  log: decode.Log,
  options: decode.Options & { strict: boolean },
) {
  const { data, topics } = log
  const { strict } = options

  const selector = topics[0]
  if (selector !== getSelector(abiEvent))
    throw new SelectorTopicMismatchError({
      abiEvent,
      actual: selector,
      expected: getSelector(abiEvent),
    })

  const { inputs } = abiEvent
  const isUnnamed = inputs.some((x) => !('name' in x && x.name))
  const args: any = isUnnamed ? [] : {}

  const argTopics = topics.slice(1)
  const indexedInputs: [abitype.AbiEventParameter, number][] = []
  const nonIndexedInputs: abitype.AbiEventParameter[] = []
  const missingIndexedInputs: [abitype.AbiEventParameter, number][] = []
  for (let i = 0; i < inputs.length; i++) {
    const input = inputs[i]!
    if ('indexed' in input && input.indexed) indexedInputs.push([input, i])
    else nonIndexedInputs.push(input)
  }

  for (let i = 0; i < indexedInputs.length; i++) {
    const [param, index] = indexedInputs[i]!
    const topic = argTopics[i]
    if (!topic) {
      if (strict)
        throw new TopicsMismatchError({
          abiEvent,
          param: param as abitype.AbiParameter & { indexed: boolean },
        })
      missingIndexedInputs.push([param, index])
      continue
    }
    args[isUnnamed ? index : param.name || index] = decodeTopic(param, topic, {
      checksumAddress: options.checksumAddress,
    })
  }

  const inputsToDecode = strict
    ? nonIndexedInputs
    : [...missingIndexedInputs.map(([param]) => param), ...nonIndexedInputs]

  if (inputsToDecode.length > 0) {
    if (data && data !== '0x') {
      try {
        const decodedData = AbiParameters.decode(inputsToDecode, data, {
          checksumAddress: options.checksumAddress,
        })
        if (decodedData) {
          let dataIndex = 0
          if (!strict) {
            for (const [param, index] of missingIndexedInputs) {
              args[isUnnamed ? index : param.name || index] =
                decodedData[dataIndex++]
            }
          }
          if (isUnnamed) {
            for (let i = 0; i < inputs.length; i++)
              if (args[i] === undefined && dataIndex < decodedData.length)
                args[i] = decodedData[dataIndex++]
          } else {
            for (let i = 0; i < nonIndexedInputs.length; i++) {
              const input = nonIndexedInputs[i]!
              args[input.name!] = decodedData[dataIndex++]
            }
          }
        }
      } catch (err) {
        if (strict) {
          if (
            err instanceof AbiParameters.DataSizeTooSmallError ||
            err instanceof Cursor.PositionOutOfBoundsError
          )
            throw new DataMismatchError({
              abiEvent,
              data: data,
              parameters: inputsToDecode,
              size: Hex.size(data),
            })
          throw err
        }
      }
    } else if (strict) {
      throw new DataMismatchError({
        abiEvent,
        data: '0x',
        parameters: inputsToDecode,
        size: 0,
      })
    }
  }

  return Object.values(args).length > 0 ? args : undefined
}

function decodeTopic(
  param: abitype.AbiEventParameter,
  topic: Hex.Hex,
  options: decode.Options,
) {
  const type = param.type
  if (
    type === 'string' ||
    type === 'bytes' ||
    type === 'tuple' ||
    arraySuffixRegex.test(type)
  )
    return topic
  const fast = decodePrimitiveTopic(type, topic, options)
  if (fast) return fast[0]
  const decoded = AbiParameters.decode([param], topic, options) || []
  return decoded[0]
}

function formatExtractLog<const log extends extractLogs.Log>(log: log) {
  if (typeof log.blockNumber !== 'string') return log
  return {
    ...log,
    blockHash: log.blockHash ? log.blockHash : null,
    blockNumber: log.blockNumber ? BigInt(log.blockNumber) : null,
    blockTimestamp: log.blockTimestamp
      ? BigInt(log.blockTimestamp)
      : log.blockTimestamp === null
        ? null
        : undefined,
    logIndex: log.logIndex ? Number(log.logIndex) : null,
    transactionHash: log.transactionHash ? log.transactionHash : null,
    transactionIndex: log.transactionIndex
      ? Number(log.transactionIndex)
      : null,
  }
}

function includesArgs(abiEvent: AbiEvent, args: unknown, matchArgs: unknown) {
  try {
    if (!matchArgs) return true
    assertArgs(abiEvent, args, matchArgs)
    return true
  } catch {
    return false
  }
}

/**
 * ABI-encodes the provided event input (`inputs`) into an array of [Event Topics](https://info.etherscan.com/what-is-event-logs/).
 *
 * :::tip
 *
 * This function is typically used to encode event arguments into [Event Topics](https://info.etherscan.com/what-is-event-logs/).
 *
 * See the [End-to-end Example](#end-to-end).
 *
 * :::
 *
 * @example
 * ```ts twoslash
 * import { AbiEvent } from 'ox'
 *
 * const transfer = AbiEvent.from(
 *   'event Transfer(address indexed from, address indexed to, uint256 value)'
 * )
 *
 * const { topics } = AbiEvent.encode(transfer)
 * // @log: ['0x406dade31f7ae4b5dbc276258c28dde5ae6d5c2773c5745802c493a2360e55e0']
 * ```
 *
 * @example
 * ### Passing Arguments
 *
 * You can pass `indexed` parameter values to `AbiEvent.encode`.
 *
 * TypeScript types will be inferred from the ABI Event, to guard you from inserting the wrong values.
 *
 * For example, the `Transfer` event below accepts an `address` type for the `from` and `to` attributes.
 *
 * ```ts twoslash
 * import { AbiEvent } from 'ox'
 *
 * const transfer = AbiEvent.from(
 *   'event Transfer(address indexed from, address indexed to, uint256 value)'
 * )
 *
 * const { topics } = AbiEvent.encode(transfer, {
 *   from: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', // [!code hl]
 *   to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8' // [!code hl]
 * })
 * // @log: [
 * // @log:   '0x406dade31f7ae4b5dbc276258c28dde5ae6d5c2773c5745802c493a2360e55e0',
 * // @log:   '0x00000000000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266',
 * // @log:   '0x0000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8'
 * // @log: ]
 * ```
 *
 * @example
 * ### ABI-shorthand
 *
 * You can also specify an entire ABI object and an event name as parameters to {@link ox#AbiEvent.(encode:function)}:
 *
 * ```ts twoslash
 * // @noErrors
 * import { Abi, AbiEvent } from 'ox'
 *
 * const abi = Abi.from([...])
 *
 * const { topics } = AbiEvent.encode(
 *   abi, // [!code focus]
 *   'Transfer', // [!code focus]
 *   {
 *     from: '0xf39fd6e51aad88f6f4ce6ab882779cfffb92266', // [!code focus]
 *     to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
 *   }
 * )
 * // @log: [
 * // @log:   '0x406dade31f7ae4b5dbc276258c28dde5ae6d5c2773c5745802c493a2360e55e0',
 * // @log:   '0x00000000000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266',
 * // @log:   '0x0000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8'
 * // @log: ]
 * ```
 *
 * @example
 * ### End-to-end
 *
 * Below is an end-to-end example of using `AbiEvent.encode` to encode the topics of a `Transfer` event and query for events matching the encoded topics on the [Wagmi Mint Example contract](https://etherscan.io/address/0xfba3912ca04dd458c843e2ee08967fc04f3579c2).
 *
 * ```ts twoslash
 * import 'ox/window'
 * import { AbiEvent, Hex } from 'ox'
 *
 * // 1. Instantiate the `Transfer` ABI Event.
 * const transfer = AbiEvent.from(
 *   'event Transfer(address indexed from, address indexed to, uint256 value)'
 * )
 *
 * // 2. Encode the ABI Event into Event Topics.
 * const { topics } = AbiEvent.encode(transfer)
 *
 * // 3. Query for events matching the encoded Topics.
 * const logs = await window.ethereum!.request({
 *   method: 'eth_getLogs',
 *   params: [
 *     {
 *       address: '0xfba3912ca04dd458c843e2ee08967fc04f3579c2',
 *       fromBlock: Hex.fromNumber(19760235n),
 *       toBlock: Hex.fromNumber(19760240n),
 *       topics
 *     }
 *   ]
 * })
 * // @log: [
 * // @log:   "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
 * // @log:   "0x0000000000000000000000000000000000000000000000000000000000000000",
 * // @log:   "0x0000000000000000000000000c04d9e9278ec5e4d424476d3ebec70cb5d648d1",
 * // @log:   "0x000000000000000000000000000000000000000000000000000000000000025b",
 * // @log: ]
 * ```
 *
 * :::note
 *
 * For simplicity, the above example uses `window.ethereum.request`, but you can use any
 * type of JSON-RPC interface.
 *
 * :::
 *
 * @param abiEvent - The event to encode.
 * @param args - The arguments to encode.
 * @returns The encoded event topics.
 */
export function encode<
  const abi extends Abi.Abi | readonly unknown[],
  name extends Name<abi>,
  const args extends AbiItem_internal.ExtractArgs<abi, name> | undefined =
    undefined,
  //
  abiEvent extends AbiEvent = AbiItem.fromAbi.ReturnType<
    abi,
    name,
    args,
    AbiEvent
  >,
  allNames = Name<abi>,
>(
  abi: abi | Abi.Abi | readonly unknown[],
  name: Hex.Hex | (name extends allNames ? name : never),
  ...[args]: encode.Args<abiEvent>
): encode.ReturnType
export function encode<const abiEvent extends AbiEvent>(
  abiEvent: abiEvent | AbiEvent,
  ...[args]: encode.Args<abiEvent>
): encode.ReturnType
// eslint-disable-next-line jsdoc-js/require-jsdoc
export function encode(
  ...parameters:
    | [
        abi: Abi.Abi | readonly unknown[],
        name: Hex.Hex | string,
        args?: readonly unknown[] | Record<string, unknown>,
      ]
    | [abiEvent: AbiEvent, args?: readonly unknown[] | Record<string, unknown>]
): encode.ReturnType {
  const [abiEvent, args] = (() => {
    if (Array.isArray(parameters[0])) {
      const [abi, name, args] = parameters as [
        Abi.Abi | readonly unknown[],
        Hex.Hex | string,
        readonly unknown[] | Record<string, unknown> | undefined,
      ]
      return [fromAbi(abi, name), args]
    }
    const [abiEvent, args] = parameters as [
      AbiEvent,
      readonly unknown[] | Record<string, unknown> | undefined,
    ]
    return [abiEvent, args]
  })()

  let topics: (Hex.Hex | Hex.Hex[] | null)[] = []
  if (args && abiEvent.inputs) {
    const indexedInputs = abiEvent.inputs.filter(
      (param) => 'indexed' in param && param.indexed,
    )
    const args_ = Array.isArray(args)
      ? args
      : Object.values(args).length > 0
        ? (indexedInputs?.map(
            (x: any, i: number) => (args as any)[x.name ?? i],
          ) ?? [])
        : []

    if (args_.length > 0) {
      const encode = (param: abitype.AbiParameter, value: unknown) => {
        const t = param.type
        if (t === 'string')
          return Hash.keccak256(Hex.fromString(value as string), { as: 'Hex' })
        if (t === 'bytes') {
          const hex =
            typeof value === 'string'
              ? (value as Hex.Hex)
              : Hex.fromBytes(value as Bytes.Bytes)
          return Hash.keccak256(hex, { as: 'Hex' })
        }
        if (t === 'tuple' || arraySuffixRegex.test(t))
          throw new FilterTypeNotSupportedError(t)
        // Fast path for 32-byte primitives.
        const fast = encodePrimitiveTopic(t, value)
        if (fast) return fast
        return AbiParameters.encode([param], [value])
      }

      topics =
        indexedInputs?.map((param, i) => {
          if (Array.isArray(args_[i]))
            return args_[i].map((_: any, j: number) =>
              encode(param, args_[i][j]),
            )
          return typeof args_[i] !== 'undefined' && args_[i] !== null
            ? encode(param, args_[i])
            : null
        }) ?? []
    }
  }

  if (abiEvent.anonymous === true) return { topics }

  const selector = (() => {
    if (abiEvent.hash) return abiEvent.hash
    return getSelector(abiEvent)
  })()

  return { topics: [selector, ...topics] }
}

export declare namespace encode {
  type Args<abiEvent extends AbiEvent> =
    IsNarrowable<abiEvent, AbiEvent> extends true
      ? abiEvent['inputs'] extends readonly []
        ? []
        : internal.ParametersToPrimitiveTypes<
              abiEvent['inputs']
            > extends infer result
          ? result extends readonly []
            ? []
            : [result] | []
          : []
      : [readonly unknown[] | Record<string, unknown>] | []

  type ReturnType = {
    topics:
      | Compute<[selector: Hex.Hex, ...(Hex.Hex | readonly Hex.Hex[] | null)[]]>
      | readonly (Hex.Hex | readonly Hex.Hex[] | null)[]
  }

  type ErrorType =
    | AbiParameters.encode.ErrorType
    | getSelector.ErrorType
    | Hex.fromString.ErrorType
    | Hash.keccak256.ErrorType
    | Errors.GlobalErrorType
}

/**
 * Formats an {@link ox#AbiEvent.AbiEvent} into a **Human Readable ABI Error**.
 *
 * @example
 * ```ts twoslash
 * import { AbiEvent } from 'ox'
 *
 * const formatted = AbiEvent.format({
 *   type: 'event',
 *   name: 'Transfer',
 *   inputs: [
 *     { name: 'from', type: 'address', indexed: true },
 *     { name: 'to', type: 'address', indexed: true },
 *     { name: 'value', type: 'uint256' }
 *   ]
 * })
 *
 * formatted
 * //    ^?
 * ```
 *
 * @param abiEvent - The ABI Event to format.
 * @returns The formatted ABI Event.
 */
export function format<const abiEvent extends AbiEvent>(
  abiEvent: abiEvent | AbiEvent,
): format.ReturnType<abiEvent> {
  return formatAbiItem.formatAbiItem(abiEvent) as never
}

export declare namespace format {
  type ReturnType<abiEvent extends AbiEvent = AbiEvent> =
    formatAbiItem.FormatAbiItem<abiEvent>

  type ErrorType = Errors.GlobalErrorType
}

/**
 * Parses an arbitrary **JSON ABI Event** or **Human Readable ABI Event** into a typed {@link ox#AbiEvent.AbiEvent}.
 *
 * @example
 * ### JSON ABIs
 *
 * ```ts twoslash
 * import { AbiEvent } from 'ox'
 *
 * const transfer = AbiEvent.from({
 *   name: 'Transfer',
 *   type: 'event',
 *   inputs: [
 *     { name: 'from', type: 'address', indexed: true },
 *     { name: 'to', type: 'address', indexed: true },
 *     { name: 'value', type: 'uint256' }
 *   ]
 * })
 *
 * transfer
 * //^?
 * ```
 *
 * @example
 * ### Human Readable ABIs
 *
 * A Human Readable ABI can be parsed into a typed ABI object:
 *
 * ```ts twoslash
 * import { AbiEvent } from 'ox'
 *
 * const transfer = AbiEvent.from(
 *   'event Transfer(address indexed from, address indexed to, uint256 value)' // [!code hl]
 * )
 *
 * transfer
 * //^?
 * ```
 *
 * @param abiEvent - The ABI Event to parse.
 * @returns Typed ABI Event.
 */
export function from<
  const abiEvent extends AbiEvent | string | readonly string[],
>(
  abiEvent: (abiEvent | AbiEvent | string | readonly string[]) &
    (
      | (abiEvent extends string ? internal.Signature<abiEvent> : never)
      | (abiEvent extends readonly string[]
          ? internal.Signatures<abiEvent>
          : never)
      | AbiEvent
    ),
  options: from.Options = {},
): from.ReturnType<abiEvent> {
  return AbiItem.from(abiEvent as AbiEvent, options) as never
}

export declare namespace from {
  type Options = {
    /**
     * Whether or not to prepare the extracted event (optimization for encoding performance).
     * When `true`, the `hash` property is computed and included in the returned value.
     *
     * @default true
     */
    prepare?: boolean | undefined
  }

  type ReturnType<abiEvent extends AbiEvent | string | readonly string[]> =
    AbiItem.from.ReturnType<abiEvent>

  type ErrorType = AbiItem.from.ErrorType | Errors.GlobalErrorType
}

/**
 * Extracts an {@link ox#AbiEvent.AbiEvent} from an {@link ox#Abi.Abi} given a name and optional arguments.
 *
 * @example
 * ### Extracting by Name
 *
 * ABI Events can be extracted by their name using the `name` option:
 *
 * ```ts twoslash
 * import { Abi, AbiEvent } from 'ox'
 *
 * const abi = Abi.from([
 *   'function foo()',
 *   'event Transfer(address owner, address to, uint256 tokenId)',
 *   'function bar(string a) returns (uint256 x)'
 * ])
 *
 * const item = AbiEvent.fromAbi(abi, 'Transfer') // [!code focus]
 * //    ^?
 * ```
 *
 * @example
 * ### Extracting by Selector
 *
 * ABI Events can be extract by their selector when {@link ox#Hex.Hex} is provided to `name`.
 *
 * ```ts twoslash
 * import { Abi, AbiEvent } from 'ox'
 *
 * const abi = Abi.from([
 *   'function foo()',
 *   'event Transfer(address owner, address to, uint256 tokenId)',
 *   'function bar(string a) returns (uint256 x)'
 * ])
 * const selector =
 *   '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
 * const item = AbiEvent.fromAbi(abi, selector) // [!code focus]
 * //    ^?
 * ```
 *
 * :::note
 *
 * Extracting via a hex selector is useful when extracting an ABI Event from the first topic of a Log.
 *
 * :::
 *
 * @param abi - The ABI to extract from.
 * @param name - The name (or selector) of the ABI item to extract.
 * @param options - Extraction options.
 * @returns The ABI item.
 */
export function fromAbi<
  const abi extends Abi.Abi | readonly unknown[],
  name extends Name<abi>,
  const args extends AbiItem_internal.ExtractArgs<abi, name> | undefined =
    undefined,
  //
  allNames = Name<abi>,
>(
  abi: abi | Abi.Abi | readonly unknown[],
  name: Hex.Hex | (name extends allNames ? name : never),
  options?: AbiItem.fromAbi.Options<
    abi,
    name,
    args,
    AbiItem_internal.ExtractArgs<abi, name>
  >,
): AbiItem.fromAbi.ReturnType<abi, name, args, AbiEvent> {
  const item = AbiItem.fromAbi(abi, name, options as any)
  if (item.type !== 'event')
    throw new AbiItem.NotFoundError({ name, type: 'event' })
  return item as never
}

export declare namespace fromAbi {
  type ErrorType = AbiItem.fromAbi.ErrorType | Errors.GlobalErrorType
}

/**
 * Computes the event selector (hash of event signature) for an {@link ox#AbiEvent.AbiEvent}.
 *
 * @example
 * ```ts twoslash
 * import { AbiEvent } from 'ox'
 *
 * const selector = AbiEvent.getSelector(
 *   'event Transfer(address indexed from, address indexed to, uint256 value)'
 * )
 * // @log: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f556a2'
 * ```
 *
 * @example
 * ```ts twoslash
 * import { AbiEvent } from 'ox'
 *
 * const selector = AbiEvent.getSelector({
 *   name: 'Transfer',
 *   type: 'event',
 *   inputs: [
 *     { name: 'from', type: 'address', indexed: true },
 *     { name: 'to', type: 'address', indexed: true },
 *     { name: 'value', type: 'uint256' }
 *   ]
 * })
 * // @log: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f556a2'
 * ```
 *
 * @param abiItem - The ABI event to compute the selector for.
 * @returns The {@link ox#Hash.(keccak256:function)} hash of the event signature.
 */
export function getSelector(abiItem: string | AbiEvent): Hex.Hex {
  return AbiItem.getSignatureHash(abiItem)
}

export declare namespace getSelector {
  type ErrorType = AbiItem.getSignatureHash.ErrorType | Errors.GlobalErrorType
}

/**
 * Thrown when the provided arguments do not match the expected arguments.
 *
 * @example
 * ```ts twoslash
 * import { AbiEvent } from 'ox'
 *
 * const abiEvent = AbiEvent.from(
 *   'event Transfer(address indexed from, address indexed to, uint256 value)'
 * )
 *
 * const args = AbiEvent.decode(abiEvent, {
 *   data: '0x0000000000000000000000000000000000000000000000000000000000000001',
 *   topics: [
 *     '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
 *     '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac',
 *     '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ad'
 *   ]
 * })
 *
 * AbiEvent.assertArgs(abiEvent, args, {
 *   from: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ad',
 *   to: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac',
 *   value: 1n
 * })
 * // @error: AbiEvent.ArgsMismatchError: Given arguments do not match the expected arguments.
 * // @error: Event: event Transfer(address indexed from, address indexed to, uint256 value)
 * // @error: Expected Arguments:
 * // @error:   from:   0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac
 * // @error:   to:     0xa5cc3c03994db5b0d9a5eedd10cabab0813678ad
 * // @error:   value:  1
 * // @error: Given Arguments:
 * // @error:   from:   0xa5cc3c03994db5b0d9a5eedd10cabab0813678ad
 * // @error:   to:     0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac
 * // @error:   value:  1
 * ```
 *
 * ### Solution
 *
 * The provided arguments need to match the expected arguments.
 *
 * ```ts twoslash
 * // @noErrors
 * import { AbiEvent } from 'ox'
 *
 * const abiEvent = AbiEvent.from(
 *   'event Transfer(address indexed from, address indexed to, uint256 value)'
 * )
 *
 * const args = AbiEvent.decode(abiEvent, {
 *   data: '0x0000000000000000000000000000000000000000000000000000000000000001',
 *   topics: [
 *     '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
 *     '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac',
 *     '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ad'
 *   ]
 * })
 *
 * AbiEvent.assertArgs(abiEvent, args, {
 *   from: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ad', // [!code --]
 *   from: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac', // [!code ++]
 *   to: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac', // [!code --]
 *   to: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ad', // [!code ++]
 *   value: 1n
 * })
 * ```
 */
export class ArgsMismatchError extends Errors.BaseError {
  override readonly name = 'AbiEvent.ArgsMismatchError'

  constructor({
    abiEvent,
    expected,
    given,
  }: {
    abiEvent: AbiEvent
    expected: unknown
    given: unknown
  }) {
    super('Given arguments do not match the expected arguments.', {
      metaMessages: [
        `Event: ${format(abiEvent)}`,
        `Expected Arguments: ${!expected ? 'None' : ''}`,
        expected ? prettyPrint(expected) : undefined,
        `Given Arguments: ${!given ? 'None' : ''}`,
        given ? prettyPrint(given) : undefined,
      ],
    })
  }
}

/**
 * Thrown when no argument was found on the event signature.
 *
 * @example
 * ```ts twoslash
 * // @noErrors
 * import { AbiEvent } from 'ox'
 *
 * const abiEvent = AbiEvent.from(
 *   'event Transfer(address indexed from, address indexed to, uint256 value)'
 * )
 *
 * const args = AbiEvent.decode(abiEvent, {
 *   data: '0x0000000000000000000000000000000000000000000000000000000000000001',
 *   topics: [
 *     '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
 *     '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac',
 *     '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ad'
 *   ]
 * })
 *
 * AbiEvent.assertArgs(abiEvent, args, {
 *   a: 'b',
 *   from: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac',
 *   to: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ad',
 *   value: 1n
 * })
 * // @error: AbiEvent.InputNotFoundError: Parameter "a" not found on `event Transfer(address indexed from, address indexed to, uint256 value)`.
 * ```
 *
 * ### Solution
 *
 * Ensure the arguments match the event signature.
 *
 * ```ts twoslash
 * // @noErrors
 * import { AbiEvent } from 'ox'
 *
 * const abiEvent = AbiEvent.from(
 *   'event Transfer(address indexed from, address indexed to, uint256 value)'
 * )
 *
 * const args = AbiEvent.decode(abiEvent, {
 *   data: '0x0000000000000000000000000000000000000000000000000000000000000001',
 *   topics: [
 *     '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
 *     '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac',
 *     '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ad'
 *   ]
 * })
 *
 * AbiEvent.assertArgs(abiEvent, args, {
 *   a: 'b', // [!code --]
 *   from: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac',
 *   to: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ad',
 *   value: 1n
 * })
 * ```
 */
export class InputNotFoundError extends Errors.BaseError {
  override readonly name = 'AbiEvent.InputNotFoundError'

  constructor({ abiEvent, name }: { abiEvent: AbiEvent; name: string }) {
    super(`Parameter "${name}" not found on \`${format(abiEvent)}\`.`)
  }
}

/**
 * Thrown when the provided data size does not match the expected size from the non-indexed parameters.
 *
 * @example
 * ```ts twoslash
 * import { AbiEvent } from 'ox'
 *
 * const abiEvent = AbiEvent.from(
 *   'event Transfer(address indexed from, address to, uint256 value)'
 *   //                                    ↑ 32 bytes + ↑ 32 bytes = 64 bytes
 * )
 *
 * const args = AbiEvent.decode(abiEvent, {
 *   data: '0x0000000000000000000000000000000000000000000000000000000023c34600',
 *   //       ↑ 32 bytes ❌
 *   topics: [
 *     '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
 *     '0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266'
 *   ]
 * })
 * // @error: AbiEvent.DataMismatchError: Data size of 32 bytes is too small for non-indexed event parameters.
 * // @error: Non-indexed Parameters: (address to, uint256 value)
 * // @error: Data:   0x0000000000000000000000000000000000000000000000000000000023c34600 (32 bytes)
 * ```
 *
 * ### Solution
 *
 * Ensure that the data size matches the expected size.
 *
 * ```ts twoslash
 * import { AbiEvent } from 'ox'
 *
 * const abiEvent = AbiEvent.from(
 *   'event Transfer(address indexed from, address to, uint256 value)'
 *   //                                    ↑ 32 bytes + ↑ 32 bytes = 64 bytes
 * )
 *
 * const args = AbiEvent.decode(abiEvent, {
 *   data: '0x0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000000000000000000000000000000000000023c34600',
 *   //       ↑ 64 bytes ✅
 *   topics: [
 *     '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
 *     '0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266'
 *   ]
 * })
 * ```
 */
export class DataMismatchError extends Errors.BaseError {
  override readonly name = 'AbiEvent.DataMismatchError'

  abiEvent: AbiEvent
  data: Hex.Hex
  parameters: readonly abitype.AbiParameter[]
  size: number

  constructor({
    abiEvent,
    data,
    parameters,
    size,
  }: {
    abiEvent: AbiEvent
    data: Hex.Hex
    parameters: readonly abitype.AbiParameter[]
    size: number
  }) {
    super(
      [
        `Data size of ${size} bytes is too small for non-indexed event parameters.`,
      ].join('\n'),
      {
        metaMessages: [
          `Non-indexed Parameters: (${AbiParameters.format(parameters as any)})`,
          `Data:   ${data} (${size} bytes)`,
        ],
      },
    )

    this.abiEvent = abiEvent
    this.data = data
    this.parameters = parameters
    this.size = size
  }
}

/**
 * Thrown when the provided topics do not match the expected number of topics.
 *
 * @example
 * ```ts twoslash
 * import { AbiEvent } from 'ox'
 *
 * const abiEvent = AbiEvent.from(
 *   'event Transfer(address indexed from, address indexed to, uint256 value)'
 * )
 *
 * const args = AbiEvent.decode(abiEvent, {
 *   data: '0x0000000000000000000000000000000000000000000000000000000000000001',
 *   topics: [
 *     '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
 *     '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac'
 *   ]
 * })
 * // @error: AbiEvent.TopicsMismatchError: Expected a topic for indexed event parameter "to" for "event Transfer(address indexed from, address indexed to, uint256 value)".
 * ```
 *
 * ### Solution
 *
 * Ensure that the topics match the expected number of topics.
 *
 * ```ts twoslash
 * import { AbiEvent } from 'ox'
 *
 * const abiEvent = AbiEvent.from(
 *   'event Transfer(address indexed from, address indexed to, uint256 value)'
 * )
 *
 * const args = AbiEvent.decode(abiEvent, {
 *   data: '0x0000000000000000000000000000000000000000000000000000000000000001',
 *   topics: [
 *     '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
 *     '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac',
 *     '0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266' // [!code ++]
 *   ]
 * })
 * ```
 *
 */
export class TopicsMismatchError extends Errors.BaseError {
  override readonly name = 'AbiEvent.TopicsMismatchError'

  abiEvent: AbiEvent

  constructor({
    abiEvent,
    param,
  }: {
    abiEvent: AbiEvent
    param: abitype.AbiParameter & { indexed: boolean }
  }) {
    super(
      [
        `Expected a topic for indexed event parameter${
          param.name ? ` "${param.name}"` : ''
        } for "${format(abiEvent)}".`,
      ].join('\n'),
    )

    this.abiEvent = abiEvent
  }
}

/**
 * Thrown when the provided selector does not match the expected selector.
 *
 * @example
 * ```ts twoslash
 * import { AbiEvent } from 'ox'
 *
 * const transfer = AbiEvent.from(
 *   'event Transfer(address indexed from, address indexed to, bool sender)'
 * )
 *
 * AbiEvent.decode(transfer, {
 *   topics: [
 *     '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
 *     '0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045',
 *     '0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266'
 *   ]
 * })
 * // @error: AbiEvent.SelectorTopicMismatchError: topics[0]="0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" does not match the expected topics[0]="0x3da3cd3cf420c78f8981e7afeefa0eab1f0de0eb56e78ad9ba918ed01c0b402f".
 * // @error: Event: event Transfer(address indexed from, address indexed to, bool sender)
 * // @error: Selector: 0x3da3cd3cf420c78f8981e7afeefa0eab1f0de0eb56e78ad9ba918ed01c0b402f
 * ```
 *
 * ### Solution
 *
 * Ensure that the provided selector matches the selector of the event signature.
 *
 * ```ts twoslash
 * import { AbiEvent } from 'ox'
 *
 * const transfer = AbiEvent.from(
 *   'event Transfer(address indexed from, address indexed to, bool sender)'
 * )
 *
 * AbiEvent.decode(transfer, {
 *   topics: [
 *     '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', // [!code --]
 *     '0x3da3cd3cf420c78f8981e7afeefa0eab1f0de0eb56e78ad9ba918ed01c0b402f', // [!code ++]
 *     '0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045',
 *     '0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266'
 *   ]
 * })
 * ```
 */
export class SelectorTopicMismatchError extends Errors.BaseError {
  override readonly name = 'AbiEvent.SelectorTopicMismatchError'

  constructor({
    abiEvent,
    actual,
    expected,
  }: {
    abiEvent: AbiEvent
    actual: Hex.Hex | undefined
    expected: Hex.Hex
  }) {
    super(
      `topics[0]="${actual}" does not match the expected topics[0]="${expected}".`,
      {
        metaMessages: [`Event: ${format(abiEvent)}`, `Selector: ${expected}`],
      },
    )
  }
}

/**
 * Thrown when the selector topic is not found.
 *
 * @example
 * ```ts twoslash
 * import { Abi, AbiEvent } from 'ox'
 *
 * const abi = Abi.from([
 *   'event Transfer(address indexed from)'
 * ])
 *
 * AbiEvent.decodeLog(abi, { topics: [], data: '0x' })
 * // @error: AbiEvent.SelectorTopicNotFoundError: Selector topic not found.
 * ```
 */
export class SelectorTopicNotFoundError extends Errors.BaseError {
  override readonly name = 'AbiEvent.SelectorTopicNotFoundError'

  constructor() {
    super('Selector topic not found.')
  }
}

/**
 * Thrown when the provided filter type is not supported.
 *
 * @example
 * ```ts twoslash
 * import { AbiEvent } from 'ox'
 *
 * const transfer = AbiEvent.from(
 *   'event Transfer((string) indexed a, string b)'
 * )
 *
 * AbiEvent.encode(transfer, {
 *   a: ['hello']
 * })
 * // @error: AbiEvent.FilterTypeNotSupportedError: Filter type "tuple" is not supported.
 * ```
 *
 * ### Solution
 *
 * Provide a valid event input type.
 *
 * ```ts twoslash
 * // @noErrors
 * import { AbiEvent } from 'ox'
 *
 * const transfer = AbiEvent.from(
 *   'event Transfer((string) indexed a, string b)'
 * ) // [!code --]
 * const transfer = AbiEvent.from(
 *   'event Transfer(string indexed a, string b)'
 * ) // [!code ++]
 * ```
 *
 *
 */
export class FilterTypeNotSupportedError extends Errors.BaseError {
  override readonly name = 'AbiEvent.FilterTypeNotSupportedError'
  constructor(type: string) {
    super(`Filter type "${type}" is not supported.`)
  }
}
