/*
 * Copyright (c) 2010, 2023 BSI Business Systems Integration AG
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */
import {Action, aria, arrays, ComboMenu, EllipsisMenu, InitModelOf, Menu, MenuDestinations, ObjectOrChildModel, scout, Widget} from '../index';
import $ from 'jquery';

export type MenuFilterOptions = {
  onlyVisible?: boolean;
  enableDisableKeyStrokes?: boolean;
  notAllowedTypes?: string | string[];
  defaultMenuTypes?: string | string[];
};

export const menus = {
  filterAccordingToSelection(prefix: string, selectionLength: number, menuArr: Menu[], destination: MenuDestinations, options?: MenuFilterOptions): Menu[] {
    let allowedTypes: string[] = [],
      {notAllowedTypes} = options || {};

    if (destination === MenuDestinations.MENU_BAR) {
      allowedTypes = [prefix + '.EmptySpace', prefix + '.SingleSelection', prefix + '.MultiSelection'];
    } else if (destination === MenuDestinations.CONTEXT_MENU) {
      allowedTypes = [prefix + '.SingleSelection', prefix + '.MultiSelection'];
    } else if (destination === MenuDestinations.HEADER) {
      allowedTypes = [prefix + '.Header'];
    }

    if (allowedTypes.indexOf(prefix + '.SingleSelection') > -1 && selectionLength !== 1) {
      arrays.remove(allowedTypes, prefix + '.SingleSelection');
    }
    if (allowedTypes.indexOf(prefix + '.MultiSelection') > -1 && selectionLength <= 1) {
      arrays.remove(allowedTypes, prefix + '.MultiSelection');
    }
    notAllowedTypes = arrays.ensure(notAllowedTypes);
    let fixedNotAllowedTypes = [];
    // ensure prefix
    prefix = prefix + '.';
    notAllowedTypes.forEach(type => {
      if (type.slice(0, prefix.length) !== prefix) {
        type = prefix + type;
      }
      fixedNotAllowedTypes.push(type);
    });
    return menus.filter(menuArr, allowedTypes, $.extend({}, options, {notAllowedTypes: fixedNotAllowedTypes}));
  },

  /**
   * Filters menus that don't match the given types, or in other words: only menus with the given types are returned
   * from this method. The visible state is only checked if the parameter onlyVisible is set to true. Otherwise, invisible items are returned and added to the
   * menu-bar DOM (invisible, however). They may change their visible state later. If there are any types in notAllowedTypes each menu is checked also against
   * these types and if they are matching the menu is filtered.
   */
  filter(menuArr: Menu[], types?: string | string[], options?: MenuFilterOptions): Menu[] {
    if (!menuArr) {
      return;
    }
    types = arrays.ensure(types);
    let {onlyVisible, enableDisableKeyStrokes, notAllowedTypes, defaultMenuTypes} = options || {};
    notAllowedTypes = arrays.ensure(notAllowedTypes);
    defaultMenuTypes = arrays.ensure(defaultMenuTypes);

    let filteredMenus = [], separatorCount = 0;

    menuArr.forEach(menu => {
      let childMenus = menu.childActions;
      if (childMenus.length > 0) {
        childMenus = menus.filter(childMenus, types, options);
        if (childMenus.length === 0) {
          menus._enableDisableMenuKeyStroke(menu, enableDisableKeyStrokes, true);
          return;
        }
      } else if (!menus._checkType(menu, types as string[], defaultMenuTypes) || (notAllowedTypes.length !== 0 && menus._checkType(menu, notAllowedTypes as string[], defaultMenuTypes))) {
        // Don't check the menu type for a group
        menus._enableDisableMenuKeyStroke(menu, enableDisableKeyStrokes, true);
        return;
      }

      if (onlyVisible && !menu.visible) {
        menus._enableDisableMenuKeyStroke(menu, enableDisableKeyStrokes, true);
        return;
      }
      if (menu.separator) {
        separatorCount++;
      }
      menus._enableDisableMenuKeyStroke(menu, enableDisableKeyStrokes, false);
      filteredMenus.push(menu);
    });

    // Ignore menus with only separators
    if (separatorCount === filteredMenus.length) {
      return [];
    }
    return filteredMenus;
  },

  /**
   * Makes leading, trailing and duplicate separators invisible or reverts the visibility change if needed.
   */
  updateSeparatorVisibility(menuArr: Menu | Menu[]) {
    menuArr = arrays.ensure(menuArr);
    menuArr = menuArr.filter(menu => menu.visible || menu.separator);
    if (menuArr.length === 0) {
      return;
    }

    let hasMenuBefore = false;
    let hasMenuAfter = false;
    menuArr.forEach((menu: Menu & { visibleOrig: boolean }, i: number) => {
      if (menu.ellipsis) {
        return;
      }
      if (!menu.separator) {
        hasMenuBefore = true;
        return;
      }
      hasMenuAfter = menuArr[i + 1] && !menuArr[i + 1].separator && !menuArr[i + 1].ellipsis;

      // If the separator has a separator next to it, make it invisible
      if (!hasMenuBefore || !hasMenuAfter) {
        if (menu.visibleOrig === undefined) {
          menu.visibleOrig = menu.visible;
          menu.setVisible(false);
        }
      } else if (menu.visibleOrig !== undefined) {
        // Revert to original state
        menu.setVisible(menu.visibleOrig);
        menu.visibleOrig = undefined;
      }
    });
  },

  checkType(menu: Menu, types: string | string[], defaultMenuTypes: string | string[] = []): boolean {
    types = arrays.ensure(types);
    if (menu.childActions.length > 0) {
      let childMenus = menus.filter(menu.childActions, types, {defaultMenuTypes});
      return childMenus.length > 0;
    }
    return menus._checkType(menu, types, defaultMenuTypes);
  },

  /** @internal */
  _enableDisableMenuKeyStroke(menu: Menu, activated: boolean, exclude: boolean) {
    if (activated) {
      menu.excludedByFilter = exclude;
    }
  },

  /**
   * Checks the type of menu. Don't use this for menu groups.
   * @internal
   */
  _checkType(menu: Menu, types: string[], defaultMenuTypes: string | string[] = []): boolean {
    if (!types || types.length === 0) {
      return false;
    }
    let menuTypes = arrays.ensure(menu.menuTypes);
    defaultMenuTypes = arrays.ensure(defaultMenuTypes);
    if (menuTypes.length === 0) {
      menuTypes = defaultMenuTypes;
    }
    for (let j = 0; j < types.length; j++) {
      if (menuTypes.indexOf(types[j]) > -1) {
        return true;
      }
    }
    return false;
  },

  createEllipsisMenu(options: InitModelOf<EllipsisMenu>): EllipsisMenu {
    return scout.create(EllipsisMenu, options);
  },

  moveMenuIntoEllipsis(menu: Menu, ellipsis: EllipsisMenu) {
    menu.remove();
    menu._setOverflown(true);
    menu.overflowMenu = ellipsis;

    let menusInEllipsis = ellipsis.childActions.slice();
    menusInEllipsis.unshift(menu); // add as first element
    ellipsis.setChildActions(menusInEllipsis);
  },

  removeMenuFromEllipsis(menu: Menu, $parent?: JQuery) {
    menu._setOverflown(false);
    menu.overflowMenu = null;
    if (!menu.rendered) {
      menu.render($parent);
    }
  },

  /**
   * If the given actions contain a {@link ComboMenu}, the resulting array contains the child actions of the combo menu instead of the combo menu itself.
   */
  flatTopLevelActions<TAction extends Action>(actions: TAction[]): TAction[] {
    return arrays.flatMap(actions, item => {
      if (item instanceof ComboMenu) {
        return item.childActions;
      }
      return [item];
    });
  },

  /**
   * Appends the given menus to the existing menus of the menu owner and calls {@link MenuOwner.setMenus} to set the new list of menus.
   */
  insertMenus(menuOwner: MenuOwner, menusToInsert: ObjectOrChildModel<Menu>[]) {
    menusToInsert = arrays.ensure(menusToInsert);
    if (menusToInsert.length === 0) {
      return;
    }
    let menus = menuOwner.menus as ObjectOrChildModel<Menu>[];
    menuOwner.setMenus(menus.concat(menusToInsert));
  },

  /**
   * Removes the given menus from the existing menus of the menu owner and calls {@link MenuOwner.setMenus} to set the new list of menus.
   */
  deleteMenus(menuOwner: MenuOwner, menusToDelete: Menu[]) {
    menusToDelete = arrays.ensure(menusToDelete);
    if (menusToDelete.length === 0) {
      return;
    }
    let menus = arrays.diff(menuOwner.menus, menusToDelete);
    menuOwner.setMenus(menus);
  },

  updateAriaActiveDescendant($container: JQuery, $body: JQuery, menuItemClass?: string, $selectedItem?: JQuery) {
    menuItemClass = menuItemClass || 'menu-item';
    $selectedItem = $selectedItem || $body.find('.' + menuItemClass + '.selected');
    aria.linkElementWithActiveDescendant($container, $selectedItem);
  }
};

export type MenuOwner = Widget & { menus: Menu[]; setMenus: (menus: ObjectOrChildModel<Menu>[]) => void };
