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

import { Message } from '@lumino/messaging';
import { Widget } from '@lumino/widgets';
import { Dialog, showDialog } from './dialog';

const INPUT_DIALOG_CLASS = 'jp-Input-Dialog';

const INPUT_BOOLEAN_DIALOG_CLASS = 'jp-Input-Boolean-Dialog';

/**
 * Namespace for input dialogs
 */
export namespace InputDialog {
  /**
   * Common constructor options for input dialogs
   */
  export interface IOptions {
    /**
     * The top level text for the dialog. Defaults to an empty string.
     */
    title: Dialog.Header;

    /**
     * The host element for the dialog. Defaults to `document.body`.
     */
    host?: HTMLElement;

    /**
     * Label of the requested input
     */
    label?: string;

    /**
     * An optional renderer for dialog items. Defaults to a shared
     * default renderer.
     */
    renderer?: Dialog.IRenderer;

    /**
     * Label for ok button.
     */
    okLabel?: string;

    /**
     * Label for cancel button.
     */
    cancelLabel?: string;

    /**
     * The checkbox to display in the footer. Defaults no checkbox.
     */
    checkbox?: Partial<Dialog.ICheckbox> | null;
  }

  /**
   * Constructor options for boolean input dialogs
   */
  export interface IBooleanOptions extends IOptions {
    /**
     * Default value
     */
    value?: boolean;
  }

  /**
   * Create and show a input dialog for a boolean.
   *
   * @param options - The dialog setup options.
   *
   * @returns A promise that resolves with whether the dialog was accepted
   */
  export function getBoolean(
    options: IBooleanOptions
  ): Promise<Dialog.IResult<boolean>> {
    return showDialog({
      ...options,
      body: new InputBooleanDialog(options),
      buttons: [
        Dialog.cancelButton({ label: options.cancelLabel }),
        Dialog.okButton({ label: options.okLabel })
      ],
      focusNodeSelector: 'input'
    });
  }

  /**
   * Constructor options for number input dialogs
   */
  export interface INumberOptions extends IOptions {
    /**
     * Default value
     */
    value?: number;
  }

  /**
   * Create and show a input dialog for a number.
   *
   * @param options - The dialog setup options.
   *
   * @returns A promise that resolves with whether the dialog was accepted
   */
  export function getNumber(
    options: INumberOptions
  ): Promise<Dialog.IResult<number>> {
    return showDialog({
      ...options,
      body: new InputNumberDialog(options),
      buttons: [
        Dialog.cancelButton({ label: options.cancelLabel }),
        Dialog.okButton({ label: options.okLabel })
      ],
      focusNodeSelector: 'input'
    });
  }

  /**
   * Constructor options for item selection input dialogs
   */
  export interface IItemOptions extends IOptions {
    /**
     * List of choices
     */
    items: Array<string>;
    /**
     * Default choice
     *
     * If the list is editable a string with a default value can be provided
     * otherwise the index of the default choice should be given.
     */
    current?: number | string;
    /**
     * Is the item editable?
     */
    editable?: boolean;
    /**
     * Placeholder text for editable input
     */
    placeholder?: string;
  }

  /**
   * Create and show a input dialog for a choice.
   *
   * @param options - The dialog setup options.
   *
   * @returns A promise that resolves with whether the dialog was accepted
   */
  export function getItem(
    options: IItemOptions
  ): Promise<Dialog.IResult<string>> {
    return showDialog({
      ...options,
      body: new InputItemsDialog(options),
      buttons: [
        Dialog.cancelButton({ label: options.cancelLabel }),
        Dialog.okButton({ label: options.okLabel })
      ],
      focusNodeSelector: options.editable ? 'input' : 'select'
    });
  }

  /**
   * Constructor options for item selection input dialogs
   */
  export interface IMultipleItemsOptions extends IOptions {
    /**
     * List of choices
     */
    items: Array<string>;
    /**
     * Default choices
     */
    defaults?: string[];
  }

  /**
   * Create and show a input dialog for a choice.
   *
   * @param options - The dialog setup options.
   *
   * @returns A promise that resolves with whether the dialog was accepted
   */
  export function getMultipleItems(
    options: IMultipleItemsOptions
  ): Promise<Dialog.IResult<string[]>> {
    return showDialog({
      ...options,
      body: new InputMultipleItemsDialog(options),
      buttons: [
        Dialog.cancelButton({ label: options.cancelLabel }),
        Dialog.okButton({ label: options.okLabel })
      ]
    });
  }

  /**
   * Constructor options for text input dialogs
   */
  export interface ITextOptions extends IOptions {
    /**
     * Default input text
     */
    text?: string;
    /**
     * Placeholder text
     */
    placeholder?: string;
    /**
     * Selection range
     *
     * Number of characters to pre-select when dialog opens.
     * Default is to select the whole input text if present.
     */
    selectionRange?: number;
  }

  /**
   * Create and show a input dialog for a text.
   *
   * @param options - The dialog setup options.
   *
   * @returns A promise that resolves with whether the dialog was accepted
   */
  export function getText(
    options: ITextOptions
  ): Promise<Dialog.IResult<string>> {
    return showDialog({
      ...options,
      body: new InputTextDialog(options),
      buttons: [
        Dialog.cancelButton({ label: options.cancelLabel }),
        Dialog.okButton({ label: options.okLabel })
      ],
      focusNodeSelector: 'input'
    });
  }

  /**
   * Create and show a input dialog for a password.
   *
   * @param options - The dialog setup options.
   *
   * @returns A promise that resolves with whether the dialog was accepted
   */
  export function getPassword(
    options: Omit<ITextOptions, 'selectionRange'>
  ): Promise<Dialog.IResult<string>> {
    return showDialog({
      ...options,
      body: new InputPasswordDialog(options),
      buttons: [
        Dialog.cancelButton({ label: options.cancelLabel }),
        Dialog.okButton({ label: options.okLabel })
      ],
      focusNodeSelector: 'input'
    });
  }
}

/**
 * Base widget for input dialog body
 */
class InputDialogBase<T> extends Widget implements Dialog.IBodyWidget<T> {
  /**
   * InputDialog constructor
   *
   * @param label Input field label
   */
  constructor(label?: string) {
    super();
    this.addClass(INPUT_DIALOG_CLASS);

    this._input = document.createElement('input');
    this._input.classList.add('jp-mod-styled');
    this._input.id = 'jp-dialog-input-id';

    if (label !== undefined) {
      const labelElement = document.createElement('label');
      labelElement.textContent = label;
      labelElement.htmlFor = this._input.id;

      // Initialize the node
      this.node.appendChild(labelElement);
    }

    this.node.appendChild(this._input);
  }

  /** Input HTML node */
  protected _input: HTMLInputElement;
}

/**
 * Widget body for input boolean dialog
 */
class InputBooleanDialog extends InputDialogBase<boolean> {
  /**
   * InputBooleanDialog constructor
   *
   * @param options Constructor options
   */
  constructor(options: InputDialog.IBooleanOptions) {
    super(options.label);
    this.addClass(INPUT_BOOLEAN_DIALOG_CLASS);

    this._input.type = 'checkbox';
    this._input.checked = options.value ? true : false;
  }

  /**
   * Get the text specified by the user
   */
  getValue(): boolean {
    return this._input.checked;
  }
}

/**
 * Widget body for input number dialog
 */
class InputNumberDialog extends InputDialogBase<number> {
  /**
   * InputNumberDialog constructor
   *
   * @param options Constructor options
   */
  constructor(options: InputDialog.INumberOptions) {
    super(options.label);

    this._input.type = 'number';
    this._input.value = options.value ? options.value.toString() : '0';
  }

  /**
   * Get the number specified by the user.
   */
  getValue(): number {
    if (this._input.value) {
      return Number(this._input.value);
    } else {
      return Number.NaN;
    }
  }
}

/**
 * Widget body for input text dialog
 */
class InputTextDialog extends InputDialogBase<string> {
  /**
   * InputTextDialog constructor
   *
   * @param options Constructor options
   */
  constructor(options: InputDialog.ITextOptions) {
    super(options.label);

    this._input.type = 'text';
    this._input.value = options.text ? options.text : '';
    if (options.placeholder) {
      this._input.placeholder = options.placeholder;
    }
    this._initialSelectionRange = Math.min(
      this._input.value.length,
      Math.max(0, options.selectionRange ?? this._input.value.length)
    );
  }

  /**
   *  A message handler invoked on an `'after-attach'` message.
   */
  protected onAfterAttach(msg: Message): void {
    super.onAfterAttach(msg);
    if (this._initialSelectionRange > 0 && this._input.value) {
      this._input.setSelectionRange(0, this._initialSelectionRange);
    }
  }

  /**
   * Get the text specified by the user
   */
  getValue(): string {
    return this._input.value;
  }

  private _initialSelectionRange: number;
}

/**
 * Widget body for input password dialog
 */
class InputPasswordDialog extends InputDialogBase<string> {
  /**
   * InputPasswordDialog constructor
   *
   * @param options Constructor options
   */
  constructor(options: InputDialog.ITextOptions) {
    super(options.label);

    this._input.type = 'password';
    this._input.value = options.text ? options.text : '';
    if (options.placeholder) {
      this._input.placeholder = options.placeholder;
    }
  }

  /**
   *  A message handler invoked on an `'after-attach'` message.
   */
  protected onAfterAttach(msg: Message): void {
    super.onAfterAttach(msg);
    if (this._input.value) {
      this._input.select();
    }
  }

  /**
   * Get the text specified by the user
   */
  getValue(): string {
    return this._input.value;
  }
}

/**
 * Widget body for input list dialog
 */
class InputItemsDialog extends InputDialogBase<string> {
  /**
   * InputItemsDialog constructor
   *
   * @param options Constructor options
   */
  constructor(options: InputDialog.IItemOptions) {
    super(options.label);

    this._editable = options.editable || false;

    let current = options.current || 0;
    let defaultIndex: number;
    if (typeof current === 'number') {
      defaultIndex = Math.max(0, Math.min(current, options.items.length - 1));
      current = '';
    }

    this._list = document.createElement('select');
    options.items.forEach((item, index) => {
      const option = document.createElement('option');
      if (index === defaultIndex) {
        option.selected = true;
        current = item;
      }
      option.value = item;
      option.textContent = item;
      this._list.appendChild(option);
    });

    if (options.editable) {
      /* Use of list and datalist */
      const data = document.createElement('datalist');
      data.id = 'input-dialog-items';
      data.appendChild(this._list);

      this._input.type = 'list';
      this._input.value = current;
      this._input.setAttribute('list', data.id);
      if (options.placeholder) {
        this._input.placeholder = options.placeholder;
      }
      this.node.appendChild(data);
    } else {
      /* Use select directly */
      this._input.remove();
      this.node.appendChild(this._list);
    }
  }

  /**
   * Get the user choice
   */
  getValue(): string {
    if (this._editable) {
      return this._input.value;
    } else {
      return this._list.value;
    }
  }

  private _list: HTMLSelectElement;
  private _editable: boolean;
}

/**
 * Widget body for input list dialog
 */
class InputMultipleItemsDialog extends InputDialogBase<string> {
  /**
   * InputMultipleItemsDialog constructor
   *
   * @param options Constructor options
   */
  constructor(options: InputDialog.IMultipleItemsOptions) {
    super(options.label);

    let defaults = options.defaults || [];

    this._list = document.createElement('select');
    this._list.setAttribute('multiple', '');

    options.items.forEach(item => {
      const option = document.createElement('option');
      option.value = item;
      option.textContent = item;
      this._list.appendChild(option);
    });

    // use the select
    this._input.remove();
    this.node.appendChild(this._list);

    // select the current ones
    const htmlOptions = this._list.options;
    for (let i: number = 0; i < htmlOptions.length; i++) {
      const option = htmlOptions[i];
      if (defaults.includes(option.value)) {
        option.selected = true;
      } else {
        option.selected = false;
      }
    }
  }

  /**
   * Get the user choices
   */
  getValue(): string[] {
    let result = [];
    for (let opt of this._list.options) {
      if (opt.selected && !opt.classList.contains('hidden')) {
        result.push(opt.value || opt.text);
      }
    }
    return result;
  }

  private _list: HTMLSelectElement;
}
