/*
 * 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 {
  CollapseHandle, CollapseHandleHorizontalAlignment, CompositeField, Dimension, EnumObject, FormField, graphics, GroupBox, HtmlComponent, HtmlEnvironment, InitModelOf, KeyStroke, ObjectIdProvider, PropertyChangeEvent, scout,
  SplitBoxCollapseKeyStroke, SplitBoxEventMap, SplitBoxFirstCollapseKeyStroke, SplitBoxLayout, SplitBoxModel, SplitBoxSecondCollapseKeyStroke
} from '../../../index';
import $ from 'jquery';

export class SplitBox extends CompositeField {
  declare model: SplitBoxModel;
  declare eventMap: SplitBoxEventMap;
  declare self: SplitBox;

  firstField: FormField;
  secondField: FormField;
  collapsibleField: FormField;
  fieldCollapsed: boolean;
  toggleCollapseKeyStroke: SplitBoxCollapseKeyStroke;
  firstCollapseKeyStroke: SplitBoxFirstCollapseKeyStroke;
  secondCollapseKeyStroke: SplitBoxSecondCollapseKeyStroke;
  splitHorizontal: boolean;
  splitterEnabled: boolean;
  splitterPosition: number;
  minSplitterPosition: number;
  splitterPositionType: SplitBoxSplitterPositionType;
  fieldMinimized: boolean;
  minimizeEnabled: boolean;
  htmlSplitArea: HtmlComponent;
  collapseHandle: CollapseHandle;

  protected _oldSplitterPositionType: string;
  protected _$splitArea: JQuery;
  protected _$splitter: JQuery;
  protected _$window: JQuery<Window>;
  protected _$body: JQuery<Body>;

  constructor() {
    super();
    this._addWidgetProperties(['firstField', 'secondField', 'collapsibleField']);
    this._addPreserveOnPropertyChangeProperties(['collapsibleField']);

    this.firstField = null;
    this.secondField = null;
    this.collapsibleField = null;
    this.fieldCollapsed = false;
    this.toggleCollapseKeyStroke = null;
    this.firstCollapseKeyStroke = null;
    this.secondCollapseKeyStroke = null;
    this.splitHorizontal = true;
    this.splitterEnabled = true;
    this.splitterPosition = 0.5;
    this.minSplitterPosition = 0;
    this.splitterPositionType = SplitBox.SplitterPositionType.RELATIVE_FIRST;
    this.fieldMinimized = false;
    this.minimizeEnabled = true;
    this._$splitArea = null;
    this._$splitter = null;
  }

  static SplitterPositionType = {
    RELATIVE_FIRST: 'relativeFirst',
    RELATIVE_SECOND: 'relativeSecond',
    ABSOLUTE_FIRST: 'absoluteFirst',
    ABSOLUTE_SECOND: 'absoluteSecond'
  } as const;

  /** @deprecated use SplitBox.SplitterPositionType instead */
  static SPLITTER_POSITION_TYPE_RELATIVE_FIRST = SplitBox.SplitterPositionType.RELATIVE_FIRST;
  /** @deprecated use SplitBox.SplitterPositionType instead */
  static SPLITTER_POSITION_TYPE_RELATIVE_SECOND = SplitBox.SplitterPositionType.RELATIVE_SECOND;
  /** @deprecated use SplitBox.SplitterPositionType instead */
  static SPLITTER_POSITION_TYPE_ABSOLUTE_FIRST = SplitBox.SplitterPositionType.ABSOLUTE_FIRST;
  /** @deprecated use SplitBox.SplitterPositionType instead */
  static SPLITTER_POSITION_TYPE_ABSOLUTE_SECOND = SplitBox.SplitterPositionType.ABSOLUTE_SECOND;

  protected override _init(model: InitModelOf<this>) {
    super._init(model);
    this._setToggleCollapseKeyStroke(model.toggleCollapseKeyStroke);
    this._setFirstCollapseKeyStroke(model.firstCollapseKeyStroke);
    this._setSecondCollapseKeyStroke(model.secondCollapseKeyStroke);
    this._updateCollapseHandle();
    this._initResponsive();
  }

  /**
   * Set the group boxes of the split box to responsive if not set otherwise.
   */
  protected _initResponsive() {
    this.getFields().forEach(field => {
      if (field instanceof GroupBox && field.responsive === null) {
        field.setResponsive(true);
      }
    });
  }

  protected override _render() {
    this.addContainer(this.$parent, 'split-box');
    // This widget does not support label, mandatoryIndicator and status

    // Create split area
    this._$splitArea = this.$parent.makeDiv('split-area');
    this.addField(this._$splitArea);
    this.htmlSplitArea = HtmlComponent.install(this._$splitArea, this.session);
    this.htmlSplitArea.setLayout(new SplitBoxLayout(this));
    this._$window = this.$parent.window();
    this._$body = this.$parent.body();

    // Add fields and splitter
    if (this.firstField) {
      this.firstField.render(this._$splitArea);
      this.firstField.$container
        .addClass('first-field')
        .addClass(this.splitHorizontal ? 'x-axis' : 'y-axis');
      this.firstField.on('propertyChange', onInnerFieldPropertyChange.bind(this));

      if (this.secondField) {
        this.secondField.render(this._$splitArea);
        this.secondField.$container
          .addClass('second-field')
          .addClass(this.splitHorizontal ? 'x-axis' : 'y-axis');
        this.secondField.on('propertyChange', onInnerFieldPropertyChange.bind(this));

        this._$splitter = this._$splitArea.appendDiv('splitter')
          .addClass(this.splitHorizontal ? 'x-axis' : 'y-axis')
          .on('mousedown', resizeSplitter.bind(this));
      }
    }
    this._updateFieldVisibilityClasses();

    // --- Helper functions ---

    function resizeSplitter(event: JQuery.MouseDownEvent): boolean {
      if (event.which !== 1) {
        return; // only handle left mouse button
      }
      let mousePosition: { x: number; y: number },
        splitAreaPosition: JQuery.Coordinates,
        splitAreaSize: Dimension,
        splitterSize: Dimension,
        splitterPosition: JQuery.Coordinates,
        $tempSplitter: JQuery;
      if (this.splitterEnabled) {
        // Update mouse position (see resizeMove() for details)
        mousePosition = {
          x: event.pageX,
          y: event.pageY
        };

        // Add listeners (we add them to the window to make sure we get the mouseup event even when the cursor it outside the window)
        this._$window
          .on('mousemove.splitbox', resizeMove.bind(this))
          .on('mouseup.splitbox', resizeEnd.bind(this));
        // Ensure the correct cursor is always shown while moving
        this._$body.addClass(this.splitHorizontal ? 'col-resize' : 'row-resize');
        $('iframe').addClass('dragging-in-progress');

        // Get initial area and splitter bounds
        splitAreaPosition = this._$splitArea.offset();
        splitAreaSize = graphics.size(this._$splitArea, true);
        splitterPosition = this._$splitter.offset();
        splitterSize = graphics.size(this._$splitter, true);

        // Create temporary splitter
        $tempSplitter = this._$splitArea.appendDiv('temp-splitter')
          .addClass(this.splitHorizontal ? 'x-axis' : 'y-axis');
        if (this.splitHorizontal) { // "|"
          $tempSplitter.cssLeft(splitterPosition.left - splitAreaPosition.left);
        } else { // "--"
          $tempSplitter.cssTop(splitterPosition.top - splitAreaPosition.top);
        }
        this._$splitter.addClass('dragging');
      }

      let newSplitterPosition: number = this.splitterPosition;
      let SNAP_SIZE = 10;

      function resizeMove(event: JQuery.MouseMoveEvent) {
        if (event.pageX === mousePosition.x && event.pageY === mousePosition.y) {
          // Chrome bug: https://code.google.com/p/chromium/issues/detail?id=161464
          // When holding the mouse, but not moving it, a 'mousemove' event is fired every second nevertheless.
          return;
        }
        mousePosition = {
          x: event.pageX,
          y: event.pageY
        };

        if (this.splitHorizontal) { // "|"
          // Calculate target splitter position (in area)
          let targetSplitterPositionLeft = event.pageX - splitAreaPosition.left;

          // De-normalize minimum splitter position to allowed splitter range in pixel [minSplitterPositionLeft, maxSplitterPositionLeft]
          let minSplitterPositionLeft: number;
          let maxSplitterPositionLeft: number;

          // Splitter width plus margin on right side, if temporary splitter position is x, the splitter div position is x-splitterOffset
          let splitterOffset = Math.floor((splitterSize.width + HtmlEnvironment.get().fieldMandatoryIndicatorWidth) / 2);

          if (this.splitterPositionType === SplitBox.SplitterPositionType.ABSOLUTE_FIRST) {
            minSplitterPositionLeft = scout.nvl(this.minSplitterPosition, 0);
            // allow to move the splitter to right side, leaving minimal space for splitter div without right margin (=total splitter size minus offset)
            maxSplitterPositionLeft = splitAreaSize.width - splitterSize.width + splitterOffset;
          } else if (this.splitterPositionType === SplitBox.SplitterPositionType.RELATIVE_FIRST) {
            minSplitterPositionLeft = (splitAreaSize.width - splitterSize.width) * scout.nvl(this.minSplitterPosition, 0);
            // allow to move the splitter to right side, leaving minimal space for splitter div without right margin (=total splitter size minus offset)
            maxSplitterPositionLeft = splitAreaSize.width - splitterSize.width + splitterOffset;
          } else if (this.splitterPositionType === SplitBox.SplitterPositionType.ABSOLUTE_SECOND) {
            minSplitterPositionLeft = 0;
            // allow to move the splitter to right side, leaving minimal space for splitter div without right margin, reserving space for minimum splitter size
            maxSplitterPositionLeft = splitAreaSize.width - splitterSize.width + splitterOffset - scout.nvl(this.minSplitterPosition, 0);
          } else if (this.splitterPositionType === SplitBox.SplitterPositionType.RELATIVE_SECOND) {
            minSplitterPositionLeft = 0;
            // allow to move the splitter to right side, leaving minimal space for splitter div without right margin, reserving space for minimum splitter size
            maxSplitterPositionLeft = splitAreaSize.width - splitterSize.width + splitterOffset - Math.floor(scout.nvl(this.minSplitterPosition, 0) * (splitAreaSize.width - splitterSize.width));
          }

          // Snap to begin and end
          let tempSplitterOffsetX = splitterOffset;

          if (targetSplitterPositionLeft < (minSplitterPositionLeft + splitterOffset + SNAP_SIZE)) { // snap left if minimum position is reached (+ snap range)
            targetSplitterPositionLeft = minSplitterPositionLeft; // set splitter directly to left minimal bound
            tempSplitterOffsetX = 0; // setting splitter to left minimal bound, does not require an additional offset
          } else if (targetSplitterPositionLeft > (maxSplitterPositionLeft - SNAP_SIZE)) {
            targetSplitterPositionLeft = maxSplitterPositionLeft;
          }

          // Update temporary splitter
          $tempSplitter.cssLeft(targetSplitterPositionLeft - tempSplitterOffsetX);

          // Normalize target position (available splitter area is (splitAreaSize.width - splitterSize.width))
          newSplitterPosition = (targetSplitterPositionLeft - tempSplitterOffsetX);
          if (this.splitterPositionType === SplitBox.SplitterPositionType.RELATIVE_FIRST) {
            newSplitterPosition = newSplitterPosition / (splitAreaSize.width - splitterSize.width);
          } else if (this.splitterPositionType === SplitBox.SplitterPositionType.RELATIVE_SECOND) {
            newSplitterPosition = 1 - (newSplitterPosition / (splitAreaSize.width - splitterSize.width));
          } else if (this.splitterPositionType === SplitBox.SplitterPositionType.ABSOLUTE_SECOND) {
            newSplitterPosition = splitAreaSize.width - splitterSize.width - newSplitterPosition;
          }
        } else { // "--"
          // Calculate target splitter position (in area)
          let targetSplitterPositionTop = event.pageY - splitAreaPosition.top;

          // Snap to begin and end
          let tempSplitterOffsetY = Math.floor(splitterSize.height / 2);
          if (targetSplitterPositionTop < SNAP_SIZE) {
            targetSplitterPositionTop = 0;
            tempSplitterOffsetY = 0;
          } else if (splitAreaSize.height - targetSplitterPositionTop < SNAP_SIZE) {
            targetSplitterPositionTop = splitAreaSize.height;
            tempSplitterOffsetY = splitterSize.height;
          }

          // Update temporary splitter
          $tempSplitter.cssTop(targetSplitterPositionTop - tempSplitterOffsetY);
          // Normalize target position
          newSplitterPosition = targetSplitterPositionTop - tempSplitterOffsetY;
          if (this.splitterPositionType === SplitBox.SplitterPositionType.RELATIVE_FIRST) {
            newSplitterPosition = newSplitterPosition / (splitAreaSize.height - splitterSize.height);
          } else if (this.splitterPositionType === SplitBox.SplitterPositionType.RELATIVE_SECOND) {
            newSplitterPosition = 1 - (newSplitterPosition / (splitAreaSize.height - splitterSize.height));
          } else if (this.splitterPositionType === SplitBox.SplitterPositionType.ABSOLUTE_SECOND) {
            newSplitterPosition = splitAreaSize.height - newSplitterPosition - splitterSize.height;
          }
        }
      }

      function resizeEnd(event: JQuery.MouseUpEvent) {
        if (event.which !== 1) {
          return; // only handle left mouse button
        }
        // Remove listeners and reset cursor
        this._$window
          .off('mousemove.splitbox')
          .off('mouseup.splitbox');
        if ($tempSplitter) { // instead of check for this.splitterEnabled, if splitter is currently moving it must be finished correctly
          this._$body.removeClass((this.splitHorizontal ? 'col-resize' : 'row-resize'));
          $('iframe').removeClass('dragging-in-progress');

          // Remove temporary splitter
          $tempSplitter.remove();
          this._$splitter.removeClass('dragging');

          // Update split box
          this.newSplitterPosition(newSplitterPosition, true);
        }
      }

      return false;
    }

    function onInnerFieldPropertyChange(event: PropertyChangeEvent<any, FormField>) {
      if (event.propertyName === 'visible') {
        this._updateFieldVisibilityClasses();
        // Mark layout as invalid
        this.htmlSplitArea.invalidateLayoutTree(false);
      }
    }
  }

  protected override _renderProperties() {
    super._renderProperties();
    this._renderSplitterPosition();
    this._renderSplitterEnabled();
    this._renderCollapsibleField(); // renders collapsibleField _and_ fieldCollapsed
    this._renderCollapseHandle(); // renders collapseHandle _and_ toggleCollapseKeyStroke _and_ firstCollapseKeyStroke _and_ secondCollapseKeyStroke
    this._renderFieldMinimized();
  }

  protected override _remove() {
    this._$splitArea = null;
    this._$splitter = null;
    super._remove();
  }

  protected _setSplitterPosition(splitterPosition: number) {
    this._setProperty('splitterPosition', splitterPosition);
    // If splitter position is explicitly set by an event, no recalculation is necessary
    this._oldSplitterPositionType = null;
  }

  protected _renderSplitterPosition() {
    this.newSplitterPosition(this.splitterPosition, false); // do not update (override) field minimized if new position is set by model
  }

  protected _setSplitterPositionType(splitterPositionType: string) {
    if (this.rendered && !this._oldSplitterPositionType) {
      this._oldSplitterPositionType = this.splitterPositionType;
      // We need to recalculate the splitter position. Because this requires the proper
      // size of the split box, this can only be done in _renderSplitterPositionType().
    }
    this._setProperty('splitterPositionType', splitterPositionType);
  }

  protected _renderSplitterPositionType() {
    if (this._oldSplitterPositionType) {
      // splitterPositionType changed while the split box was rendered --> convert splitterPosition
      // to the target type such that the current position in screen does not change.
      let splitAreaSize = this.htmlSplitArea.size(),
        splitterPosition = this.splitterPosition,
        splitterSize = graphics.size(this._$splitter, true),
        minSplitterPosition = this.minSplitterPosition,
        totalSize = 0;
      if (this.splitHorizontal) { // "|"
        totalSize = splitAreaSize.width - splitterSize.width;
      } else { // "--"
        totalSize = splitAreaSize.height - splitterSize.height;
      }

      // Convert value depending on the old and new type system
      let oldIsRelative = this._isSplitterPositionTypeRelative(this._oldSplitterPositionType);
      let newIsRelative = this._isSplitterPositionTypeRelative(this.splitterPositionType);
      let oldIsAbsolute = !oldIsRelative;
      let newIsAbsolute = !newIsRelative;
      if (oldIsRelative && newIsAbsolute) {
        // From relative to absolute
        if ((this._oldSplitterPositionType === SplitBox.SplitterPositionType.RELATIVE_FIRST && this.splitterPositionType === SplitBox.SplitterPositionType.ABSOLUTE_SECOND) ||
          (this._oldSplitterPositionType === SplitBox.SplitterPositionType.RELATIVE_SECOND && this.splitterPositionType === SplitBox.SplitterPositionType.ABSOLUTE_FIRST)) {
          splitterPosition = totalSize - (totalSize * splitterPosition); // changed from first to second field or from second to first field, invert splitter position
        } else {
          splitterPosition = totalSize * splitterPosition;
        }
        // convert minimum splitter position
        if (minSplitterPosition) {
          minSplitterPosition = totalSize * minSplitterPosition;
        }
      } else if (oldIsAbsolute && newIsRelative) {
        // From absolute to relative
        if ((this._oldSplitterPositionType === SplitBox.SplitterPositionType.ABSOLUTE_FIRST && this.splitterPositionType === SplitBox.SplitterPositionType.RELATIVE_SECOND) ||
          (this._oldSplitterPositionType === SplitBox.SplitterPositionType.ABSOLUTE_SECOND && this.splitterPositionType === SplitBox.SplitterPositionType.RELATIVE_FIRST)) {
          splitterPosition = (totalSize - splitterPosition) / totalSize; // changed from first to second field or from second to first field, invert splitter position
        } else {
          splitterPosition = splitterPosition / totalSize;
        }

        // convert minimum splitter position
        if (minSplitterPosition) {
          minSplitterPosition = minSplitterPosition / totalSize;
        }
      } else if (oldIsAbsolute && newIsAbsolute) {
        splitterPosition = (totalSize - splitterPosition);
        // do not convert minimum splitter position, unit did not change
      } else { // oldIsRelative && newIsRelative
        splitterPosition = 1 - splitterPosition;
        // do not convert minimum splitter position, unit did not change
      }
      // set new minimum splitter position
      this.setMinSplitterPosition(minSplitterPosition);

      // Set as new splitter position
      this._oldSplitterPositionType = null;
      this.newSplitterPosition(splitterPosition, true);
    }
  }

  protected _isSplitterPositionTypeRelative(positionType: string): boolean {
    return (positionType === SplitBox.SplitterPositionType.RELATIVE_FIRST)
      || (positionType === SplitBox.SplitterPositionType.RELATIVE_SECOND);
  }

  protected _renderSplitterEnabled() {
    if (this._$splitter) {
      this._$splitter.setEnabled(this.splitterEnabled);
    }
  }

  setFieldCollapsed(collapsed: boolean) {
    this.setProperty('fieldCollapsed', collapsed);
    this._updateCollapseHandleButtons();
  }

  protected _renderFieldCollapsed() {
    this._renderCollapsibleField();
  }

  setCollapsibleField(field: FormField) {
    this.setProperty('collapsibleField', field);
    this._updateCollapseHandle();
  }

  protected _updateCollapseHandle() {
    // always unregister key stroke first (although it may have been added by _setToggleCollapseKeyStroke before)
    if (this.toggleCollapseKeyStroke) {
      this.unregisterKeyStrokes(this.toggleCollapseKeyStroke);
    }
    if (this.firstCollapseKeyStroke) {
      this.unregisterKeyStrokes(this.firstCollapseKeyStroke);
    }
    if (this.secondCollapseKeyStroke) {
      this.unregisterKeyStrokes(this.secondCollapseKeyStroke);
    }

    if (this.collapsibleField) {
      let horizontalAlignment: CollapseHandleHorizontalAlignment = CollapseHandle.HorizontalAlignment.LEFT;
      if (this.collapsibleField !== this.firstField) {
        horizontalAlignment = CollapseHandle.HorizontalAlignment.RIGHT;
      }

      if (!this.collapseHandle) {
        // create new collapse handle
        this.collapseHandle = scout.create(CollapseHandle, {
          parent: this,
          horizontalAlignment: horizontalAlignment
        });
        this.collapseHandle.on('action', this.collapseHandleButtonPressed.bind(this));
        if (this.toggleCollapseKeyStroke) {
          this.registerKeyStrokes(this.toggleCollapseKeyStroke);
        }
        if (this.firstCollapseKeyStroke) {
          this.registerKeyStrokes(this.firstCollapseKeyStroke);
        }
        if (this.secondCollapseKeyStroke) {
          this.registerKeyStrokes(this.secondCollapseKeyStroke);
        }
        if (this.rendered) {
          this._renderCollapseHandle();
        }
      } else {
        // update existing collapse handle
        this.collapseHandle.setHorizontalAlignment(horizontalAlignment);
      }

      this._updateCollapseHandleButtons();
    } else {
      if (this.collapseHandle) {
        this.collapseHandle.destroy();
        this.collapseHandle = null;
      }
    }
  }

  protected _updateCollapseHandleButtons() {
    if (!this.collapseHandle) {
      return;
    }
    let leftVisible: boolean, rightVisible: boolean,
      collapsed = this.fieldCollapsed,
      minimized = this.fieldMinimized,
      minimizable = this._isMinimizable(),
      positionTypeFirstField = ((this.splitterPositionType === SplitBox.SplitterPositionType.RELATIVE_FIRST) || (this.splitterPositionType === SplitBox.SplitterPositionType.ABSOLUTE_FIRST)),
      positionNotAccordingCollapsibleField = (positionTypeFirstField && this.collapsibleField === this.secondField) || (!positionTypeFirstField && this.collapsibleField === this.firstField);

    if (positionTypeFirstField) {
      if (positionNotAccordingCollapsibleField) {
        leftVisible = (!minimized && minimizable) || collapsed; // left = decrease collapsible field size. Decrease field in this order [minimized <- default <- collapsed]
        rightVisible = !collapsed; // right = increase collapsible field size. Increase field in this order [minimized -> default -> collapsed]
      } else {
        leftVisible = !collapsed; // left = increase collapsible field size. Increase field in this order [default <- minimized <- collapsed]
        rightVisible = collapsed || (minimized && minimizable); // right = decrease collapsible field size. Decrease field in this order [default -> minimized -> collapsed]
      }
    } else {
      if (positionNotAccordingCollapsibleField) {
        leftVisible = !collapsed; // left = decrease collapsible field size. Decrease field in this order [collapsed <- default <- minimized]
        rightVisible = (!minimized && minimizable) || collapsed; // right = increase collapsible field size. Increase field in this order [collapsed -> default -> minimized]
      } else {
        leftVisible = collapsed || (minimized && minimizable); // left = decrease collapsible field size. Decrease field in this order [collapsed <- minimized <- default]
        rightVisible = !collapsed; // right = increase collapsible field size. Increase field in this order [collapsed -> minimized -> default]
      }
    }

    this.collapseHandle.setLeftVisible(leftVisible);
    this.collapseHandle.setRightVisible(rightVisible);

    // update allowed keystrokes
    if (this.firstCollapseKeyStroke) {
      if (leftVisible) {
        this.registerKeyStrokes(this.firstCollapseKeyStroke);
      } else {
        this.unregisterKeyStrokes(this.firstCollapseKeyStroke);
      }
    }
    if (this.secondCollapseKeyStroke) {
      if (rightVisible) {
        this.registerKeyStrokes(this.secondCollapseKeyStroke);
      } else {
        this.unregisterKeyStrokes(this.secondCollapseKeyStroke);
      }
    }
  }

  getEffectiveSplitterPosition(): number {
    if (this._isMinimizable() && this.fieldMinimized) {
      return this.minSplitterPosition;
    }
    return this.splitterPosition;
  }

  setMinSplitterPosition(minSplitterPosition: number) {
    this.setProperty('minSplitterPosition', minSplitterPosition);
    this._updateCollapseHandleButtons();
  }

  protected _renderMinSplitterPosition() {
    // minimum splitter position is considered automatically when layout is updated
    if (this.rendered) { // don't invalidate layout on initial rendering
      this.htmlSplitArea.invalidateLayoutTree(false);
    }
  }

  setFieldMinimized(minimized: boolean) {
    this.setProperty('fieldMinimized', minimized);
    this._updateCollapseHandleButtons();
  }

  protected _renderFieldMinimized() {
    this.$container.removeClass('first-field-minimized second-field-minimized');
    if (this.firstField) {
      this.firstField.$container.removeClass('minimized');
    }
    if (this.secondField) {
      this.secondField.$container.removeClass('minimized');
    }
    if (this.collapsibleField && this.fieldMinimized) {
      this.collapsibleField.$container.addClass('minimized');
      this.$container.toggleClass('first-field-minimized', this.firstField === this.collapsibleField);
      this.$container.toggleClass('second-field-minimized', this.secondField === this.collapsibleField);
    }

    // field minimized state is considered automatically when layout is updated
    if (this.rendered) { // don't invalidate layout on initial rendering
      this.htmlSplitArea.invalidateLayoutTree(false);
    }
  }

  setMinimizeEnabled(enabled: boolean) {
    this.setProperty('minimizeEnabled', enabled);
    if (this._isMinimizable() && this._isSplitterPositionInMinimalRange(this.splitterPosition)) {
      this.setFieldMinimized(true);
    }

    this._updateCollapseHandleButtons();
  }

  protected _renderMinimizeEnabled() {
    // minimize enabled is considered automatically when layout is updated
    if (this.rendered) { // don't invalidate layout on initial rendering
      this.htmlSplitArea.invalidateLayoutTree(false);
    }
  }

  protected _isMinimizable(): boolean {
    return !!this.minSplitterPosition && this.minimizeEnabled;
  }

  protected _renderCollapsibleField() {
    this.$container.removeClass('first-field-collapsed second-field-collapsed');
    if (this.firstField) {
      this.firstField.$container.removeClass('collapsed');
    }
    if (this.secondField) {
      this.secondField.$container.removeClass('collapsed');
    }
    if (this.collapsibleField && this.fieldCollapsed) {
      this.collapsibleField.$container.addClass('collapsed');
      this.$container.toggleClass('first-field-collapsed', this.firstField === this.collapsibleField);
      this.$container.toggleClass('second-field-collapsed', this.secondField === this.collapsibleField);
    }
    if (this.rendered) { // don't invalidate layout on initial rendering
      this.htmlSplitArea.invalidateLayoutTree(false);
    }
  }

  protected _setToggleCollapseKeyStroke(keyStroke: string) {
    if (keyStroke) {
      if (this.toggleCollapseKeyStroke instanceof KeyStroke) {
        this.unregisterKeyStrokes(this.toggleCollapseKeyStroke);
      }
      this.toggleCollapseKeyStroke = new SplitBoxCollapseKeyStroke(this, keyStroke);
      if (this.collapseHandle) {
        this.registerKeyStrokes(this.toggleCollapseKeyStroke);
      }
    }
  }

  protected _setFirstCollapseKeyStroke(keyStroke: string) {
    if (keyStroke) {
      if (this.firstCollapseKeyStroke instanceof KeyStroke) {
        this.unregisterKeyStrokes(this.firstCollapseKeyStroke);
      }
      this.firstCollapseKeyStroke = new SplitBoxFirstCollapseKeyStroke(this, keyStroke);
      if (this.collapseHandle) {
        this.registerKeyStrokes(this.firstCollapseKeyStroke);
      }
    }
  }

  protected _setSecondCollapseKeyStroke(keyStroke: string) {
    if (keyStroke) {
      if (this.secondCollapseKeyStroke instanceof KeyStroke) {
        this.unregisterKeyStrokes(this.secondCollapseKeyStroke);
      }
      this.secondCollapseKeyStroke = new SplitBoxSecondCollapseKeyStroke(this, keyStroke);
      if (this.collapseHandle) {
        this.registerKeyStrokes(this.secondCollapseKeyStroke);
      }
    }
  }

  protected _renderCollapseHandle() {
    if (this.collapseHandle) {
      this.collapseHandle.render();
    }
  }

  newSplitterPosition(newSplitterPosition: number, updateFieldMinimizedState: boolean) {
    if (this._isSplitterPositionTypeRelative(this.splitterPositionType)) {
      // Ensure range 0..1
      newSplitterPosition = Math.max(0, Math.min(1, newSplitterPosition));
    } else {
      // Ensure not negative
      newSplitterPosition = Math.max(0, newSplitterPosition);
    }

    // Ensure splitter within allowed range, toggle field minimized state if new splitter position is within minimal range
    if (this._isMinimizable() && this._isSplitterPositionInMinimalRange(newSplitterPosition)) {
      this.setFieldMinimized(true);
      return;
    }

    // Set new value (send to server if changed
    let positionChanged = (this.splitterPosition !== newSplitterPosition);
    this.splitterPosition = newSplitterPosition;

    if (positionChanged) {
      this.trigger('positionChange', {
        position: newSplitterPosition
      });

      if (updateFieldMinimizedState) {
        this._updateFieldMinimized();
      }
    }

    this._updateCollapseHandleButtons();

    // Mark layout as invalid
    this.htmlSplitArea.invalidateLayoutTree(false);
  }

  protected _updateFieldMinimized() {
    if (this._isMinimizable()) {
      this.setFieldMinimized(this._isSplitterPositionInMinimalRange(this.splitterPosition));
    } else {
      this.setFieldMinimized(false);
    }
  }

  protected _isSplitterPositionInMinimalRange(newSplitterPosition: number): boolean {
    if (!this._isMinimizable()) {
      return false;
    }
    return newSplitterPosition <= this.minSplitterPosition;
  }

  toggleFieldCollapsed() {
    this.setFieldCollapsed(!this.fieldCollapsed);
  }

  collapseHandleButtonPressed(event: { left?: boolean; right?: boolean } /* CollapseHandleActionEvent */) {
    let collapsed = this.fieldCollapsed,
      minimized = this.fieldMinimized,
      minimizable = this._isMinimizable(),
      positionTypeFirstField = ((this.splitterPositionType === SplitBox.SplitterPositionType.RELATIVE_FIRST) || (this.splitterPositionType === SplitBox.SplitterPositionType.ABSOLUTE_FIRST)),
      increaseField = (!!event.left && !positionTypeFirstField) || (!!event.right && positionTypeFirstField);

    if ((positionTypeFirstField && this.collapsibleField === this.secondField) || (!positionTypeFirstField && this.collapsibleField === this.firstField)) {
      // Splitter is not positioned according (absolute or relative) to collapsible field
      // - Mode toggles to increase collapsible field size: field collapsed --> field default --> field minimized
      // - Mode toggles to decrease collapsible field size: field collapsed <-- field default <-- field minimized
      if (increaseField) {
        if (collapsed) {
          // not possible, button is not visible (field is collapsed and cannot further increase its size)
        } else if (minimized && minimizable) {
          this.setFieldMinimized(false);
        } else {
          this.setFieldCollapsed(true);
        }
      } else {
        if (collapsed) {
          this.setFieldCollapsed(false);
        } else if (minimized) {
          // not possible, button is not visible (field is minimized and cannot further decrease its size)
        } else if (minimizable) {
          this.setFieldMinimized(true);
        }
      }
    } else {
      // Splitter is positioned according (absolute or relative) to collapsible field
      // - Mode toggles to increase collapsible field size: field collapsed --> field minimized --> field default
      // - Mode toggles to decrease collapsible field size: field collapsed <-- field minimized <-- field default
      if (increaseField) {
        if (collapsed) {
          this.setFieldCollapsed(false);
        } else if (minimized) {
          this.setFieldMinimized(false);
        } else {
          // not possible, button is not visible (field has default size and cannot further increase its size)
        }
      } else {
        if (collapsed) {
          // not possible, button is not visible (field is collapsed and cannot further decrease its size)
        } else if (minimized || !minimizable) {
          this.setFieldCollapsed(true);
        } else {
          this.setFieldMinimized(true);
        }
      }
    }
  }

  getFields(): FormField[] {
    let fields: FormField[] = [];
    if (this.firstField) {
      fields.push(this.firstField);
    }
    if (this.secondField) {
      fields.push(this.secondField);
    }
    return fields;
  }

  protected _updateFieldVisibilityClasses() {
    if (!this.rendered && !this.rendering) {
      return;
    }
    let hasFirstField = (this.firstField && this.firstField.visible);
    let hasSecondField = (this.secondField && this.secondField.visible);
    let hasTwoFields = hasFirstField && hasSecondField;
    let hasOneField = !hasTwoFields && (hasFirstField || hasSecondField);

    // Mark container if only one field is visible (i.e. there is no splitter)
    this.$container.toggleClass('single-field', hasOneField);
  }
}

export type SplitBoxSplitterPositionType = EnumObject<typeof SplitBox.SplitterPositionType>;

ObjectIdProvider.uuidPathSkipWidgets.add(SplitBox);
