// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import { Dialog, setToolbar, ToolbarButton } from '@jupyterlab/apputils';
import { PathExt } from '@jupyterlab/coreutils';
import { IDocumentManager } from '@jupyterlab/docmanager';
import { Contents } from '@jupyterlab/services';
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
import { IScore, newFolderIcon, refreshIcon } from '@jupyterlab/ui-components';
import { PanelLayout, Widget } from '@lumino/widgets';
import { FileBrowser } from './browser';
import { FilterFileBrowserModel } from './model';
import { IFileBrowserFactory } from './tokens';
import { PromiseDelegate } from '@lumino/coreutils';

/**
 * The class name added to open file dialog
 */
const OPEN_DIALOG_CLASS = 'jp-Open-Dialog';

/**
 * The class name added to (optional) label in the file dialog
 */
const OPEN_DIALOG_LABEL_CLASS = 'jp-Open-Dialog-label';

/**
 * Namespace for file dialog
 */
export namespace FileDialog {
  /**
   * Options for the open directory dialog
   */
  export interface IDirectoryOptions
    extends Partial<
      Pick<
        Dialog.IOptions<Promise<Contents.IModel[]>>,
        Exclude<
          keyof Dialog.IOptions<Promise<Contents.IModel[]>>,
          'body' | 'buttons' | 'defaultButton'
        >
      >
    > {
    /**
     * Document manager
     */
    manager: IDocumentManager;

    /**
     * The application language translator.
     */
    translator?: ITranslator;

    /**
     * Default path to open
     */
    defaultPath?: string;

    /**
     * Text to display above the file browser.
     */
    label?: string;
  }

  /**
   * Options for the open file dialog
   */
  export interface IFileOptions extends IDirectoryOptions {
    /**
     * Filter function on file browser item model
     */
    filter?: (value: Contents.IModel) => Partial<IScore> | null;

    /**
     * The application language translator.
     */
    translator?: ITranslator;
  }

  /**
   * Create and show a open files dialog.
   *
   * Note: if nothing is selected when `getValue` will return the browser
   * model current path.
   *
   * @param options - The dialog setup options.
   *
   * @returns A promise that resolves with whether the dialog was accepted.
   */
  export async function getOpenFiles(
    options: IFileOptions
  ): Promise<Dialog.IResult<Contents.IModel[]>> {
    const dialog = new OpenDialog(options);

    return dialog.launch();
  }

  /**
   * Create and show a open directory dialog.
   *
   * Note: if nothing is selected when `getValue` will return the browser
   * model current path.
   *
   * @param options - The dialog setup options.
   *
   * @returns A promise that resolves with whether the dialog was accepted.
   */
  export function getExistingDirectory(
    options: IDirectoryOptions
  ): Promise<Dialog.IResult<Contents.IModel[]>> {
    return getOpenFiles({
      ...options,
      filter: model => {
        return model.type === 'directory' ? {} : null;
      }
    });
  }
}

class OpenDialog extends Dialog<Contents.IModel[]> {
  constructor(options: FileDialog.IFileOptions) {
    const translator = options.translator || nullTranslator;
    const trans = translator.load('jupyterlab');

    const handleOpenFile = () => {
      // Resolve the dialog with current filebrowser selection
      this.resolve();
    };

    const openDialog = new OpenDialogBody(
      options.manager,
      options.filter,
      translator,
      options.defaultPath,
      options.label,
      true,
      handleOpenFile
    );

    super({
      title: options.title,
      buttons: [
        Dialog.cancelButton(),
        Dialog.okButton({
          label: trans.__('Select')
        })
      ],
      focusNodeSelector: options.focusNodeSelector,
      host: options.host,
      renderer: options.renderer,
      body: openDialog
    });
  }
}

/**
 * Open dialog widget
 */
class OpenDialogBody
  extends Widget
  implements Dialog.IBodyWidget<Contents.IModel[]>
{
  constructor(
    manager: IDocumentManager,
    filter?: (value: Contents.IModel) => Partial<IScore> | null,
    translator?: ITranslator,
    defaultPath?: string,
    label?: string,
    filterDirectories?: boolean,
    handleOpenFile?: (path: string) => void
  ) {
    super();
    translator = translator ?? nullTranslator;
    const trans = translator.load('jupyterlab');
    this.addClass(OPEN_DIALOG_CLASS);

    Private.createFilteredFileBrowser(
      'filtered-file-browser-dialog',
      manager,
      filter,
      {},
      translator,
      defaultPath,
      filterDirectories,
      handleOpenFile
    )
      .then(browser => {
        this._browser = browser;

        // Add toolbar items
        setToolbar(this._browser, (browser: FileBrowser) => [
          {
            name: 'new-folder',
            widget: new ToolbarButton({
              icon: newFolderIcon,
              onClick: () => {
                void browser.createNewDirectory();
              },
              tooltip: trans.__('New Folder')
            })
          },
          {
            name: 'refresher',
            widget: new ToolbarButton({
              icon: refreshIcon,
              onClick: () => {
                browser.model.refresh().catch(reason => {
                  console.error(
                    'Failed to refresh file browser in open dialog.',
                    reason
                  );
                });
              },
              tooltip: trans.__('Refresh File List')
            })
          }
        ]);

        // Build the sub widgets
        const layout = new PanelLayout();
        if (label) {
          const labelWidget = new Widget();
          labelWidget.addClass(OPEN_DIALOG_LABEL_CLASS);
          labelWidget.node.textContent = label;
          layout.addWidget(labelWidget);
        }
        layout.addWidget(this._browser);

        /**
         * Dispose browser model when OpenDialogBody
         * is disposed.
         */
        this.dispose = () => {
          if (this.isDisposed) {
            return;
          }
          this._browser.model.dispose();
          super.dispose();
        };

        // Set Widget content
        this.layout = layout;

        this._ready.resolve();
      })
      .catch(reason => {
        console.error(
          'Error while creating file browser in open dialog',
          reason
        );
        this._ready.reject(void 0);
      });
  }

  /**
   * Get the selected items.
   */
  getValue(): Contents.IModel[] {
    const selection = Array.from(this._browser.selectedItems());
    if (selection.length === 0) {
      // Return current path
      return [
        {
          path: this._browser.model.path,
          name: PathExt.basename(this._browser.model.path),
          type: 'directory',
          content: undefined,
          writable: false,
          created: 'unknown',
          last_modified: 'unknown',
          mimetype: 'text/plain',
          format: 'text'
        }
      ];
    } else {
      return selection;
    }
  }

  /**
   * A promise that resolves when openDialog is successfully created.
   */
  get ready(): Promise<void> {
    return this._ready.promise;
  }

  private _ready: PromiseDelegate<void> = new PromiseDelegate<void>();
  private _browser: FileBrowser;
}

namespace Private {
  /**
   * Create a new file browser instance.
   *
   * @param id - The widget/DOM id of the file browser.
   *
   * @param manager - A document manager instance.
   *
   * @param filter - function to filter file browser item.
   *
   * @param options - The optional file browser configuration object.
   *
   * #### Notes
   * The ID parameter is used to set the widget ID. It is also used as part of
   * the unique key necessary to store the file browser's restoration data in
   * the state database if that functionality is enabled.
   *
   * If, after the file browser has been generated by the factory, the ID of the
   * resulting widget is changed by client code, the restoration functionality
   * will not be disrupted as long as there are no ID collisions, i.e., as long
   * as the initial ID passed into the factory is used for only one file browser
   * instance.
   */
  export const createFilteredFileBrowser = async (
    id: string,
    manager: IDocumentManager,
    filter?: (value: Contents.IModel) => Partial<IScore> | null,
    options: IFileBrowserFactory.IOptions = {},
    translator?: ITranslator,
    defaultPath?: string,
    filterDirectories?: boolean,
    handleOpenFile?: (path: string) => void
  ): Promise<FileBrowser> => {
    translator = translator || nullTranslator;
    const model = new FilterFileBrowserModel({
      manager,
      filter,
      translator,
      driveName: options.driveName,
      refreshInterval: options.refreshInterval,
      filterDirectories
    });

    const widget = new FileBrowser({
      id,
      model,
      translator,
      handleOpenFile
    });

    if (defaultPath) {
      await widget.model.cd(defaultPath);
    }

    return widget;
  };
}
