/*
 * Copyright (c) 2010, 2026 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 {AbstractLayout, BasicField, Dimension, EventHandler, FormField, graphics, HtmlComponent, HtmlCompPrefSizeOptions, HtmlEnvironment, Insets, PropertyChangeEvent, Rectangle, scout, scrollbars} from '../../index';

/**
 * Form-Field Layout, for a form-field with label, status, mandatory-indicator and a field.
 * This layout class works with a FormField instance, since we must access properties of the model.
 * Note: we use optGet() here, since some form-fields have only a bare HTML element as field, other
 * (composite) form-fields work with a HtmlComponent which has its own LayoutManager.
 */
export class FormFieldLayout extends AbstractLayout {
  formField: FormField;
  mandatoryIndicatorWidth: number;
  statusWidth: number;
  rowHeight: number;
  compactFieldWidth: number;
  htmlPropertyChangeHandler: EventHandler<PropertyChangeEvent<any, HtmlEnvironment>>;

  constructor(formField: FormField) {
    super();
    this.formField = formField;
    this._initDefaults();

    this.htmlPropertyChangeHandler = this._onHtmlEnvironmentPropertyChange.bind(this);
    HtmlEnvironment.get().on('propertyChange', this.htmlPropertyChangeHandler);
    this.formField.one('remove', () => {
      HtmlEnvironment.get().off('propertyChange', this.htmlPropertyChangeHandler);
    });
  }

  // Minimum field with to normal state, for smaller widths the "compact" style is applied.
  static COMPACT_FIELD_WIDTH = 61;

  protected _initDefaults() {
    this.mandatoryIndicatorWidth = HtmlEnvironment.get().fieldMandatoryIndicatorWidth;
    this.statusWidth = HtmlEnvironment.get().fieldStatusWidth;
    this.rowHeight = HtmlEnvironment.get().formRowHeight;
    this.compactFieldWidth = FormFieldLayout.COMPACT_FIELD_WIDTH;
  }

  protected _onHtmlEnvironmentPropertyChange() {
    this._initDefaults();
    this.formField.invalidateLayoutTree();
  }

  override layout($container: JQuery) {
    let labelHasFieldWidth: boolean,
      fieldBounds: Rectangle,
      htmlContainer = HtmlComponent.get($container),
      formField = this.formField,
      labelWidth = this.labelWidth(),
      statusWidth = this.statusWidth;

    // Note: Position coordinates start _inside_ the border, therefore we only use the padding
    let containerPadding = htmlContainer.insets({
      includeBorder: false
    });
    let top = containerPadding.top;
    let right = containerPadding.right;
    let bottom = containerPadding.bottom;
    let left = containerPadding.left;

    if (this._isLabelVisible()) {
      // currently a gui only flag, necessary for sequence box
      if (formField.labelWidthInPixel === FormField.LabelWidth.UI || formField.labelUseUiWidth) {
        if (formField.$label.hasClass('empty')) {
          labelWidth = 0;
        } else {
          labelWidth = graphics.prefSize(formField.$label).width;
        }
      }
      if (scout.isOneOf(formField.labelPosition, FormField.LabelPosition.DEFAULT, FormField.LabelPosition.LEFT)) {
        graphics.setBounds(formField.$label, left, top, labelWidth, this.rowHeight);
        left += labelWidth + formField.$label.cssMarginX();
      } else if (formField.labelPosition === FormField.LabelPosition.TOP) {
        let labelHeight = graphics.prefSize(formField.$label).height;
        // prefSize rounds the value -> ensure label height is set to that value to prevent gaps between container and label.
        // In addition, this also ensures that the correct height is set when changing the label position from left to top
        formField.$label.cssHeight(labelHeight);
        top += labelHeight + formField.$label.cssMarginY();
        labelHasFieldWidth = true;
      }
    }
    if (formField.$mandatory && formField.$mandatory.isVisible()) {
      formField.$mandatory
        .cssTop(top)
        .cssLeft(left)
        .cssWidth(this.mandatoryIndicatorWidth);
      left += formField.$mandatory.outerWidth(true);
    }
    if (this._isStatusVisible()) {
      formField.$status.cssWidth(statusWidth);
      // If both status and label position is "top", pull status up (without margin on the right side)
      if (formField.statusPosition === FormField.StatusPosition.TOP && labelHasFieldWidth) {
        let statusHeight = graphics.prefSize(formField.$status, {
          useCssSize: true
        }).height;
        // Vertically center status with label
        let statusTop = containerPadding.top + formField.$label.cssPaddingTop() + (formField.$label.height() / 2) - (statusHeight / 2);
        formField.$status
          .cssTop(statusTop)
          .cssRight(right + formField.$label.cssMarginRight())
          .cssHeight(statusHeight);
        // Add padding to label to prevent overlay of text and status icon
        let w = graphics.size(formField.$status, true).width;
        formField.$label.cssPaddingRight(w);
      } else {
        // Default status position
        formField.$status
          .cssTop(top)
          .cssRight(right)
          .cssHeight(this.rowHeight);
        right += statusWidth + formField.$status.cssMarginX();
      }
    }

    if (formField.$fieldContainer) {
      // Calculate the additional field offset (because of label, mandatory indicator etc.) without the containerInset.
      let fieldOffset = new Insets(
        top - containerPadding.top,
        right - containerPadding.right,
        bottom - containerPadding.bottom,
        left - containerPadding.left);
      // Calculate field size: "available size" - "insets (border and padding)" - "additional offset" - "field's margin"
      let fieldMargins = graphics.margins(formField.$fieldContainer);
      let fieldSize = htmlContainer.availableSize({
        exact: true
      })
        .subtract(htmlContainer.insets())
        .subtract(fieldOffset)
        .subtract(fieldMargins);
      fieldBounds = new Rectangle(left, top, fieldSize.width, fieldSize.height);
      if (formField.$fieldContainer.css('position') !== 'absolute') {
        fieldBounds.x = 0;
        fieldBounds.y = 0;
      }
      let htmlField = HtmlComponent.optGet(formField.$fieldContainer);
      if (htmlField) {
        htmlField.setBounds(fieldBounds);
      } else {
        graphics.setBounds(formField.$fieldContainer, fieldBounds);
      }
      if (this.compactFieldWidth > -1) {
        formField.$field.toggleClass('compact', fieldBounds.width <= this.compactFieldWidth);
        formField.$container.toggleClass('compact', fieldBounds.width <= this.compactFieldWidth);
      }

      if (labelHasFieldWidth) {
        let fieldWidth = fieldSize.add(fieldMargins).width - formField.$label.cssMarginX();
        if (formField.$mandatory && formField.$mandatory.isVisible()) {
          fieldWidth += formField.$mandatory.outerWidth(true);
        }
        formField.$label.cssWidth(fieldWidth);
      }

      if (formField.$maskedIndicator) {
        this._layoutMaskedIndicator(formField, fieldBounds, right);
      }
    }

    if (formField.$fieldContainer) {
      // Icons are placed inside the field (as overlay)
      let $iconInput = this._$elementForIconLayout();
      let fieldBorder = graphics.borders($iconInput);
      let inputBounds = graphics.offsetBounds($iconInput);
      top += fieldBorder.top;
      right += fieldBorder.right;
      fieldBounds.x += fieldBorder.left;
      fieldBounds.y += fieldBorder.top;
      fieldBounds.height = inputBounds.height - fieldBorder.top - fieldBorder.bottom;
      fieldBounds.width = inputBounds.width - fieldBorder.left - fieldBorder.right;

      if (formField.$icon) {
        this._layoutIcon(formField, fieldBounds, right, top);
      }

      // Clear icon if present
      if (formField.$clearIcon) {
        this._layoutClearIcon(formField, fieldBounds, right, top);
      }
    }

    // Check for scrollbars, update them if necessary
    if (formField.$field) {
      scrollbars.update(formField.$field, true);
    }
  }

  protected _isLabelVisible(): boolean {
    return !!this.formField.$label && this.formField.labelVisible;
  }

  protected _isStatusVisible(): boolean {
    return !!this.formField.$status && (this.formField.statusVisible || this.formField.$status.isVisible());
  }

  override preferredLayoutSize($container: JQuery, options?: HtmlCompPrefSizeOptions): Dimension {
    let htmlContainer = HtmlComponent.get(this.formField.$container);
    let formField = this.formField;
    let prefSizeLabel = new Dimension();
    let prefSizeMandatory = new Dimension();
    let prefSizeStatus = new Dimension();
    let prefSizeField = new Dimension();
    let widthHint = scout.nvl(options.widthHint, 0);
    let heightHint = scout.nvl(options.heightHint, 0);
    // Status is only pulled up if status AND label are on top
    let statusOnTop = formField.statusPosition === FormField.StatusPosition.TOP && this._isLabelVisible() && formField.labelPosition === FormField.LabelPosition.TOP;

    // Calculate the preferred sizes of the individual parts
    // Mandatory indicator
    if (formField.$mandatory && formField.$mandatory.isVisible()) {
      prefSizeMandatory.width = this.mandatoryIndicatorWidth + formField.$mandatory.cssMarginX();
      widthHint -= prefSizeMandatory.width;
    }

    // Label
    if (this._isLabelVisible()) {
      prefSizeLabel.width = this.labelWidth() + formField.$label.cssMarginX();
      prefSizeLabel.height = this.rowHeight;
      if (formField.labelPosition === FormField.LabelPosition.TOP) {
        // Label is always as width as the field if it is on top
        prefSizeLabel.width = 0;
        prefSizeLabel.height = graphics.prefSize(formField.$label, true).height;
      } else if (formField.labelWidthInPixel === FormField.LabelWidth.UI || formField.labelUseUiWidth) {
        if (formField.$label.hasClass('empty')) {
          prefSizeLabel.width = 0;
        } else {
          prefSizeLabel = graphics.prefSize(formField.$label, true);
        }
      }

      if (scout.isOneOf(formField.labelPosition, FormField.LabelPosition.DEFAULT, FormField.LabelPosition.LEFT)) {
        widthHint -= prefSizeLabel.width;
      } else if (formField.labelPosition === FormField.LabelPosition.TOP) {
        heightHint -= prefSizeLabel.height;
      }
    }

    // Status
    if (this._isStatusVisible()) {
      prefSizeStatus.width = this.statusWidth + formField.$status.cssMarginX();
      if (!statusOnTop) {
        prefSizeStatus.height = this.rowHeight;
        widthHint -= prefSizeStatus.width;
      }
    }

    // Field
    if (formField.$fieldContainer) {
      let fieldMargins = graphics.margins(formField.$fieldContainer);
      let htmlField = HtmlComponent.optGet(formField.$fieldContainer);
      if (!htmlField) {
        widthHint -= fieldMargins.horizontal();
        heightHint -= fieldMargins.vertical();
      }
      if (options.widthHint) {
        options.widthHint = widthHint;
      }
      if (options.heightHint) {
        options.heightHint = heightHint;
      }
      if (htmlField) {
        prefSizeField = htmlField.prefSize(options)
          .add(fieldMargins);
      } else {
        prefSizeField = graphics.prefSize(formField.$fieldContainer, options)
          .add(fieldMargins);
      }
    }

    // Now sum up to calculate the preferred size of the container
    let prefSize = new Dimension();

    // Field is the base, and it should be at least as height as a form row height.
    prefSize.width = prefSizeField.width;
    prefSize.height = prefSizeField.height;

    // Mandatory
    prefSize.width += prefSizeMandatory.width;
    prefSize.height = Math.max(prefSize.height, prefSizeMandatory.height);

    // Label
    if (scout.isOneOf(formField.labelPosition, FormField.LabelPosition.DEFAULT, FormField.LabelPosition.LEFT)) {
      prefSize.width += prefSizeLabel.width;
      prefSize.height = Math.max(prefSize.height, prefSizeLabel.height);
    } else if (formField.labelPosition === FormField.LabelPosition.TOP) {
      prefSize.width = Math.max(prefSize.width, prefSizeLabel.width);
      prefSize.height += prefSizeLabel.height;
    }

    // Status
    if (!statusOnTop) {
      prefSize.width += prefSizeStatus.width;
      prefSize.height = Math.max(prefSize.height, prefSizeStatus.height);
    }

    // Add padding and border
    prefSize = prefSize.add(htmlContainer.insets());

    return prefSize;
  }

  /**
   * @returns the input element used to position the icon. May be overridden if another element than $field should be used.
   */
  protected _$elementForIconLayout(): JQuery {
    return this.formField.$field;
  }

  protected _layoutIcon(formField: FormField, fieldBounds: Rectangle, right: number, top: number) {
    let height = this.rowHeight;
    if (fieldBounds) {
      // If field is bigger than rowHeight (e.g. if used in desktop cell editor), make sure icon is as height as field
      height = fieldBounds.height;
    }
    formField.$icon
      .cssRight(right)
      .cssTop(fieldBounds.y)
      .cssHeight(height);
  }

  protected _layoutMaskedIndicator(formField: FormField, fieldBounds: Rectangle, right: number) {
    const isMultiline = formField.gridData.h > 1 || formField.gridData.weightY > 0;

    // If field is bigger than rowHeight (e.g. if used in desktop cell editor), make sure icon is as high as field
    let height = Math.max(this.rowHeight, fieldBounds.height);
    let top = fieldBounds.y;
    if (isMultiline) {
      height = this.rowHeight; // align icon at the top if field spans multiple grid lines
      right += 11; // make room for scrollbar; magic number to align well with magnifying glass in LookupBoxes
      top += 4; // more space from top in larger fields
    }

    // masked indicator is always right-aligned, because there's no value in the field when it's present, so it can't overlap with contents
    formField.$maskedIndicator
      .cssLeft('')
      .cssRight(right)
      .cssTop(top)
      .cssHeight(height);
  }

  protected _layoutClearIcon(formField: FormField, fieldBounds: Rectangle, right: number, top: number) {
    let height = this.rowHeight;
    if (fieldBounds) {
      // If field is bigger than rowHeight (e.g. if used in desktop cell editor), make sure icon is as height as field
      height = fieldBounds.height;
    }
    if (formField instanceof BasicField && formField.gridData.horizontalAlignment > 0) {
      formField.$clearIcon
        .cssLeft(fieldBounds.x)
        .cssRight('')
        .cssTop(fieldBounds.y)
        .cssHeight(height);
    } else {
      formField.$clearIcon
        .cssLeft('')
        .cssRight(right)
        .cssTop(fieldBounds.y)
        .cssHeight(height);
    }
  }

  labelWidth(): number {
    // use configured label width in pixel or default label width
    if (FormField.LabelWidth.DEFAULT === this.formField.labelWidthInPixel) {
      return HtmlEnvironment.get().fieldLabelWidth;
    }
    return this.formField.labelWidthInPixel;
  }
}
