import {
  Failure,
  IMAGES,
  LambdaSave,
  LambdaTransformSound,
  Miscellaneous,
  ModalOpen,
  SOUNDS,
  SPRITE_SPECS,
  SpriteSpec,
  Sprites,
} from "../config";
import { Entity, EntityNames, FunctionBox } from ".";
import {
  BoundingBox,
  Colliding,
  ComponentNames,
  Grid,
  GridSpawn,
  Highlight,
  Interactable,
  Sprite,
  Text,
} from "../components";
import { Coord2D, Direction } from "../interfaces";
import { openModal, closeModal } from "../utils";
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 { parse } from "../../interpreter";

interface CodeEditorState {
  view: EditorView;
  editorElement: HTMLElement;
  syntaxError: HTMLElement;
  canvas: HTMLCanvasElement;
  closeButton: HTMLButtonElement;
}

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: "16pt",
  },
});
const FontSizeThemeExtension: Extension = [FontSizeTheme];
const syntaxErrorDecoration = Decoration.mark({
  class: "syntax-error",
});

export class LambdaFactory extends Entity {
  private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
    Sprites.LAMBDA_FACTORY
  ) as SpriteSpec;

  private codeEditorState: CodeEditorState | null;
  private spawns: number;
  private code: string;

  constructor(gridPosition: Coord2D, code: string, spawns: number) {
    super(EntityNames.LambdaFactory);

    this.spawns = spawns;
    this.code = code;
    this.codeEditorState = null;

    this.addComponent(
      new BoundingBox(
        {
          x: 0,
          y: 0,
        },
        {
          width: LambdaFactory.spriteSpec.width,
          height: LambdaFactory.spriteSpec.height,
        },
        0
      )
    );

    this.addComponent(new Text(spawns.toString()));

    this.addComponent(new Colliding());

    this.addComponent(
      new GridSpawn(
        this.spawns,
        () => new FunctionBox({ x: 0, y: 0 }, this.code)
      )
    );

    this.addComponent(new Grid(gridPosition));

    this.addComponent(
      new Sprite(
        IMAGES.get(LambdaFactory.spriteSpec.sheet)!,
        { x: 0, y: 0 },
        {
          width: LambdaFactory.spriteSpec.width,
          height: LambdaFactory.spriteSpec.height,
        },
        LambdaFactory.spriteSpec.msPerFrame,
        LambdaFactory.spriteSpec.frames
      )
    );

    this.addComponent(
      new Highlight(
        (direction) => this.onHighlight(direction),
        () => this.onUnhighlight()
      )
    );
  }

  private onUnhighlight() {
    closeModal();
    this.removeComponent(ComponentNames.Interactable);
  }

  private spawnNewLambda(direction: Direction) {
    try {
      parse(this.code);
    } catch (e: any) {
      SOUNDS.get(Failure.name)!.play();
      return;
    }

    const spawner = this.getComponent<GridSpawn>(ComponentNames.GridSpawn);
    spawner.spawnEntity(direction);

    const textComponent = this.getComponent<Text>(ComponentNames.Text);
    textComponent.text = spawner.spawnsLeft.toString();
    this.addComponent(textComponent);

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

  private openCodeEditor() {
    const modalContent =
      "<div class='code'><div id='code'></div><br><p id='syntax-error' class='error'></p><button id='close-modal'>Save</button></div>";
    openModal(modalContent);

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

    const codeBox = document.getElementById("code")!;
    const syntaxError = document.getElementById("syntax-error")!;
    const canvas = document.getElementById(
      Miscellaneous.CANVAS_ID
    ) as HTMLCanvasElement;
    const closeButton = document.getElementById(
      "close-modal"
    ) as HTMLButtonElement;
    closeButton.addEventListener("click", () => this.saveAndCloseCodeEditor());

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

    this.codeEditorState = {
      view: editorView,
      editorElement: codeBox,
      syntaxError,
      canvas,
      closeButton,
    };

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

  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 saveAndCloseCodeEditor() {
    if (!this.codeEditorState) {
      return;
    }

    const { canvas, view, editorElement, syntaxError } = this.codeEditorState;
    const text = view.state.doc.toString();
    this.refreshCodeEditorText(text);
    syntaxError.innerText = "";

    try {
      parse(text);
    } catch (e: any) {
      if (!e.location) {
        return;
      }
      const {
        location: {
          start: { offset: start },
          end: { offset: end },
        },
      } = e;

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

      syntaxError.innerText = e.message;
      SOUNDS.get(Failure.name)!.play();
      return;
    }

    this.code = text;

    view.destroy();
    editorElement.innerHTML = "";
    this.codeEditorState = null;
    closeModal();

    canvas.focus();
    SOUNDS.get(LambdaSave.name)!.play();
  }

  private onHighlight(direction: Direction) {
    if (direction === Direction.LEFT || direction === Direction.RIGHT) {
      this.addComponent(new Interactable(() => this.spawnNewLambda(direction)));
      return;
    }

    const interaction = () => {
      if (this.codeEditorState) {
        this.saveAndCloseCodeEditor();
        return;
      }

      this.openCodeEditor();
    };

    this.addComponent(new Interactable(interaction));
  }
}
