import {Command} from 'app/client/components/commands';
import {NeedUpgradeError, reportError} from 'app/client/models/errors';
import {colors, testId, vars} from 'app/client/ui2018/cssVars';
import {IconName} from 'app/client/ui2018/IconList';
import {icon} from 'app/client/ui2018/icons';
import {commonUrls} from 'app/common/gristUrls';
import {dom, DomElementArg, DomElementMethod} from 'grainjs';
import {MaybeObsArray, Observable, styled} from 'grainjs';
import * as weasel from 'popweasel';
import {IAutocompleteOptions} from 'popweasel';

export interface IOptionFull<T> {
  value: T;
  label: string;
  disabled?: boolean;
  icon?: IconName;
}

// For string options, we can use a string for label and value without wrapping into an object.
export type IOption<T> = (T & string) | IOptionFull<T>;

export function menu(createFunc: weasel.MenuCreateFunc, options?: weasel.IMenuOptions): DomElementMethod {
  return weasel.menu(createFunc, {...defaults, ...options});
}

// TODO Weasel doesn't allow other options for submenus, but probably should.
export type ISubMenuOptions = weasel.ISubMenuOptions & weasel.IPopupOptions;

export function menuItemSubmenu(
  submenu: weasel.MenuCreateFunc,
  options: ISubMenuOptions,
  ...args: DomElementArg[]
): Element {
  return weasel.menuItemSubmenu(submenu, {...defaults, ...options}, ...args);
}

const cssMenuElem = styled('div', `
  font-family: ${vars.fontFamily};
  font-size: ${vars.mediumFontSize};
  line-height: initial;
  max-width: 400px;
  padding: 8px 0px 16px 0px;
  box-shadow: 0 2px 20px 0 rgba(38,38,51,0.6);
  min-width: 160px;
  z-index: 999;
  --weaseljs-selected-background-color: ${vars.primaryBg};
  --weaseljs-menu-item-padding: 8px 24px;

  @media print {
    & {
      display: none;
    }
  }
`);

const menuItemStyle = `
  justify-content: flex-start;
  align-items: center;
  --icon-color: ${colors.lightGreen};
  .${weasel.cssMenuItem.className}-sel {
    --icon-color: ${colors.light};
  }
  &.disabled {
    cursor: default;
    opacity: 0.2;
  }
`;

export const menuCssClass = cssMenuElem.className;

// Add grist-floating-menu class to support existing browser tests
const defaults = { menuCssClass: menuCssClass + ' grist-floating-menu' };

/**
 * Creates a select dropdown widget. The observable `obs` reflects the value of the selected
 * option, and `optionArray` is an array (regular or observable) of option values and labels.
 * These may be either strings, or {label, value, icon, disabled} objects. Icons are optional
 * and must be IconName strings from 'app/client/ui2018/IconList'.
 *
 * The type of value may be any type at all; it is opaque to this widget.
 *
 * If obs is set to an invalid or disabled value, then defLabel option is used to determine the
 * label that the select box will show, blank by default.
 *
 * Usage:
 *    const fruit = observable("apple");
 *    select(fruit, ["apple", "banana", "mango"]);
 *
 *    const employee = observable(17);
 *    const allEmployees = Observable.create(owner, [
 *      {value: 12, label: "Bob", disabled: true},
 *      {value: 17, label: "Alice"},
 *      {value: 21, label: "Eve"},
 *    ]);
 *    select(employee, allEmployees, {defLabel: "Select employee:"});
 *
 * Note that this select element is not compatible with browser address autofill for usage in
 * forms, and that formSelect should be used for this purpose.
 */
export function select<T>(obs: Observable<T>, optionArray: MaybeObsArray<IOption<T>>,
                          options: weasel.ISelectUserOptions = {}) {
  const _menu = cssSelectMenuElem(testId('select-menu'));
  const _btn = cssSelectBtn(testId('select-open'));

  const selectOptions = {
    buttonArrow: cssInlineCollapseIcon('Collapse'),
    menuCssClass: _menu.className,
    buttonCssClass: _btn.className,
    ...options,
  };

  return weasel.select(obs, optionArray, selectOptions, (op) =>
    cssOptionRow(
      op.icon ? cssOptionRowIcon(op.icon) : null,
      cssOptionLabel(op.label),
      testId('select-row')
    )
  ) as HTMLElement; // TODO: should be changed in weasel
}

/**
 * Same as select(), but the main element looks like a link rather than a button.
 */
export function linkSelect<T>(obs: Observable<T>, optionArray: MaybeObsArray<IOption<T>>,
                              options: weasel.ISelectUserOptions = {}) {
  const _btn = cssSelectBtnLink(testId('select-open'));
  const elem = select(obs, optionArray, {buttonCssClass: _btn.className, ...options});
  // It feels strange to have focus stay on this link; remove tabIndex that makes it focusable.
  elem.removeAttribute('tabIndex');
  return elem;
}

/**
 * Creates a select dropdown widget that is more ideal for forms. Implemented using the <select>
 * element to work with browser form autofill and typing in the desired value to quickly set it.
 * The appearance of the opened menu is OS dependent.
 *
 * The observable `obs` reflects the value of the selected option, and `optionArray` is an
 * array (regular or observable) of option values and labels. These may be either strings,
 * or {label, value} objects.
 *
 * If obs is set to an empty string value, then defLabel option is used to determine the
 * label that the select box will show, blank by default.
 *
 * Usage:
 *    const fruit = observable("");
 *    formSelect(fruit, ["apple", "banana", "mango"], {defLabel: "Select fruit:"});
 */
export function formSelect(obs: Observable<string>, optionArray: MaybeObsArray<IOption<string>>,
                           options: {defaultLabel?: string} = {}) {
  const {defaultLabel = ""} = options;
  const container: Element = cssSelectBtnContainer(
    dom('select', {class: cssSelectBtn.className, style: 'height: 42px; padding: 12px 30px 12px 12px;'},
      dom.prop('value', obs),
      dom.on('change', (_, elem) => { obs.set(elem.value); }),
      dom('option', {value: '', hidden: 'hidden'}, defaultLabel),
      dom.forEach(optionArray, (option) => {
        const obj: weasel.IOptionFull<string> = weasel.getOptionFull(option);
        return dom('option', {value: obj.value}, obj.label);
      })
    ),
    cssCollapseIcon('Collapse')
  );
  return container;
}

export function inputMenu(createFunc: weasel.MenuCreateFunc, options?: weasel.IMenuOptions): DomElementMethod {
  // Triggers the input menu on 'input' events, if the input has text inside.
  function inputTrigger(triggerElem: Element, ctl: weasel.PopupControl): void {
    dom.onElem(triggerElem, 'input', () => {
      (triggerElem as HTMLInputElement).value.length > 0 ? ctl.open() : ctl.close();
    });
  }
  return weasel.inputMenu(createFunc, {
    trigger: [inputTrigger],
    menuCssClass: `${cssMenuElem.className} ${cssInputButtonMenuElem.className}`,
    ...options
  });
}

// A menu item that leads to the billing page if the desired operation requires an upgrade.
// Such menu items are marked with a little sparkle unicode.
export function upgradableMenuItem(needUpgrade: boolean, action: () => void, ...rem: any[]) {
  if (needUpgrade) {
    return menuItem(() => reportError(new NeedUpgradeError()), ...rem, " *");
  } else {
    return menuItem(action, ...rem);
  }
}

export function upgradeText(needUpgrade: boolean) {
  if (!needUpgrade) { return null; }
  return menuText(dom('span', '* Workspaces are available on team plans. ',
        dom('a', {href: commonUrls.plans}, 'Upgrade now')));
}

/**
 * Create an autocomplete element and tie it to an input or textarea element.
 *
 * Usage:
 *      const employees = ['Thomas', 'June', 'Bethany', 'Mark', 'Marjorey', 'Zachary'];
 *      const inputElem = input(...);
 *      autocomplete(inputElem, employees);
 */
export function autocomplete(
  inputElem: HTMLInputElement,
  choices: MaybeObsArray<string>,
  options: IAutocompleteOptions = {}
) {
  return weasel.autocomplete(inputElem, choices, {
    ...defaults, ...options,
    menuCssClass: menuCssClass + ' ' + cssSelectMenuElem.className,
  });
}

export const menuSubHeader = styled('div', `
  font-size: ${vars.xsmallFontSize};
  text-transform: uppercase;
  font-weight: ${vars.bigControlTextWeight};
  padding: 8px 24px 16px 24px;
  cursor: default;
`);

export const menuText = styled('div', `
  display: flex;
  align-items: center;
  font-size: ${vars.smallFontSize};
  color: ${colors.slate};
  padding: 8px 24px 4px 24px;
  max-width: 250px;
  cursor: default;
`);

export const menuItem = styled(weasel.menuItem, menuItemStyle);

export const menuItemLink = styled(weasel.menuItemLink, menuItemStyle);

export function menuItemCmd(cmd: Command, label: string, ...args: DomElementArg[]) {
  return menuItem(
    cmd.run,
    dom('span', label, testId('cmd-name')),
    cmd.humanKeys.length ? cssCmdKey(cmd.humanKeys[0]) : null,
    cssMenuItemCmd.cls(''), // overrides some menu item styles
    ...args
  );
}

export const menuDivider = styled(weasel.cssMenuDivider, `
  margin: 8px 0;
`);

export const menuIcon = styled(icon, `
  flex: none;
  margin-right: 8px;
`);

const cssSelectMenuElem = styled(cssMenuElem, `
  max-height: 400px;
  overflow-y: auto;

  --weaseljs-menu-item-padding: 8px 16px;
`);

const cssSelectBtnContainer = styled('div', `
  position: relative;
  width: 100%;
`);

const cssSelectBtn = styled('div', `
  width: 100%;
  height: 30px;
  line-height: 16px;
  background-color: white;
  font-size: ${vars.mediumFontSize};
  padding: 5px;
  border: 1px solid ${colors.darkGrey};
  color: ${colors.dark};
  --icon-color: ${colors.dark};
  border-radius: 3px;
  cursor: pointer;
  outline: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  display: flex;
`);

const cssSelectBtnLink = styled('div', `
  display: flex;
  align-items: center;
  font-size: ${vars.mediumFontSize};
  color: ${colors.lightGreen};
  --icon-color: ${colors.lightGreen};
  width: initial;
  height: initial;
  line-height: inherit;
  background-color: initial;
  padding: initial;
  border: initial;
  border-radius: initial;
  box-shadow: initial;
  cursor: pointer;
  outline: none;
  -webkit-appearance: none;
  -moz-appearance: none;

  &:hover, &:focus, &:active {
    color: ${colors.darkGreen};
    --icon-color: ${colors.darkGreen};
    box-shadow: initial;
  }
`);

const cssOptionIcon = styled(icon, `
  height: 16px;
  width: 16px;
  background-color: ${colors.slate};
  margin: -3px 8px 0 2px;
`);

const cssOptionRow = styled('span', `
  display: flex;
  align-items: center;
  width: 100%;
`);

const cssOptionRowIcon = styled(cssOptionIcon, `
  margin: 0 8px 0 0;
  flex: none;

  .${weasel.cssMenuItem.className}-sel & {
    background-color: white;
  }
`);

const cssOptionLabel = styled('div', `
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
`);

const cssInlineCollapseIcon = styled(icon, `
  margin: 0 2px;
  pointer-events: none;
`);

const cssCollapseIcon = styled(icon, `
  position: absolute;
  right: 12px;
  top: calc(50% - 8px);
  pointer-events: none;
  background-color: ${colors.dark};
`);

const cssInputButtonMenuElem = styled(cssMenuElem, `
  padding: 4px 0px;
`);

const cssMenuItemCmd = styled('div', `
  justify-content: space-between;
`);

const cssCmdKey = styled('span', `
  margin-left: 16px;
  color: ${colors.slate};
  margin-right: -12px;
`);