import { isDesktopApplication } from '@/utils';
import pull from 'lodash/pull';
import {
  ProtectedAction,
  ApplicationEvent,
  SNTag,
  SNNote,
  SNUserPrefs,
  ContentType,
  SNSmartTag,
  PayloadSource
} from 'snjs';
import { WebApplication } from '@/ui_models/application';
import { Editor } from '@/ui_models/editor';

export enum AppStateEvent {
  TagChanged = 1,
  ActiveEditorChanged = 2,
  PreferencesChanged = 3,
  PanelResized = 4,
  EditorFocused = 5,
  BeganBackupDownload = 6,
  EndedBackupDownload = 7,
  DesktopExtsReady = 8,
  WindowDidFocus = 9,
  WindowDidBlur = 10,
};

export enum EventSource {
  UserInteraction = 1,
  Script = 2
};

type ObserverCallback = (event: AppStateEvent, data?: any) => Promise<void>

export class AppState {
  $rootScope: ng.IRootScopeService
  $timeout: ng.ITimeoutService
  application: WebApplication
  observers: ObserverCallback[] = []
  locked = true
  unsubApp: any
  rootScopeCleanup1: any
  rootScopeCleanup2: any
  onVisibilityChange: any
  selectedTag?: SNTag
  userPreferences?: SNUserPrefs
  multiEditorEnabled = false

  /* @ngInject */
  constructor(
    $rootScope: ng.IRootScopeService,
    $timeout: ng.ITimeoutService,
    application: WebApplication
  ) {
    this.$timeout = $timeout;
    this.$rootScope = $rootScope;
    this.application = application;
    this.registerVisibilityObservers();
    this.addAppEventObserver();
    this.streamNotesAndTags();
    const onVisibilityChange = () => {
      const visible = document.visibilityState === "visible";
      const event = visible
        ? AppStateEvent.WindowDidFocus
        : AppStateEvent.WindowDidBlur;
      this.notifyEvent(event);
    }
    this.onVisibilityChange = onVisibilityChange.bind(this);
  }

  deinit() {
    this.unsubApp();
    this.unsubApp = undefined;
    this.observers.length = 0;
    if (this.rootScopeCleanup1) {
      this.rootScopeCleanup1();
      this.rootScopeCleanup2();
      this.rootScopeCleanup1 = undefined;
      this.rootScopeCleanup2 = undefined;
    }
    document.removeEventListener('visibilitychange', this.onVisibilityChange);
    this.onVisibilityChange = undefined;
  }

  /**
   * Creates a new editor if one doesn't exist. If one does, we'll replace the 
   * editor's note with an empty one.
   */
  createEditor(title?: string) {
    const activeEditor = this.getActiveEditor();
    if (!activeEditor || this.multiEditorEnabled) {
      this.application.editorGroup.createEditor(undefined, title);
    } else {
      activeEditor.reset(title);
    }
  }

  async openEditor(noteUuid: string) {
    const note = this.application.findItem(noteUuid) as SNNote;
    const run = async () => {
      const activeEditor = this.getActiveEditor();
      if (!activeEditor || this.multiEditorEnabled) {
        this.application.editorGroup.createEditor(noteUuid);
      } else {
        activeEditor.setNote(note);
      }
      await this.notifyEvent(AppStateEvent.ActiveEditorChanged);
    };
    if (note && note.safeContent.protected &&
      await this.application.privilegesService!.actionRequiresPrivilege(
        ProtectedAction.ViewProtectedNotes
      )) {
      return new Promise((resolve) => {
        this.application.presentPrivilegesModal(
          ProtectedAction.ViewProtectedNotes,
          () => {
            run().then(resolve);
          }
        );
      });
    } else {
      return run();
    }
  }

  getActiveEditor() {
    return this.application.editorGroup.editors[0];
  }

  getEditors() {
    return this.application.editorGroup.editors;
  }

  closeEditor(editor: Editor) {
    this.application.editorGroup.closeEditor(editor);
  }

  closeActiveEditor() {
    this.application.editorGroup.closeActiveEditor();
  }

  closeAllEditors() {
    this.application.editorGroup.closeAllEditors();
  }

  editorForNote(note: SNNote) {
    for (const editor of this.getEditors()) {
      if (editor.note.uuid === note.uuid) {
        return editor;
      }
    }
  }

  streamNotesAndTags() {
    this.application!.streamItems(
      [ContentType.Note, ContentType.Tag],
      async (items, source) => {
        /** Close any editors for deleted/trashed/archived notes */
        if (source === PayloadSource.PreSyncSave) {
          const notes = items.filter((candidate) =>
            candidate.content_type === ContentType.Note
          ) as SNNote[];
          for (const note of notes) {
            const editor = this.editorForNote(note);
            if (!editor) {
              continue;
            }
            if (note.deleted) {
              this.closeEditor(editor);
            } else if (note.trashed && !this.selectedTag?.isTrashTag) {
              this.closeEditor(editor);
            } else if (note.archived && !this.selectedTag?.isArchiveTag) {
              this.closeEditor(editor);
            }
          }
        }
        if (this.selectedTag) {
          const matchingTag = items.find((candidate) => candidate.uuid === this.selectedTag!.uuid);
          if (matchingTag) {
            this.selectedTag = matchingTag as SNTag;
          }
        }
      }
    );
  }

  addAppEventObserver() {
    this.unsubApp = this.application.addEventObserver(async (eventName) => {
      if (eventName === ApplicationEvent.Started) {
        this.locked = true;
      } else if (eventName === ApplicationEvent.Launched) {
        this.locked = false;
      }
    });
  }

  isLocked() {
    return this.locked;
  }

  registerVisibilityObservers() {
    if (isDesktopApplication()) {
      this.rootScopeCleanup1 = this.$rootScope.$on('window-lost-focus', () => {
        this.notifyEvent(AppStateEvent.WindowDidBlur);
      });
      this.rootScopeCleanup2 = this.$rootScope.$on('window-gained-focus', () => {
        this.notifyEvent(AppStateEvent.WindowDidFocus);
      });
    } else {
      /* Tab visibility listener, web only */
      document.addEventListener('visibilitychange', this.onVisibilityChange);
    }
  }

  /** @returns  A function that unregisters this observer */
  addObserver(callback: ObserverCallback) {
    this.observers.push(callback);
    return () => {
      pull(this.observers, callback);
    };
  }

  async notifyEvent(eventName: AppStateEvent, data?: any) {
    /** 
     * Timeout is particullary important so we can give all initial 
     * controllers a chance to construct before propogting any events *
     */
    return new Promise((resolve) => {
      this.$timeout(async () => {
        for (const callback of this.observers) {
          await callback(eventName, data);
        }
        resolve();
      });
    });
  }

  setSelectedTag(tag: SNTag) {
    if (this.selectedTag === tag) {
      return;
    }
    const previousTag = this.selectedTag;
    this.selectedTag = tag;
    this.notifyEvent(
      AppStateEvent.TagChanged,
      {
        tag: tag,
        previousTag: previousTag
      }
    );
  }

  /** Returns the tags that are referncing this note */
  public getNoteTags(note: SNNote) {
    return this.application.referencingForItem(note).filter((ref) => {
      return ref.content_type === ContentType.Tag;
    }) as SNTag[]
  }

  /** Returns the notes this tag references */
  public getTagNotes(tag: SNTag) {
    if (tag.isSmartTag()) {
      return this.application.notesMatchingSmartTag(tag as SNSmartTag);
    } else {
      return this.application.referencesForItem(tag).filter((ref) => {
        return ref.content_type === ContentType.Note;
      }) as SNNote[]
    }
  }

  public getSelectedTag() {
    return this.selectedTag;
  }

  setUserPreferences(preferences: SNUserPrefs) {
    this.userPreferences = preferences;
    this.notifyEvent(
      AppStateEvent.PreferencesChanged
    );
  }

  panelDidResize(name: string, collapsed: boolean) {
    this.notifyEvent(
      AppStateEvent.PanelResized,
      {
        panel: name,
        collapsed: collapsed
      }
    );
  }

  editorDidFocus(eventSource: EventSource) {
    this.notifyEvent(
      AppStateEvent.EditorFocused,
      { eventSource: eventSource }
    );
  }

  beganBackupDownload() {
    this.notifyEvent(
      AppStateEvent.BeganBackupDownload
    );
  }

  endedBackupDownload(success: boolean) {
    this.notifyEvent(
      AppStateEvent.EndedBackupDownload,
      { success: success }
    );
  }

  /**
   * When the desktop appplication extension server is ready.
   */
  desktopExtensionsReady() {
    this.notifyEvent(
      AppStateEvent.DesktopExtsReady
    );
  }
}
