import { NotebookPanel } from '@jupyterlab/notebook';

import { CodeCell, MarkdownCell, Cell } from '@jupyterlab/cells';

import { map, toArray, each } from '@lumino/algorithm';

import * as React from 'react';

import {
  WidgetTracker,
  CommandToolbarButton,
  IWidgetTracker,
  Toolbar,
  ReactWidget
  // MainAreaWidget,
} from '@jupyterlab/apputils';

import { CommandRegistry } from '@lumino/commands';

import { Token } from '@lumino/coreutils';

import { Widget } from '@lumino/widgets';

import { Message } from '@lumino/messaging';

import { IDragEvent } from '@lumino/dragdrop';

import { DashboardLayout } from './layout';

import { DashboardWidget } from './widget';

import { WidgetPosition, Widgetstore } from './widgetstore';

import {
  addCellId,
  addNotebookId,
  getNotebookById,
  getCellId,
  updateMetadata
} from './utils';

import {
  DocumentWidget,
  DocumentRegistry,
  ABCWidgetFactory,
  IDocumentWidget
} from '@jupyterlab/docregistry';

import { IDashboardModel, DashboardModel } from './model';

import { CommandIDs } from './commands';

import { HTMLSelect } from '@jupyterlab/ui-components';

import { UUID } from '@lumino/coreutils';

// HTML element classes

export const DASHBOARD_CLASS = 'pr-JupyterDashboard';

export const DROP_TARGET_CLASS = 'pr-DropTarget';

export const TOOLBAR_MODE_SWITCHER_CLASS = 'pr-ToolbarModeSwitcher';

export const TOOLBAR_SELECT_CLASS = 'pr-ToolbarSelector';

export const TOOLBAR_CLASS = 'pr-DashboardToolbar';

export const IDashboardTracker = new Token<IDashboardTracker>(
  'jupyterlab-interactive-dashboard-editor'
);

export type IDashboardTracker = IWidgetTracker<Dashboard>;

export class DashboardTracker extends WidgetTracker<Dashboard> {}

/**
 * Main content widget for the Dashboard widget.
 */
export class Dashboard extends Widget {
  constructor(options: Dashboard.IOptions) {
    super(options);

    this.id = UUID.uuid4();

    const { outputTracker, model } = options;
    this._model = model;
    if (options.context !== undefined) {
      this._context = options.context;
    }
    const { widgetstore, mode } = model;

    this.layout = new DashboardLayout({
      widgetstore,
      outputTracker,
      model,
      mode,
      width: options.dashboardWidth || Dashboard.DEFAULT_WIDTH,
      height: options.dashboardHeight || Dashboard.DEFAULT_HEIGHT
    });

    widgetstore.connectDashboard(this);

    this._model.loaded.connect(this.updateLayoutFromWidgetstore, this);

    this.addClass(DASHBOARD_CLASS);
  }

  /**
   * Create click listeners on attach
   */
  onAfterAttach(msg: Message): void {
    super.onAfterAttach(msg);
    this.node.addEventListener('lm-dragenter', this, true);
    this.node.addEventListener('lm-dragleave', this, true);
    this.node.addEventListener('lm-dragover', this, true);
    this.node.addEventListener('lm-drop', this, true);
    this.node.addEventListener('lm-dragend', this, true);
    this.node.addEventListener('scroll', this);
  }

  /**
   * Remove click listeners on detach
   */
  onBeforeDetach(msg: Message): void {
    super.onBeforeDetach(msg);
    this.node.removeEventListener('lm-dragenter', this, true);
    this.node.removeEventListener('lm-dragleave', this, true);
    this.node.removeEventListener('lm-dragover', this, true);
    this.node.removeEventListener('lm-drop', this, true);
    this.node.removeEventListener('scroll', this);
  }

  /**
   * Handle the `'lm-dragenter'` event for the widget.
   */
  private _evtDragEnter(event: IDragEvent): void {
    event.preventDefault();
    event.stopPropagation();
  }

  /**
   * Handle the `'lm-dragleave'` event for the widget.
   */
  private _evtDragLeave(event: IDragEvent): void {
    this.removeClass(DROP_TARGET_CLASS);
    event.preventDefault();
    event.stopPropagation();
  }

  /**
   * Handle the `'lm-dragover'` event for the widget.
   */
  private _evtDragOver(event: IDragEvent): void {
    this.addClass(DROP_TARGET_CLASS);
    event.dropAction = 'copy';
    const source = event.source as DashboardWidget;
    const pos = source?.pos;
    if (pos && source.mode === 'grid-edit') {
      pos.left = event.offsetX + this.node.scrollLeft;
      pos.top = event.offsetY + this.node.scrollTop;
      (this.layout as DashboardLayout).drawDropZone(pos, '#2b98f0');
    }
    event.preventDefault();
    event.stopPropagation();
  }

  /**
   * Handle the `'lm-drop'` event for the widget.
   */
  private _evtDrop(event: IDragEvent): void {
    event.preventDefault();
    event.stopPropagation();
    const left = event.offsetX + this.node.scrollLeft;
    const top = event.offsetY + this.node.scrollTop;

    if (event.proposedAction === 'move') {
      const widget = event.source as DashboardWidget;
      const oldDashboard = widget.parent as Dashboard;
      const width = widget.node.offsetWidth;
      const height = widget.node.offsetHeight;
      const pos = { left, top, width, height };

      if (oldDashboard === this) {
        // dragging in same dashboard.ono
        this.updateWidget(widget, pos);
      } else {
        // dragging between dashboards
        const info: Widgetstore.WidgetInfo = {
          widgetId: DashboardWidget.createDashboardWidgetId(),
          notebookId: widget.notebookId,
          cellId: widget.cellId,
          pos,
          removed: false
        };

        const newWidget = this.createWidget(info);
        this.addWidget(newWidget, pos);
        oldDashboard.deleteWidget(widget);
      }

      // dragging from notebook -> dashboard.
    } else if (event.proposedAction === 'copy') {
      const notebook = event.source.parent as NotebookPanel;
      let cell: Cell;
      if (event.source.activeCell instanceof MarkdownCell) {
        cell = notebook.content.activeCell as MarkdownCell;
      } else {
        cell = notebook.content.activeCell as CodeCell;
      }

      const info: Widgetstore.WidgetInfo = {
        widgetId: DashboardWidget.createDashboardWidgetId(),
        notebookId: addNotebookId(notebook),
        cellId: addCellId(cell),
        pos: {
          left,
          top,
          width: DashboardWidget.DEFAULT_WIDTH,
          height: DashboardWidget.DEFAULT_HEIGHT
        },
        removed: false
      };

      const newWidget = this.createWidget(info, true);
      this.addWidget(newWidget, info.pos);
    } else {
      return;
    }

    this.removeClass(DROP_TARGET_CLASS);
  }

  handleEvent(event: Event): void {
    switch (event.type) {
      case 'scroll':
        this._evtScroll(event);
        break;
      case 'lm-dragenter':
        this._evtDragEnter(event as IDragEvent);
        break;
      case 'lm-dragleave':
        this._evtDragLeave(event as IDragEvent);
        break;
      case 'lm-dragover':
        this._evtDragOver(event as IDragEvent);
        break;
      case 'lm-drop':
        this._evtDrop(event as IDragEvent);
        break;
    }
  }

  private _evtScroll(_event: Event): void {
    const model = this.model;

    if (model.scrollMode !== 'infinite') {
      return;
    }

    const elem = this.node;
    const rightEdge = elem.offsetWidth + elem.scrollLeft;
    const bottomEdge = elem.offsetHeight + elem.scrollTop;

    if (rightEdge >= model.width && rightEdge > this._oldRightEdge) {
      model.width += 200;
    }
    if (bottomEdge >= model.height && bottomEdge > this._oldBottomEdge) {
      model.height += 200;
    }

    this._oldBottomEdge = bottomEdge;
    this._oldRightEdge = rightEdge;
  }

  /**
   * Add a widget to the layout.
   *
   * @param widget - the widget to add.
   */
  addWidget(widget: DashboardWidget, pos: Widgetstore.WidgetPosition): void {
    (this.layout as DashboardLayout).addWidget(widget, pos);
  }

  updateWidget(
    widget: DashboardWidget,
    pos: Widgetstore.WidgetPosition
  ): boolean {
    return (this.layout as DashboardLayout).updateWidget(widget, pos);
  }

  /**
   * Remove a widget from the layout.
   *
   * @param widget - the widget to remove.
   *
   * ### Notes
   * This is basically the same as deleteWidget but fulfills the type
   * signature requirements of the extended class.
   */
  removeWidget(widget: DashboardWidget): void {
    (this.layout as DashboardLayout).removeWidget(widget);
  }

  /**
   * Remove a widget from the layout.
   *
   * @param widget - the widget to remove.
   *
   */
  deleteWidget(widget: DashboardWidget): boolean {
    return (this.layout as DashboardLayout).deleteWidget(widget);
  }

  /**
   * Adds a dashboard widget's information to the widgetstore.
   *
   * @param info - the information to add to the widgetstore.
   */
  updateWidgetInfo(info: Widgetstore.WidgetInfo): void {
    (this.layout as DashboardLayout).updateWidgetInfo(info);
  }

  /**
   * Mark a widget as deleted in the widgetstore.
   *
   * @param widget - the widget to mark as deleted.
   */
  deleteWidgetInfo(widget: DashboardWidget): void {
    (this.layout as DashboardLayout).deleteWidgetInfo(widget);
  }

  /**
   * Update a widgetstore entry for a widget given that widget.
   *
   * @param widget - the widget to update from.
   */
  updateInfoFromWidget(widget: DashboardWidget): void {
    (this.layout as DashboardLayout).updateInfoFromWidget(widget);
  }

  /**
   * Updates the layout based on the state of the datastore.
   */
  updateLayoutFromWidgetstore(): void {
    (this.layout as DashboardLayout).updateLayoutFromWidgetstore();
  }

  /**
   * Undo the last change to the layout.
   */
  undo(): void {
    (this.layout as DashboardLayout).undo();
  }

  /**
   * Redo the last change to the layout.
   */
  redo(): void {
    (this.layout as DashboardLayout).redo();
  }

  createWidget(info: Widgetstore.WidgetInfo, fit?: boolean): DashboardWidget {
    return (this.layout as DashboardLayout).createWidget(info, fit);
  }

  saveToNotebookMetadata(): void {
    // Get a list of all notebookIds used in the dashboard.
    const widgets = toArray(this.model.widgetstore.getWidgets());

    const notebookIds = toArray(map(widgets, record => record.notebookId));

    if (!notebookIds.every(v => v === notebookIds[0])) {
      throw new Error(
        'Only single notebook dashboards can be saved to metadata.'
      );
    }

    const notebookId = notebookIds[0];
    const notebookTracker = this.model.notebookTracker;
    const notebook = getNotebookById(notebookId, notebookTracker);

    updateMetadata(notebook, { hasDashboard: true });

    const cells = notebook.content.widgets;

    const widgetMap = new Map<string, WidgetPosition>(
      widgets.map(widget => [widget.cellId, widget.pos])
    );

    each(cells, cell => {
      const cellId = getCellId(cell);
      const pos = widgetMap.get(cellId);
      if (pos != null) {
        updateMetadata(cell, { pos, hidden: false });
      } else {
        updateMetadata(cell, { hidden: true });
      }
    });

    notebook.context.save();
  }

  get model(): IDashboardModel {
    return this._model;
  }

  get context(): DocumentRegistry.IContext<DocumentRegistry.IModel> {
    return this._context;
  }

  private _model: IDashboardModel;
  private _context: DocumentRegistry.IContext<DocumentRegistry.IModel>;
  private _oldRightEdge = 0;
  private _oldBottomEdge = 0;
}

/**
 * Namespace for DashboardArea options.
 */
export namespace Dashboard {
  export type Mode = 'free-edit' | 'present' | 'grid-edit';

  export type ScrollMode = 'infinite' | 'constrained';

  export const DEFAULT_WIDTH = 1920;

  export const DEFAULT_HEIGHT = 1080;

  export interface IOptions extends Widget.IOptions {
    /**
     * Tracker for child widgets.
     */
    outputTracker: WidgetTracker<DashboardWidget>;

    /**
     * Dashboard name.
     */
    name?: string;

    store?: Widgetstore;

    dashboardWidth?: number;

    dashboardHeight?: number;

    model: IDashboardModel;

    context?: DocumentRegistry.IContext<DocumentRegistry.IModel>;
  }
}

export class DashboardDocument extends DocumentWidget<Dashboard> {
  constructor(options: DashboardDocument.IOptions) {
    let { content, reveal } = options;
    const { context, commandRegistry } = options;
    const model = context.model as DashboardModel;
    model.path = context.path;
    content = content || new Dashboard({ ...options, model, context });
    reveal = Promise.all([reveal, context.ready]);
    super({
      ...options,
      content: content as Dashboard,
      reveal
    });

    // Build the toolbar

    this.toolbar.addClass(TOOLBAR_CLASS);

    const commands = commandRegistry;
    const { save, undo, redo, cut, copy, paste } = CommandIDs;

    const args = { toolbar: true, dashboardId: content.id };

    const makeToolbarButton = (
      id: string,
      tooltip: string
    ): CommandToolbarButton => {
      const button = new CommandToolbarButton({ args, commands, id });
      button.node.title = tooltip;
      return button;
    };

    const saveButton = makeToolbarButton(save, 'Save');
    const undoButton = makeToolbarButton(undo, 'Undo');
    const redoButton = makeToolbarButton(redo, 'Redo');
    const cutButton = makeToolbarButton(cut, 'Cut the selected outputs');
    const copyButton = makeToolbarButton(copy, 'Copy the selected outputs');
    const pasteButton = makeToolbarButton(
      paste,
      'Paste outputs from the clipboard'
    );

    this.toolbar.addItem(save, saveButton);
    this.toolbar.addItem(undo, undoButton);
    this.toolbar.addItem(redo, redoButton);
    this.toolbar.addItem(cut, cutButton);
    this.toolbar.addItem(copy, copyButton);
    this.toolbar.addItem(paste, pasteButton);
    this.toolbar.addItem('spacer', Toolbar.createSpacerItem());
    this.toolbar.addItem(
      'switchMode',
      new DashboardDocument.DashboardModeSwitcher(content as Dashboard)
    );
  }
}

export namespace DashboardDocument {
  export interface IOptions extends DocumentWidget.IOptionsOptionalContent {
    /**
     * Tracker for child widgets.
     */
    outputTracker: WidgetTracker<DashboardWidget>;

    /**
     * Command registry for building the toolbar.
     */
    commandRegistry: CommandRegistry;

    /**
     * Dashboard name.
     */
    name?: string;

    /**
     * Optional widgetstore to restore from.
     */
    store?: Widgetstore;

    /**
     * Dashboard canvas width (default is 1280).
     */
    dashboardWidth?: number;

    /**
     * Dashboard canvas height (default is 720).
     */
    dashboardHeight?: number;
  }

  export class DashboardModeSwitcher extends ReactWidget {
    constructor(dashboard: Dashboard) {
      super();
      this.addClass(TOOLBAR_MODE_SWITCHER_CLASS);
      this._dashboard = dashboard;

      if (dashboard.model) {
        this.update();
      }

      dashboard.model.stateChanged.connect((_sender, change) => {
        if (change.name === 'mode') {
          this.update();
        }
      }, this);
    }

    private _handleChange(
      that: DashboardModeSwitcher
    ): (event: React.ChangeEvent<HTMLSelectElement>) => void {
      return (event: React.ChangeEvent<HTMLSelectElement>): void => {
        that.dashboard.model.mode = event.target.value as Dashboard.Mode;
      };
    }

    render(): JSX.Element {
      const value = this._dashboard.model.mode;
      return (
        <HTMLSelect
          className={TOOLBAR_SELECT_CLASS}
          onChange={this._handleChange(this)}
          value={value}
          aria-label={'Mode'}
        >
          <option value="present">Present</option>
          {/* <option value="free-edit">Free Layout</option> */}
          <option value="grid-edit">Edit</option>
        </HTMLSelect>
      );
    }

    get dashboard(): Dashboard {
      return this._dashboard;
    }

    private _dashboard: Dashboard;
  }
}

export class DashboardDocumentFactory extends ABCWidgetFactory<
  DashboardDocument
> {
  constructor(options: DashboardDocumentFactory.IOptions) {
    super(options);
    this._commandRegistry = options.commandRegistry;
    this._outputTracker = options.outputTracker;
  }

  createNewWidget(context: DocumentRegistry.Context): DashboardDocument {
    return new DashboardDocument({
      context,
      commandRegistry: this._commandRegistry,
      outputTracker: this._outputTracker
    });
  }

  private _commandRegistry: CommandRegistry;
  private _outputTracker: WidgetTracker<DashboardWidget>;
}

export namespace DashboardDocumentFactory {
  export interface IOptions
    extends DocumentRegistry.IWidgetFactoryOptions<IDocumentWidget> {
    commandRegistry: CommandRegistry;
    outputTracker: WidgetTracker<DashboardWidget>;
  }
}

/**
 * CURRENTLY UNUSED
 *
 * Opens a visual HTMLElement in fullscreen.
 *
 * @param node- the element to open in fullscreen.
 */
export function openfullscreen(node: HTMLElement): void {
  // Trigger fullscreen
  const docElmWithBrowsersFullScreenFunctions = node as HTMLElement & {
    mozRequestFullScreen(): Promise<void>;
    webkitRequestFullscreen(): Promise<void>;
    msRequestFullscreen(): Promise<void>;
  };

  if (docElmWithBrowsersFullScreenFunctions.requestFullscreen) {
    docElmWithBrowsersFullScreenFunctions.requestFullscreen();
  } else if (docElmWithBrowsersFullScreenFunctions.mozRequestFullScreen) {
    /* Firefox */
    docElmWithBrowsersFullScreenFunctions.mozRequestFullScreen();
  } else if (docElmWithBrowsersFullScreenFunctions.webkitRequestFullscreen) {
    /* Chrome, Safari and Opera */
    docElmWithBrowsersFullScreenFunctions.webkitRequestFullscreen();
  } else if (docElmWithBrowsersFullScreenFunctions.msRequestFullscreen) {
    /* IE/Edge */
    docElmWithBrowsersFullScreenFunctions.msRequestFullscreen();
  }
}
