import { OscilloscopeConfig, runOscilloscope } from "./Visuals/Oscilloscope";
import { EditorState, Compartment } from "@codemirror/state";
import { scriptBlinkers } from "./Visuals/Blinkers";
import { javascript } from "@codemirror/lang-javascript";
import { markdown } from "@codemirror/lang-markdown";
import { Extension } from "@codemirror/state";
import { outputSocket } from "./IO/OSC";
import { getCodeMirrorTheme } from "./EditorSetup";
import {
  initializeSelectedUniverse,
  AppSettings,
  Universe,
  loadUniverserFromUrl,
} from "./FileManagement";
import { singleElements, buttonGroups, ElementMap } from "./DomElements";
import { registerFillKeys, registerOnKeyDown } from "./KeyActions";
import { installEditor } from "./EditorSetup";
import { documentation_factory } from "./Documentation";
import { EditorView } from "codemirror";
import { Clock } from "./Clock";
import { loadSamples, UserAPI } from "./API";
import * as oeis from "jisg";
import * as zpatterns from "zifferjs/src/patterns.ts";
import { makeArrayExtensions } from "./extensions/ArrayExtensions";
import "./style.css";
import { Universes, File } from "./FileManagement";
import { tryEvaluate } from "./Evaluator";
// @ts-ignore
import showdown from "showdown";
import { makeStringExtensions } from "./extensions/StringExtensions";
import { installInterfaceLogic } from "./InterfaceLogic";
import { installWindowBehaviors } from "./WindowBehavior";
import { makeNumberExtensions } from "./extensions/NumberExtensions";
// @ts-ignore
import { registerSW } from "virtual:pwa-register";
import colors from "./colors.json";

if ("serviceWorker" in navigator) {
  registerSW();
}

export class Editor {
  // Universes and settings
  settings: AppSettings = new AppSettings();
  universes: Universes = {};
  selected_universe: string = "Welcome";

  fill: boolean = false;
  local_index: number = 1;

  // Editor logic
  editor_mode: "global" | "local" | "init" | "notes" = "global";
  hidden_interface: boolean = false;
  fontSize!: Compartment;
  withLineNumbers!: Compartment;
  themeCompartment!: Compartment;
  vimModeCompartment!: Compartment;
  hoveringCompartment!: Compartment;
  completionsCompartment!: Compartment;
  chosenLanguage!: Compartment;
  dynamicPlugins!: Compartment;
  currentDocumentationPane: string = "introduction";
  exampleCounter: number = 0;
  exampleIsPlaying: boolean = false;
  editorExtensions: Extension[] = [];
  userPlugins: Extension[] = [];
  state!: EditorState;
  view!: EditorView;
  selectedExample: string | null = "";
  docs: { [key: string]: string } = {};
  public _mouseX: number = 0;
  public _mouseY: number = 0;
  show_error: boolean = false;
  currentThemeName: string = "Everblush";
  buttonElements: Record<string, HTMLButtonElement[]> = {};
  interface: ElementMap = {};
  blinkTimeouts: Record<number, number> = {};
  osc: OscilloscopeConfig = {
    enabled: false,
    color: "#fdba74",
    thickness: 4,
    refresh: 1,
    fftSize: 1024,
    orientation: "horizontal",
    offsetX: 0,
    offsetY: 0,
    mode: "scope",
    size: 1,
  };

  // UserAPI
  api: UserAPI;

  // Audio stuff
  audioContext: AudioContext;
  clock: Clock;
  dough_nudge: number = 20;
  manualPlay: boolean = false;
  isPlaying: boolean = false;

  // OSC
  outputSocket: WebSocket = outputSocket;

  // Hydra
  public hydra_backend: any;
  public hydra: any;

  constructor() {
    /**
     * This is the entry point of the application. The Editor instance is created when the page is loaded.
     * It is responsible for:
     * - Initializing the user interface
     * - Loading the universe from local storage
     * - Initializing the audio context and the clock
     * - Building the user API
     * - Building the documentation
     * - Installing event listeners
     * - Building the CodeMirror editor
     * - Evaluating the init file
     */

    // ================================================================================
    // Build user interface
    // ================================================================================

    this.initializeElements();
    this.initializeButtonGroups();
    this.setCanvas(this.interface.feedback as HTMLCanvasElement);
    this.setCanvas(this.interface.scope as HTMLCanvasElement);
    this.setCanvas(this.interface.drawings as HTMLCanvasElement);
    try {
      this.loadHydraSynthAsync();
    } catch (error) {
      console.log("Couldn't start Hydra: ", error);
    }

    // ================================================================================
    // Loading the universe from local storage
    // ================================================================================

    this.universes = {
      ...this.settings.universes,
      //...template_universes,
    };
    initializeSelectedUniverse(this);

    // ================================================================================
    // Audio context and clock
    // ================================================================================

    this.audioContext = new AudioContext({ latencyHint: "playback" });
    this.clock = new Clock(this, this.audioContext);

    // ================================================================================
    // User API
    // ================================================================================

    this.api = new UserAPI(this);
    makeArrayExtensions(this.api);
    makeStringExtensions(this.api);
    makeNumberExtensions(this.api);

    // Passing the API to the User
    Object.entries(this.api).forEach(([name, value]) => {
      (globalThis as Record<string, any>)[name] = value;
    });

    // Passing OEIS generators to the User
    Object.entries(oeis).forEach(([name, value]) => {
      (globalThis as Record<string, any>)[name] = value;
    });

    // Passing ziffers sequences to the User
    Object.entries(zpatterns).forEach(([name, value]) => {
      (globalThis as Record<string, any>)[name] = value;
    });

    // ================================================================================
    // Building Documentation
    // ================================================================================

    let pre_loading = async () => {
      await loadSamples();
    };
    pre_loading().then(() => {
      this.docs = documentation_factory(this);
    });

    // ================================================================================
    // Application event listeners
    // ================================================================================

    registerFillKeys(this);
    registerOnKeyDown(this);
    installInterfaceLogic(this);
    scriptBlinkers();

    // ================================================================================
    // Building CodeMirror Editor
    // ================================================================================

    installEditor(this);
    runOscilloscope(this.interface.scope as HTMLCanvasElement, this);

    // First evaluation of the init file
    tryEvaluate(this, this.universes[this.selected_universe.toString()].init);

    // Changing to global when opening
    this.changeModeFromInterface("global");

    // Loading universe from URL (if needed)
    loadUniverserFromUrl(this);

    // Set the color scheme for the application
    let available_themes = Object.keys(colors);
    if (this.settings.theme in available_themes) {
      this.readTheme(this.settings.theme);
    } else {
      this.settings.theme = "Everblush";
      this.readTheme(this.settings.theme);
    }
  }

  private getBuffer(type: string): any {
    /**
     * Retrieves the buffer based on the specified type.
     * @param type - The type of buffer to retrieve.
     * @returns The buffer object.
     */
    const universe = this.universes[this.selected_universe.toString()];
    return type === "locals"
      ? universe[type][this.local_index]
      : universe[type as keyof Universe];
  }

  get note_buffer() {
    return this.getBuffer("notes");
  }

  get example_buffer() {
    return this.getBuffer("example");
  }

  get global_buffer() {
    return this.getBuffer("global");
  }

  get init_buffer() {
    return this.getBuffer("init");
  }

  get local_buffer() {
    return this.getBuffer("locals");
  }

  updateKnownUniversesView = () => {
    /**
     * Updates the known universes view.
     * This function generates and populates a list of known universes based on the data stored in the 'universes' property.
     * It retrieves the necessary HTML elements and template, creates the list, and attaches event listeners to the generated items.
     * If any required elements or templates are missing, warning messages are logged and the function returns early.
     */
    let itemTemplate = document.getElementById(
      "ui-known-universe-item-template",
    ) as HTMLTemplateElement;
    if (!itemTemplate) {
      return;
    }

    let existing_universes = document.getElementById("existing-universes");
    if (!existing_universes) {
      return;
    }

    let list = document.createElement("ul");
    list.className =
      "lg:h-80 lg:text-normal text-normal h-auto lg:w-80 w-auto lg:pb-2 lg:pt-2 overflow-y-scroll text-brightwhite bg-background lg:mb-4 border rounded-lg";
    list.append(
      ...Object.keys(this.universes).map((it) => {
        let item = itemTemplate.content.cloneNode(true) as DocumentFragment;
        let api = window as unknown as UserAPI;
        item.querySelector(".universe-name")!.textContent = it;
        item
          .querySelector(".load-universe")
          ?.addEventListener("click", () => api._loadUniverseFromInterface(it));
        item
          .querySelector(".delete-universe")
          ?.addEventListener("click", () =>
            api._deleteUniverseFromInterface(it),
          );
        return item;
      }),
    );

    existing_universes.innerHTML = "";
    existing_universes.append(list);
  };

  changeToLocalBuffer(i: number) {
    /**
     * Changes the local buffer based on the provided index.
     * Updates the CSS accordingly by adding a specific class to the selected tab and removing it from other tabs.
     * Updates the local index and updates the editor view.
     *
     * @param i The index of the tab to change the local buffer to.
     */
    const tabs = document.querySelectorAll('[id^="tab-"]');
    const tab = tabs[i] as HTMLElement;
    tab.classList.add("bg-foreground");
    for (let j = 0; j < tabs.length; j++) {
      if (j != i) tabs[j].classList.remove("bg-foreground");
    }
    let tab_id = tab.id.split("-")[1];
    this.local_index = parseInt(tab_id);
    this.updateEditorView();
  }

  changeModeFromInterface(mode: "global" | "local" | "init" | "notes") {
    /**
     * Changes the mode of the interface.
     *
     * @param mode - The mode to change to. Can be one of "global", "local", "init", or "notes".
     */
    const interface_buttons: HTMLElement[] = [
      this.interface.local_button,
      this.interface.global_button,
      this.interface.init_button,
      this.interface.note_button,
    ];

    let changeColor = (button: HTMLElement) => {
      interface_buttons.forEach((button) => {
        let svg = button.children[0] as HTMLElement;
        if (svg.classList.contains("text-foreground_selection")) {
          svg.classList.remove("text-foreground_selection");
          button.classList.remove("text-foreground_selection");
        }
      });
      button.children[0].classList.remove("text-white");
      button.children[0].classList.add("text-foreground_selection");
      button.classList.add("text-foreground_selection");
      button.classList.add("fill-foreground_selection");
    };

    switch (mode) {
      case "local":
        if (this.interface.local_script_tabs.classList.contains("hidden")) {
          this.interface.local_script_tabs.classList.remove("hidden");
        }
        this.editor_mode = "local";
        this.local_index = 0;
        document.getElementById("editor")!.style.height = "calc(100% - 100px)";
        this.changeToLocalBuffer(this.local_index);
        changeColor(this.interface.local_button);
        break;
      case "global":
        if (!this.interface.local_script_tabs.classList.contains("hidden")) {
          this.interface.local_script_tabs.classList.add("hidden");
        }
        this.editor_mode = "global";
        document.getElementById("editor")!.style.height = "100%";
        changeColor(this.interface.global_button);
        break;
      case "init":
        if (!this.interface.local_script_tabs.classList.contains("hidden")) {
          this.interface.local_script_tabs.classList.add("hidden");
        }
        this.editor_mode = "init";
        changeColor(this.interface.init_button);
        break;
      case "notes":
        if (!this.interface.local_script_tabs.classList.contains("hidden")) {
          this.interface.local_script_tabs.classList.add("hidden");
        }
        this.editor_mode = "notes";
        changeColor(this.interface.note_button);
        break;
    }

    // If the editor is in notes mode, we need to update the selectedLanguage

    this.view.dispatch({
      effects: this.chosenLanguage.reconfigure(
        this.editor_mode == "notes" ? [markdown()] : [javascript()],
      ),
    });

    this.updateEditorView();
  }

  setButtonHighlighting(
    button: "play" | "pause" | "stop" | "clear",
    highlight: boolean,
  ) {
    /**
     * Sets the highlighting for a specific button.
     *
     * @param button - The button to highlight ("play", "pause", "stop", or "clear").
     * @param highlight - A boolean indicating whether to highlight the button or not.
     */
    document.getElementById("play-label")!.textContent =
      button !== "pause" ? "Pause" : "Play";
    if (button !== "pause") {
      document.getElementById("pause-icon")!.classList.remove("hidden");
      document.getElementById("play-icon")!.classList.add("hidden");
    } else {
      document.getElementById("pause-icon")!.classList.add("hidden");
      document.getElementById("play-icon")!.classList.remove("hidden");
    }

    if (button === "stop") {
      this.isPlaying == false;
      document.getElementById("play-label")!.textContent = "Play";
      document.getElementById("pause-icon")!.classList.add("hidden");
      document.getElementById("play-icon")!.classList.remove("hidden");
    }

    this.flashBackground("#404040", 200);
    const possible_selectors = [
      '[id^="play-button-"]',
      '[id^="clear-button-"]',
      '[id^="stop-button-"]',
    ];
    let selector: number;
    switch (button) {
      case "play":
        selector = 0;
        break;
      case "pause":
        selector = 1;
        break;
      case "clear":
        selector = 2;
        break;
      case "stop":
        selector = 3;
        break;
    }
    document
      .querySelectorAll(possible_selectors[selector])
      .forEach((button) => {
        if (highlight) button.children[0].classList.add("animate-pulse");
      });
    // All other buttons must lose the highlighting
    document
      .querySelectorAll(
        possible_selectors.filter((_, index) => index != selector).join(","),
      )
      .forEach((button) => {
        button.children[0].classList.remove("animate-pulse");
        button.children[1].classList.remove("animate-pulse");
      });
  }

  unfocusPlayButtons() {
    document.querySelectorAll('[id^="play-button-"]').forEach((button) => {
      button.children[0].classList.remove("fill-foreground_selection");
      button.children[0].classList.remove("animate-pulse");
    });
  }

  updateEditorView(): void {
    this.view.dispatch({
      changes: {
        from: 0,
        to: this.view.state.doc.toString().length,
        insert: this.currentFile().candidate,
      },
    });
  }

  currentFile(): File {
    switch (this.editor_mode) {
      case "global":
        return this.global_buffer;
      case "local":
        return this.local_buffer;
      case "init":
        return this.init_buffer;
      case "notes":
        return this.note_buffer;
    }
  }

  flashBackground(color: string, duration: number): void {
    /**
     * Flashes the background of the view and its gutters.
     * @param {string} color - The color to set.
     * @param {number} duration - Duration in milliseconds to maintain the color.
     */
    const domElement = this.view.dom;
    const gutters = domElement.getElementsByClassName(
      "cm-gutter",
    ) as HTMLCollectionOf<HTMLElement>;

    domElement.classList.add("fluid-bg-transition");
    Array.from(gutters).forEach((gutter) =>
      gutter.classList.add("fluid-bg-transition"),
    );

    domElement.style.backgroundColor = color;
    Array.from(gutters).forEach(
      (gutter) => (gutter.style.backgroundColor = color),
    );

    setTimeout(() => {
      domElement.style.backgroundColor = "";
      Array.from(gutters).forEach(
        (gutter) => (gutter.style.backgroundColor = ""),
      );

      domElement.classList.remove("fluid-bg-transition");
      Array.from(gutters).forEach((gutter) =>
        gutter.classList.remove("fluid-bg-transition"),
      );
    }, duration);
  }

  private initializeElements(): void {
    for (const [key, value] of Object.entries(singleElements)) {
      this.interface[key] = document.getElementById(
        value,
      ) as ElementMap[keyof ElementMap];
    }
  }

  private initializeButtonGroups(): void {
    for (const [key, ids] of Object.entries(buttonGroups)) {
      this.buttonElements[key] = ids.map(
        (id) => document.getElementById(id) as HTMLButtonElement,
      );
    }
  }

  private loadHydraSynthAsync(): void {
    /**
     * Loads the Hydra Synth asynchronously by creating a script element
     * and appending it to the document head.  * Once the script is
     * loaded successfully, it initializes the Hydra Synth. If there
     * is an error loading the script, it logs an error message.
     */
    var script = document.createElement("script");
    script.src = "https://unpkg.com/hydra-synth";
    script.async = true;
    script.onload = () => {
      console.log("Hydra loaded successfully");
      this.initializeHydra();
    };
    script.onerror = function () {
      console.error("Error loading Hydra script");
    };
    document.head.appendChild(script);
  }

  private initializeHydra(): void {
    /**
     * Initializes the Hydra backend and sets up the Hydra synth.
     */
    // @ts-ignore
    this.hydra_backend = new Hydra({
      canvas: this.interface.hydra_canvas as HTMLCanvasElement,
      detectAudio: false,
      enableStreamCapture: false,
    });
    this.hydra = this.hydra_backend.synth;
    (globalThis as any).hydra = this.hydra;
    this.hydra.setResolution(1024, 768);
  }

  private setCanvas(canvas: HTMLCanvasElement): void {
    /**
     * Sets the canvas element and configures its size and context.
     *
     * @param canvas - The HTMLCanvasElement to set.
     */
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    const dpr = window.devicePixelRatio || 1;
    // Assuming the canvas takes up the whole window
    canvas.width = window.innerWidth * dpr;
    canvas.height = window.innerHeight * dpr;
    if (ctx) {
      ctx.scale(dpr, dpr);
    }
  }

  private updateInterfaceTheme(selected_theme: {[key: string]: string}): void {
    function hexToRgb(hex: string): {r: number, g: number, b: number} | null {
      let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
          return result ? {
            r: parseInt(result[1], 16),
            g: parseInt(result[2], 16),
            b: parseInt(result[3], 16)
          } : null;
        };
        for (const [key, value] of Object.entries(selected_theme)) {
          let color = hexToRgb(value);
          if (color) {
            let colorString = `${color.r} ${color.g} ${color.b}`
            document.documentElement.style.setProperty("--" + key, colorString);
          }
        }
      }

      getColorScheme(theme_name: string): {[key: string]: string} {
    // Check if the theme exists in colors.json
    let themes: Record<string, { [key: string]: any }> = colors;
    return themes[theme_name];
  }

  readTheme(theme_name: string): void {
    // Check if the theme exists in colors.json
    let themes: Record<string, { [key: string]: any }> = colors;
    let selected_theme = themes[theme_name];
    if (selected_theme) {
      this.currentThemeName = theme_name;
      this.updateInterfaceTheme(selected_theme);
      let codeMirrorTheme = getCodeMirrorTheme(selected_theme);
      // Reconfigure the view with the new theme
      this.view.dispatch({
        effects: this.themeCompartment.reconfigure(codeMirrorTheme),
      });
    }
  }
}

let app = new Editor();
installWindowBehaviors(app, window, false);
