import { CONFLICT_STRATEGIES } from '@/env'
import { isAsyncFunction } from '@/utils'
import { Execute } from '@commands/implementations/Execute'
import { toMCFunctionName } from '@datapack/minecraft'
import { ConditionClass, coordinatesParser } from '@variables'
import { Score } from '@variables/Score'

import { CombinedConditions, getConditionScore } from './conditions'

import type { LiteralUnion } from '@/generalTypes'
import type {
  BLOCKS, Coordinates, SingleEntityArgument,
} from '@arguments'
import type { CommandsRoot } from '@commands'
import type { Datapack } from '@datapack'
import type { CommandArgs } from '@datapack/minecraft'
import type { FunctionResource, ResourceConflictStrategy } from '@datapack/resourcesTree'
import type { ConditionType } from './conditions'

const ASYNC_CALLBACK_NAME = '__await_flow'

function valueToCondition(value: unknown[]): ConditionClass {
  const condition = new ConditionClass()
  condition._toMinecraftCondition = () => ({ value })
  return condition
}

/** Call a given callback function, and inline it if possible */
function callOrInlineFunction(datapack: Datapack, callbackFunction: FunctionResource, forceInlineScore?: Score) {
  const { commandsRoot } = datapack

  if (
    callbackFunction?.isResource && (
      (
        // Either our command is 1-line-long, then it can be inlined (if it's not an execute!)
        callbackFunction.commands.length <= 1
        && callbackFunction.commands?.[0]?.[0] !== 'execute'
      ) || (
        /*
         * Either it has 2 commands, but the 2nde one is used in an if/else context,
         * and can be dropped in favor of an `execute store`
         */
        callbackFunction.commands.length === 2
        && forceInlineScore
      )
    )
  ) {
    /*
     * If our callback only has 1 command, inline this command. We CANNOT inline executes, for complicated reasons.
     * If you want to understand the reasons, see @vdvman1#9510 explanation =>
     * https://discordapp.com/channels/154777837382008833/154777837382008833/754985742706409492
     */

    const { commands } = callbackFunction

    // If our resource has children, just set it as a folder. Else, entirely, destroy it.
    if (callbackFunction.children.size > 0) {
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-expect-error
      callbackFunction.isResource = false
      callbackFunction.commands = []
    } else {
      datapack.resources.deleteResource(callbackFunction.path, 'functions')
    }

    if (commands.length) {
      if (commands.length === 2 && forceInlineScore) {
        // If we have 2 commands, add the execute store
        if (commandsRoot.arguments.length === 0) {
          commandsRoot.arguments.push('execute')
        }

        commandsRoot.arguments.push('store', 'success', 'score', forceInlineScore)
      }

      if (commandsRoot.arguments.length > 0) {
        commandsRoot.arguments.push('run')
      }

      commandsRoot.addAndRegister(...commands[0])
    } else {
      commandsRoot.arguments = []
    }
  } else {
    // Else, register the function call
    commandsRoot.functionCmd(toMCFunctionName(callbackFunction.path))
  }
}

type FlowStatementConfig = {
  callbackName: string
  absoluteName?: string
  forceInlineScore?: Score
  mightHaveRemainingArguments?: boolean
} & (
    {
      initialCondition: false,
      loopCondition: false,
      condition?: undefined
    } | {
      initialCondition: boolean,
      loopCondition: boolean,
      condition: ConditionType
    }
  )

export class Flow {
  private commandsRoot

  private datapack

  arguments: CommandArgs

  executeState: CommandsRoot['executeState']

  constructor(
    datapack: Datapack,
  ) {
    this.datapack = datapack
    this.commandsRoot = datapack.commandsRoot
    this.arguments = []
    this.executeState = 'outside'
  }

  /** CONDITIONS */

  /**
   * Compares the block at a given position to a given block. Suceeds if both are identical.
   *
   * @param pos Position of a target block to test.
   *
   * @param block A block to test against.
   */
  block = (coords: Coordinates, block: LiteralUnion<BLOCKS>): ConditionClass => (
    valueToCondition(['if', 'block', coordinatesParser(coords), block])
  )

  /**
   * Compares the blocks in two equally sized volumes. Suceeds if both are identical.
   *
   * @param start Positions of the first diagonal corner of the source volume (the comparand; the volume to compare).
   *
   * @param end Positions of the second diagonal corner of the source volume (the comparand; the volume to compare)
   *
   * @param destination
   * Position of the lower northwest (the smallest X, Y and Z value) corner of the destination volume
   * (the comparator; the volume to compare to). Assumed to be of the same size as the source volume.
   *
   * @param scanMode Specifies whether all blocks in the source volume should be compared, or if air blocks should be masked/ignored.
   */
  blocks = (start: Coordinates, end: Coordinates, destination: Coordinates, scanMode: 'all' | 'masked'): ConditionClass => (
    valueToCondition(['if', 'blocks', coordinatesParser(start), coordinatesParser(end), coordinatesParser(destination), scanMode])
  )

  /**
   * Checks if the given target has any data for a given tag.
   *
   * @example
   * // Check whether the current block has an Inventory
   * _.if(_.data.block(rel(0, 0, 0), 'Inventory'), () => {
   *   say('The current block has data in its Inventory tag.')
   * })
   *
   * // Check whether the player has at least one slot with dirt
   * _.if(_.data.entity(`@r`, 'Inventory[{id: "minecraft:dirt"}]'), () => {
   *   say('The random player has dirt.')
   * })
   *
   * // Check whether there is data in the "Test" tag of the storage
   * _.if(_.data.storage('namespace:mystorage', 'Test'), () => {
   *   say('There is data in the "Test" tag of mystorage.')
   * })
   */
  data = {
    /**
     * Checks whether the targeted block has any data for a given tag.
     * @param pos Position of the block to be tested.
     * @param path Data tag to check for.
     */
    block: (pos: Coordinates, path: string) => valueToCondition(['if', 'data', 'block', coordinatesParser(pos), path]),

    /**
     * Checks whether the targeted entity has any data for a given tag
     * @param target One single entity to be tested.
     * @param path Data tag to check for.
     */
    entity: (target: SingleEntityArgument, path: string) => valueToCondition(['if', 'data', 'entity', target, path]),

    /**
     * Checks whether the targeted storage has any data for a given tag
     * @param source The storage to check in.
     * @param path Data tag to check for.
     */
    storage: (source: string, path: string) => valueToCondition(['if', 'data', 'storage', source, path]),
  }

  /** Logical operators */

  /**
   * Check if multiple conditions are true at the same time.
   * @param conditions The conditions to check.
   */
  and = (...conditions: (ConditionType)[]) => new CombinedConditions(this.commandsRoot, conditions, 'and')

  /**
   * Check if at least one of the given conditions is true.
   * @param conditions The conditions to check.
   */
  or = (...conditions: (ConditionType)[]) => new CombinedConditions(this.commandsRoot, conditions, 'or')

  /**
   * Check if the given condition is not true.
   * @param condition The condition to check.
   */
  not = (condition: ConditionType) => new CombinedConditions(this.commandsRoot, [condition], 'not')

  /** Flow statements */
  flowStatementAsync = async (callback: () => Promise<void>, config: FlowStatementConfig) => {
    /*
     * Sometimes, there are a few arguments left inside the commandsRoot (for execute.run mostly).
     * Keep them aside, & register them after.
     */
    const previousArguments = config.mightHaveRemainingArguments ? this.commandsRoot.arguments : undefined
    const previousExecuteState = config.mightHaveRemainingArguments ? this.commandsRoot.executeState : undefined

    if (config.mightHaveRemainingArguments) {
      this.commandsRoot.reset()
    } else {
      this.commandsRoot.register(true)
    }

    const args = this.arguments.slice(1)
    this.arguments = []

    const { currentFunction } = this.datapack

    const { fullName: asyncCallbackName } = this.datapack.getUniqueChildName(ASYNC_CALLBACK_NAME)

    // First, enter the callback
    let callbackFunctionName: string

    if (config.absoluteName) {
      callbackFunctionName = this.datapack.createEnterRootFunction(config.absoluteName, 'throw')
    } else {
      callbackFunctionName = this.datapack.createEnterChildFunction(config.callbackName)
    }

    const callbackMCFunction = this.datapack.currentFunction!

    await callback()

    this.commandsRoot.register(true)

    // Add its commands
    if (config.initialCondition && !config.loopCondition) {
      // If we're in a if/else if/else, call the next lines at the end of each branch
      this.commandsRoot.functionCmd(asyncCallbackName)
    }

    // At the end of the callback, add the given conditions to call it again
    if (config.loopCondition) {
      /*
       * In an asynchronous flow statement, we need to recursively call the function if the condition is met, else we need to enter a new child function.
       * We create a new Flow object to prevent interfering with the current flow object, which might cause problems.
       */
      const flow = new Flow(this.datapack)
      flow.arguments = ['execute', ...args]
      flow
        .if(config.condition, () => {
          this.commandsRoot.functionCmd(callbackFunctionName)
        })
        .else(() => {
          this.commandsRoot.functionCmd(asyncCallbackName)
        })
    }

    // Exit the callback
    this.datapack.currentFunction = currentFunction

    // Put back the old arguments
    if (previousArguments) {
      this.commandsRoot.arguments = previousArguments
    }
    if (previousExecuteState) {
      this.commandsRoot.executeState = previousExecuteState
    }

    // Register the initial condition (in the root function) to enter the callback.
    if (config.initialCondition) {
      // In while statements, if the condition isn't met the 1st time, directly call the next lines of code
      if (config.loopCondition) {
        const flow = new Flow(this.datapack)
        flow.arguments = ['execute', ...args]
        flow
          .if(config.condition, () => { callOrInlineFunction(this.datapack, callbackMCFunction) })
          .else(() => { this.commandsRoot.functionCmd(asyncCallbackName) })
      } else {
        /*
         * In if/else/else if, the respective functions have to ensure the next lines of code will be called no matter what.
         * Therefore, this function just has to register the initial condition.
         */
        registerCondition(this.commandsRoot, config.condition, args)
        this.commandsRoot.executeState = 'after'
        callOrInlineFunction(this.datapack, callbackMCFunction)
      }
    } else {
      callOrInlineFunction(this.datapack, callbackMCFunction)
    }

    // Reset the _.execute.as().at()... arguments.
    this.arguments = []
  }

  flowStatement = (callback: () => void, config: FlowStatementConfig) => {
    /*
     * Sometimes, there are a few arguments left inside the commandsRoot (for execute.run mostly).
     * Keep them aside, & register them after.
     */
    const previousArguments = config.mightHaveRemainingArguments ? this.commandsRoot.arguments : undefined
    const previousExecuteState = config.mightHaveRemainingArguments ? this.commandsRoot.executeState : undefined

    if (config.mightHaveRemainingArguments) {
      this.commandsRoot.reset()
    } else {
      this.commandsRoot.register(true)
    }

    const args = this.arguments.slice(1)
    this.arguments = []

    const { currentFunction } = this.datapack

    // First, enter the callback
    let callbackFunctionName: string

    if (config.absoluteName) {
      let conflictStrat: Exclude<ResourceConflictStrategy<'functions'>, 'append' | 'prepend'>
      if (CONFLICT_STRATEGIES.MCFUNCTION === 'append' || CONFLICT_STRATEGIES.MCFUNCTION === 'prepend') {
        conflictStrat = 'throw'
      } else {
        conflictStrat = CONFLICT_STRATEGIES.MCFUNCTION
      }

      callbackFunctionName = this.datapack.createEnterRootFunction(config.absoluteName, conflictStrat)
    } else {
      callbackFunctionName = this.datapack.createEnterChildFunction(config.callbackName)
    }

    const callbackMCFunction = this.datapack.currentFunction!

    // Add its commands
    callback()

    this.commandsRoot.register(true)

    const isEmpty = callbackMCFunction.isResource && callbackMCFunction.commands.length === 0

    // At the end of the callback, add the given conditions to call it again
    if (!isEmpty && config.loopCondition) {
      // In a synchronous flow statement, we just have to recursively call the function
      registerCondition(this.commandsRoot, config.condition, args)
      this.commandsRoot.executeState = 'after'
      this.commandsRoot.functionCmd(callbackFunctionName)
    }

    // Exit the callback
    this.datapack.currentFunction = currentFunction

    // Put back the old arguments
    if (previousArguments) {
      this.commandsRoot.arguments = previousArguments
    }
    if (previousExecuteState) {
      this.commandsRoot.executeState = previousExecuteState
    }
    // Register the initial condition (in the root function) to enter the callback
    if (!isEmpty && config.initialCondition) {
      registerCondition(this.commandsRoot, config.condition, args)
      this.commandsRoot.executeState = 'after'
    }

    if (!isEmpty) {
      callOrInlineFunction(this.datapack, callbackMCFunction, config.forceInlineScore)
    } else {
      this.datapack.resources.deleteResource(callbackMCFunction.path, 'functions')
    }

    this.arguments = []
  }

  private if_ = <R extends void | Promise<void>>(
    condition: ConditionType,
    callback: () => R,
    callbackName: string,
    ifScore: Score,
    forceInlineScore = false,
  ): (R extends void ? ElifElseFlow<R> : ElifElseFlow<R> & PromiseLike<void>) => {
    function ensureConsistency(nextCallback: () => void) {
      if (!isAsyncFunction(callback) && isAsyncFunction(nextCallback)) {
        throw new Error('Passed an asynchronous callback in a synchronous if/else if/else. If/else if/else must be all synchronous, or all asynchronous.')
      }
      if (isAsyncFunction(callback) && !isAsyncFunction(nextCallback)) {
        throw new Error('Passed a synchronous callback in an asynchronous if/else if/else. If/else if/else must be all synchronous, or all asynchronous.')
      }
    }

    if (!isAsyncFunction(callback)) {
      // Register the current if
      this.flowStatement(callback, {
        callbackName,
        initialCondition: true,
        loopCondition: false,
        condition,
        forceInlineScore: forceInlineScore ? ifScore : undefined,
      })

      // We know the callback is synchronous. We must prevent the user to pass an asynchronous callback in else/else if.
      return {
        elseIf: (nextCondition: ConditionType, nextCallback: () => void) => {
          // Ensure the callback is synchronous.
          ensureConsistency(nextCallback)

          return this.if_(this.and(this.not(ifScore.matches([0, null])), nextCondition), () => {
            nextCallback()
            ifScore.set(1)
          }, 'else_if', ifScore, true)
        },
        else: (nextCallback: () => void) => {
          // Ensure the callback is synchronous.
          ensureConsistency(nextCallback)

          this.if_(this.not(ifScore.matches([0, null])), nextCallback, 'else', ifScore, false)
        },
      } as ElifElseFlow<void> as any
    }

    const getPreviousPromise = () => this.flowStatementAsync(callback, {
      callbackName,
      initialCondition: true,
      loopCondition: false,
      condition,
    })

    const { currentFunction: parentFunction } = this.datapack

    return {
      elseIf: (nextCondition: ConditionType, nextCallback: () => Promise<void>) => {
        // Ensure the callback is asynchronous.
        ensureConsistency(nextCallback)

        return this.if_(this.and(nextCondition, this.not(ifScore.matches([0, null]))), async () => {
          // We keep the function where the "else if" is running
          const { currentFunction: newCallback } = this.datapack

          // Go back in the parent function
          this.datapack.currentFunction = parentFunction
          // Run the previous "if/else if" code
          await getPreviousPromise()

          // Now, we're going back in the current "else if"
          this.datapack.currentFunction = newCallback

          // First, we run all synchronous code (that will end up in the .mcfunction instantly called by the "else if")
          const returnedPromise = nextCallback()

          // We notice Sandstone that the condition has successfully passed
          ifScore.set(1)

          // Then we run the asynchronous code, that will create other .mcfunction called with /schedule.
          await returnedPromise
        }, 'else_if', ifScore)
      },
      else: (nextCallback: () => Promise<void>) => {
        // Ensure the callback is asynchronous.
        ensureConsistency(nextCallback)

        /*
         * We return the "if" result, which theoritically could allow our users to
         * write `.if().else().if()`, however this is forbidden thanks to our TypeScript types.
         * We have to return the result for the `then` part.
         */
        return this.if_(this.not(ifScore.matches([0, null])), async () => {
          // We keep the function where the "else" is running
          const { currentFunction: newCallback } = this.datapack

          // Go back in the parent function
          this.datapack.currentFunction = parentFunction
          // Run the previous "if"/"else if" code
          await getPreviousPromise()

          // Now, we're going back in the current "else"
          this.datapack.currentFunction = newCallback

          // And we run the "else" code.
          await nextCallback()
        }, 'else', ifScore)
      },
      then: async (onfulfilled: () => void) => {
        // In theory, we are already in the parent function so we shouldn't need to go back in it.

        // Run the previous "if/else if/else" code
        await getPreviousPromise()

        // Go back in the parent function, because we don't know where the last "if/else if/else" code ended up.
        this.datapack.currentFunction = parentFunction

        // Finally enter the callback function
        this.datapack.createEnterChildFunction(ASYNC_CALLBACK_NAME)
        return onfulfilled?.()
      },
    } as any
  }

  if = <R extends void | Promise<void>>(condition: ConditionType, callback: () => R): (R extends void ? ElifElseFlow<R> : ElifElseFlow<R> & PromiseLike<void>) => {
    const ifScore = getConditionScore(this.commandsRoot.Datapack)

    if (!isAsyncFunction(callback)) {
      // /!\ Complicated stuff happening here.
      let callbackFunction: FunctionResource
      const { elseIf: realElseIf, else: realElse } = this.if_(condition, () => { callbackFunction = this.datapack.currentFunction!; callback() }, 'if', ifScore, false)
      const { currentFunction } = this.datapack

      // for Typescript
      if (!currentFunction?.isResource) { throw new Error('Impossible') }

      const ifCommandIndex = currentFunction.commands.length - 1

      const switchToComplicatedIf = () => {
        const command = currentFunction.commands[ifCommandIndex]

        try {
          // If this doesn't raise an error, it means the function didn't get inlined
          this.datapack.resources.getResource(callbackFunction.path, 'functions')

          // The function wasn't inlined - add the '/scoreboard players set' at the end of the function
          if (!callbackFunction?.isResource) { throw new Error('Impossible') }
          callbackFunction.commands.push(['scoreboard', 'players', 'set', ifScore, 1])
        } catch (e) {
          // The function was inlined - add the 'store success' part to the execute
          currentFunction.commands[ifCommandIndex] = ['execute', 'store', 'success', 'score', ifScore, ...command.slice(1)]
        }

        // Add the reset
        currentFunction.commands = [
          ...currentFunction.commands.slice(0, ifCommandIndex),
          ['scoreboard', 'players', 'reset', ifScore],
          ...currentFunction.commands.slice(ifCommandIndex),
        ]
      }

      return {
        elseIf: (...args: Parameters<typeof realElseIf>) => {
          switchToComplicatedIf()
          return realElseIf(...args)
        },
        else: (cb: Parameters<typeof realElse>['0']) => {
          switchToComplicatedIf()
          return realElse(cb)
        },
      } as ElifElseFlow<void> as any
    }

    // First, specify the `if` didn't pass yet (it's in order to chain elif/else)
    ifScore.reset()

    // Async function
    return this.if_(condition, async () => {
      const returnedPromise = callback()
      ifScore.set(1)
      await returnedPromise
    }, 'if', ifScore) as any
  }

  binaryMatch = (score: Score, minimum: number, maximum: number, callback: (num: number) => void) => {
    // First, specify we didn't find a match yet
    const foundMatch = this.datapack.Variable(0)

    const callCallback = (num: number) => {
      this.if(this.and(score.equalTo(num), foundMatch.equalTo(0)), () => {
        // If we found the correct score, call the callback & specify we found a match
        callback(num)
        foundMatch.set(1)
      })
    }

    // Recursively match the score
    const recursiveMatch = (min: number, max: number) => {
      const diff = max - min

      if (diff < 0) {
        return
      }

      if (diff === 3) {
        callCallback(min)
        callCallback(min + 1)
        callCallback(min + 2)
        return
      }
      if (diff === 2) {
        callCallback(min)
        callCallback(min + 1)
        return
      }
      if (diff === 1) {
        callCallback(min)
        return
      }

      const mean = Math.floor((min + max) / 2)

      this.if(score.lowerThan(mean), () => recursiveMatch(min, mean))
      this.if(score.greaterOrEqualThan(mean), () => recursiveMatch(mean, max))
    }

    recursiveMatch(minimum, maximum)
  }

  private _while = <R extends void | Promise<void>>(condition: ConditionClass | CombinedConditions, callback: () => R, type: 'while' | 'do_while'): R => {
    if (!isAsyncFunction(callback)) {
      this.flowStatement(callback, {
        callbackName: type,
        initialCondition: type === 'while',
        loopCondition: true,
        condition,
      })

      return undefined as any
    }

    const { currentFunction: parentFunction } = this.datapack

    return {
      then: async (onfulfilled: () => void) => {
        // In theory, we are already in the parent function so we shouldn't need to go back in it.

        // Run the previous code
        await this.flowStatementAsync(callback, {
          callbackName: type,
          initialCondition: type === 'while',
          loopCondition: true,
          condition,
        })

        // Go back in the parent function, because we don't know where the last code ended up.
        this.datapack.currentFunction = parentFunction

        // Finally enter the callback function
        this.datapack.createEnterChildFunction(ASYNC_CALLBACK_NAME)
        return onfulfilled?.()
      },
    } as PromiseLike<void> as any
  }

  while = <R extends void | Promise<void>>(condition: ConditionClass | CombinedConditions, callback: () => R): R => this._while(condition, callback, 'while')

  doWhile = <R extends void | Promise<void>>(condition: ConditionClass | CombinedConditions, callback: () => R): R => this._while(condition, callback, 'do_while')

  binaryFor = (from: Score | number, to: Score | number, callback: (amount: number) => void, maximum = 128) => {
    if (typeof from === 'number' && typeof to === 'number') {
      callback(to - from)
    }

    const realStart = from instanceof Score ? from : this.datapack.Variable(from)
    const realEnd = to instanceof Score ? to : this.datapack.Variable(to)

    const iterations = realEnd.minus(realStart)

    const _ = this

    /*
     * For all iterations above the maximum,
     * just do a while loop that calls `maximum` times the callback,
     * until there is less than `maximum` iterations
     */
    _.while(iterations.lowerThan(maximum), () => {
      callback(maximum)
      iterations.remove(maximum)
    })

    /*
     * There is now less iterations than the allowed MAXIMUM
     * Start the binary part
     */
    for (let i = 1; i < maximum; i *= 2) {
      _.if(iterations.moduloBy(2).equalTo(1), () => {
        callback(i)
      })

      iterations.dividedBy(2)
    }
  }

  forRange = <R extends void | Promise<void>>(from: Score | number, to: Score | number, callback: (score: Score) => R) => {
    const scoreTracker = from instanceof Score ? from : this.datapack.Variable(from)

    // Small optimization: if we know the loop will run at least once, use a do while
    let loop = this.while
    if (typeof from === 'number' && typeof to === 'number' && to > from) {
      loop = this.doWhile
    }

    if (!isAsyncFunction(callback)) {
      return loop(scoreTracker.lowerThan(to), () => {
        callback(scoreTracker)
        scoreTracker.add(1)
      })
    }

    return loop(scoreTracker.lowerThan(to), async () => {
      await callback(scoreTracker)
      scoreTracker.add(1)
    })
  }

  forScore = <R extends void | Promise<void>>(
    score: Score | number,
    // eslint-disable-next-line no-shadow
    condition: ((score: Score) => ConditionType) | ConditionType,
    // eslint-disable-next-line no-shadow
    modifier: (score: Score) => void,
    // eslint-disable-next-line no-shadow
    callback: (score: Score) => R,
  ): R => {
    const realScore = score instanceof Score ? score : this.datapack.Variable(score)
    const realCondition = typeof condition === 'function' ? condition(realScore) : condition

    if (!isAsyncFunction(callback)) {
      return this.while(realCondition, () => {
        callback(realScore)
        modifier(realScore)
      }) as any
    }

    return this.while(realCondition, async () => {
      await callback(realScore)
      modifier(realScore)
    }) as any
  }

  private register = (soft?: boolean) => { }

  get execute(): Omit<Execute<Flow>, 'run' | 'runOne'> {
    return new Execute(this)
  }
}

function registerCondition(commandsRoot: CommandsRoot, condition: ConditionType, args: unknown[] = []) {
  let commands: CommandArgs[]

  if (condition instanceof CombinedConditions) {
    const realCondition = condition.removeOr().simplify()

    if (realCondition instanceof CombinedConditions) {
      const { callableExpression, requiredExpressions } = realCondition.toExecutes()
      commands = [...requiredExpressions, ['execute', ...callableExpression.slice(1), ...args]]
    } else {
      commands = [['execute', ...realCondition._toMinecraftCondition().value, ...args]]
    }
  } else {
    commands = [['execute', ...condition._toMinecraftCondition().value, ...args]]
  }

  // Add & register all required commands
  for (const command of commands.slice(0, -1)) {
    commandsRoot.addAndRegister(...command)
  }

  // Add the callable command, WITHOUT REGISTERING IT. It must be appended with the command to run.
  const callableCommand = commands[commands.length - 1]
  commandsRoot.arguments.push(...callableCommand)
}

export type PublicFlow = Omit<Flow, 'arguments' | 'flowStatement' | 'elseIf' | 'else'>

type ElifElseFlow<R extends void | Promise<void>> = {
  elseIf: (condition: ConditionType, callback: () => R) => (R extends void ? ElifElseFlow<R> : ElifElseFlow<R> & PromiseLike<void>)
  else: (callback: () => R) => void
}