/*
 * 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 {Device, FormField, FormFieldStatusPosition, Popup, scout, strings, TabItem, ValueField, Widget, widgets} from '../../index';
import $ from 'jquery';

/**
 * Calls JQuery $.text() for touch-devices and $.val() for all other devices, used together with #makeInputOrDiv().
 * Works as setter when called with 2 arguments, works a getter when called with 1 argument.
 *
 * @returns when called with 1 argument: $field.text() or $field.val()
 */
function valOrText($field: JQuery, text: string);
function valOrText($field: JQuery): string;
function valOrText($field: JQuery, text?: string): string {
  let isDiv = $field.is('div');
  if (arguments.length === 2) {
    if (isDiv) {
      $field.text(text);
    } else {
      $field.val(text);
    }
  } else {
    return isDiv ? $field.text() : $field.val() as string;
  }
}

export const fields = {
  /**
   * @param $parent used to determine which HTML document is used to create the new HTML element
   * @returns an INPUT element as used in Scout forms.
   */
  makeTextField($parent: JQuery, cssClass?: string): JQuery<HTMLInputElement> {
    return $parent.makeElement('<input>', cssClass)
      .attr('type', 'text')
      .attr('autocomplete', 'NoAutocomplete') /* off and false are currently ignored in Chrome */
      .disableSpellcheck() as JQuery<HTMLInputElement>;
  },

  appendIcon($field: JQuery, cssClass?: string): JQuery {
    /*
     * Note: the field usually does $field.focus() when the icon is clicked.
     * Unfocusable is required because when the icon is clicked the browser is in the middle of setting
     * a new focus target, so we cannot simply change the focus target, because at the end the wrong target would be
     * focused and the popup would be closed immediately. With 'unfocusable' the focus manager will prevent the default focus
     */
    let $icon = $field.appendSpan('icon unfocusable text-field-icon');
    if (cssClass) {
      $icon.addClass(cssClass);
    }
    return $icon;
  },

  initTouch(field: { embedded: boolean; touchMode: boolean }, model: { embedded?: boolean; touchMode?: boolean }) {
    field.embedded = scout.nvl(model.embedded, false);
    // when 'touchMode' is not set explicitly, check the device
    field.touchMode = scout.nvl(model.touchMode, Device.get().supportsOnlyTouch());
  },

  valOrText,

  /**
   * Creates a DIV element for touch-devices and an INPUT element for all other devices,
   * depending on the touch flag of the given field.
   */
  makeInputOrDiv(field: Widget & { touchMode?: boolean }, cssClass?: string): JQuery {
    if (field.touchMode) {
      return fields.makeInputDiv(field, cssClass);
    }
    return fields.makeTextField(field.$container, cssClass);
  },

  /**
   * Creates a DIV element that looks like an INPUT element.
   */
  makeInputDiv(field: Widget, cssClass?: string): JQuery {
    return field.$container.makeDiv(strings.join(' ', 'input-field', cssClass));
  },

  /**
   * The INPUT element does not process the click event when the field is disabled.
   * However, the DIV element used in touch-mode does process the event anyway, that's why this check is required.
   */
  handleOnClick(field: Widget & { popup?: Popup; embedded?: boolean }): boolean {
    return field.enabledComputed && !field.embedded && !field.popup;
  },

  /**
   * Calls activate() on the first focusable field of the given fields. Does nothing if the widget is disabled or not rendered.
   */
  activateFirstField(widget: Widget, fieldArr: FormField[]) {
    let firstField = widgets.findFirstFocusableWidget(fieldArr, widget) as FormField;
    if (firstField) {
      firstField.activate();
    }
  },

  /**
   * @param field a ValueField which works like a Proposal- or SmartField.
   * @returns Whether or not the target is on the field (including popup and tooltip)
   */
  eventOutsideProposalField(field: ValueField<any> & { popup: Popup }, target: JQuery | Element): boolean {
    let eventOnField = safeIsOrHas(field.$field, target)
      || safeIsOrHas(field.$icon, target)
      || safeIsOrHas(field.$clearIcon, target);
    let eventOnPopup = safeWidgetIsOrHas(field.popup, target);
    let eventOnTooltip = safeWidgetIsOrHas(field.tooltip(), target);

    return !eventOnField && !eventOnPopup && !eventOnTooltip;

    function safeIsOrHas($elem: JQuery, target: JQuery | Element): boolean {
      return $elem && $elem.isOrHas(target);
    }

    function safeWidgetIsOrHas(widget: Widget, target: JQuery | Element): boolean {
      return widget && widget.rendered && widget.$container.isOrHas(target);
    }
  },

  /**
   * Selects the tab containing the given {@link FormField} for all parent tabBoxes.
   * This ensures that the given field could be seen (if visible itself).
   *
   * @param field The field whose parent tabs should be selected.
   */
  selectAllParentTabsOf(field: FormField) {
    if (!field || !(field instanceof FormField)) {
      return;
    }
    field.visitParentFields(fields.selectIfIsTab);
  },

  /**
   * Selects the given tabItem if it is a {@link TabItem}.
   *
   * @param tabItem The tab to be selected.
   */
  selectIfIsTab(tabItem: FormField) {
    if (!tabItem || !(tabItem instanceof TabItem)) {
      return;
    }
    tabItem.select();
  },

  /**
   * Toggles {@link FormField.statusPosition} based on the given predicate in order to enlarge or reset the box header line while scrolling.
   * The header line is enlarged to match the width of the scroll shadow.
   */
  adjustStatusPositionForScrollShadow(box: FormField, predicate: () => boolean) {
    if (box.htmlComp) {
      // Suppress layout invalidation to prevent dialogs from resetting the height.
      // The box itself must not change its size while scrolling anyway, so there is no need to propagate the invalidation.
      box.htmlComp.suppressInvalidate = true;
    }
    if (predicate()) {
      widgets.preserveAndSetProperty(() => box.setStatusPosition(FormField.StatusPosition.TOP), () => box.statusPosition, box, '_statusPositionOrig');
    } else {
      widgets.resetProperty((preservedValue: FormFieldStatusPosition) => box.setStatusPosition(preservedValue), box, '_statusPositionOrig');
    }
    if (box.htmlComp) {
      box.htmlComp.suppressInvalidate = false;
    }
  },

  /**
   * Focuses the given input and calls the insertText command to insert the text at the current cursor position.
   *
   * Using the exec command allows the user to undo the operation (e.g. using ctrl-z).
   */
  focusAndInsertText($input: JQuery<HTMLInputElement | HTMLTextAreaElement>, text: string) {
    $input[0].focus();
    $input.document(true).execCommand('insertText', false, text);
  },

  /**
   * @returns true if the element supports the readonly attribute
   */
  supportsReadonlyAttribute($element: JQuery | HTMLElement) {
    let $elem = $.ensure($element);
    if (!$elem.length) {
      return false;
    }
    if ($elem.is('textarea')) {
      return true;
    }
    // According to https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/readonly
    return fields.isInputType($elem, 'text', 'search', 'tel', 'url', 'email', 'password', 'date', 'month', 'week', 'time', 'datetime-local', 'datetime-number');
  },

  /**
   * Returns true if the given element is an input element and its type attribute matches one of the given types.
   * Also returns true if the element is an input without a type attribute and the specified types contain the 'text' type
   * because an input without type attribute defaults to the type text.
   */
  isInputType($element: JQuery | HTMLElement, ...types: string[]): boolean | null {
    let $elem = $.ensure($element);
    if (!$elem.length) {
      return false;
    }
    if (!$elem.is('input')) {
      return false;
    }
    let type = $elem.attr('type') || 'text'; // input without type defaults to text
    return types.includes(type);
  }
};
