import * as AbiFunction from '../core/AbiFunction.js'
import * as Hex from '../core/Hex.js'
import { AbiParameters } from '../index.js'
import * as Calls from './Calls.js'

export type Batch = {
  calls: readonly Call[]
  opData?: Hex.Hex | undefined
}

export type Call = Calls.Call

/** Cached ABI parameters for the encoded `bytes[]` payload of batch-of-batches. */
const batchesParameters = /*#__PURE__*/ AbiParameters.from('bytes[]')

export const abiFunction = {
  type: 'function',
  name: 'execute',
  inputs: [
    {
      name: 'mode',
      type: 'bytes32',
      internalType: 'bytes32',
    },
    {
      name: 'executionData',
      type: 'bytes',
      internalType: 'bytes',
    },
  ],
  outputs: [],
  stateMutability: 'payable',
} satisfies AbiFunction.AbiFunction

export const mode = {
  default: '0x0100000000000000000000000000000000000000000000000000000000000000',
  opData: '0x0100000000007821000100000000000000000000000000000000000000000000',
  batchOfBatches:
    '0x0100000000007821000200000000000000000000000000000000000000000000',
} as const

/**
 * Decodes calls from ERC-7821 `execute` function data.
 *
 * @example
 * ```ts twoslash
 * import { Execute } from 'ox/erc7821'
 *
 * const { calls } = Execute.decodeData('0x...')
 * ```
 *
 * @param data - The encoded data.
 * @returns The decoded calls and optional opData.
 */
export function decodeData(data: Hex.Hex): decodeData.ReturnType {
  const [m, executionData] = AbiFunction.decodeData(abiFunction, data) as [
    Hex.Hex,
    Hex.Hex,
  ]
  return Calls.decode(executionData, { opData: m !== mode.default })
}

export declare namespace decodeData {
  type ReturnType = {
    calls: Call[]
    opData?: Hex.Hex | undefined
  }
}

/**
 * Decodes batches from ERC-7821 `execute` function data in "batch of batches" mode.
 *
 * @example
 * ```ts twoslash
 * import { Execute } from 'ox/erc7821'
 *
 * const batches = Execute.decodeBatchOfBatchesData('0x...')
 * ```
 *
 * @param data - The encoded data.
 * @returns The decoded batches.
 */
export function decodeBatchOfBatchesData(
  data: Hex.Hex,
): decodeBatchOfBatchesData.ReturnType {
  const [, executionData] = AbiFunction.decodeData(abiFunction, data) as [
    Hex.Hex,
    Hex.Hex,
  ]

  const [encodedBatches] = AbiParameters.decode(
    batchesParameters,
    executionData,
  ) as readonly [Hex.Hex[]]

  return encodedBatches.map((encodedBatch) => {
    // Structurally detect whether the batch was encoded with opData by
    // inspecting the first ABI head word (the offset to the first dynamic
    // param). For a single dynamic param tuple `(Call[] calls)` the head
    // is one word, so the offset is 0x20. For a two dynamic param tuple
    // `(Call[] calls, bytes opData)` the head is two words, so the offset
    // is 0x40.
    const firstHead = Hex.toBigInt(Hex.slice(encodedBatch, 0, 32))
    const withOpData = firstHead === 64n

    const decoded = Calls.decode(encodedBatch, { opData: withOpData })
    if (withOpData && decoded.opData)
      return { calls: decoded.calls, opData: decoded.opData }
    return { calls: decoded.calls }
  })
}

export declare namespace decodeBatchOfBatchesData {
  type ReturnType = Batch[]
}

/**
 * Encodes calls for the ERC-7821 `execute` function with "batch of batches" mode.
 *
 * @example
 * ```ts twoslash
 * import { Execute } from 'ox/erc7821'
 *
 * const data = Execute.encodeBatchOfBatchesData([
 *   {
 *     calls: [
 *       {
 *         data: '0xcafebabe',
 *         to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
 *         value: 1n
 *       }
 *     ]
 *   },
 *   {
 *     calls: [
 *       {
 *         data: '0xdeadbeef',
 *         to: '0xcafebabecafebabecafebabecafebabecafebabe',
 *         value: 2n
 *       }
 *     ],
 *     opData: '0xcafebabe'
 *   }
 * ])
 * ```
 *
 * @param calls - The calls to encode.
 * @param options - The options.
 * @returns The encoded data.
 */
export function encodeBatchOfBatchesData(batches: readonly Batch[]) {
  const b = AbiParameters.encode(batchesParameters, [
    batches.map((b) => {
      const batch = b as Batch
      return Calls.encode(batch.calls, {
        opData: batch.opData,
      })
    }),
  ])
  return AbiFunction.encodeData(abiFunction, [mode.batchOfBatches, b])
}

/**
 * Encodes calls for the ERC-7821 `execute` function.
 *
 * @example
 * ```ts twoslash
 * import { Execute } from 'ox/erc7821'
 *
 * const data = Execute.encodeData([
 *   {
 *     data: '0xcafebabe',
 *     to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
 *     value: 1n
 *   }
 * ])
 * ```
 *
 * @param calls - The calls to encode.
 * @param options - The options.
 * @returns The encoded data.
 */
export function encodeData(
  calls: readonly Call[],
  options: encodeData.Options = {},
) {
  const { opData } = options
  const c = Calls.encode(calls, { opData })
  const m = opData ? mode.opData : mode.default
  return AbiFunction.encodeData(abiFunction, [m, c])
}

export declare namespace encodeData {
  type Options = {
    opData?: Hex.Hex | undefined
  }
}
