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 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
}

/**
 * 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)) === arg
    if (input.type === 'bytes') return Hash.keccak256(value as Hex.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.
 * @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,
): decode.ReturnType<abiEvent>
export function decode<const abiEvent extends AbiEvent>(
  abiEvent: abiEvent | AbiEvent,
  log: decode.Log,
): decode.ReturnType<abiEvent>
// eslint-disable-next-line jsdoc/require-jsdoc
export function decode(
  ...parameters:
    | [
        abi: Abi.Abi | readonly unknown[],
        name: Hex.Hex | string,
        log: decode.Log,
      ]
    | [abiEvent: AbiEvent, log: decode.Log]
): decode.ReturnType {
  const [abiEvent, log] = (() => {
    if (Array.isArray(parameters[0])) {
      const [abi, name, log] = parameters as [
        Abi.Abi | readonly unknown[],
        Hex.Hex | string,
        decode.Log,
      ]
      return [fromAbi(abi, name), log]
    }
    return parameters as [AbiEvent, decode.Log]
  })()

  const { data, topics } = log

  const [selector_, ...argTopics] = topics

  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 ? [] : {}

  // Decode topics (indexed args).
  const indexedInputs = inputs.filter((x) => 'indexed' in x && x.indexed)
  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] = (() => {
      if (
        param.type === 'string' ||
        param.type === 'bytes' ||
        param.type === 'tuple' ||
        param.type.match(/^(.*)\[(\d+)?\]$/)
      )
        return topic
      const decoded = AbiParameters.decode([param], topic) || []
      return decoded[0]
    })()
  }

  // Decode data (non-indexed args).
  const nonIndexedInputs = inputs.filter((x) => !('indexed' in x && x.indexed))
  if (nonIndexedInputs.length > 0) {
    if (data && data !== '0x') {
      try {
        const decodedData = AbiParameters.decode(nonIndexedInputs, data)
        if (decodedData) {
          if (isUnnamed) args = [...args, ...decodedData]
          else {
            for (let i = 0; i < nonIndexedInputs.length; i++) {
              const index = inputs.indexOf(nonIndexedInputs[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 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
}

/**
 * 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/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) => {
        if (param.type === 'string')
          return Hash.keccak256(Hex.fromString(value as string))
        if (param.type === 'bytes') return Hash.keccak256(value as Hex.Hex)
        if (param.type === 'tuple' || param.type.match(/^(.*)\[(\d+)?\]$/))
          throw new FilterTypeNotSupportedError(param.type)
        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
        }) ?? []
    }
  }

  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)[]]
    >
  }

  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,
): abitype.FormatAbiItem<abiEvent> {
  return abitype.formatAbiItem(abiEvent) as never
}

export declare namespace format {
  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 item = AbiEvent.fromAbi(abi, '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef') // [!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 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.`)
  }
}
