import type {
  AbiParameter,
  AbiParameterKind,
  AbiParametersToPrimitiveTypes,
  AbiParameterToPrimitiveType,
} from 'abitype'

import {
  AbiEncodingArrayLengthMismatchError,
  type AbiEncodingArrayLengthMismatchErrorType,
  AbiEncodingBytesSizeMismatchError,
  type AbiEncodingBytesSizeMismatchErrorType,
  AbiEncodingLengthMismatchError,
  type AbiEncodingLengthMismatchErrorType,
  InvalidAbiEncodingTypeError,
  type InvalidAbiEncodingTypeErrorType,
  InvalidArrayError,
  type InvalidArrayErrorType,
} from '../../errors/abi.js'
import {
  InvalidAddressError,
  type InvalidAddressErrorType,
} from '../../errors/address.js'
import { BaseError } from '../../errors/base.js'
import { IntegerOutOfRangeError } from '../../errors/encoding.js'
import type { ErrorType } from '../../errors/utils.js'
import type { Hex } from '../../types/misc.js'
import { type IsAddressErrorType, isAddress } from '../address/isAddress.js'
import { type ConcatErrorType, concat } from '../data/concat.js'
import { type PadHexErrorType, padHex } from '../data/pad.js'
import { type SizeErrorType, size } from '../data/size.js'
import { type SliceErrorType, slice } from '../data/slice.js'
import {
  type BoolToHexErrorType,
  boolToHex,
  type NumberToHexErrorType,
  numberToHex,
  type StringToHexErrorType,
  stringToHex,
} from '../encoding/toHex.js'
import { integerRegex } from '../regex.js'

export type EncodeAbiParametersReturnType = Hex

export type EncodeAbiParametersErrorType =
  | AbiEncodingLengthMismatchErrorType
  | PrepareParamsErrorType
  | EncodeParamsErrorType
  | ErrorType

/**
 * @description Encodes a list of primitive values into an ABI-encoded hex value.
 *
 * - Docs: https://viem.sh/docs/abi/encodeAbiParameters#encodeabiparameters
 *
 *   Generates ABI encoded data using the [ABI specification](https://docs.soliditylang.org/en/latest/abi-spec), given a set of ABI parameters (inputs/outputs) and their corresponding values.
 *
 * @param params - a set of ABI Parameters (params), that can be in the shape of the inputs or outputs attribute of an ABI Item.
 * @param values - a set of values (values) that correspond to the given params.
 * @example
 * ```typescript
 * import { encodeAbiParameters } from 'viem'
 *
 * const encodedData = encodeAbiParameters(
 *   [
 *     { name: 'x', type: 'string' },
 *     { name: 'y', type: 'uint' },
 *     { name: 'z', type: 'bool' }
 *   ],
 *   ['wagmi', 420n, true]
 * )
 * ```
 *
 * You can also pass in Human Readable parameters with the parseAbiParameters utility.
 *
 * @example
 * ```typescript
 * import { encodeAbiParameters, parseAbiParameters } from 'viem'
 *
 * const encodedData = encodeAbiParameters(
 *   parseAbiParameters('string x, uint y, bool z'),
 *   ['wagmi', 420n, true]
 * )
 * ```
 */
export function encodeAbiParameters<
  const params extends readonly AbiParameter[] | readonly unknown[],
>(
  params: params,
  values: params extends readonly AbiParameter[]
    ? AbiParametersToPrimitiveTypes<params, AbiParameterKind, true>
    : never,
): EncodeAbiParametersReturnType {
  if (params.length !== values.length)
    throw new AbiEncodingLengthMismatchError({
      expectedLength: params.length as number,
      givenLength: values.length as any,
    })
  // Prepare the parameters to determine dynamic types to encode.
  const preparedParams = prepareParams({
    params: params as readonly AbiParameter[],
    values: values as any,
  })
  const data = encodeParams(preparedParams)
  if (data.length === 0) return '0x'
  return data
}

/////////////////////////////////////////////////////////////////

type PreparedParam = { dynamic: boolean; encoded: Hex }

type TupleAbiParameter = AbiParameter & { components: readonly AbiParameter[] }
type Tuple = AbiParameterToPrimitiveType<TupleAbiParameter>

type PrepareParamsErrorType = PrepareParamErrorType | ErrorType

function prepareParams<const params extends readonly AbiParameter[]>({
  params,
  values,
}: {
  params: params
  values: AbiParametersToPrimitiveTypes<params>
}) {
  const preparedParams: PreparedParam[] = []
  for (let i = 0; i < params.length; i++) {
    preparedParams.push(prepareParam({ param: params[i], value: values[i] }))
  }
  return preparedParams
}

type PrepareParamErrorType =
  | EncodeAddressErrorType
  | EncodeArrayErrorType
  | EncodeBytesErrorType
  | EncodeBoolErrorType
  | EncodeNumberErrorType
  | EncodeStringErrorType
  | EncodeTupleErrorType
  | GetArrayComponentsErrorType
  | InvalidAbiEncodingTypeErrorType
  | ErrorType

function prepareParam<const param extends AbiParameter>({
  param,
  value,
}: {
  param: param
  value: AbiParameterToPrimitiveType<param>
}): PreparedParam {
  const arrayComponents = getArrayComponents(param.type)
  if (arrayComponents) {
    const [length, type] = arrayComponents
    return encodeArray(value, { length, param: { ...param, type } })
  }
  if (param.type === 'tuple') {
    return encodeTuple(value as unknown as Tuple, {
      param: param as TupleAbiParameter,
    })
  }
  if (param.type === 'address') {
    return encodeAddress(value as unknown as Hex)
  }
  if (param.type === 'bool') {
    return encodeBool(value as unknown as boolean)
  }
  if (param.type.startsWith('uint') || param.type.startsWith('int')) {
    const signed = param.type.startsWith('int')
    const [, , size = '256'] = integerRegex.exec(param.type) ?? []
    return encodeNumber(value as unknown as number, {
      signed,
      size: Number(size),
    })
  }
  if (param.type.startsWith('bytes')) {
    return encodeBytes(value as unknown as Hex, { param })
  }
  if (param.type === 'string') {
    return encodeString(value as unknown as string)
  }
  throw new InvalidAbiEncodingTypeError(param.type, {
    docsPath: '/docs/contract/encodeAbiParameters',
  })
}

/////////////////////////////////////////////////////////////////

type EncodeParamsErrorType = NumberToHexErrorType | SizeErrorType | ErrorType

function encodeParams(preparedParams: PreparedParam[]): Hex {
  // 1. Compute the size of the static part of the parameters.
  let staticSize = 0
  for (let i = 0; i < preparedParams.length; i++) {
    const { dynamic, encoded } = preparedParams[i]
    if (dynamic) staticSize += 32
    else staticSize += size(encoded)
  }

  // 2. Split the parameters into static and dynamic parts.
  const staticParams: Hex[] = []
  const dynamicParams: Hex[] = []
  let dynamicSize = 0
  for (let i = 0; i < preparedParams.length; i++) {
    const { dynamic, encoded } = preparedParams[i]
    if (dynamic) {
      staticParams.push(numberToHex(staticSize + dynamicSize, { size: 32 }))
      dynamicParams.push(encoded)
      dynamicSize += size(encoded)
    } else {
      staticParams.push(encoded)
    }
  }

  // 3. Concatenate static and dynamic parts.
  return concat([...staticParams, ...dynamicParams])
}

/////////////////////////////////////////////////////////////////

type EncodeAddressErrorType =
  | InvalidAddressErrorType
  | IsAddressErrorType
  | ErrorType

function encodeAddress(value: Hex): PreparedParam {
  if (!isAddress(value)) throw new InvalidAddressError({ address: value })
  return { dynamic: false, encoded: padHex(value.toLowerCase() as Hex) }
}

type EncodeArrayErrorType =
  | AbiEncodingArrayLengthMismatchErrorType
  | ConcatErrorType
  | EncodeParamsErrorType
  | InvalidArrayErrorType
  | NumberToHexErrorType
  // TODO: Add back once circular type reference is resolved
  // | PrepareParamErrorType
  | ErrorType

function encodeArray<const param extends AbiParameter>(
  value: AbiParameterToPrimitiveType<param>,
  {
    length,
    param,
  }: {
    length: number | null
    param: param
  },
): PreparedParam {
  const dynamic = length === null

  if (!Array.isArray(value)) throw new InvalidArrayError(value)
  if (!dynamic && value.length !== length)
    throw new AbiEncodingArrayLengthMismatchError({
      expectedLength: length!,
      givenLength: value.length,
      type: `${param.type}[${length}]`,
    })

  let dynamicChild = false
  const preparedParams: PreparedParam[] = []
  for (let i = 0; i < value.length; i++) {
    const preparedParam = prepareParam({ param, value: value[i] })
    if (preparedParam.dynamic) dynamicChild = true
    preparedParams.push(preparedParam)
  }

  if (dynamic || dynamicChild) {
    const data = encodeParams(preparedParams)
    if (dynamic) {
      const length = numberToHex(preparedParams.length, { size: 32 })
      return {
        dynamic: true,
        encoded: preparedParams.length > 0 ? concat([length, data]) : length,
      }
    }
    if (dynamicChild) return { dynamic: true, encoded: data }
  }
  return {
    dynamic: false,
    encoded: concat(preparedParams.map(({ encoded }) => encoded)),
  }
}

type EncodeBytesErrorType =
  | AbiEncodingBytesSizeMismatchErrorType
  | ConcatErrorType
  | PadHexErrorType
  | NumberToHexErrorType
  | SizeErrorType
  | ErrorType

function encodeBytes<const param extends AbiParameter>(
  value: Hex,
  { param }: { param: param },
): PreparedParam {
  const [, paramSize] = param.type.split('bytes')
  const bytesSize = size(value)
  if (!paramSize) {
    let value_ = value
    // If the size is not divisible by 32 bytes, pad the end
    // with empty bytes to the ceiling 32 bytes.
    if (bytesSize % 32 !== 0)
      value_ = padHex(value_, {
        dir: 'right',
        size: Math.ceil((value.length - 2) / 2 / 32) * 32,
      })
    return {
      dynamic: true,
      encoded: concat([padHex(numberToHex(bytesSize, { size: 32 })), value_]),
    }
  }
  if (bytesSize !== Number.parseInt(paramSize, 10))
    throw new AbiEncodingBytesSizeMismatchError({
      expectedSize: Number.parseInt(paramSize, 10),
      value,
    })
  return { dynamic: false, encoded: padHex(value, { dir: 'right' }) }
}

type EncodeBoolErrorType = PadHexErrorType | BoolToHexErrorType | ErrorType

function encodeBool(value: boolean): PreparedParam {
  if (typeof value !== 'boolean')
    throw new BaseError(
      `Invalid boolean value: "${value}" (type: ${typeof value}). Expected: \`true\` or \`false\`.`,
    )
  return { dynamic: false, encoded: padHex(boolToHex(value)) }
}

type EncodeNumberErrorType = NumberToHexErrorType | ErrorType

function encodeNumber(
  value: number,
  { signed, size = 256 }: { signed: boolean; size?: number | undefined },
): PreparedParam {
  if (typeof size === 'number') {
    const max = 2n ** (BigInt(size) - (signed ? 1n : 0n)) - 1n
    const min = signed ? -max - 1n : 0n
    if (value > max || value < min)
      throw new IntegerOutOfRangeError({
        max: max.toString(),
        min: min.toString(),
        signed,
        size: size / 8,
        value: value.toString(),
      })
  }
  return {
    dynamic: false,
    encoded: numberToHex(value, {
      size: 32,
      signed,
    }),
  }
}

type EncodeStringErrorType =
  | ConcatErrorType
  | NumberToHexErrorType
  | PadHexErrorType
  | SizeErrorType
  | SliceErrorType
  | StringToHexErrorType
  | ErrorType

function encodeString(value: string): PreparedParam {
  const hexValue = stringToHex(value)
  const partsLength = Math.ceil(size(hexValue) / 32)
  const parts: Hex[] = []
  for (let i = 0; i < partsLength; i++) {
    parts.push(
      padHex(slice(hexValue, i * 32, (i + 1) * 32), {
        dir: 'right',
      }),
    )
  }
  return {
    dynamic: true,
    encoded: concat([
      padHex(numberToHex(size(hexValue), { size: 32 })),
      ...parts,
    ]),
  }
}

type EncodeTupleErrorType =
  | ConcatErrorType
  | EncodeParamsErrorType
  // TODO: Add back once circular type reference is resolved
  // | PrepareParamErrorType
  | ErrorType

function encodeTuple<
  const param extends AbiParameter & { components: readonly AbiParameter[] },
>(
  value: AbiParameterToPrimitiveType<param>,
  { param }: { param: param },
): PreparedParam {
  let dynamic = false
  const preparedParams: PreparedParam[] = []
  for (let i = 0; i < param.components.length; i++) {
    const param_ = param.components[i]
    const index = Array.isArray(value) ? i : param_.name
    const preparedParam = prepareParam({
      param: param_,
      value: (value as any)[index!] as readonly unknown[],
    })
    preparedParams.push(preparedParam)
    if (preparedParam.dynamic) dynamic = true
  }
  return {
    dynamic,
    encoded: dynamic
      ? encodeParams(preparedParams)
      : concat(preparedParams.map(({ encoded }) => encoded)),
  }
}

type GetArrayComponentsErrorType = ErrorType

export function getArrayComponents(
  type: string,
): [length: number | null, innerType: string] | undefined {
  const matches = type.match(/^(.*)\[(\d+)?\]$/)
  return matches
    ? // Return `null` if the array is dynamic.
      [matches[2] ? Number(matches[2]) : null, matches[1]]
    : undefined
}
