/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *     http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  OnDestroy,
  OnInit,
  QueryList,
  ViewChildren
} from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { isNil } from 'lodash';
import { Subject } from 'rxjs';
import { distinctUntilKeyChanged, map, startWith, takeUntil } from 'rxjs/operators';

import { NzResizeEvent } from 'ng-zorro-antd/resizable';

import { MessageListener, MessageListenersManager } from '@zeppelin/core';
import { Permissions } from '@zeppelin/interfaces';
import {
  DynamicFormParams,
  InterpreterBindingItem,
  MessageReceiveDataTypeMap,
  Note,
  OP,
  RevisionListItem
} from '@zeppelin/sdk';
import {
  MessageService,
  NgZService,
  NoteStatusService,
  NoteVarShareService,
  SecurityService,
  ThemeService,
  TicketService
} from '@zeppelin/services';

import { scrollIntoViewIfNeeded } from '@zeppelin/utility';
import { NotebookParagraphComponent } from './paragraph/paragraph.component';

@Component({
  selector: 'zeppelin-notebook',
  templateUrl: './notebook.component.html',
  styleUrls: ['./notebook.component.less'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class NotebookComponent extends MessageListenersManager implements OnInit, OnDestroy {
  @ViewChildren(NotebookParagraphComponent) listOfNotebookParagraphComponent!: QueryList<NotebookParagraphComponent>;
  private destroy$ = new Subject();
  note?: Exclude<Note['note'], undefined>;
  permissions?: Permissions;
  selectId: string | null = null;
  scrolledId: string | null = null;
  isOwner = true;
  noteRevisions: RevisionListItem[] = [];
  currentRevision?: string;
  collaborativeMode = false;
  revisionView = false;
  collaborativeModeUsers: string[] = [];
  isNoteDirty: boolean | null = false;
  isShowNoteForms = false;
  saveTimer: ReturnType<typeof setTimeout> | null = null;
  interpreterBindings: InterpreterBindingItem[] = [];
  activatedExtension: 'interpreter' | 'permissions' | 'revisions' | 'hide' = 'hide';
  sidebarWidth = 370;
  sidebarAnimationFrame = -1;
  isSidebarOpen = false;

  @MessageListener(OP.NOTE)
  getNote(data: MessageReceiveDataTypeMap[OP.NOTE]) {
    const note = data.note;
    if (isNil(note)) {
      this.router.navigate(['/']).then();
    } else {
      this.removeParagraphFromNgZ();
      this.note = note;
      const { paragraphId } = this.activatedRoute.snapshot.params;
      if (paragraphId) {
        this.note = this.cleanParagraphExcept(this.note, paragraphId);
        this.initializeLookAndFeel(this.note);
      } else {
        this.initializeLookAndFeel(this.note);
        this.getInterpreterBindings(this.note);
        this.getPermissions(this.note);
        this.note.config.personalizedMode =
          this.note.config.personalizedMode === undefined ? 'false' : this.note.config.personalizedMode;
      }
      if (this.note!.noteForms && this.note!.noteParams) {
        this.saveNoteForms({
          formsData: {
            forms: this.note!.noteForms,
            params: this.note!.noteParams
          }
        });
      }
      this.titleService.setTitle(`${this.note?.name} - Zeppelin`);
      this.themeService.updateMonacoTheme();
      this.cdr.markForCheck();
    }
  }

  @MessageListener(OP.INTERPRETER_BINDINGS)
  loadInterpreterBindings(data: MessageReceiveDataTypeMap[OP.INTERPRETER_BINDINGS]) {
    this.interpreterBindings = data.interpreterBindings;
    if (!this.interpreterBindings.some(item => item.selected)) {
      this.activatedExtension = 'interpreter';
    }
    this.cdr.markForCheck();
  }

  @MessageListener(OP.PARAGRAPH_REMOVED)
  removeParagraph(data: MessageReceiveDataTypeMap[OP.PARAGRAPH_REMOVED]) {
    const { paragraphId } = this.activatedRoute.snapshot.params;
    if (paragraphId || this.revisionView) {
      return;
    }
    if (!this.note) {
      return;
    }
    const definedNote = this.note;
    const paragraphIndex = definedNote.paragraphs.findIndex(p => p.id === data.id);
    definedNote.paragraphs = definedNote.paragraphs.filter((p, index) => index !== paragraphIndex);
    const adjustedCursorIndex =
      paragraphIndex === definedNote.paragraphs.length ? paragraphIndex - 1 : paragraphIndex + 1;
    const targetParagraph = this.listOfNotebookParagraphComponent.find((_, index) => index === adjustedCursorIndex);
    if (targetParagraph) {
      targetParagraph.focusEditor();
    }
    this.cdr.markForCheck();
  }

  @MessageListener(OP.PARAGRAPH_ADDED)
  addParagraph(data: MessageReceiveDataTypeMap[OP.PARAGRAPH_ADDED]) {
    const { paragraphId } = this.activatedRoute.snapshot.params;
    if (paragraphId || this.revisionView) {
      return;
    }
    if (!this.note) {
      return;
    }
    const definedNote = this.note;
    definedNote.paragraphs.splice(data.index, 0, data.paragraph);
    const paragraphIndex = definedNote.paragraphs.findIndex(p => p.id === data.paragraph.id);

    definedNote.paragraphs[paragraphIndex].focus = true;
    this.cdr.markForCheck();
  }

  @MessageListener(OP.SAVE_NOTE_FORMS)
  saveNoteForms(data: MessageReceiveDataTypeMap[OP.SAVE_NOTE_FORMS]) {
    if (!this.note) {
      return;
    }
    const definedNote = this.note;
    definedNote.noteForms = data.formsData.forms;
    definedNote.noteParams = data.formsData.params;
    this.setNoteFormsStatus();
  }

  @MessageListener(OP.NOTE_REVISION)
  getNoteRevision(data: MessageReceiveDataTypeMap[OP.NOTE_REVISION]) {
    const note = data.note;
    if (isNil(note)) {
      this.router.navigate(['/']).then();
    } else {
      this.note = note;
      this.initializeLookAndFeel(this.note);
      this.cdr.markForCheck();
    }
  }

  @MessageListener(OP.SET_NOTE_REVISION)
  setNoteRevision(_data: MessageReceiveDataTypeMap[OP.SET_NOTE_REVISION]) {
    const { noteId } = this.activatedRoute.snapshot.params;
    this.router.navigate(['/notebook', noteId]).then();
  }

  @MessageListener(OP.PARAGRAPH_MOVED)
  moveParagraph(data: MessageReceiveDataTypeMap[OP.PARAGRAPH_MOVED]) {
    if (!this.note) {
      return;
    }
    if (!this.revisionView) {
      const movedPara = this.note.paragraphs.find(p => p.id === data.id);
      if (movedPara) {
        const listOfRestPara = this.note.paragraphs.filter(p => p.id !== data.id);
        this.note.paragraphs = [...listOfRestPara.slice(0, data.index), movedPara, ...listOfRestPara.slice(data.index)];
        const paragraphComponent = this.listOfNotebookParagraphComponent.find(e => e.paragraph.id === data.id);
        this.cdr.markForCheck();
        if (paragraphComponent) {
          // Call when next tick
          setTimeout(() => {
            scrollIntoViewIfNeeded(paragraphComponent.getElement());
            paragraphComponent.focusEditor();
          });
        }
      }
    }
  }

  @MessageListener(OP.COLLABORATIVE_MODE_STATUS)
  getCollaborativeModeStatus(data: MessageReceiveDataTypeMap[OP.COLLABORATIVE_MODE_STATUS]) {
    this.collaborativeMode = Boolean(data.status);
    this.collaborativeModeUsers = data.users;
    this.cdr.markForCheck();
  }

  @MessageListener(OP.PATCH_PARAGRAPH)
  patchParagraph(_data: MessageReceiveDataTypeMap[OP.PATCH_PARAGRAPH]) {
    this.collaborativeMode = true;
    this.cdr.markForCheck();
  }

  @MessageListener(OP.NOTE_UPDATED)
  noteUpdated(data: MessageReceiveDataTypeMap[OP.NOTE_UPDATED]) {
    if (!this.note) {
      return;
    }
    if (data.name !== this.note.name) {
      this.note.name = data.name;
    }
    this.note.config = data.config;
    this.note.info = data.info;
    this.initializeLookAndFeel(this.note);
    this.cdr.markForCheck();
  }

  @MessageListener(OP.LIST_REVISION_HISTORY)
  listRevisionHistory(data: MessageReceiveDataTypeMap[OP.LIST_REVISION_HISTORY]) {
    this.noteRevisions = data.revisionList;
    if (this.noteRevisions) {
      if (this.noteRevisions.length === 0 || this.noteRevisions[0].id !== 'Head') {
        this.noteRevisions.splice(0, 0, { id: 'Head', message: 'Head' });
      }
      const { revisionId } = this.activatedRoute.snapshot.params;
      if (revisionId) {
        const revisionItemFound = this.noteRevisions.find(r => r.id === revisionId);
        if (!revisionItemFound) {
          throw new Error(`Revision ${revisionId} not found`);
        }
        this.currentRevision = revisionItemFound.message;
      } else {
        this.currentRevision = 'Head';
      }
    }
    this.cdr.markForCheck();
  }

  onParagraphSearch(term: string) {
    this.listOfNotebookParagraphComponent.forEach(comp => comp.highlightMatches(term || ''));
  }

  saveParagraph(id: string) {
    const paragraphFound = this.listOfNotebookParagraphComponent.toArray().find(p => p.paragraph.id === id);
    if (!paragraphFound) {
      throw new Error(`Paragraph ${id} not found`);
    }
    paragraphFound.saveParagraph();
  }

  killSaveTimer() {
    if (this.saveTimer) {
      clearTimeout(this.saveTimer);
      this.saveTimer = null;
    }
  }

  startSaveTimer() {
    this.killSaveTimer();
    this.isNoteDirty = true;
    this.saveTimer = setTimeout(() => {
      this.saveNote();
    }, 10000);
  }

  onParagraphSelect(id: string | null) {
    this.selectId = id;
  }

  onParagraphScrolled(id: string | null) {
    this.scrolledId = id;
  }

  onSelectAtIndex(index: number) {
    if (!this.note) {
      throw new Error(`"note" is not defined. Please check if note data is loaded before calling this method.`);
    }
    const scopeIndex = Math.min(this.note.paragraphs.length, Math.max(0, index));
    if (this.note.paragraphs[scopeIndex]) {
      this.selectId = this.note.paragraphs[scopeIndex].id;
    }
  }

  saveNote() {
    if (this.note && this.note.paragraphs && this.listOfNotebookParagraphComponent) {
      this.listOfNotebookParagraphComponent.toArray().forEach(p => {
        p.saveParagraph();
      });
      this.isNoteDirty = null;
      this.cdr.markForCheck();
    }
  }

  getInterpreterBindings(note: Exclude<Note['note'], undefined>) {
    this.messageService.getInterpreterBindings(note.id);
  }

  getPermissions(note: Exclude<Note['note'], undefined>) {
    this.securityService.getPermissions(note.id).subscribe(data => {
      this.permissions = data;
      this.isOwner = !(
        this.permissions.owners.length && this.permissions.owners.indexOf(this.ticketService.ticket.principal) < 0
      );
      this.cdr.markForCheck();
    });
  }

  get viewOnly(): boolean {
    if (!this.note) {
      return false;
    }
    return this.noteStatusService.viewOnly(this.note);
  }

  initializeLookAndFeel(note: Exclude<Note['note'], undefined>) {
    note.config.looknfeel = note.config.looknfeel || 'default';
    if (note.paragraphs && note.paragraphs[0]) {
      note.paragraphs[0].focus = true;
    }
  }

  cleanParagraphExcept(note: Exclude<Note['note'], undefined>, paragraphId: string) {
    const targetParagraph = note.paragraphs.find(p => p.id === paragraphId);
    if (!targetParagraph) {
      throw new Error(`Paragraph ${paragraphId} not found`);
    }
    const config = targetParagraph.config || {};
    config.editorHide = true;
    config.tableHide = false;
    const paragraphs = [{ ...targetParagraph, config }];
    return { ...note, paragraphs };
  }

  setAllParagraphTableHide(tableHide: boolean) {
    this.listOfNotebookParagraphComponent.forEach(p => p.setTableHide(tableHide));
  }

  setAllParagraphEditorHide(editorHide: boolean) {
    this.listOfNotebookParagraphComponent.forEach(p => p.setEditorHide(editorHide));
  }

  onNoteFormChange(noteParams: DynamicFormParams) {
    if (!this.note) {
      throw new Error(`"note" is not defined. Please check if note data is loaded before calling this method.`);
    }
    this.messageService.saveNoteForms({
      noteParams,
      id: this.note.id
    });
  }

  onFormNameRemove(formName: string) {
    if (!this.note) {
      throw new Error(`"note" is not defined. Please check if note data is loaded before calling this method.`);
    }
    this.messageService.removeNoteForms(this.note, formName);
  }

  onNoteTitleChange(noteFormTitle: string) {
    if (!this.note) {
      throw new Error(`"note" is not defined. Please check if note data is loaded before calling this method.`);
    }
    this.messageService.updateNote(this.note.id, this.note.name, {
      ...this.note.config,
      noteFormTitle
    });
  }

  setNoteFormsStatus() {
    this.isShowNoteForms = !!this.note && this.note.noteForms && Object.keys(this.note.noteForms).length !== 0;
    this.cdr.markForCheck();
  }

  onSidebarOpenChange(isSidebarOpen: boolean) {
    this.isSidebarOpen = isSidebarOpen;
  }

  onResizeSidebar({ width }: NzResizeEvent): void {
    cancelAnimationFrame(this.sidebarAnimationFrame);
    this.sidebarAnimationFrame = requestAnimationFrame(() => {
      this.sidebarWidth = width!;
    });
  }

  constructor(
    public messageService: MessageService,
    protected ngZService: NgZService,
    private activatedRoute: ActivatedRoute,
    private cdr: ChangeDetectorRef,
    private noteStatusService: NoteStatusService,
    private noteVarShareService: NoteVarShareService,
    private ticketService: TicketService,
    private securityService: SecurityService,
    private router: Router,
    private titleService: Title,
    private themeService: ThemeService
  ) {
    super(messageService);
  }

  ngOnInit() {
    this.activatedRoute.queryParamMap
      .pipe(
        startWith(this.activatedRoute.snapshot.queryParamMap),
        takeUntil(this.destroy$),
        map(data => data.get('paragraph'))
      )
      .subscribe(id => {
        this.onParagraphSelect(id);
        this.onParagraphScrolled(id);
      });
    this.activatedRoute.params.pipe(takeUntil(this.destroy$), distinctUntilKeyChanged('noteId')).subscribe(() => {
      this.noteVarShareService.clear();
    });
    this.activatedRoute.params.pipe(takeUntil(this.destroy$)).subscribe(param => {
      this.revisionView = !!param.revisionId;
      this.cdr.markForCheck();
    });
    this.revisionView = !!this.activatedRoute.snapshot.params.revisionId;

    // Fetch note when WebSocket connects or reconnects
    this.messageService.connectedStatus$
      .pipe(startWith(this.messageService.connectedStatus), takeUntil(this.destroy$))
      .subscribe(connected => {
        console.log('connectedStatus$ changed to ', connected ? 'connected' : 'disconnected');
        if (connected) {
          const { noteId, revisionId } = this.activatedRoute.snapshot.params;
          if (!noteId) {
            throw new Error('Route parameter `noteId` is required.');
          }
          if (revisionId) {
            this.messageService.noteRevision(noteId, revisionId);
          } else {
            this.messageService.getNote(noteId);
          }
          this.cdr.markForCheck();
          this.messageService.listRevisionHistory(noteId);
          // TODO(hsuanxyz) scroll to current paragraph
        }
      });
  }

  removeParagraphFromNgZ(): void {
    if (this.note && Array.isArray(this.note.paragraphs)) {
      this.note.paragraphs.forEach(p => {
        this.ngZService.removeParagraph(p.id);
      });
    }
  }

  ngOnDestroy(): void {
    super.ngOnDestroy();
    this.killSaveTimer();
    this.saveNote();
    this.destroy$.next();
    this.destroy$.complete();
    this.titleService.setTitle('Zeppelin');
  }
}
