/*
 * 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 {Dimension, Form, FormLayout, graphics, HtmlComponent, Insets, Point, Rectangle} from '../index';

export class DialogLayout extends FormLayout {
  declare form: Form;

  autoSize: boolean;
  shrinkEnabled: boolean;

  constructor(form: Form) {
    super(form);
    this.autoSize = true;
    this.shrinkEnabled = false;
  }

  override layout($container: JQuery) {
    if (!this.autoSize) {
      super.layout($container);
      return;
    }

    let currentBounds,
      htmlComp = this.form.htmlComp,
      prefBounds = this.form.prefBounds(),
      dialogMargins = htmlComp.margins(),
      windowSize = $container.windowSize();

    if (prefBounds) {
      currentBounds = prefBounds;
    } else {
      currentBounds = htmlComp.bounds();
    }
    let dialogSize = this._calcSize($container, currentBounds, prefBounds);

    // Add markers to be able to style the dialog in a different way when it uses the full width or height
    $container
      .toggleClass('full-width', this.form.maximized || (currentBounds.x === 0 && dialogMargins.horizontal() === 0 && windowSize.width === dialogSize.width))
      .toggleClass('full-height', this.form.maximized || (currentBounds.y === 0 && dialogMargins.vertical() === 0 && windowSize.height === dialogSize.height));

    // Ensure the dialog can only get larger, not smaller.
    // This prevents 'snapping' the dialog back to the calculated size when a field changes its visibility, but the user previously enlarged the dialog.
    // This must not happen when the dialog is laid out the first time (-> when it is opened, because it has not the right size yet and may get too big)
    if (htmlComp.layouted && !this.shrinkEnabled) {
      dialogSize.width = Math.max(dialogSize.width, currentBounds.width);
      dialogSize.height = Math.max(dialogSize.height, currentBounds.height);
    }

    graphics.setSize($container, dialogSize);
    super.layout($container);
  }

  /**
   * @param currentBounds
   *          bounds as returned by the graphics.bounds() function, i.e. position is the CSS
   *          position (top-left of "margin box"), dimension excludes margins
   * @param prefBounds
   *          optional preferred bounds (same expectations as with "currentBounds")
   * @returns
   *          adjusted size excluding margins (suitable to pass to graphics.setSize())
   */
  protected _calcSize($container: JQuery, currentBounds: Rectangle, prefBounds?: Rectangle): Dimension {
    let dialogSize,
      htmlComp = this.form.htmlComp,
      dialogMargins = htmlComp.margins(),
      windowSize = $container.windowSize();

    if (this.form.maximized) {
      return windowSize;
    }

    if (prefBounds) {
      dialogSize = prefBounds.dimension();
      // Assume (0,0) as position so that as much as possible of the stored size can be used.
      // The true position will be set later in Form#position (called by FormController).
      dialogSize = DialogLayout.fitContainerInWindow(windowSize, new Point(), dialogSize, dialogMargins);
      return dialogSize;
    }

    // Calculate preferred width first...
    dialogSize = this.preferredLayoutSize($container, {
      widthOnly: true
    });
    dialogSize = DialogLayout.fitContainerInWindow(windowSize, currentBounds.point(), dialogSize, dialogMargins);

    // ...then calculate the actual preferred size based on the width. This is necessary because the dialog may contain fields with wrapping content. Without a width hint the height would not be correct.
    dialogSize = this.preferredLayoutSize($container, {
      widthHint: dialogSize.width
    }).ceil(); // always round up. If we'd round a height of 380.00005 pixel down
    // there is not enough space to display the group-box, thus the browser would show scrollbars.

    dialogSize = DialogLayout.fitContainerInWindow(windowSize, currentBounds.point(), dialogSize, dialogMargins);
    return dialogSize;
  }

  /**
   * Calculates the new container size and position. If the given containerSize is larger than the windowSize, the size will be adjusted.
   *
   * @param windowSize total size of the window
   * @param containerPosition current CSS position of the container (top-left of the "margin box")
   * @param containerSize preferred size of container (excluding margins)
   * @param containerMargins margins of the container
   * @returns the new, adjusted container size (excluding margins)
   */
  static fitContainerInWindow(windowSize: Dimension, containerPosition: Point, containerSize: Dimension, containerMargins: Insets): Dimension {
    // class .dialog may specify a margin
    // currentBounds.y and x are 0 initially, but if size changes while dialog is open they are greater than 0
    // This guarantees the dialog size may not exceed the document size
    let maxWidth = (windowSize.width - containerMargins.horizontal() - containerPosition.x);
    let maxHeight = (windowSize.height - containerMargins.vertical() - containerPosition.y);

    // Calculate new dialog size, ensuring that the dialog is not larger than container
    let size = new Dimension();
    size.width = Math.min(maxWidth, containerSize.width);
    size.height = Math.min(maxHeight, containerSize.height);

    return size;
  }

  /**
   * Returns the coordinates to place the given container in the optical middle of the window.
   *
   * @param $container
   * @returns new X,Y position of the container
   */
  static positionContainerInWindow($container: JQuery): Point {
    let
      windowSize = $container.windowSize(),
      containerSize = HtmlComponent.get($container).size(true),
      left = (windowSize.width - containerSize.width) / 2,
      top = (windowSize.height - containerSize.height) / 2;

    // optical middle (move up 20% of distance between window and dialog)
    let opticalMiddleOffset = (top / 5);
    top -= opticalMiddleOffset;

    // Ensure integer numbers
    left = Math.floor(left);
    top = Math.floor(top);

    return new Point(left, top);
  }
}
