import {
  AfterViewInit,
  Component,
  ElementRef,
  EventEmitter,
  HostListener,
  Input,
  OnDestroy,
  OnInit,
  Output,
  ViewChild,
} from "@angular/core";
declare var CodeMirror;
declare var twemoji;
import * as emojiRegex from "twemoji-parser/dist/lib/regex";
import { fromHexcodeToCodepoint, fromCodepointToUnicode } from "emojibase";
import * as emojiBaseData from "emojibase-data/en/data.json";
import * as emojiBaseShortcodes from "emojibase-data/en/shortcodes/joypixels.json";
import { styleChangedObserver } from "src/app/components/header/header.component";
//import data from 'emojibase-data/en/data.json';

import { DeviceDetectorService } from "ngx-device-detector";
import { RouterHandler } from "src/app/shared/services/router-handler.service";

@Component({
  selector: "app-markdown-editor",
  templateUrl: "./markdown-editor.component.html",
  styleUrls: ["./markdown-editor.component.scss"],
})
export class MarkdownEditorComponent
  implements OnInit, AfterViewInit, OnDestroy
{
  public static emojiUrl = "assets/img/twemoji/13.0.1/72x72/";
  public static emojiExt = "png";

  public toggledEmojiPopup: boolean = false;
  public toggleStyleMenu: boolean = false;

  public isInputComposing: boolean = false;
  public submitRequestStarted: boolean = false;

  private codeMirror;
  private doc; // Document from codemirror
  public messageLengthTooLong: boolean = false;
  public messageMaxLength: number = 1000;

  public isMessageEmpty: boolean = true;

  /**
   * All emoji from emojibase
   */
  public allEmoji: any;

  /**
   * Dict {emoji hex: shortcode}
   */
  public emojiHexcodeToShortcode: any;

  /**
   * Dict {shortcode: unicode}
   */
  public emojiShortcodeToUnicode: any;

  /**
   * true if emojiShortcodeToUnicode built
   */
  public shortCodesLoaded: boolean = false;

  public showShortCodePicker: boolean = true;

  /**
   * The actual prefix, what you typed in codemirror, it is passed to the
   * emoji shortcode picker component as Input
   */
  public shortcodePrefix: string = "";

  /**
   * Initial content of the editor
   */
  @Input("content") content?: string = "";

  /**
   * Editor DOM Node for CodeMirror
   */
  @ViewChild("editor") editor: ElementRef;
  @ViewChild("emojiBtn", { read: ElementRef }) emojiBtn: ElementRef;

  @Output("change") changeEvent = new EventEmitter<string>();
  @Output("submit") submitEvent = new EventEmitter<string>();
  @Output("close") closeEvent = new EventEmitter<boolean>();
  @Output("focusChange") focusChangeEvent = new EventEmitter<boolean>();

  @Input("autofocus") autofocus?: boolean = false; // autofocus
  @Input("messageSending") messageSending: boolean = false;

  // on mobile the enter input should line break instead of form submit
  private isOnDesktop: boolean = true;

  constructor(
    private ref: ElementRef,
    private deviceService: DeviceDetectorService,
    private routerHandler: RouterHandler
  ) {
    this.allEmoji = [];
    let opt: any = {
      shortcodes: ["discord"],
    };

    this.isOnDesktop = this.deviceService.isDesktop();

    // load from cdn
    /*fetchFromCDN("en/data.json", opt).then((res) => {
      this.allEmoji = res;
    });*/
    this.allEmoji = emojiBaseData; // load locally (csp)

    //fetchShortcodes("en", "discord").then((res) => {

    this.emojiHexcodeToShortcode = emojiBaseShortcodes;
    this.emojiShortcodeToUnicode = {};

    for (let key in this.emojiHexcodeToShortcode) {
      let arr: any;

      if (!Array.isArray(this.emojiHexcodeToShortcode[key])) {
        arr = [this.emojiHexcodeToShortcode[key]];
      } else {
        arr = this.emojiHexcodeToShortcode[key];
      }

      arr.forEach((el) => {
        let emoji = fromCodepointToUnicode(fromHexcodeToCodepoint(key));
        this.emojiShortcodeToUnicode[el] = emoji;
      });
    }

    this.shortCodesLoaded = true;
    //});
  }

  ngOnInit(): void {
    this.routerHandler.subscribe("room/?/chat/?", this.routerChange);
  }

  ngAfterViewInit(): void {
    // replace unicode emoji with twemoji
    let unicodeEmojiParse = (doc, change) => {
      let re = emojiRegex.default;
      let allMatches = [];

      for (
        let line = change.from.line, ch = 0, last = doc.lastLine();
        line <= last;
        line++, ch = 0
      ) {
        let string = doc.getLine(line);
        let match;

        // replace all the unicode emoji with twemoji in this line, using emojiRegex
        while ((match = re.exec(string))) {
          allMatches.push(match);
          let pos = match.index;

          let src;
          twemoji.parse(match[0], {
            callback: (icon, options) => {
              src = icon;
            },
          });

          let img = document.createElement("img");
          img.src =
            MarkdownEditorComponent.emojiUrl +
            src +
            "." +
            MarkdownEditorComponent.emojiExt;
          doc.markText(
            { line: line, ch: pos },
            { line: line, ch: pos + match[0].length },
            {
              atomic: true,
              replacedWith: img,
            }
          );
        }
      }
    };

    // replace shortcode emoji with twemoji
    let shortcodeEmojiParse = (doc, change) => {
      for (
        let line = change.from.line, ch = 0, last = doc.lastLine();
        line <= last;
        line++, ch = 0
      ) {
        let string = doc.getLine(line);
        let match;

        let shortcodeRe = /:([a-z0-9_]+):/gu;
        // search and replace shortcode emoji string with the real emoji

        let emojiShortReplace = [];
        while ((match = shortcodeRe.exec(string))) {
          if (match[1] in this.emojiShortcodeToUnicode) {
            emojiShortReplace.push({
              from: {
                line,
                ch: match.index,
              },
              to: {
                line,
                ch: match.index + match[0].length,
              },
              original: match[0], // original
              text: this.emojiShortcodeToUnicode[match[1]],
            });
          }
        }

        emojiShortReplace.forEach((replace) => {
          let src;
          twemoji.parse(replace.text, {
            callback: (icon, options) => {
              src = icon;
            },
          });
          let img = document.createElement("img");
          img.src =
            MarkdownEditorComponent.emojiUrl +
            src +
            "." +
            MarkdownEditorComponent.emojiExt;

          /*doc.markText(replace.from, replace.to, {
            atomic: true,
            replacedWith: img,
            selectLeft: true
          });*/
          this.doc.replaceRange(
            replace.text,
            replace.from, 
            replace.to
          );
        });
      }
      return true;
    };

    this.codeMirror = CodeMirror(this.editor.nativeElement, {
      value: "" + this.content,
      mode: "claraMarkdown",
      lineWrapping: true,
      placeholder: "Type message here...",
      autofocus: this.autofocus,
      extraKeys: {
        Enter: () => {
          if (this.isOnDesktop) {
            if (
              this.messageLengthTooLong ||
              this.codeMirror.getValue().trim().length == 0
            )
              return;

            this.submitEvent.emit(this.codeMirror.getValue());
          } else {
            this.insertEnter();
          }
        },
        "Alt-Enter": () => {
          this.insertEnter();
        },
        "Shift-Enter": () => {
          this.insertEnter();
        },
        "Ctrl-Enter": () => {
          this.insertEnter();
        },
        Esc: () => {
          this.closeEvent.emit(true);
        },
      },
    });

    this.codeMirror.getInputField().addEventListener("input", (ev) => {
      if (ev.isComposing) {
        this.isInputComposing = true;
      }
    });

    // detect and replace shortcode emoji ":emoji:" with twemoji <img>
    this.codeMirror.on("change", shortcodeEmojiParse);

    // detect and replace the unicode emoji with twemoji <img>
    this.codeMirror.on("change", unicodeEmojiParse);

    this.codeMirror.on("change", (ev) => {
      this.isInputComposing = false;
      let value = this.codeMirror.getValue();

      this.changeEvent.emit(value);

      if (this.submitRequestStarted) {
        this.submitRequestStarted = false;
        this.submitEvent.emit(this.codeMirror.getValue());
      }
    });

    // don't allow text length over 1000 characters
    this.codeMirror.on("change", (ev, change) => {
      var currentText: string = this.codeMirror.getValue();
      var newText: string = change.text;
      var fullText = currentText + newText;
      this.messageLengthTooLong = fullText.length > this.messageMaxLength;
    });

    // 1. show style-format menu, when text selected and
    // 2. show shortcode emoji helper popup, when ":xx" detected at the cursor
    this.codeMirror.on("cursorActivity", (doc) => {
      this.toggleStyleMenu = doc.somethingSelected();

      this.findEmojiShortcodeSyntaxAtCursor(
        (word, line, startPos, endPos) => {
          this.shortcodePrefix = word.substring(1);
        },
        () => {
          this.shortcodePrefix = "";
        }
      );
    });

    this.codeMirror.on("focus", () => {
      this.focusChangeEvent.emit(true);
    });
    this.codeMirror.on("blur", () => {
      this.focusChangeEvent.emit(false);
    });

    this.doc = this.codeMirror["getDoc"]();

    // on init replace all unicode and shortcode emoji with twemoji
    let allRange = {
      from: { line: 0, ch: 0 },
      to: { line: this.doc.lastLine(), ch: 0 },
    };
    unicodeEmojiParse(this.doc, allRange);
    shortcodeEmojiParse(this.doc, allRange);

    // style change dinamically
    let handleStyleChange = (style) => {
      switch (style) {
        case "light":
          this.codeMirror.setOption("theme", "default");
          break;
        case "dark":
          this.codeMirror.setOption("theme", "dracula");
          break;
      }
    };

    styleChangedObserver.subscribe(handleStyleChange);

    // we can not get the initial changes via observer in the afterViewInit
    // because it triggered in the app's constructor
    let styleMode = localStorage.getItem("style_mode");
    handleStyleChange(styleMode);
  }

  public messageSent(success: boolean) {
    if (success) this.codeMirror.setValue("");
  }

  public submit() {
    if (this.isInputComposing) {
      this.submitRequestStarted = true;
      this.isInputComposing = false;
    } else {
      this.submitEvent.emit(this.codeMirror.getValue());
    }
  }

  routerChange = (res) => {
    if (this.autofocus) {
      this.codeMirror.focus();
    }
  };

  handleEmojiInsert(emoji) {
    let cursor = this.doc.getCursor();
    this.doc.replaceRange(emoji, cursor);
  }

  handleEmojiAutocomplete(emoji) {
    //let insertText = ':'+emoji.shortcode+':'; // the viewer cant import the emoji library, so we should convert it into ascii-emoji
    let insertText = emoji.emoji;

    this.findEmojiShortcodeSyntaxAtCursor((word, line, startPos, endPos) => {
      this.doc.replaceRange(
        insertText,
        { line, ch: startPos },
        { line, ch: endPos }
      );
    });
  }

  /**
   * Get the word under the CodeMirror cursor position. If it is match the
   * emoji shortcode syntax prefix, like ":something", it will call the foundCallback
   * otherwise it is not an emoji shortcode prefix, so call the notfoundCallback
   * @param foundCallback callback, when the cursor under an emoji shortcode prefix, like ":smil"
   *    parameters: foundCallback(word, line, startPos, endPos)
   *      word: the found word under the cursor with the ":" character
   *      line: the line in the CodeMirror, where the word found
   *      startPos: the character position on the "line" line, where the word start
   *      endPos: the character position on the "line" line, where the word end
   * @param notFoundCallback callback, when the cursor not under an amoji shortcode prefix
   * @param minLength the minimal expected length of the word after the ":", for example: ":em" == 2
   */
  private findEmojiShortcodeSyntaxAtCursor(
    foundCallback,
    notFoundCallback?,
    minLength = 2
  ) {
    // looking for :prefix emoji shortcode at the current cursor position
    let A1 = this.codeMirror.getCursor().line;
    let A2 = this.codeMirror.getCursor().ch;

    let B1 = this.codeMirror.findWordAt({ line: A1, ch: A2 }).anchor.ch;
    let B2 = this.codeMirror.findWordAt({ line: A1, ch: A2 }).head.ch;

    if (B1 > 0) {
      // if its zero, you can not start with ":", because it is a whitespace for findWordAt
      B1--; // include the prev character too, what is should be ":"
      let word = this.codeMirror.getRange(
        { line: A1, ch: B1 },
        { line: A1, ch: B2 }
      );
      if (word.charAt(0) == ":" && word.length > minLength) {
        if (foundCallback) foundCallback(word, A1, B1, B2);
      } else {
        if (notFoundCallback) notFoundCallback();
      }
    } else {
      if (notFoundCallback) notFoundCallback();
    }
  }

  ngOnDestroy(): void {
    this.routerHandler.unsubscribe("room/?/chat/?", this.routerChange);
  }

  // for style-menu only
  private tokens = {
    bold: { before: "**", after: "**" },
    italics: { before: "*", after: "*" },
    underline: { before: "__", after: "__" },
    strikethrough: { before: "~~", after: "~~" },
    quotes: { before: "\n> ", after: "" },
    code: { before: "`", after: "`" },
  };

  /**
   * Set the actual markdown format around the selected texts
   * @param format string, the name of the format, what refers to the this.tokens arr
   */
  public setFormat(format) {
    let token = this.tokens[format];
    if (token) {
      let selections = this.doc.getSelections();
      for (let i = 0; i < selections.length; i++) {
        selections[i] = token.before + selections[i] + token.after;
      }
      this.doc.replaceSelections(selections);
    }
  }

  public insertEnter() {
    this.doc.replaceRange("\r\n", this.doc.getCursor());
  }

  @HostListener("document:click", ["$event"])
  documentClick(event: MouseEvent) {
    var emojiPickerElement =
      this.ref.nativeElement.querySelector("app-emoji-picker");
    var emojiButtonElement = this.emojiBtn.nativeElement;

    if (
      !emojiPickerElement.contains(event.target) &&
      !emojiButtonElement.contains(event.target)
    ) {
      this.toggledEmojiPopup = false;
    }
  }

  public onToggleEmojiPopup() {
    this.toggledEmojiPopup = !this.toggledEmojiPopup;
    if (this.toggledEmojiPopup) {
      history.pushState({ emoji: true }, "");
    }
  }

  @HostListener("window:popstate", ["$event"])
  onWindowPopState($event) {
    this.toggledEmojiPopup = false;
  }
}
