/**
 * A script for demonstrating the line-point-distance.
 *
 * @require PlotBoilerplate, MouseHandler, gup, dat.gui
 *
 * @author   Ikaros Kappler
 * @date     2023-07-25
 * @modified 2023-10-10 Renamed private method `showJSON` to `requestShowJSON`.
 * @version  1.0.0
 **/

import { gup } from "./gup";
import { detectDarkMode } from "./detectDarkMode";
import { KeyHandler, MouseHandler, PBParams, PlotBoilerplate, XYDimension } from "plotboilerplate";
import { RPGDialogueLogic } from "./RPGDialogueLogic";
import { IDialogueConfig, IDialogueListener, IGlobalLibs, IMiniQuestionaire, IMiniQuestionaireWithPosition } from "./interfaces";
import { EditorHelper } from "./editorHelpers";
import { EditorRenderer } from "./editorRenderer";
import { TouchHandler } from "./TouchHandler";
import { FileDrop } from "plotboilerplate/src/cjs/utils/io/FileDrop";
import { Modal } from "./modal";
import { detectMobileDevice } from "./detectMobileDevice";
import { DefaultDialogueRenderer } from "./DefaultDialogueRenderer";

export class Editor {
  currentMouseHandler: MouseHandler | null = null;
  currentTouchHandler: TouchHandler | null = null;
  editorHelpers: EditorHelper;
  editorRenderer: EditorRenderer;
  dialogConfig: IDialogueConfig<IMiniQuestionaireWithPosition> | null;
  pb: PlotBoilerplate;
  private autosaveTimer: NodeJS.Timer;
  constructor(dialogueConfigJSONPath: string, isRecoveryFromLocalStorageActive: boolean, globalLibs: IGlobalLibs) {
    const _self = this;
    console.log("Initializing plotboilerplate");
    // Fetch the GET params
    const GUP = gup();
    const isDarkmode: boolean = detectDarkMode(GUP);
    const isMobileDevice: boolean = detectMobileDevice(GUP);

    // All config params are optional.
    this.pb = new PlotBoilerplate(
      PlotBoilerplate.utils.safeMergeByKeys(
        {
          canvas: document.getElementById("my-canvas") as HTMLCanvasElement,
          fullSize: true,
          fitToParent: true,
          scaleX: 1.0,
          scaleY: 1.0,
          rasterGrid: true,
          drawOrigin: false,
          rasterAdjustFactor: 2.0,
          redrawOnResize: true,
          defaultCanvasWidth: 1024,
          defaultCanvasHeight: 768,
          canvasWidthFactor: 1.0,
          canvasHeightFactor: 1.0,
          cssScaleX: 1.0,
          cssScaleY: 1.0,
          cssUniformScale: true,
          autoAdjustOffset: true,
          offsetAdjustXPercent: 50,
          offsetAdjustYPercent: 50,
          backgroundColor: isDarkmode ? "#000000" : "#ffffff",
          enableMouse: true,
          enableKeys: true
        } as any,
        GUP
      ) as PBParams
    );

    const boxSize: XYDimension = {
      width: 120,
      height: 20
    };
    this.editorHelpers = new EditorHelper(this, this.pb, boxSize);
    this.editorRenderer = new EditorRenderer(this.pb, boxSize, this.editorHelpers, isDarkmode);

    // +---------------------------------------------------------------------------------
    // | On 'escape' key press un-select selected items.
    // +-------------------------------
    const keyHandler = new KeyHandler({ element: document.getElementsByTagName("body")[0], trackAll: false }).down(
      "escape",
      () => {
        // Esc?
        console.log("Escape down");
        if (this.editorHelpers.selectedOption) {
          this.editorHelpers.setSelectedOption(null);
        } else if (this.editorHelpers.selectedNode) {
          this.editorHelpers.setSelectedNode(null, null);
          this.editorHelpers.setSelectedOption(null);
        }
      }
    );

    // +---------------------------------------------------------------------------------
    // | Make HTML buttons a bit larger on mobile devices.
    // +-------------------------------
    console.debug("isMobileDevice", isMobileDevice);
    if (isMobileDevice) {
      document.getElementsByTagName("body")[0].classList.add("is-mobile-device");
    }

    // +---------------------------------------------------------------------------------
    // | The render method.
    // +-------------------------------
    this.pb.config.postDraw = (_draw, _fill) => {
      if (!_self.dialogConfig) {
        return;
      }
      _self.editorRenderer.renderBoxes(_self.dialogConfig);
      _self.editorRenderer.renderConnections(_self.dialogConfig);
    };

    if (isRecoveryFromLocalStorageActive) {
      console.debug("Trying to recover config from localstorage.");
      this.tryLoadFromLocalStorage()
        .then(dc => {
          _self.handleDialogConfigLoaded(dc);
        })
        .catch(() => {
          console.debug("Loading from localstorage failed. Falling back loading from specified path.");
          _self.tryLoadFromJSON(dialogueConfigJSONPath, globalLibs);
        });
    } else {
      _self.tryLoadFromJSON(dialogueConfigJSONPath, globalLibs);
    }

    // Install DnD with FileDrop
    const fileDrop = new FileDrop(this.pb.eventCatcher);
    fileDrop.onFileJSONDropped((jsonObject: object) => {
      console.debug("[onFileJSONDropped] jsonObject", jsonObject);
      // TODO: properly convert to dialog-config
      _self.handleDialogConfigLoaded(EditorHelper.fromObject(jsonObject));
    });

    // Also accept uploads via button
    const importJSON = () => {
      document.getElementById("input-upload-file")?.click();
    };
    document.getElementById("b-import-json")?.addEventListener("click", importJSON);
    document.getElementById("input-upload-file")?.addEventListener("change", (_evt: Event) => {
      var fileInput = document.getElementById("input-upload-file") as HTMLInputElement;
      if (!fileInput.files || fileInput.files.length === 0) {
        return;
      }
      // console.log("inputFile", fileInput.files[0]);
      var reader = new FileReader();
      reader.onload = () => {
        const jsonText = reader.result as string;
        console.log(reader.result);
        _self.handleDialogConfigLoaded(EditorHelper.fromObject(JSON.parse(jsonText)));
      };
      reader.readAsText(fileInput.files[0]);
    });

    document.getElementById("b-run-test")?.addEventListener("click", () => {
      _self.testCurrentDialogueConfig();
    });

    document.getElementById("b-new")?.addEventListener("click", _self.requestCreateNewGraph());
    document.getElementById("b-show-json")?.addEventListener("click", _self.requestShowJSON());
    document.getElementById("b-goto-github")?.addEventListener("click", () => {
      window.open("https://github.com/IkarosKappler/rpg-dialogue", "_blank");
    });
  }
  // END constructor

  private tryStartAutosaveLoop() {
    if (this.autosaveTimer) {
      return;
    }
    const _self = this;
    this.autosaveTimer = globalThis.setInterval(() => {
      _self.tryAutoSave();
    }, 10000);
  }

  private tryAutoSave() {
    if (this.editorHelpers.domHelper.isAutoSave()) {
      // console.log("Putting to localstorage.");
      this.putToLocalStorage();
    }
  }

  requestCreateNewGraph(): () => void {
    const _self = this;
    return () => {
      _self.editorHelpers.domHelper.modal.setTitle("Drop current graph?");
      _self.editorHelpers.domHelper.modal.setBody(`Do you really want to create a new graph and lose unsaved changes?`);
      _self.editorHelpers.domHelper.modal.setFooter("");
      _self.editorHelpers.domHelper.modal.setActions([
        Modal.ACTION_CANCEL,
        {
          label: "Yes",
          action: () => {
            _self.editorHelpers.domHelper.modal.close();
            _self.performNewGraph();
          }
        }
      ]);
      _self.editorHelpers.domHelper.modal.open();
    };
  }

  private performNewGraph() {
    const newConfig: IDialogueConfig<IMiniQuestionaireWithPosition> = {
      meta: { name: "dialogue_A", npcs: [{ name: "NPC #0" }] },
      graph: {
        intro: { q: "Hello world!", o: [{ a: "Hello, NPC!", next: null }], editor: { position: { x: 0, y: 0 } } }
      }
    };
    this.handleDialogConfigLoaded(newConfig);
  }

  private requestShowJSON() {
    const _self = this;
    return () => {
      const removePositions = _self.editorHelpers.domHelper.isExportWithoutPositions();
      const dConfig: IDialogueConfig<IMiniQuestionaire> | IDialogueConfig<IMiniQuestionaireWithPosition> = removePositions
        ? EditorHelper.removePositions(_self.editorHelpers.dialogConfigWithPositions)
        : _self.editorHelpers.dialogConfigWithPositions;
      const jsonString = JSON.stringify(dConfig, null, 4);
      const jsonArea = document.createElement("textarea");
      jsonArea.setAttribute("readonly", "true");
      jsonArea.innerHTML = jsonString;
      jsonArea.classList.add("json-preview");
      _self.editorHelpers.domHelper.modal.setTitle("Current Graph as JSON");
      _self.editorHelpers.domHelper.modal.setBody(jsonArea);
      _self.editorHelpers.domHelper.modal.setFooter("");
      _self.editorHelpers.domHelper.modal.setActions([Modal.ACTION_CLOSE]);
      _self.editorHelpers.domHelper.modal.open();
    };
  }

  /**
   * Open a modal and test the current dialogue config (runs a RPGDialogueLogic instant).
   */
  testCurrentDialogueConfig() {
    if (!this.dialogConfig) {
      console.warn("Warning: cannot test null dialogue.");
      return;
    }
    const _self = this;
    // Create this structure:
    // <div class="rpg-output">
    //    <div class="rpg-output-question"></div>
    //    <ul class="rpg-output-options"></ul>
    // </div>

    const outputContainer = document.createElement("div");
    const outputQuestion = document.createElement("div");
    const outputOptions = document.createElement("ul");
    outputContainer.classList.add("rpg-output");
    outputQuestion.classList.add("rpg-output-question");
    outputOptions.classList.add("rpg-output-options");
    outputContainer.appendChild(outputQuestion);
    outputContainer.appendChild(outputOptions);

    const dialogueListener: IDialogueListener<IMiniQuestionaireWithPosition> = (
      _dialogueConfig: IDialogueConfig<IMiniQuestionaireWithPosition>,
      nextNodeName: string | null,
      _oldNodeName: string | null,
      _selectedOptionIndex: number
    ) => {
      // Highlight current node in the graph editor :)
      _self.editorHelpers.setHighlightedNode(nextNodeName);
    };

    const rpgLogic = new RPGDialogueLogic(this.dialogConfig, false);
    rpgLogic.addDialogueChangeListener(dialogueListener);
    const alternateStartNodeName: string | null = this.editorHelpers.selectedNodeName;
    this.editorHelpers.setSelectedNode(null, null);
    rpgLogic.beginConversation(new DefaultDialogueRenderer(outputQuestion, outputOptions), alternateStartNodeName);

    this.editorHelpers.domHelper.modal.setTitle("Test"); // Will be changed later when invoked.
    this.editorHelpers.domHelper.modal.setBody(outputContainer);
    this.editorHelpers.domHelper.modal.setFooter("");
    this.editorHelpers.domHelper.modal.setActions([Modal.ACTION_CLOSE]);
    this.editorHelpers.domHelper.modal.open();
  }

  private handleDialogConfigLoaded(config: IDialogueConfig<IMiniQuestionaire>) {
    // Check if all graph nodes have positions to render.
    this.dialogConfig = this.editorHelpers.enrichPositions(config);
    this.editorHelpers.enrichMetaData(this.dialogConfig);
    // console.debug("Enriched meta data", this.dialogConfig);
    this.editorHelpers.setDialogConfig(this.dialogConfig);

    // Ad DnD support for boxes.
    if (this.currentMouseHandler) {
      this.currentMouseHandler.destroy();
      this.currentMouseHandler = null;
    }
    this.currentMouseHandler = this.editorHelpers.boxMovehandler();

    // Ad DnD support for boxes.
    if (this.currentTouchHandler) {
      this.currentTouchHandler.destroy();
      this.currentTouchHandler = null;
    }
    this.currentTouchHandler = new TouchHandler(this.pb, this.dialogConfig, this.editorHelpers);

    this.pb.redraw();
    this.tryStartAutosaveLoop();
  }

  private putToLocalStorage() {
    const jsonString = JSON.stringify(this.dialogConfig);
    globalThis.localStorage.setItem("__rpgeditor.dialogueconfig", jsonString);
  }

  private tryLoadFromJSON(dialogueConfigJSONPath: string, globalLibs: IGlobalLibs) {
    const _self = this;
    RPGDialogueLogic.loadConfigFromJSON(dialogueConfigJSONPath, globalLibs).then((config: IDialogueConfig<IMiniQuestionaire>) => {
      console.debug("structure", config);
      _self.handleDialogConfigLoaded(config);
    });
  }

  private tryLoadFromLocalStorage(): Promise<IDialogueConfig<IMiniQuestionaireWithPosition>> {
    return new Promise<IDialogueConfig<IMiniQuestionaireWithPosition>>((accept, reject) => {
      const jsonString = globalThis.localStorage.getItem("__rpgeditor.dialogueconfig");
      if (!jsonString || jsonString === "") {
        reject();
      } else {
        try {
          const jsonObject = JSON.parse(jsonString);
          if (!jsonObject) {
            reject();
            return;
          }
          const dialogueConfig = EditorHelper.fromObject(jsonObject);
          accept(dialogueConfig);
        } catch (exception) {
          console.warn(exception);
          reject();
        }
      }
    });
  }

  // +---------------------------------------------------------------------------------
  // | END Editor
  // +-------------------------------
}
