import {
  AfterViewInit,
  Component,
  ElementRef,
  EventEmitter,
  HostListener,
  Input,
  OnChanges,
  OnDestroy,
  OnInit,
  Output,
  SimpleChanges,
  TemplateRef,
  ViewChild,
} from '@angular/core';
import { createEditor, Editor, Element, Node, Range, Scrubber, Selection, Transforms } from 'slate';
import { AngularEditor, withAngular } from 'slate-angular';
import { withHistory } from 'slate-history';
import { SlateEditorLeafComponent } from './slate-components/slate-editor-leaf/slate-editor-leaf.component';
import { SlateEmojiComponent } from './slate-components/slate-emoji/slate-emoji/slate-emoji.component';
import { CODE_LINE_NODE_TYPE, withMarkdown } from './with-markdown';
import { withUnit } from './with-unit';

import { marker } from '@biesbjerg/ngx-translate-extract-marker';
import { DeviceDetectorService } from 'ngx-device-detector';
import { RouterHandler } from 'src/app/shared/services/router-handler.service';
import { markdownTokens, TOKEN_TYPE } from './markdown';
import { SlateCodeLineComponent } from './slate-components/slate-code-line/slate-code-line.component';
import { Subscription } from 'rxjs';
import { focusChatInput } from '../../focus-chat-input-observer';

@Component({
  selector: 'app-slate-editor',
  templateUrl: './slate-editor.component.html',
  styleUrls: ['./slate-editor.component.scss'],
})
export class SlateEditorComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges {
  /**
   * Initial content of the editor
   */
  @Input() content?: string = '';
  @ViewChild('emojiBtn', { read: ElementRef }) emojiBtn: ElementRef;
  @ViewChild('slateEditorElement', { read: ElementRef }) slateEditorElement: ElementRef;
  @ViewChild('styleMenu', { read: ElementRef }) styleMenu: ElementRef;

  /**
   * This editor is a message editor input (and not the new message input)
   */
  @Input() isMsgEdit?: boolean = false;

  @Output() readonly changeEvent = new EventEmitter<string>();
  @Output() readonly submitEvent = new EventEmitter<string>();
  @Output() readonly closeEvent = new EventEmitter<boolean>();
  @Output() readonly focusChangeEvent = new EventEmitter<boolean>();

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

  public toggledEmojiPopup: boolean = false;
  public messageLengthTooLong: boolean = false;
  public messageMaxLength: number = 1000;
  public toggleStyleMenu: boolean = false;

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

  defaultValue = [
    {
      type: 'paragraph',
      children: [{ text: '' }],
    },
  ];
  public value = {
    type: 'paragraph',
    children: [{ text: '' }],
  };
  public plainText = '';
  public placeholder = marker('Type message here...');

  /**
   * For emoji insert, or style toggle we should know the last selection
   */
  public lastSelection: Selection | null = null;

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

  @ViewChild('paragraph', { read: TemplateRef, static: true })
  paragraphTemplate!: TemplateRef<any>;
  editor = withSaveLastSelection(withMarkdown(withUnit(withAngular(withHistory(createEditor())))));

  private focusChatInputSubsctiption: Subscription;

  constructor(
    private ref: ElementRef,
    private deviceService: DeviceDetectorService,
    private routerHandler: RouterHandler
  ) {
    this.isOnDesktop = this.deviceService.isDesktop();

    // check changes events for emojiPrefix
    const { onChange } = this.editor;
    this.editor.onChange = (d) => {
      onChange(d); // keep the prev behaviour
      // check the word under the cursor
      if (this.editor.selection) {
        if (Range.isCollapsed(this.editor.selection)) {
          let node = Node.get(this.editor, this.editor.selection.anchor.path);
          if (node['token'] && node['token'].includes(TOKEN_TYPE.EMOJI_SHORTCODE_STARTED)) {
            this.shortcodePrefix = Node.string(node).substring(1); // cut the ":" part
          } else {
            this.shortcodePrefix = '';
          }
          this.toggleStyleMenu = false;
        } else {
          this.toggleStyleMenu = true;
          this.shortcodePrefix = '';
        }
        //console.log('curr node', Node.get(this.editor, this.editor.selection.anchor.path));
      }

      if (this.isOnDesktop) {
        this.focusChatInputSubsctiption = focusChatInput.subscribe(() => {
          AngularEditor.focus(this.editor);
        });
      }
    };

    /*
    window['editor'] = this.editor;
    window['Transforms'] = Transforms;
    //window['Path'] = Path;
    window['Editor'] = Editor;
    window['node'] = Node;
    window['text'] = Text;
    window['range'] = Range;
    */
  }

  ngAfterViewInit(): void {
    Transforms.insertNodes(this.editor, this.defaultValue);

    if (this.content && this.content.length > 0) {
      Transforms.select(this.editor, [0]);
      this.insertText(this.content);
    }

    this.slateEditorElement.nativeElement.addEventListener('focus', () => {
      this.focusChangeEvent.emit(true);
    });
    this.slateEditorElement.nativeElement.addEventListener('blur', () => {
      this.focusChangeEvent.emit(false);
    });

    if (this.autofocus) {
      AngularEditor.focus(this.editor);
    }
  }

  ngOnChanges(changes: SimpleChanges): void {
    if (changes['content']) {
      if (
        this.content != undefined &&
        this.content != null &&
        this.editor.children.length > 0 &&
        this.plainText != this.content
      ) {
        this.emptyEditor();
        this.insertText(this.content);
      }
    }
  }

  public keydown = (e: KeyboardEvent) => {
    if (e.key == 'Escape') {
      this.closeEvent.emit(true);
    } else if (e.key == 'Enter') {
      if (e.altKey || e.ctrlKey || e.shiftKey || !this.isOnDesktop) {
        // just insert enter normally
      } else {
        e.preventDefault(); // prevent inserting enter
        this.submitEvent.emit(this.plainText);
      }
    }
  };

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

  valueChange(value?: Element[]) {
    if (this.editor.children.length == 0) {
      Transforms.insertNodes(this.editor, this.defaultValue);
      if (this.content) {
        this.insertText(this.content);
      }
    }
    this.plainText = Node.string(Node.get(this.editor, [0]));
    for (let i = 1; i < this.editor.children.length; i++) {
      this.plainText += '\n' + Node.string(Node.get(this.editor, [i]));
    }
    this.messageLengthTooLong = this.plainText.length > this.messageMaxLength;
    this.changeEvent.emit(this.plainText);
  }

  renderElement = (element: any) => {
    switch (element.type) {
      case 'emoji':
        return SlateEmojiComponent;
      case CODE_LINE_NODE_TYPE:
        return SlateCodeLineComponent;
      default:
        return this.paragraphTemplate;
    }
  };

  renderLeaf = (text: any) => {
    //console.log('leaf', text);
    return SlateEditorLeafComponent;
  };

  public submit() {
    AngularEditor.focus(this.editor);
    this.submitEvent.next(this.plainText);
  }

  public messageSent(success: boolean) {
    if (success) this.emptyEditor();
  }

  public handleEmojiInsert(emoji) {
    this.insertText(emoji);
  }

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

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

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

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

    if (
      !this.ref.nativeElement.contains(event.target) &&
      !emojiShortcodePickerElement.contains(event.target)
    ) {
      this.shortcodePrefix = '';
    }

    if (
      !this.ref.nativeElement.contains(event.target) &&
      (!this.styleMenu || !this.styleMenu.nativeElement.contains(event.target))
    ) {
      this.toggleStyleMenu = false;
    }
  }

  routerChange = (res) => {
    if (this.autofocus && this.isOnDesktop) {
      AngularEditor.focus(this.editor);
    }

    if (!this.isMsgEdit) {
      // on router change reset new message field, leave msg editor fields

      this.emptyEditor();
    }
  };

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

  private emptyEditor() {
    Transforms.insertText(this.editor, '.', {
      at: [
        this.editor.children.length - 1,
        this.editor.children[this.editor.children.length - 1].children.length - 1,
      ],
    });
    Transforms.select(this.editor, []);
    Transforms.select(this.editor, {
      anchor: Editor.start(this.editor, []),
      focus: Editor.end(this.editor, []),
    });
    Transforms.delete(this.editor);
  }

  public handleEmojiAutocomplete(event) {
    // it should be, because we wrote some emoji prefix
    if (this.editor['lastSelection']) {
      Transforms.select(this.editor, this.editor['lastSelection'].anchor.path);
      Transforms.delete(this.editor); // clear the prev word
      // insert the new
      this.insertText(event.emoji);
    }
  }

  /**
   * Insert text at the selection
   * We should call the editor's insertData because it triggers
   * the token parser
   * @param text
   */
  private insertText(text: string) {
    if (!this.editor.selection && this.editor['lastSelection']) {
      Transforms.select(this.editor, this.editor['lastSelection']);
    }

    if (this.editor.selection) {
      let dataTransfer = new DataTransfer();
      dataTransfer.setData('text/plain', text);
      this.editor.insertData(dataTransfer);
    }
  }

  private deleteWordAtCursor() {
    let { path, offset } = this.editor.selection.anchor;
    let nodeString = Node.string(Node.get(this.editor, path));
    let findWordStartOffset = offset;

    while (findWordStartOffset > 0 && nodeString[findWordStartOffset - 1] != ' ') {
      findWordStartOffset--;
    }

    let findWordEndOffset = offset;

    while (findWordEndOffset < nodeString.length && nodeString[findWordEndOffset] != ' ') {
      findWordEndOffset++;
    }

    Transforms.select(this.editor, {
      anchor: {
        path,
        offset: findWordStartOffset,
      },
      focus: {
        path,
        offset: findWordEndOffset,
      },
    });
    Transforms.delete(this.editor);
  }

  public setFormat(format: TOKEN_TYPE) {
    for (let i = 0; i < markdownTokens.length; i++) {
      if (format == markdownTokens[i].type) {
        let currSelection = this.editor['lastSelection'];
        let [start, end] = Range.edges(currSelection);
        //console.log('start end', start, end);
        // to the last first, if you do the start, it can change the whole structure
        // so you wont find the end position
        if (markdownTokens[i].end) {
          Transforms.select(this.editor, end);
          this.insertText(markdownTokens[i].end);
        }
        // to this last
        if (markdownTokens[i].start) {
          Transforms.select(this.editor, start);
          this.insertText((markdownTokens[i].lineStart ? '\n' : '') + markdownTokens[i].start);
        }

        // Chrome selection bug, after this operations, the selection API not triggering if you click at the end
        // of the editor. So you can not select the last character, it ignores it. It works anywhere else.
        // So repair the selection with the end position
        Transforms.select(this.editor, Editor.end(this.editor, [this.editor.children.length - 1]));
        return;
      }
    }
  }
}

/**
 * It fails when adding editor to parameter
 */
Scrubber.stringify = (value) => {
  try {
    return JSON.stringify(value, Scrubber['_scrubber']);
  } catch (e) {
    console.error('scrubber fail at', Object.assign({}, value));
    return '';
  }
};

/**
 * For emoji short code insert we should know the last selection
 * because if you click out of the editor, it clears the selection
 * @param editor
 */
const withSaveLastSelection = (editor) => {
  const { onChange } = editor;
  editor.onChange = (d) => {
    if (editor.selection) {
      editor['lastSelection'] = editor.selection;
    }
    onChange(d);
  };
  return editor;
};
