import {Component, OnInit, ViewChild, HostListener} from '@angular/core';

import {
  AlertController,
  ModalController,
  Platform,
  PopoverController,
  LoadingController,
  ToastController, NavController
} from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { ApiService } from './service/api.service';
import { Router } from '@angular/router';
import {TREE_ACTIONS, TreeComponent} from '@circlon/angular-tree-component';
import {BehaviorSubject, fromEvent, Observable} from 'rxjs';
import {OptionPickerComponent} from './components/option-picker/option-picker.component';
import {OptionMenuComponent} from './components/option-menu/option-menu.component';
import {SelectorComponent} from './components/sharing/selector/selector.component';
import {SessionService} from './service/session.service';
import {SearchComponent} from './components/search/Search.component';
import {NodeTypeIcons} from './structures/node-types';
import {NavigationService} from './service/navigation.service';
import {DatabaseService} from './service/db/database.service';
import {EditorService} from './service/editor.service';
import {debug} from './utility';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss']
})
export class AppComponent implements OnInit {
  @ViewChild('menuTree') menuTree: TreeComponent;
  public readonly ready$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
  public addChildTarget: any = false;
  public deleteTarget: any = false;
  public menuTarget: any = false;
  public refreshingMenu = false;
  public lastClickEvent: Array<any> = [];
  public nodes = [];
  public currentPageId: string;
  public virtualRootPageId?: string;
  public options = {
    isExpandedField: 'expanded',
    animateExpand: false,
    scrollOnActivate: false,
    allowDrag: true,
    allowDrop: (element, { parent, index }) => {
      return (
          !this.api.isOffline
          && (element.data.type === 'page' || element.data.type === 'form')
          && (parent.data.type === 'page' || parent.data.userRootPage)
          && !element.data.userRootPage
          && element.data.id !== parent.data.id
      );
    },
    actionMapping: {
      mouse: {
        dblClick: (tree, node, $event) => {
          this.navigateEditorToNode(node);
        },
        click: (tree, node, $event) => {
          TREE_ACTIONS.FOCUS(tree, node, $event);
          TREE_ACTIONS.EXPAND(tree, node, $event);

          this.addChildTarget = false;
          this.deleteTarget = false;
          this.menuTarget = false;

          if ( !node.data.noChildren && (!node.data.level || node.data.level === 'manage') ) {
            this.addChildTarget = node;
          }

          if ( !node.data.noDelete && (!node.data.level || node.data.level === 'manage') ) {
            this.deleteTarget = node;
          }

          this.menuTarget = node;
          this.lastClickEvent = [tree, node, $event];
        }
      }
    }
  };

  public typeIcons = NodeTypeIcons;

  public get appName(): string {
    return this.session.appName || 'Noded';
  }

  public darkMode = false;
  protected loader?: any;
  protected hasSearchOpen = false;
  protected versionInterval?: any;
  protected showedNewVersionAlert = false;
  protected showedOfflineAlert = false;
  protected backbuttonSubscription: any;
  protected initialized$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);

  constructor(
    private platform: Platform,
    private splashScreen: SplashScreen,
    private statusBar: StatusBar,
    public readonly api: ApiService,
    protected navCtrl: NavController,
    protected router: Router,
    protected alerts: AlertController,
    protected popover: PopoverController,
    protected modal: ModalController,
    protected session: SessionService,
    protected loading: LoadingController,
    protected navService: NavigationService,
    protected toasts: ToastController,
    protected db: DatabaseService,
    protected editor: EditorService,
  ) { }

  async checkNewVersion() {
    if ( !this.showedNewVersionAlert && await this.session.newVersionAvailable() ) {
      const toast = await this.toasts.create({
        cssClass: 'compat-toast-container',
        header: 'Update Available',
        message: `A new version of ${this.appName} is available. Please refresh to update.`,
        buttons: [
          {
            side: 'end',
            text: 'Refresh',
            handler: () => {
              window.location.reload();
            },
          },
        ],
      });

      this.showedNewVersionAlert = true;
      await toast.present();
    }
  }

  ngOnInit() {
    debug('Initializing application.');
    this.initializeApp();
  }

  @HostListener('window:popstate', ['$event'])
  dismissModal(event) {
    const modal = this.modal.getTop();

    if ( modal ) {
      event.preventDefault();
      event.stopPropagation();
      this.modal.dismiss();
    }
  }

  showOptions($event) {
    this.popover.create({
      event: $event,
      component: OptionPickerComponent,
      componentProps: {
        toggleDark: () => this.toggleDark(),
        isDark: () => this.isDark(),
        showSearch: () => this.handleKeyboardEvent(),
        isPrefetch: () => this.isPrefetch(),
        togglePrefetch: () => this.togglePrefetch(),
        doPrefetch: () => this.doPrefetch(),
      }
    }).then(popover => popover.present());
  }

  @HostListener('document:keyup.control./', ['$event'])
  async handleKeyboardEvent() {
    if ( this.hasSearchOpen ) {
      return;
    }

    const modal = await this.modal.create({
      component: SearchComponent,
      cssClass: 'modal-med',
    });

    const modalState = {
      modal : true,
      desc : 'Search everything'
    };

    history.pushState(modalState, null);

    this.hasSearchOpen = true;
    await modal.present();

    await modal.onDidDismiss();
    this.hasSearchOpen = false;
  }

  public navigateEditorToNode(node: any) {
    if ( !node.data ) {
      node = { data: node };
    }

    const id = node.data.id;
    const nodeId = node.data.node_id;
    if ( !node.data.virtual ) {
      debug('Navigating editor to node:', {id, nodeId});

      this.currentPageId = id;
      this.router.navigate(['/editor', { id, ...(nodeId ? { node_id: nodeId } : {}) }]);
    }
  }

  async onNodeMenuClick($event) {
    let canManage = this.menuTarget.data.level === 'manage';
    if ( !canManage ) {
      if ( !this.menuTarget.data.level ) {
        canManage = true;
      }
    }

    if ( !this.menuTarget.data.id ) {
      return;
    }

    const options = [
      {name: 'Make Virtual Root', icon: 'fa fa-search-plus', value: 'virtual_root'},
      {name: 'Export to HTML', icon: 'fa fa-file-export', value: 'export_html'},
    ];

    const manageOptions = [
      {name: 'Share Sub-Tree', icon: 'fa fa-share-alt', value: 'share'},
    ];

    const popover = await this.popover.create({
      component: OptionMenuComponent,
      componentProps: {
        menuItems: [
            ...(!canManage ? options : [...options, ...manageOptions]),
        ],
      },
      event: $event,
    });

    popover.onDidDismiss().then((result) => {
      if ( result.data === 'share' ) {
        this.modal.create({
          component: SelectorComponent,
          cssClass: 'modal-med',
          componentProps: {
            node: this.menuTarget.data,
          }
        }).then(modal => {
          const modalState = {
            modal : true,
            desc : 'Share page'
          };

          history.pushState(modalState, null);

          modal.present();
        });
      } else if ( result.data === 'export_html' ) {
        this.exportTargetAsHTML();
      } else if ( result.data === 'virtual_root' ) {
        this.setVirtualRoot();
      }
    });

    await popover.present();
  }

  async setVirtualRoot() {
    if ( this.menuTarget && this.menuTarget.data?.type === 'page' ) {
      debug('virtual root menu target', this.menuTarget);
      this.virtualRootPageId = this.menuTarget.data.id;
      this.reloadMenuItems().subscribe();
    }
  }

  onVirtualRootClear(event) {
    delete this.virtualRootPageId;
    this.reloadMenuItems().subscribe();
  }

  async exportTargetAsHTML() {
    const exportRecord: any = await new Promise((res, rej) => {
      const reqData = {
        format: 'html',
        PageId: this.menuTarget.data.id,
      };

      this.api.post(`/exports/subtree`, reqData).subscribe({
        next: (result) => {
          res(result.data);
        },
        error: rej
      });
    });

    const dlUrl = this.api._build_url(`/exports/${exportRecord.UUID}/download`);
    window.open(dlUrl, '_blank');
  }

  async onCreateClick($event: MouseEvent) {
    const menuItems = [
      {
        name: 'Top-Level Note',
        icon: 'fa fa-sticky-note noded-note',
        value: 'top-level',
        title: 'Create a new top-level note page',
      },
      ...(this.addChildTarget ? [
        {
          name: 'Child Note',
          icon: 'fa fa-sticky-note noded-note',
          value: 'child',
          title: 'Create a note page as a child of the given note',
        },
        {
          name: 'Form',
          icon: 'fa fa-clipboard-list noded-form',
          value: 'form',
          title: 'Create a new form page as a child of the given note',
        },
      ] : []),
    ];

    const popover = await this.popover.create({
      event: $event,
      component: OptionMenuComponent,
      componentProps: {
        menuItems,
      },
    });

    popover.onDidDismiss().then(({ data: value }) => {
      if ( value === 'top-level' ) {
        this.onTopLevelCreate();
      } else if ( value === 'child' ) {
        this.onChildCreate();
      } else if ( value === 'form' ) {
        this.onChildCreate('form');
      }
    });

    await popover.present();
  }

  async onTopLevelCreate() {
    const alert = await this.alerts.create({
      header: 'Create Page',
      message: 'Please enter a new name for the page:',
      cssClass: 'page-prompt',
      inputs: [
        {
          name: 'name',
          type: 'text',
          placeholder: 'My Awesome Page'
        }
      ],
      buttons: [
        {
          text: 'Cancel',
          role: 'cancel',
          cssClass: 'secondary'
        },
        {
          text: 'Create',
          handler: async args => {
            const page = await this.editor.createPage(args.name);
            this.reloadMenuItems().subscribe();
            await this.router.navigate(['/editor', { id: page.UUID }]);
          }
        }
      ]
    });

    await alert.present();
  }

  async onChildCreate(pageType?: string) {
    const alert = await this.alerts.create({
      header: 'Create Sub-Page',
      message: 'Please enter a new name for the page:',
      cssClass: 'page-prompt',
      inputs: [
        {
          name: 'name',
          type: 'text',
          placeholder: 'My Awesome Page'
        }
      ],
      buttons: [
        {
          text: 'Cancel',
          role: 'cancel',
          cssClass: 'secondary'
        },
        {
          text: 'Create',
          handler: async args => {
            args = {
              name: args.name,
              parentId: this.addChildTarget.data.id,
              pageType,
            };
            this.api.post('/page/create-child', args).subscribe(res => {
              this.reloadMenuItems().subscribe(() => {
                TREE_ACTIONS.EXPAND(
                  this.lastClickEvent[0],
                  this.lastClickEvent[1],
                  this.lastClickEvent[2]
                );
                this.router.navigate(['/editor', { id: res.data.UUID }]);
              });
            });
          }
        }
      ]
    });

    await alert.present();
  }

  async onDeleteClick() {
    const alert = await this.alerts.create({
      header: 'Delete page?',
      message:
        'Deleting this page will make its contents and  all of its children inaccessible. Are you sure you want to continue?',
      buttons: [
        {
          text: 'Keep It',
          role: 'cancel'
        },
        {
          text: 'Delete It',
          handler: async () => {
            this.api
              .post(`/page/delete/${this.deleteTarget.data.id}`)
              .subscribe(res => {
                if ( this.currentPageId === this.deleteTarget.data.id ) {
                  this.router.navigate(['/home']);
                }

                this.reloadMenuItems().subscribe();
                this.deleteTarget = false;
                this.addChildTarget = false;
                this.menuTarget = false;
              });
          }
        }
      ]
    });

    await alert.present();
  }

  onMenuRefresh(quiet = false) {
    if ( !quiet ) {
      this.refreshingMenu = true;
    }

    this.reloadMenuItems().subscribe();

    setTimeout(() => {
      if ( !quiet ) {
        this.refreshingMenu = false;
      }
    }, 2000);
  }

  reloadMenuItems() {
    return new Observable(sub => {
      this.api.getMenuItems(false, this.virtualRootPageId).then(nodes => {
        this.nodes = nodes;
        sub.next();
        sub.complete();
      });
    });
  }

  async initializeApp() {
    debug('app', this);
    this.loader = await this.loading.create({
      message: 'Starting up...',
      cssClass: 'noded-loading-mask',
      showBackdrop: true,
    });

    debug('Initializing platform and database...');
    await this.loader.present();
    await this.platform.ready();
    await this.db.createSchemata();

    let toast: any;

    debug('Subscribing to offline changes...');
    this.api.offline$.subscribe(async isOffline => {
      if ( isOffline && !this.showedOfflineAlert ) {
        debug('Application went offline!');
        toast = await this.toasts.create({
          cssClass: 'compat-toast-container',
          message: 'Uh, oh! It looks like you\'re offline. Some features might not work as expected...',
        });

        this.showedOfflineAlert = true;
        await toast.present();
      } else if ( !isOffline && this.showedOfflineAlert ) {
        debug('Appliation went online!');
        await toast.dismiss();
        this.showedOfflineAlert = false;
        await this.api.syncOfflineData();
      }
    });

    debug('Getting initial status...');
    let stat: any = await this.session.stat();
    debug('Got stat:', stat);

    if ( stat.public_user ) {
      this.api.isPublicUser = true;
    }

    if ( stat.authenticated_user ) {
      this.api.isAuthenticated = true;
    }

    this.api.systemBase = stat.system_base;

    if ( !this.api.isAuthenticated || this.api.isPublicUser ) {
      debug('Unauthenticated or public user...');
      if ( !this.api.isOffline ) {
        debug('Trying to resume session...');
        await this.api.resumeSession();
        debug('Checking new status...');
        stat = await this.session.stat();
        debug('Got session resume stat:', stat);

        this.api.isAuthenticated = stat.authenticated_user;
        this.api.isPublicUser = stat.public_user;

        if ( !stat.authenticated_user ) {
          debug('Not authenticated! Redirecting.');
          window.location.href = `${stat.system_base}start`;
          return;
        }
      } else {
        debug('Unauthenticated offline user. Purging local data!');
        await this.db.purge();
        window.location.href = `${stat.system_base}start`;
        return;
      }
    }

    debug('Set app name and system base:', stat.app_name, stat.system_base);
    this.session.appName = stat.app_name;
    this.session.systemBase = stat.system_base;

    debug('Initializing session...');
    await this.session.initialize();

    if ( this.session.get('user.preferences.dark_mode') ) {
      this.toggleDark();
    }

    debug('Hiding native splash screen & setting status bar styles...');
    await this.statusBar.styleDefault();
    await this.splashScreen.hide();

    // If we went online after being offline, sync the local data
    if ( !this.api.isOffline && await this.api.needsSync() ) {
      this.loader.message = 'Syncing data...';
      try {
        await this.api.syncOfflineData();
      } catch (e) {
        this.toasts.create({
          cssClass: 'compat-toast-container',
          message: 'An error occurred while syncing offline data. Not all data was saved.',
          buttons: [
            'Okay'
          ],
        }).then(tst => {
          tst.present();
        });
      }
    }

    if ( this.isPrefetch() && !this.api.isPublicUser ) {
      debug('Pre-fetching offline data...');
      this.loader.message = 'Downloading data...';
      try {
        await this.api.prefetchOfflineData();
      } catch (e) {
        debug('Pre-fetch error:', e);
        this.toasts.create({
          cssClass: 'compat-toast-container',
          message: 'An error occurred while pre-fetching offline data. Not all data was saved.',
          buttons: [
              'Okay'
          ],
        }).then(tst => {
          tst.present();
        });
      }
    }

    this.initialized$.next(true);

    if ( !this.api.isPublicUser && this.session.get('user.preferences.default_page') ) {
      debug('Navigating to default page!');
      const id = this.session.get('user.preferences.default_page');
      const node = this.findNode(id);
      if ( node ) {
        this.navigateEditorToNode(node);
      }
    }

    debug('Creating menu subscription...');
    this.navService.sidebarRefresh$.subscribe(([_, quiet]) => {
      this.onMenuRefresh(quiet);
    });

    this.navService.navigationRequest$.subscribe(pageId => {
      debug('Page navigation request: ', {pageId});
      if ( !pageId ) {
        debug('Empty page ID. Will not navigate.');
        return;
      }

      this.currentPageId = pageId;
      this.router.navigate(['/editor', { id: pageId }]);
    });

    debug('Reloading menu items...');
    this.reloadMenuItems().subscribe(() => {
      debug('Reloaded menu items. Displaying interface.');
      this.ready$.next(true);

      setTimeout(() => {
        this.loader.dismiss();
        this.menuTree?.treeModel?.expandAll();
      }, 10);

      if ( !this.versionInterval ) {
        this.versionInterval = setInterval(() => {
          debug('Checking for new application version.');
          this.checkNewVersion();
        }, 1000 * 60 * 5); // Check for new version every 5 mins
      }
    });
  }

  async doPrefetch() {
    if ( this.api.isOffline ) {
      return;
    }

    this.loader = await this.loading.create({
      message: 'Pre-fetching data...',
      cssClass: 'noded-loading-mask',
      showBackdrop: true,
    });

    await new Promise(res => setTimeout(res, 2000));

    await this.loader.present();

    try {
      if (await this.api.needsSync()) {
        this.loader.message = 'Syncing data...';
        await this.api.syncOfflineData();
      }

      this.loader.message = 'Downloading data...';
      await this.api.prefetchOfflineData();
    } catch (e) {
      const msg = await this.alerts.create({
        header: 'Uh, oh!',
        message: 'An unexpected error occurred while trying to sync offline data, and we were unable to continue.',
        buttons: [
          'OK',
        ],
      });

      await msg.present();
    }

    this.loader.dismiss();
  }

  toggleDark() {
    // const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
    this.darkMode = !this.darkMode;
    this.session.set('user.preferences.dark_mode', this.darkMode);
    document.body.classList.toggle('dark', this.darkMode);
  }

  togglePrefetch() {
    this.session.set('user.preferences.auto_prefetch', !this.isPrefetch());
  }

  findNode(id: string, nodes = this.nodes) {
    for ( const node of nodes ) {
      if ( node.id === id ) {
        return node;
      }

      if ( node.children ) {
        const foundNode = this.findNode(id, node.children);
        if ( foundNode ) {
          return foundNode;
        }
      }
    }
  }

  isDark() {
    return !!this.darkMode;
  }

  isPrefetch() {
    return !!this.session.get('user.preferences.auto_prefetch');
  }

  async onTreeNodeMove({ node, to }) {
    if ( this.api.isOffline ) {
      debug('Cannot move node. API is offline.');
      return;
    }

    const { parent } = to;
    debug('Moving node:', { node, parent });

    try {
      await this.api.moveMenuNode(node.id, to.parent.id);
    } catch (error) {
      console.error('Error moving tree node:', error);
      this.alerts.create({
        header: 'Error Moving Node',
        message: error.message,
        buttons: [
          {
            text: 'Okay',
            role: 'cancel',
          },
        ],
      }).then(x => x.present());
    }

    await this.reloadMenuItems().toPromise();
  }
}
