import {
  EditorState,
  StateField,
  StateEffect,
  Range,
  Extension,
} from "@codemirror/state";
import { Decoration, EditorView, keymap } from "@codemirror/view";
import { defaultKeymap } from "@codemirror/commands";
import rainbowBrackets from "rainbowbrackets";
import { basicSetup } from "codemirror";
import { ModalInstance, ModalSingleton } from ".";
import { EditorSave, Failure, SOUNDS } from "../config";

export interface CodeEditorError extends Error {
  location: {
    start: { offset: number };
    end: { offset: number };
  };
}

export type CodeConsumer = (code: string) => {
  consumed?: boolean;
  error?: CodeEditorError;
};

interface CodeEditorState {
  view: EditorView;
  editorElement: HTMLElement;
  errorElement: HTMLElement;
  resultElement: HTMLElement;
  closeButton: HTMLButtonElement;
  codeConsumer: CodeConsumer;
}

export class CodeEditorInstance {
  constructor(
    private modalInstance: ModalInstance = ModalSingleton,
    private codeEditorState: CodeEditorState | null = null,
  ) {}

  public close() {
    if (!this.codeEditorState) {
      return;
    }

    this.codeEditorState.view.destroy();
    this.codeEditorState = null;

    SOUNDS.get(EditorSave.name)!.play();
  }

  public open(
    initCode: string,
    codeConsumer: CodeEditorState["codeConsumer"],
    readonly: boolean = false,
    initResult: { data?: string; error?: string } = {},
  ) {
    if (this.codeEditorState) {
      throw new Error("code editor instance is already owned.");
    }
    const modalContent = `
      <div class='code'>
        <div id='code'></div>
        <br>
        <p id='error' class='error'></p>
        <p id='result' class='result'></p>
        <button id='close-modal'>Done</button>
      </div>
    `;
    this.modalInstance.open(modalContent);

    const startState = EditorState.create({
      doc: initCode,
      extensions: [
        basicSetup,
        keymap.of(defaultKeymap),
        rainbowBrackets(),
        highlightExtension,
        FontSizeThemeExtension,
        EditorState.readOnly.of(readonly),
      ],
    });

    const codeBox = document.getElementById("code")!;
    const errorElement = document.getElementById("error")!;
    const resultElement = document.getElementById("result")!;

    const closeButton = document.getElementById(
      "close-modal",
    ) as HTMLButtonElement;
    closeButton.addEventListener("click", () => this.onSave());

    const editorView = new EditorView({
      state: startState,
      parent: codeBox,
    });
    editorView.focus();

    this.codeEditorState = {
      view: editorView,
      editorElement: codeBox,
      errorElement,
      resultElement,
      closeButton,
      codeConsumer,
    };
    this.setResult(initResult);
  }

  private refreshCodeEditorText(text: string) {
    if (!this.codeEditorState) {
      return;
    }
    const { view } = this.codeEditorState;

    view.dispatch({
      changes: {
        from: 0,
        to: text.length,
        insert: "",
      },
    });
    view.dispatch({
      changes: {
        from: 0,
        to: 0,
        insert: text,
      },
    });
  }

  private onSave() {
    if (!this.codeEditorState) {
      throw new Error();
    }

    const { view } = this.codeEditorState;
    const code = view.state.doc.toString();
    this.refreshCodeEditorText(code);

    const valid = this.codeEditorState.codeConsumer(code);
    if (!valid.error) {
      return;
    }

    const {
      location: {
        start: { offset: start },
        end: { offset: end },
      },
      message,
    } = valid.error;

    view.dispatch({
      effects: highlightEffect.of([
        syntaxErrorDecoration.range(start === end ? start - 1 : start, end),
      ]),
    });

    this.setResult({ error: message });
    SOUNDS.get(Failure.name)!.play();
  }

  private setResult(result: { data?: string; error?: string }) {
    if (!this.codeEditorState) {
      return;
    }
    this.codeEditorState.resultElement.innerText = result.data ?? "";
    this.codeEditorState.errorElement.innerText = result.error ?? "";
  }
}

const highlightEffect = StateEffect.define<Range<Decoration>[]>();
const highlightExtension = StateField.define({
  create() {
    return Decoration.none;
  },
  update(value, transaction) {
    value = value.map(transaction.changes);

    for (let effect of transaction.effects) {
      if (effect.is(highlightEffect))
        value = value.update({ add: effect.value, sort: true });
    }

    return value;
  },
  provide: (f) => EditorView.decorations.from(f),
});

const FontSizeTheme = EditorView.theme({
  $: {
    fontSize: "14pt",
  },
});
const FontSizeThemeExtension: Extension = [FontSizeTheme];
const syntaxErrorDecoration = Decoration.mark({
  class: "syntax-error",
});

export const CodeEditorSingleton = new CodeEditorInstance();
