import {
  AsText,
  Concat,
  ErrorResult,
  InterpolatedPiece,
  ProcResult,
  Script,
  TextPiece,
  Word,
} from "./words";

/**
 * "Mode" of the environment a script runs in; determines access to mutability features and such.
 *
 * "action": response to a UI action; allowed to modify card fields and access time and random numbers.
 *
 * "render": deterministic generation of display markup from card and workspace state; can only modify temporary variables.
 */
export type ScriptType = "action" | "render";

export type Proc<Context> = (
  state: Vm<Context>,
  argv: TextPiece[]
) => ProcResult;

/**
 * State for running a script in.
 */
export type Vm<Context = {}> = {
  /** Mutability status */
  mode: ScriptType;
  /** Implementations of commands scripts can run */
  commands: Record<string, Proc<Context>>;
  /** Markup to render / output */
  output: string;
} & Context;

function evaluateWord<Context>(
  state: Vm<Context>,
  word: Word | InterpolatedPiece
): TextPiece | ErrorResult {
  if ("bare" in word || "text" in word || "html" in word) {
    return word;
  } else if ("variable" in word) {
    return { text: "" };
  } else if ("script" in word) {
    return runNoctl(state, word.script);
  } else {
    let fullWord = null;
    for (const piece of word.pieces) {
      const result = evaluateWord(state, piece);
      if ("error" in result) {
        return result;
      } else {
        fullWord = Concat(fullWord, result);
      }
    }
    return fullWord ?? { text: "" };
  }
}

/**
 * Runs a script in the context of a Noctl state. Potentially mutates the state.
 *
 * @param onReturn callback optionally invoked with the return word for each top-level command (not triggered by command substitutions)
 * @returns the return word of the final command in the script, or empty text if the script is empty.
 */
export function runNoctl<Context>(
  state: Vm<Context>,
  script: Script,
  onReturn?: (word: TextPiece | ErrorResult) => void
): TextPiece | ErrorResult {
  let returnWord: TextPiece | ErrorResult = { text: "" };

  for (const command of script) {
    const argv: TextPiece[] = [];
    for (const word of command) {
      const processedWord = evaluateWord(state, word);
      if ("error" in processedWord) {
        onReturn?.(processedWord);
        return processedWord;
      } else {
        argv.push(processedWord);
      }
    }

    const name = AsText(argv[0]);
    if (name in state.commands) {
      returnWord = state.commands[name](state, argv);
    } else {
      returnWord = { error: `Unknown Command: ${name}` };
    }
    onReturn?.(returnWord);
  }

  return returnWord;
}