import { AbstractEditor, EditorDelegate, Marker } from "./types";
import { basicSetup } from "codemirror";
import {
  indentMore,
  indentLess,
  toggleLineComment,
} from "@codemirror/commands";
import { EditorView, hoverTooltip, keymap, Tooltip } from "@codemirror/view";
import { Compartment, Prec } from "@codemirror/state";
import { oneDark } from "@codemirror/theme-one-dark";
import { linter } from "@codemirror/lint";
import {
  bracketMatching,
  indentService,
  LanguageSupport,
  StreamLanguage,
  StringStream,
} from "@codemirror/language";
import { ABBREV } from "./abbrev.js";

// prettier flattened this...
const keywords = [
  "let",
  "in",
  "where",
  "case",
  "of",
  "data",
  "derive",
  "U",
  "do",
  "ptype",
  "pfunc",
  "module",
  "infixl",
  "infixr",
  "infix",
  "∀",
  "forall",
  "import",
  "uses",
  "class",
  "instance",
  "record",
  "constructor",
  "if",
  "then",
  "else",
  "$",
  "λ",
  "?",
  "@",
  ".",
  "->",
  "→",
  ":",
  "=>",
  ":=",
  "$=",
  "=",
  "<-",
  "\\",
  "_",
  "|",
];

// a stack of tokenizers, current is first
// we need to push / pop {} so we can parse strings correctly
interface State {
  tokenizers: Tokenizer[];
}
type Tokenizer = (stream: StringStream, state: State) => string | null;

function tokenizer(stream: StringStream, state: State): string | null {
  if (stream.eatSpace()) return null;
  if (stream.match("--")) {
    stream.skipToEnd();
    return "comment";
  }
  // maybe keyword?
  if (stream.match(/{/)) {
    state.tokenizers.unshift(tokenizer);
    return null;
  }
  if (stream.match(/}/) && state.tokenizers.length > 1) {
    state.tokenizers.shift();
    return state.tokenizers[0] === stringTokenizer ? "keyword" : null;
  }
  if (stream.match(/^[/]-/)) {
    state.tokenizers.unshift(commentTokenizer);
    return state.tokenizers[0](stream, state);
  }
  if (stream.match(/"/)) {
    state.tokenizers.unshift(stringTokenizer);
    return stringTokenizer(stream, state);
  }
  // TODO match tokenizer better..
  if (stream.match(/[^\\(){}[\],.@;\s][^()\\{}\[\],.@;\s]*/)) {
    let word = stream.current();
    if (keywords.includes(word)) return "keyword";
    if (word[0] >= "A" && word[0] <= "Z") return "typeName";
    return "variableName";
  }
  // unhandled
  stream.next();
  return null;
}

function stringTokenizer(stream: StringStream, state: State) {
  while (true) {
    if (stream.current() && stream.match(/^\\{/, false)) {
      return "string";
    }
    if (stream.match(/^\\{/)) {
      state.tokenizers.unshift(tokenizer);
      return "keyword";
    }
    let ch = stream.next();
    if (!ch) return "string";
    if (ch === '"') {
      state.tokenizers.shift();
      return "string";
    }
  }
}

// We have a tokenizer for this because codemirror processes a line at a time.
// So we may need to end the line in `comment` state and see the -/ later
function commentTokenizer(stream: StringStream, state: State): string | null {
  console.log("ctok");
  let dash = false;
  let ch;
  while ((ch = stream.next())) {
    if (dash && ch === "/") {
      state.tokenizers.shift();
      return "comment";
    }
    dash = ch === "-";
  }
  return "comment";
}

const newtLanguage2: StreamLanguage<State> = StreamLanguage.define({
  startState: () => ({ tokenizers: [tokenizer] }),
  token(stream, st) {
    return st.tokenizers[0](stream, st);
  },
  languageData: {
    commentTokens: {
      line: "--",
    },
    // The real list would include almost every character.
    wordChars: "!#$%^&*_+-=<>|",
  },
});

export function scheme() {
  return new LanguageSupport(schemeLanguage);
}

const schemeLanguage: StreamLanguage<State> = StreamLanguage.define({
  startState: () => null,
  token(stream, st) {
    const keywords = ["define", "let", "case", "cond", "import", "include", "lambda", "else"];
    if (stream.eatSpace()) return null;
    if (stream.match("--")) {
      stream.skipToEnd();
      return "comment";
    }
    if (stream.match(/[0-9A-Za-z!%&*+./:<=>?@^_~-]+/)) {
      let word = stream.current();
      if (keywords.includes(word)) return "keyword";
      return null;
    }
    // unhandled
    stream.next();
    return null
  },
  languageData: {
    commentTokens: {
      line: ";;",
    },
    wordChars: "!%&*+-./:<=>?@^_~",
  },
});

function newt() {
  return new LanguageSupport(newtLanguage2);
}

export class CMEditor implements AbstractEditor {
  view: EditorView;
  delegate: EditorDelegate;
  theme: Compartment;
  constructor(container: HTMLElement, doc: string, delegate: EditorDelegate) {
    this.delegate = delegate;
    this.theme = new Compartment();

    this.view = new EditorView({
      doc,
      parent: container,
      extensions: [
        basicSetup,
        linter((view) => this.delegate.lint(view)),
        // For indent on return
        indentService.of((ctx, pos) => {
          let line = ctx.lineAt(pos);
          if (!line || !line.from) return null;
          let prevLine = ctx.lineAt(line.from - 1);
          if (prevLine.text.trimEnd().match(/\b(of|where|do)\s*$/)) {
            let pindent = prevLine.text.match(/^\s*/)?.[0].length ?? 0;
            return pindent + 2;
          }
          return null;
        }),
        Prec.highest(
          keymap.of([
            {
              key: "Tab",
              preventDefault: true,
              run: indentMore,
            },
            {
              key: "Cmd-/",
              run: toggleLineComment,
            },
            // ok, we can do multiple keys (we'll want this for Idris)
            {
              key: "c-c c-s",
              run: () => {
                console.log("C-c C-s");
                return false;
              },
            },
            {
              key: "Shift-Tab",
              preventDefault: true,
              run: indentLess,
            },
          ])
        ),
        EditorView.updateListener.of((update) => {
          let doc = update.state.doc;
          update.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
            if (" ')\\_".includes(inserted.toString()) || inserted.lines > 1) {
              console.log("changes", update.changes, update.changes.desc);
              let line = doc.lineAt(fromA);
              let e = fromA - line.from;
              const m = line.text.slice(0, e).match(/(\\[^ ]+)$/);
              if (m) {
                let s = e - m[0].length;
                let key = line.text.slice(s, e);
                if (ABBREV[key]) {
                  this.view.dispatch({
                    changes: {
                      from: line.from + s,
                      to: fromA,
                      insert: ABBREV[key],
                    },
                  });
                }
              }
            }
          });
        }),
          this.theme.of(EditorView.baseTheme({})),
          hoverTooltip(async (view, pos) => {
            let cursor = this.view.state.doc.lineAt(pos);
            let line = cursor.number - 1;
            let col = pos - cursor.from;
            // let range = this.view.state.wordAt(pos);
            console.log('getting hover for ',line, col);
            let entry = await this.delegate.getEntry('', line, col)

            if (entry) {
              let rval: Tooltip = {
                // TODO pull in position from LSP (currently it only has the jump-to FC)
                pos,
                above: true,
                create: () => {
                  let dom = document.createElement("div");
                  dom.className = "tooltip";
                  dom.textContent = entry.info;
                  return { dom };
                },
              };
              return rval;
            }

            // we'll iterate the syntax tree for word.
            // let entry = delegate.getEntry(word, line, col)
            return null;
          }),
        newt(),
      ],
    });
  }
  setDark(isDark: boolean) {
    this.view.dispatch({
      effects: this.theme.reconfigure(
        isDark ? oneDark : EditorView.baseTheme({})
      ),
    });
  }
  setValue(_doc: string) {
    // Is this all we can do?
    this.view.dispatch({
      changes: { from: 0, to: this.view.state.doc.length, insert: _doc },
    });
  }
  getValue() {
    // maybe?
    return this.view.state.doc.toString();
  }
  setMarkers(_: Marker[]) {}
}
