import fTextSettingsResource from "../data/fTextSettingsResource";

import * as OT from "operational-transform";

(<any>window).CodeMirror = require("codemirror");
require("codemirror/addon/search/search");
require("codemirror/addon/search/searchcursor");
require("codemirror/addon/edit/closebrackets");
require("codemirror/addon/comment/comment");
require("codemirror/addon/hint/show-hint");
require("codemirror/addon/selection/active-line");
require("codemirror/keymap/sublime");
require("codemirror/mode/javascript/javascript");
require("codemirror/mode/clike/clike");

// added for fText plugin
require("codemirror/addon/fold/foldcode");
require("codemirror/addon/fold/foldgutter");
require("codemirror/addon/fold/brace-fold");
require("codemirror/addon/fold/comment-fold");
require("codemirror/addon/fold/indent-fold");
require("codemirror/addon/fold/xml-fold");
require("codemirror/addon/fold/markdown-fold");

require("codemirror/addon/search/match-highlighter");
require("codemirror/addon/edit/matchtags"); // depends on xml-fold
require("codemirror/addon/edit/trailingspace");
require("codemirror/addon/edit/closetag"); // depends on xml-fold

require("codemirror/addon/selection/active-line");
require("codemirror/addon/hint/anyword-hint");

require("../codemirror-linters/lint");
(<any>window).CSSLint = require("csslint").CSSLint;
require("codemirror/addon/lint/css-lint");
(<any>window).JSHINT = require("jshint").JSHINT;
require("codemirror/addon/lint/javascript-lint");
(<any>window).jsonlint = require("jsonlint");
require("../codemirror-linters/json-lint");
(<any>window).csonparser = require("cson-parser");
require("../codemirror-linters/cson-lint");
(<any>window).jade = require("jade");
require("../codemirror-linters/jade-lint");
(<any>window).stylus = require("stylus");
require("../codemirror-linters/stylus-lint");
(<any>window).jsyaml = require("js-yaml");
require("../codemirror-linters/yaml-lint");

require("codemirror/keymap/emacs");
require("codemirror/keymap/vim");

require("codemirror/mode/htmlmixed/htmlmixed"); // load js, css, xml
require("codemirror/mode/jade/jade");
require("codemirror/mode/markdown/markdown"); // load xml
require("codemirror/mode/coffeescript/coffeescript");
require("codemirror/mode/stylus/stylus");
require("codemirror/mode/yaml/yaml");
// /added for fText plugin

class fTextEditorWidget {
  textEditorResource: fTextSettingsResource;
  codeMirrorInstance: CodeMirror.EditorFromTextArea;

  editCallback: EditCallback;
  sendOperationCallback: SendOperationCallback;
  saveCallback: Function;

  clientId: number;
  tmpCodeMirrorDoc = new CodeMirror.Doc("");
  texts: string[] = [];

  undoTimeout: number;
  undoStack: OT.TextOperation[] = [];
  undoQuantityByAction: number[] = [0];
  redoStack: OT.TextOperation[] = [];
  redoQuantityByAction: number[] = [0];

  sentOperation: OT.TextOperation;
  pendingOperation: OT.TextOperation;

  useSoftTab = true; // use spaces as tabs = true (indent with tabs = false)

  constructor(projectClient: SupClient.ProjectClient, clientId: number, textArea: HTMLTextAreaElement, options: TextEditorWidgetOptions) {
    let extraKeys: { [name: string]: string|Function } = {
      "F9": () => {},
      "Tab": (cm: any) => {
        if (cm.getSelection() !== "") cm.execCommand("indentMore")
        else {
          if (this.useSoftTab) cm.execCommand("insertSoftTab");
          else cm.execCommand("insertTab");
        }
      },
      "Cmd-X": () => { document.execCommand("cut"); },
      "Cmd-C": () => { document.execCommand("copy"); },
      "Cmd-V": () => { document.execCommand("paste"); },
      "Ctrl-Z": () => { this.undo(); },
      "Cmd-Z": () => { this.undo(); },
      "Shift-Ctrl-Z": () => { this.redo(); },
      "Shift-Cmd-Z": () => { this.redo(); },
      "Ctrl-Y": () => { this.redo(); },
      "Cmd-Y": () => { this.redo(); },
      "Ctrl-S": () => { this.saveCallback(); },
      "Cmd-S": () => { this.saveCallback(); }
    }
    if (options.extraKeys != null) {
      for (let keyName in options.extraKeys) {
        extraKeys[keyName] = options.extraKeys[keyName];
      }
    }

    this.editCallback = options.editCallback;
    this.sendOperationCallback = options.sendOperationCallback;
    this.saveCallback = options.saveCallback;

    let editorConfig: any = {
      // theme: "monokai",
      lineNumbers: true, matchBrackets: true, styleActiveLine: true, autoCloseBrackets: true,
      gutters: ["CodeMirror-linenumbers"],
      indentWithTabs: false, indentUnit: 2, tabSize: 2,
      extraKeys: extraKeys, keyMap: "sublime",
      viewportMargin: Infinity,
      mode: options.mode,
      readOnly: true
    };
    this.codeMirrorInstance = CodeMirror.fromTextArea(textArea, editorConfig);

    this.codeMirrorInstance.on("changes", <any>this.edit);
    this.codeMirrorInstance.on("beforeChange", this.beforeChange);
    
    this.clientId = clientId;
    projectClient.subResource("fTextSettings", this);
  }

  setText(text: string) {
    this.codeMirrorInstance.getDoc().setValue(text);
    this.codeMirrorInstance.getDoc().clearHistory();
    this.codeMirrorInstance.setOption("readOnly", false);

    this.codeMirrorInstance.focus();
  }
  
  beforeChange = (instance: CodeMirror.Editor, change: any) => {
    if (change.origin === "setValue" || change.origin === "network") return;
    let lastText = instance.getDoc().getValue();
    if (lastText !== this.texts[this.texts.length - 1]) this.texts.push(lastText);
  }

  edit = (instance: CodeMirror.Editor, changes: CodeMirror.EditorChange[]) => {
    if (this.editCallback != null)
      this.editCallback(this.codeMirrorInstance.getDoc().getValue(), (<any>changes[0]).origin);

    let undoRedo = false;
    let operationToSend: OT.TextOperation;
    for (let changeIndex = 0; changeIndex < changes.length; changeIndex++) {
      let change = changes[changeIndex];
      let origin: string = (<any>change).origin;

      // Modification from an other person
      if (origin === "setValue" || origin ==="network") continue;

      this.tmpCodeMirrorDoc.setValue(this.texts[changeIndex]);

      let operation = new OT.TextOperation(this.clientId);
      for (let line = 0; line < change.from.line; line++) operation.retain(this.tmpCodeMirrorDoc.getLine(line).length + 1);
      operation.retain(change.from.ch);

      let offset = 0;
      if (change.removed.length !== 1 || change.removed[0] !== "") {
        for (let index = 0; index < change.removed.length; index++) {
          let text = change.removed[index];
          if (index !== 0) {
            operation.delete("\n");
            offset += 1;
          }

          operation.delete(text);
          offset += text.length;
        }
      }

      if (change.text.length !== 1 || change.text[0] !== "") {
        for (let index = 0; index < change.text.length; index++) {
          if (index !== 0) operation.insert("\n");
          operation.insert(change.text[index]);
        }
      }

      let beforeLength = (operation.ops[0].attributes.amount != null) ? operation.ops[0].attributes.amount : 0;
      operation.retain(this.tmpCodeMirrorDoc.getValue().length - beforeLength - offset);

      if (operationToSend == null) operationToSend = operation.clone();
      else operationToSend = operationToSend.compose(operation);

      if (origin === "undo" || origin === "redo") undoRedo = true;
    }

    this.texts.length = 0;
    if (operationToSend == null) return;

    if (! undoRedo) {
      if (this.undoTimeout != null) {
        clearTimeout(this.undoTimeout);
        this.undoTimeout = null;
      }

      this.undoStack.push(operationToSend.clone().invert());
      this.undoQuantityByAction[this.undoQuantityByAction.length - 1] += 1;
      if (this.undoQuantityByAction[this.undoQuantityByAction.length - 1] > 20) this.undoQuantityByAction.push(0);
      else {
        this.undoTimeout = window.setTimeout(() => {
          this.undoTimeout = null;
          this.undoQuantityByAction.push(0);
        }, 500);
      }

      this.redoStack.length = 0;
      this.redoQuantityByAction.length = 0;
    }

    if (this.sentOperation == null) {
      this.sendOperationCallback(operationToSend.serialize());

      this.sentOperation = operationToSend;
    } else {
      if (this.pendingOperation != null) this.pendingOperation = this.pendingOperation.compose(operationToSend);
      else this.pendingOperation = operationToSend;
    }
  }

  receiveEditText(operationData: OperationData) {
    if (this.clientId === operationData.userId) {
      if (this.pendingOperation != null) {
        this.sendOperationCallback(this.pendingOperation.serialize());
  
        this.sentOperation = this.pendingOperation;
        this.pendingOperation = null;
      } else this.sentOperation = null;
      return;
    }

    // Transform operation and local changes
    let operation = new OT.TextOperation();
    operation.deserialize(operationData);

    if (this.sentOperation != null) {
      [this.sentOperation, operation] = this.sentOperation.transform(operation);

      if (this.pendingOperation != null) [this.pendingOperation, operation] = this.pendingOperation.transform(operation);
    }
    this.undoStack = transformStack(this.undoStack, operation);
    this.redoStack = transformStack(this.redoStack, operation);

    this.applyOperation(operation.clone(), "network", false);
  }

  applyOperation(operation: OT.TextOperation, origin: string, moveCursor: boolean) {
    let cursorPosition = 0;
    let line = 0;
    for (let op of operation.ops) {
      switch (op.type) {
        case "retain": {
          while (true) {
            if (op.attributes.amount <= this.codeMirrorInstance.getDoc().getLine(line).length - cursorPosition) break;
  
            op.attributes.amount -= this.codeMirrorInstance.getDoc().getLine(line).length + 1 - cursorPosition;
            cursorPosition = 0;
            line++;
          }
  
          cursorPosition += op.attributes.amount;
          break;
        }
        case "insert": {
          let cursor = this.codeMirrorInstance.getDoc().getCursor();
  
          let texts = op.attributes.text.split("\n");
          for (let textIndex = 0; textIndex < texts.length; textIndex++) {
            let text = texts[textIndex];
            if (textIndex !== texts.length - 1) text += "\n";
            (<any>this.codeMirrorInstance).replaceRange(text, { line, ch: cursorPosition }, null, origin);
            cursorPosition += text.length;
  
            if (textIndex !== texts.length - 1) {
              cursorPosition = 0;
              line++;
            }
          }
  
          if (line === cursor.line && cursorPosition === cursor.ch) {
            if (! operation.gotPriority(this.clientId)) {
              for (let i = 0; i < op.attributes.text.length; i++) (<any>this.codeMirrorInstance).execCommand("goCharLeft");
            }
          }
  
          if (moveCursor) (<any>this.codeMirrorInstance).setCursor(line, cursorPosition);
          //use this way insted ? this.codeMirrorInstance.getDoc().setCursor({ line, ch: cursorPosition });
          break;
        }
        case "delete": {
          let texts = op.attributes.text.split("\n");
  
          for (let textIndex = 0; textIndex < texts.length; textIndex++) {
            let text = texts[textIndex];
            if (texts[textIndex + 1] != null) (<any>this.codeMirrorInstance).replaceRange("", { line, ch: cursorPosition }, { line: line + 1, ch: 0 }, origin);
            else (<any>this.codeMirrorInstance).replaceRange("", { line, ch: cursorPosition }, { line, ch: cursorPosition + text.length }, origin);
  
            if (moveCursor) (<any>this.codeMirrorInstance).setCursor(line, cursorPosition);
          }
          break;
        }
      }
    }
  }

  undo() {
    if (this.undoStack.length === 0) return;

    if (this.undoQuantityByAction[this.undoQuantityByAction.length - 1] === 0) this.undoQuantityByAction.pop();
    let undoQuantityByAction = this.undoQuantityByAction[this.undoQuantityByAction.length - 1];
  
    for (let i = 0; i < undoQuantityByAction; i++) {
      let operationToUndo = this.undoStack[this.undoStack.length - 1];
      this.applyOperation(operationToUndo.clone(), "undo", true);
  
      this.undoStack.pop()
      this.redoStack.push(operationToUndo.invert());
    }
  
    if (this.undoTimeout != null) {
      clearTimeout(this.undoTimeout);
      this.undoTimeout = null;
    }
  
    this.redoQuantityByAction.push(this.undoQuantityByAction[this.undoQuantityByAction.length - 1]);
    this.undoQuantityByAction[this.undoQuantityByAction.length - 1] = 0;
  }

  redo() {
    if (this.redoStack.length === 0) return;

    let redoQuantityByAction = this.redoQuantityByAction[this.redoQuantityByAction.length - 1]
    for (let i = 0; i < redoQuantityByAction; i++) {
      let operationToRedo = this.redoStack[this.redoStack.length - 1];
      this.applyOperation(operationToRedo.clone(), "undo", true);
  
      this.redoStack.pop()
      this.undoStack.push(operationToRedo.invert());
    }
  
    if (this.undoTimeout != null) {
      clearTimeout(this.undoTimeout);
      this.undoTimeout = null;
  
      this.undoQuantityByAction.push(this.redoQuantityByAction[this.redoQuantityByAction.length - 1]);
    }
    else this.undoQuantityByAction[this.undoQuantityByAction.length - 1] = this.redoQuantityByAction[this.redoQuantityByAction.length - 1];
  
    this.undoQuantityByAction.push(0);
    this.redoQuantityByAction.pop();
  }

  clear() {
    if (this.undoTimeout != null) clearTimeout(this.undoTimeout);
  }

  onResourceReceived = (resourceId: string, resource: fTextSettingsResource) => {
    this.textEditorResource = resource;

    this.codeMirrorInstance.setOption("tabSize", resource.pub.tabSize);
    this.codeMirrorInstance.setOption("indentUnit", resource.pub.tabSize);
    this.codeMirrorInstance.setOption("indentWithTabs", resource.pub.indentWithTabs);
    this.useSoftTab = !resource.pub.indentWithTabs;
  }

  onResourceEdited = (resourceId: string, command: string, propertyName: string) => {
    switch(propertyName) {
      case "tabSize":
        this.codeMirrorInstance.setOption("tabSize", this.textEditorResource.pub.tabSize);
        this.codeMirrorInstance.setOption("indentUnit", this.textEditorResource.pub.tabSize);
        break;
      case "indentWithTabs":
        this.useSoftTab = !this.textEditorResource.pub.indentWithTabs;
        this.codeMirrorInstance.setOption("indentWithTabs", this.textEditorResource.pub.indentWithTabs);
        break;
    }
  }
}
export = fTextEditorWidget;

function transformStack(stack: OT.TextOperation[], operation: OT.TextOperation) {
  if (stack.length === 0) return stack;

  let newStack: OT.TextOperation[] = [];
  for (let i = stack.length - 1; i > 0; i--) {
    let pair = stack[i].transform(operation);
    newStack.push(pair[0]);
    operation = pair[1];
  }
  return newStack.reverse();
}
