import { HZEngineCore } from "../index.js";

import { HzsInfo, Storage } from "../storage/index.js";
import {
  mergeObjs2Str as joinObjs2Str,
  parseInterpolatedStr,
  removeComment,
  splitStr2Objs,
} from "./strtools.js";
import { readline } from "./readscript.js";
import { Save } from "../storage/decorator.js";
export class Script {
  constructor(public _core: HZEngineCore) {}
  /**
   * 调用栈
   * 在call时保存当前执行位置和语句栈，在return时恢复执行位置和语句栈
   */
  @Save("script.callStack")
  private accessor _callStack: {
    position: [path: string, index: number] | null;
    statementStack: Script.StatementStack;
  }[] = [];

  /**
   * 语句栈
   * 比如while, if，会在语句开始时入栈，语句结束时出栈
   */
  @Save("script.statementStack")
  private accessor _statementStack: Script.StatementStack = [];

  /**
   * 下一次执行的脚本位置
   * 注意：存储该值的时候应总是拷贝赋值而非直接引用赋值
   */
  @Save("script.nextRunPosition")
  private accessor _nextRunPosition: [path: string, index: number] | null =
    null;

  // Script Run

  /**
   * 执行_nextRunPosition，并返回下一行_nextRunPosition是否不为null
   */
  runSingleLine(): boolean {
    if (!this._nextRunPosition) {
      // 文件尾隐式执行 return
      this.return();
      return false;
    }
    let nowRunPosition: [path: string, index: number] = [
      ...this._nextRunPosition,
    ];
    this.incrementNextPosition();

    let rawCommand = readline(
      this._core,
      nowRunPosition[0],
      nowRunPosition[1]
    )!;

    // remove comment

    rawCommand = removeComment(rawCommand);

    if (rawCommand.trim().length && !rawCommand.trim().startsWith("#")) {
      if (rawCommand.trim().startsWith("*")) {
        if (this._statementStack.length) {
          throw `label between statement is not allowed, at file [${nowRunPosition[0]}] line [${nowRunPosition[1]}]`;
        }
      } else {
        this._core.debug.log("Run cmd: " + rawCommand);

        // Process Command
        this._processCmd(rawCommand, [...nowRunPosition]);
      }
    }

    return !!this._nextRunPosition;
  }

  // Label Point Control

  /**
   * 跳转到目标标签
   * @param targetLabel
   */
  jumpLabel(targetLabel: string) {
    let labelPosition = this._locateLabel(targetLabel);
    this._nextRunPosition = labelPosition;
    this._statementStack = [];
  }

  jump(path: string, index: number, clearStatementStack: boolean = false) {
    this._nextRunPosition = [path, index];

    if (clearStatementStack) {
      this._statementStack = [];
    }
  }

  /**
   * 调用目标标签
   * 保存当前执行位置至调用栈，跳转到目标位置，直到return返回
   * @param targetLabel
   */
  callLabel(targetLabel: string) {
    // console.log(
    //   `pending to call label ${targetLabel}, stack=${JSON.stringify(
    //     this._routeStack
    //   )}`
    // );
    let labelPosition = this._locateLabel(targetLabel);
    this._callStack.push({
      position: this._nextRunPosition ? [...this._nextRunPosition] : null,
      statementStack: this._statementStack,
    });
    this._nextRunPosition = labelPosition;
    this._statementStack = [];

    // console.log(
    //   `finished call label ${targetLabel}, stack=${JSON.stringify(
    //     this._routeStack
    //   )}`
    // );
  }

  hasLabel(targetLabel: string) {
    return (
      this._core.storage.preloadedData?.script?.labelMap?.[targetLabel] != null
    );
  }

  return() {
    // console.log(`pending to return, stack=${JSON.stringify(this._routeStack)}`);
    let stackItem = this._callStack.pop();
    if (stackItem) {
      this._statementStack = stackItem.statementStack;
      this._nextRunPosition = stackItem.position;
    } else {
      this._nextRunPosition = null;
      this._statementStack = [];

      // Game End
      this._core.end();
    }

    // console.log(`finished return, stack=${JSON.stringify(this._routeStack)}`);
  }

  clear() {
    this._nextRunPosition = null;
    this._callStack = [];
  }

  private _locateLabel(labelName: string) {
    if (this._core.storage.preloadedData == null)
      throw "Preloaded Data is Null";
    if (!this._core.storage.preloadedData.script.labelMap[labelName])
      throw `Error: Label [${labelName}] not found`;
    let labelData: [name: string, index: number] = [
      ...this._core.storage.preloadedData.script.labelMap[labelName],
    ] as any;
    return labelData;
  }

  public incrementNextPosition() {
    if (this._core.storage.preloadedData == null)
      throw "Preloaded Data is Null";
    if (!this._nextRunPosition) throw "_nextRunPosition is null";
    // console.log(`hzsInfoMap=${JSON.stringify(this._core.storage.preloadedData.script.hzsInfoMap)}`);

    let hzsInfo: HzsInfo | undefined =
      this._core.storage.preloadedData.script.hzsInfoMap[
        this._nextRunPosition[0]
      ];
    if (!hzsInfo)
      throw `Preloaded hzsInfo of path(${this._nextRunPosition[0]}) not found`;
    this._nextRunPosition[1]++;
    if (this._nextRunPosition[1] >= hzsInfo.totalLines)
      this._nextRunPosition = null;
  }

  // Middleware
  private _middlewares: Script.Middleware[] = [];
  use(middleware: Script.Middleware, add_front: boolean = false) {
    if (add_front) {
      this._middlewares.unshift(middleware);
    } else {
      this._middlewares.push(middleware);
    }
  }

  private _processCmd(
    cmd: string,
    nowRunPosition: [path: string, index: number]
  ) {
    let ctx = this._buildContext(cmd, nowRunPosition);
    if (this._middlewares.length === 0) {
      this._processUnsolvedCmd(cmd);
      return;
    }
    let i = 0,
      len = this._middlewares.length;
    let nextFunc = () => {
      i++;
      if (i >= len) {
        this._processUnsolvedCmd(cmd);
        return;
      }
      this._middlewares[i](ctx, nextFunc);
    };
    this._middlewares[0](ctx, nextFunc);
  }

  private _processUnsolvedCmd(cmd: string) {
    if (cmd.trim().length === 0)
      return; // Empty Line
    else if (cmd.trim().startsWith("*"))
      return; // Label Command
    else {
      throw `Can not parse command: ${cmd}`;
    }
  }

  private _buildContext(
    cmd: string,
    nowRunPosition: [path: string, index: number]
  ): Script.Context {
    return new Script.Context(
      this._core,
      cmd,
      nowRunPosition[0],
      nowRunPosition[1],
      this._statementStack
    );
  }

  // Statement Analyse
  private _statementAnalyseStack: Script.StatementStack = [];
  private _analyseStatementMiddlewares: Script.Middleware[] = [];
  private _buildAnalyseStatementContext(
    cmd: string,
    nowRunPosition: [path: string, index: number]
  ) {
    return new Script.ContextForAnalyseStatement(
      this._core,
      cmd,
      nowRunPosition[0],
      nowRunPosition[1],
      this._statementAnalyseStack
    );
  }
  useAnalyseStatement(
    middleware: Script.MiddlewareForAnalyseStatement,
    add_front?: boolean
  ) {
    if (add_front) {
      this._analyseStatementMiddlewares.unshift(middleware);
    } else {
      this._analyseStatementMiddlewares.push(middleware);
    }
  }
  /**
   * 分析statement
   *
   * Analyze the statement syntax and record the script point location and related information in advance.
   * When the script executes a statement, the regular middleware corresponding to that statement will load
   * the information saved by analyseStatement before the statement, and if there is no analysis, it will call
   * analyseStatement to analyze, and the corresponding analysis middleware will process and save the information.
   * After analysis is complete, reset _nextRunPosition to the location before the call, switch back to the normal mode,
   * and continue executing.
   */
  analyseStatement(ctx: Script.Context) {
    // this._core.debug.log("[HZEngine] Start statement analyse mode");

    // Backup _nextRunPosition
    let _nextRunPositionBackup: [path: string, index: number] | null = this
      ._nextRunPosition
      ? [...this._nextRunPosition]
      : null;
    let covered: boolean = false;
    // Set _nextRunPosition to the current position of the statement
    this._nextRunPosition = [ctx.currentPath, ctx.currentLineIndex];
    while (this._nextRunPosition) {
      let rawCommand = readline(
        this._core,
        this._nextRunPosition[0],
        this._nextRunPosition[1]
      );
      if (rawCommand == null)
        throw `Readline Error(got ${rawCommand}), at file [${
          this._nextRunPosition[0]
        }] line [${this._nextRunPosition[1] + 1}]`;

      // If the command is not empty and not a comment
      if (rawCommand.trim().length && !rawCommand.trim().startsWith("#")) {
        // If it is a label command, check if the statement stack is empty
        if (rawCommand.trim().startsWith("*")) {
          if (this._statementStack.length) {
            throw `label between statement is not allowed, at file [${
              this._nextRunPosition[0]
            }] line [${this._nextRunPosition[1] + 1}]`;
          }
        } else {
          // Build context for analyzing statement
          let sub_ctx = this._buildAnalyseStatementContext(rawCommand, [
            ...this._nextRunPosition,
          ]);

          // console.log(`[HZEngine] analyse statement command [${rawCommand}]`);

          // Process command
          if (this._analyseStatementMiddlewares.length === 0) {
            // TODO do nothing
          } else {
            let i = 0,
              len = this._analyseStatementMiddlewares.length;
            let nextFunc = () => {
              i++;
              if (i >= len) {
                // TODO do nothing
              } else {
                this._analyseStatementMiddlewares[i](sub_ctx, nextFunc);
              }
            };
            this._analyseStatementMiddlewares[0](sub_ctx, nextFunc);
          }
        }
      }

      // Move to the next line
      this.incrementNextPosition();

      if (covered) {
        if (this._statementAnalyseStack.length === 0) {
          break;
        }
      } else {
        if (this._statementAnalyseStack.length > 0) {
          covered = true;
        }
      }
    }

    this._nextRunPosition = _nextRunPositionBackup;

    // Check if the statement stack is empty
    if (this._statementAnalyseStack.length > 0) {
      throw `statement not closed, at file [${
        this._statementAnalyseStack[
          this._statementAnalyseStack.length - 1
        ][1][0]
      }] line [${
        this._statementAnalyseStack[
          this._statementAnalyseStack.length - 1
        ][1][1] + 1
      }]`;
    }

    // Reset _nextRunPosition to the backup value, and switch back to normal mode, and continue executing
    // this._core.debug.logconsole.log("[HZEngine] Finished analyse statement mode ");
  }

  // eval
  evalScope(code: string) {
    try {
      return new Function("sd", "gd", "hz", `${code}`)(
        this._core.storage.sd,
        this._core.storage.gd,
        this._core
      );
    } catch (e) {
      this._core.debug.log(`Error in evalScope: ${e}`);
    }
  }
  evalExpression(code: string) {
    this._core.debug.log(`evalExpression: ${code}`);

    try {
      return new Function("sd", "gd", "hz", `return (${code})`)(
        this._core.storage.sd,
        this._core.storage.gd,
        this._core
      );
    } catch (e) {
      this._core.debug.log(`Error in evalExpression: ${e}`);
    }
  }

  // parse string
  parseString(str: string) {
    let parsedInterpolated = parseInterpolatedStr(str);
    let res: string = "";
    for (let item of parsedInterpolated) {
      if (item.isExpression) {
        res += this.evalExpression(item.str);
      } else {
        res += item.str;
      }
    }

    return res;
  }
}

export namespace Script {
  export type Middleware = (ctx: Context, next: () => void) => void;
  export class Context {
    constructor(
      protected _core: HZEngineCore,
      private _rawtext: string,
      public readonly currentPath: string,
      public readonly currentLineIndex: number,
      private _statementStack: StatementStack
    ) {}
    public get rawtext(): string {
      return this._rawtext;
    }
    public set rawtext(rawtext: string) {
      this._rawtext = rawtext;
      this._rawtextChanged = true;
    }
    private _rawtextChanged = false;
    private _slicedArgs: Context.SlicedArg[] | null = null;
    get slicedArgs(): Context.SlicedArg[] {
      if (!this._slicedArgs || this._rawtextChanged)
        this._slicedArgs = splitStr2Objs(this.rawtext);
      this._rawtextChanged = false;
      return this._slicedArgs;
    }
    // 注意只有在修改在触发slicedArgs的时候才会更新rawtext
    set slicedArgs(slicedArgs: Context.SlicedArg[]) {
      this._slicedArgs = JSON.parse(JSON.stringify(slicedArgs)); // TODO 深拷贝 性能
      this._rawtext = joinObjs2Str(slicedArgs);
      // TODO this._rawtextChanged = true ???
    }
    /**
     * 開始一個新的Statement，返回該Statement的數據
     * Start a new statement and return the data of the new statement
     * @param identifier the identifier of the statement
     * @returns the data of the new statement
     */
    startStatement(
      identifier: string,
      data?: Storage.Saveable<unknown>
    ): StatementData {
      let statement_data = data ?? this.getStatementData();
      let statementStackItem: StatementStackItem = [
        identifier,
        [this.currentPath, this.currentLineIndex],
        statement_data,
      ];
      this._statementStack.push(statementStackItem);
      return statement_data;
    }
    endStatement(identifier: string) {
      if (this._statementStack.length === 0)
        throw `statement not open, at file [${this.currentPath}] line [${
          this.currentLineIndex + 1
        }]`;
      if (
        this._statementStack[this._statementStack.length - 1][0] !== identifier
      )
        throw `the last statement in the stack is not ${identifier}, at file [${
          this.currentPath
        }] line [${this.currentLineIndex + 1}]`;
      return this._statementStack.pop()![2] as NonNullable<
        Storage.Saveable<unknown>
      >;
    }
    get statementStack() {
      return this._statementStack;
    }

    // statement data will stored in core.storage.globalData.script.statement_data
    // the key of the statement data is the line index of the start statement,
    // for example: the key of the statement data of "menu ... end menu"
    // is stored in the key of the line index of the "menu" statement
    getStatementData(): NonNullable<Storage.Saveable<unknown>> {
      let statement_data_in_file = this._core.storage.getSaveableData(
        this._core.storage.globalData,
        true,
        "script",
        "statement_data",
        this.currentPath
      ) as Record<string, NonNullable<Storage.Saveable<unknown>>>;
      if (!statement_data_in_file["" + this.currentLineIndex]) {
        this._core.script.analyseStatement(this);
        statement_data_in_file = this._core.storage.getSaveableData(
          this._core.storage.globalData,
          true,
          "script",
          "statement_data",
          this.currentPath
        ) as Record<string, NonNullable<Storage.Saveable<unknown>>>;
        if (!statement_data_in_file[this.currentLineIndex])
          throw `analyse statement failed as statement data not found, at file [${
            this.currentPath
          }] line [${this.currentLineIndex + 1}]`;
      }

      return statement_data_in_file[this.currentLineIndex]!;
    }

    setStatementData(
      statement_data: NonNullable<Storage.Saveable<unknown>>,
      start_position: [path: string, index: number]
    ) {
      let statement_data_in_file = this._core.storage.getSaveableData(
        this._core.storage.globalData,
        true,
        "script",
        "statement_data",
        start_position[0]
      ) as Record<string, NonNullable<Storage.Saveable<unknown>>>;
      statement_data_in_file["" + start_position[1]] = statement_data;
      this._core.storage.saveGlobalData();
    }
  }
  export namespace Utils {
    export const joinSlicedArgs: typeof joinObjs2Str = joinObjs2Str;
    export const splitRawtext: typeof splitStr2Objs = splitStr2Objs;
    export function splitCommas(rawtext: string): string[] {
      let slicedArgs = splitStr2Objs(rawtext);
      // console.log(`splitCommas rawtext: ${rawtext}, slicedArgs: ${JSON.stringify(slicedArgs)}`);

      let res: string[] = [];
      for (let i = 0; i < slicedArgs.length; i++) {
        if (slicedArgs[i].isQuoted) res.push(`"${slicedArgs[i].str}"`);
        else if (slicedArgs[i].isSquared) res.push(`[${slicedArgs[i].str}]`);
        else if (slicedArgs[i].isRounded) res.push(`(${slicedArgs[i].str})`);
        else {
          slicedArgs[i].str.split(",").forEach((str) => {
            str = str.trim();
            if (str) res.push(str);
          });
        }
      }
      // console.log(`splitCommas res: ${JSON.stringify(res)}`);

      return res;
    }
    export function parseTuple(rawtext: string) {
      if (
        rawtext.length < 2 ||
        rawtext[0] !== "(" ||
        rawtext[rawtext.length - 1] !== ")"
      ) {
        throw `invalid tuple: ${rawtext}`;
      }
      rawtext = rawtext.slice(1, rawtext.length - 1);
      // console.log(`parseTuple rawtext: ${rawtext}`);

      return parseHzsArgs(rawtext);
    }
    export function parseArray(rawtext: string) {
      if (
        rawtext.length < 2 ||
        rawtext[0] !== "[" ||
        rawtext[rawtext.length - 1] !== "]"
      ) {
        throw `invalid array: ${rawtext}`;
      }
      rawtext = rawtext.slice(1, rawtext.length - 1);
      return parseHzsArgs(rawtext);
    }
    export function parseHzsArgs(rawtext: string): TupleOrArr {
      rawtext = rawtext.trim();
      let arr = splitCommas(rawtext);
      let res = arr.map((str) => {
        if (str.startsWith("(")) return parseTuple(str);
        else if (str.startsWith("[")) return parseArray(str);
        else return str;
      });
      // console.log(`parseHzsArgs from: "${rawtext}" ; res: ${JSON.stringify(res)}`);

      return res;
    }

    type TupleOrArr = (string | TupleOrArr)[];
  }
  export type MiddlewareForAnalyseStatement = (
    ctx: ContextForAnalyseStatement,
    next: () => void
  ) => void;
  export class ContextForAnalyseStatement extends Context {
    startStatement(
      identifier: string,
      data: Storage.Saveable<unknown> = {}
    ): StatementData {
      return super.startStatement(identifier, data ?? {});
    }
    endStatement(identifier: string) {
      let statement_data = super.endStatement(identifier);
      // save analysed statement data
      let statement_data_in_file = this._core.storage.getSaveableData(
        this._core.storage.globalData,
        true,
        "script",
        "statement_data",
        this.currentPath
      ) as Record<string, NonNullable<Storage.Saveable<unknown>>>;
      statement_data_in_file["" + this.currentLineIndex] = statement_data;
      this._core.storage.saveGlobalData();
      return statement_data;
    }
  }
  export namespace Context {
    export interface SlicedArg {
      str: string;
      isQuoted?: boolean; // 是否被雙引號包圍
      isSquared?: boolean; // 是否被方括號包圍
    }
  }

  export type StatementStackItem = [
    identifier: string,
    start_position: [path: string, index: number],
    statement_data: Storage.JSONValue,
  ];
  export type StatementStack = StatementStackItem[];
  export type StatementData = NonNullable<Storage.JSONValue>;
}
