/*
 * Copyright (c) 2010, 2025 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 {
  aria, ButtonAdapterMenu, ButtonEventMap, ButtonKeyStroke, ButtonLayout, ButtonModel, ContextMenuPopup, Device, DoubleClickSupport, EnumObject, FormField, icons, InitModelOf, KeyStrokeContext, LoadingSupport, Menu, objects, scout, styles,
  tooltips, Widget
} from '../../../index';

export class Button extends FormField implements ButtonModel {
  declare model: ButtonModel;
  declare eventMap: ButtonEventMap;
  declare self: Button;

  defaultButton: boolean;
  displayStyle: ButtonDisplayStyle;
  iconId: string;
  keyStroke: string;
  keyStrokeScope: Widget;
  processButton: boolean;
  selected: boolean;
  systemType: ButtonSystemType;
  preventDoubleClick: boolean;
  stackable: boolean;
  shrinkable: boolean;
  adaptedBy: ButtonAdapterMenu;
  buttonKeyStroke: ButtonKeyStroke;
  formKeyStrokeContext: KeyStrokeContext;
  popup: ContextMenuPopup;
  $buttonLabel: JQuery;
  $submenuIcon: JQuery;
  protected _doubleClickSupport: DoubleClickSupport;

  constructor() {
    super();

    this.adaptedBy = null;
    this.defaultButton = false;
    this.displayStyle = Button.DisplayStyle.DEFAULT;
    this.gridDataHints.fillHorizontal = false;
    this.iconId = null;
    this.keyStroke = null;
    this.keyStrokeScope = null;
    this.processButton = true;
    this.selected = false;
    this.statusVisible = false;
    this.systemType = Button.SystemType.NONE;
    this.preventDoubleClick = false;
    this.preventClickFocus = true;
    this.stackable = true;
    this.shrinkable = false;
    this.$buttonLabel = null;
    this.buttonKeyStroke = new ButtonKeyStroke(this, null);
    this._doubleClickSupport = new DoubleClickSupport();
    this._addCloneProperties(['defaultButton', 'displayStyle', 'iconId', 'keyStroke', 'processButton', 'selected', 'systemType', 'preventDoubleClick', 'stackable', 'shrinkable']);
  }

  static SystemType = {
    NONE: 0,
    CANCEL: 1,
    CLOSE: 2,
    OK: 3,
    RESET: 4,
    SAVE: 5
  } as const;

  static DisplayStyle = {
    DEFAULT: 0,
    TOGGLE: 1,
    RADIO: 2,
    LINK: 3,
    BORDERLESS: 4
  } as const;

  static SUBMENU_ICON = icons.ANGLE_DOWN_BOLD;

  protected override _init(model: InitModelOf<this>) {
    super._init(model);
    this.resolveIconIds(['iconId']);
    this._setKeyStroke(this.keyStroke);
    this._setKeyStrokeScope(this.keyStrokeScope);
    this._setInheritAccessibility(this.inheritAccessibility && !this._isIgnoreAccessibilityFlags());
  }

  override getContextMenuItems(onlyVisible = true): Menu[] {
    return [];
  }

  protected override _initKeyStrokeContext() {
    super._initKeyStrokeContext();

    this._initDefaultKeyStrokes();

    this.formKeyStrokeContext = new KeyStrokeContext();
    this.formKeyStrokeContext.invokeAcceptInputOnActiveValueField = true;
    this.formKeyStrokeContext.registerKeyStroke(this.buttonKeyStroke);
    this.formKeyStrokeContext.$bindTarget = () => {
      if (this.keyStrokeScope) {
        return this.keyStrokeScope.$container;
      }
      // use form if available
      let form = this.getForm();
      if (form) {
        return form.$container;
      }
      // use desktop otherwise
      return this.session.desktop.$container;
    };
  }

  protected _isIgnoreAccessibilityFlags(): boolean {
    return this.systemType === Button.SystemType.CANCEL || this.systemType === Button.SystemType.CLOSE;
  }

  protected _initDefaultKeyStrokes() {
    this.keyStrokeContext.registerKeyStrokes([
      new ButtonKeyStroke(this, 'ENTER'),
      new ButtonKeyStroke(this, 'SPACE')
    ]);
  }

  protected override _createLoadingSupport(): LoadingSupport {
    return new LoadingSupport({
      widget: this,
      $container: () => {
        return this.$field;
      }
    });
  }

  protected override _render() {
    let $button: JQuery;
    if (this.displayStyle === Button.DisplayStyle.LINK) {
      // Render as link-button
      $button = this.$parent.makeDiv('link-button menu-item');
      aria.role($button, 'link');
      this.$buttonLabel = $button.appendSpan('button-label text');
    } else {
      // render as button
      $button = this.$parent.makeElement('<button>')
        .addClass('button');
      if (this.displayStyle === Button.DisplayStyle.BORDERLESS) {
        $button.addClass('borderless');
      }
      this.$buttonLabel = $button.appendSpan('button-label text');

      if (Device.get().supportsOnlyTouch()) {
        $button.setTabbable(false);
      }
    }
    this.addContainer(this.$parent, 'button-field', new ButtonLayout(this));
    this.addField($button);
    // TODO [10.0] cgu: should we add a label? -> would make it possible to control the space left of the button using labelVisible, like it is possible with checkboxes
    this.addStatus();

    $button
      .on('mousedown', event => this._doubleClickSupport.mousedown(event))
      .on('click', this._onClick.bind(this));

    this.session.keyStrokeManager.installKeyStrokeContext(this.formKeyStrokeContext);

    tooltips.installForEllipsis(this.$buttonLabel, {
      parent: this
    });
  }

  protected override _remove() {
    super._remove();
    tooltips.uninstall(this.$buttonLabel);
    this.session.keyStrokeManager.uninstallKeyStrokeContext(this.formKeyStrokeContext);
    this.$submenuIcon = null;
  }

  protected override _renderProperties() {
    super._renderProperties();
    this._renderIconId();
    this._renderSelected();
    this._renderDefaultButton();
    this._updateLabelAndIconStyle();
  }

  protected override _renderForegroundColor() {
    super._renderForegroundColor();
    // Color button label as well, otherwise the color would not be visible because button label has already a color set using css
    styles.legacyForegroundColor(this, this.$buttonLabel);
    styles.legacyForegroundColor(this, this.get$Icon());
    styles.legacyForegroundColor(this, this.$submenuIcon);
  }

  protected override _renderBackgroundColor() {
    super._renderBackgroundColor();
    styles.legacyBackgroundColor(this, this.$fieldContainer);
  }

  protected override _renderFont() {
    super._renderFont();
    styles.legacyFont(this, this.$buttonLabel);
    // Changing the font may enlarge or shrink the field (e.g. set the style to bold makes the text bigger) -> invalidate layout
    this.invalidateLayoutTree();
  }

  override _updateMenus() {
    super._updateMenus();

    let hasMenus = this.menus.length > 0;
    aria.hasPopup(this.$field, hasMenus ? 'menu' : null);
    aria.expanded(this.$field, hasMenus ? !objects.isNullOrUndefined(this.popup) : null);

    this._renderSubmenuIcon();
  }

  protected _renderSubmenuIcon() {
    let hasMenus = this.menus?.length > 0;
    if (hasMenus && (this.label || !this.iconId)) { // no indicator when _only_ the icon is visible
      if (!this.$submenuIcon) {
        let icon = icons.parseIconId(Button.SUBMENU_ICON);
        this.$submenuIcon = this.$field
          .appendSpan('submenu-icon')
          .text(icon.iconCharacter);
        aria.hidden(this.$submenuIcon, true);
        this.invalidateLayoutTree();
      }
    } else if (this.$submenuIcon) {
      this.$submenuIcon.remove();
      this.$submenuIcon = null;
      this.invalidateLayoutTree();
    }
    if (!this.rendering) {
      this._updateLabelAndIconStyle();
    }
  }

  /**
   * @returns true if the action has been performed or false if it has not been performed (e.g. when the button is not enabled).
   */
  doAction(): boolean {
    if (!this.enabledComputed || !this.visible) {
      return false;
    }

    if (this.displayStyle === Button.DisplayStyle.TOGGLE) {
      this.setSelected(!this.selected);
    } else if (this.menus.length > 0) {
      this.togglePopup();
    }
    this._doAction();
    return true;
  }

  protected _doAction() {
    this.trigger('click');
  }

  togglePopup() {
    if (this.popup) {
      this.popup.close();
    } else {
      this.popup = this._openPopup();
      this.popup.one('destroy', event => {
        this.popup = null;
        aria.expanded(this.$field, false);
      });
      aria.expanded(this.$field, true);
    }
  }

  protected _openPopup(): ContextMenuPopup {
    let popup = scout.create(ContextMenuPopup, {
      parent: this,
      menuItems: this.menus,
      cloneMenuItems: false,
      closeOnAnchorMouseDown: false,
      $anchor: this.$field
    });
    popup.open();
    return popup;
  }

  protected _doActionTogglesSubMenu(): boolean {
    return false;
  }

  setDefaultButton(defaultButton: boolean) {
    this.setProperty('defaultButton', defaultButton);
  }

  protected _renderDefaultButton() {
    this.$field.toggleClass('default', this.defaultButton);
  }

  protected override _renderEnabled() {
    super._renderEnabled();
    if (this.displayStyle === Button.DisplayStyle.LINK) {
      this.$field.setTabbable(this.enabledComputed && !Device.get().supportsOnlyTouch());
    }
  }

  setSelected(selected: boolean) {
    this.setProperty('selected', selected);
  }

  protected _renderSelected() {
    if (this.displayStyle === Button.DisplayStyle.TOGGLE) {
      this.$field.toggleClass('selected', this.selected);
    }
    aria.pressed(this.$field, this.displayStyle === Button.DisplayStyle.TOGGLE ? this.selected : null);
  }

  protected override _renderLabel() {
    this.$buttonLabel.contentOrNbsp(this.labelHtmlEnabled, this.label, 'empty');
    if (!this.rendering) {
      this._renderSubmenuIcon();
    }

    // Invalidate layout because button may now be longer or shorter
    this.invalidateLayoutTree();
  }

  setIconId(iconId: string) {
    this.setProperty('iconId', iconId);
  }

  /**
   * Adds an image or font-based icon to the button by adding either an IMG or SPAN element to the button.
   */
  protected _renderIconId() {
    let $iconTarget = this.$fieldContainer;
    $iconTarget.icon(this.iconId);
    let $icon = $iconTarget.data('$icon') as JQuery;
    if ($icon) {
      // <img>s are loaded asynchronously. The real image size is not known until the image is loaded.
      // We add a listener to revalidate the button layout after this has happened. The 'loading' and
      // 'broken' classes ensure the incomplete icon is not taking any space.
      $icon.removeClass('loading broken');
      if ($icon.is('img')) {
        $icon.addClass('loading');
        $icon
          .off('load error')
          .on('load', updateButtonLayoutAfterImageLoaded.bind(this, true))
          .on('error', updateButtonLayoutAfterImageLoaded.bind(this, false));
      }
      if (!this.rendered) {
        styles.legacyForegroundColor(this, $icon);
      }
    }

    if (!this.rendering) {
      this._renderSubmenuIcon();
    }
    // Invalidate layout because button may now be longer or shorter
    this.invalidateLayoutTree();

    // ----- Helper functions -----

    function updateButtonLayoutAfterImageLoaded(success: boolean) {
      $icon.removeClass('loading');
      $icon.toggleClass('broken', !success);
      this.invalidateLayoutTree();
    }
  }

  get$Icon(): JQuery {
    let $iconTarget = this.$fieldContainer;
    return $iconTarget.children('.icon');
  }

  protected _updateLabelAndIconStyle() {
    let hasText = !!this.label;
    let hasIcon = !!this.iconId;
    let hasSubMenuIcon = !!this.$submenuIcon;
    let hasAnyIcon = hasIcon || hasSubMenuIcon;
    this.$buttonLabel.setVisible(hasText || !hasAnyIcon);
    this.$submenuIcon?.toggleClass('with-label', hasText);
    this.get$Icon().toggleClass('with-label', hasText);
  }

  setKeyStroke(keyStroke: string) {
    this.setProperty('keyStroke', keyStroke);
  }

  protected _setKeyStroke(keyStroke: string) {
    this._setProperty('keyStroke', keyStroke);
    this.buttonKeyStroke.parseAndSetKeyStroke(this.keyStroke);
  }

  protected _setKeyStrokeScope(keyStrokeScope: Widget | string) {
    if (typeof keyStrokeScope === 'string') {
      keyStrokeScope = this._resolveKeyStrokeScope(keyStrokeScope);
      if (!keyStrokeScope) {
        // Will be resolved later
        return;
      }
    }

    this._setProperty('keyStrokeScope', keyStrokeScope);
  }

  protected _resolveKeyStrokeScope(keyStrokeScope: string): Widget {
    // Basically, the desktop could be used to find the scope, but that would mean to traverse the whole widget tree.
    // To make it faster the form is used instead but that limits the resolving to the form.
    // This should be acceptable because the scope can still be set explicitly without using an id.
    let form = this.findNonWrappedForm();
    if (!form) {
      throw new Error('Could not resolve keyStrokeScope ' + keyStrokeScope + ' because no form has been found.');
    }
    if (!form.initialized) {
      // KeyStrokeScope is another widget (form or formfield) which may not be initialized yet.
      // The widget must be on the same form as the button, so once that form is initialized the keyStrokeScope has to be available
      form.one('init', this._setKeyStrokeScope.bind(this, keyStrokeScope));
      return;
    }
    let scope = form.widget(keyStrokeScope);
    if (!scope) {
      throw new Error('Could not resolve keyStrokeScope ' + keyStrokeScope + ' using form ' + form);
    }
    return scope;
  }

  protected _onClick(event: JQuery.ClickEvent) {
    if (event.which !== 1) {
      return; // Other button than left mouse button --> nop
    }
    if (this.preventDoubleClick && this._doubleClickSupport.doubleClicked()) {
      return; // More than one consecutive click --> nop
    }

    // When the action is clicked the user wants to execute the action and not see the tooltip -> cancel the task
    // If it is already displayed it will stay
    tooltips.cancel(this.$buttonLabel);
    tooltips.cancel(this.$fieldContainer);

    if (this.enabledComputed) {
      this.doAction();
    }
  }

  setStackable(stackable: boolean) {
    this.setProperty('stackable', stackable);
  }

  setShrinkable(shrinkable: boolean) {
    this.setProperty('shrinkable', shrinkable);
  }

  setPreventDoubleClick(preventDoubleClick: boolean) {
    this.setProperty('preventDoubleClick', preventDoubleClick);
  }

  protected override _linkWithLabel($element: JQuery) {
    super._linkWithLabel($element);
    aria.linkElementWithLabel($element, this.$buttonLabel);
  }

  override get$Focusable(): JQuery {
    if (this.adaptedBy) {
      return this.adaptedBy.get$Focusable();
    }
    return super.get$Focusable();
  }

  override isFocusable(checkTabbable?: boolean): boolean {
    if (this.adaptedBy) {
      return this.adaptedBy.isFocusable();
    }
    return super.isFocusable();
  }

  override focus(): boolean {
    if (this.adaptedBy) {
      return this.adaptedBy.focus();
    }
    return super.focus();
  }
}

export type ButtonSystemType = EnumObject<typeof Button.SystemType>;
export type ButtonDisplayStyle = EnumObject<typeof Button.DisplayStyle>;
