/**
 * Copyright (c) Tiny Technologies, Inc. All rights reserved.
 * Licensed under the LGPL or a commercial license.
 * For LGPL see License.txt in the project root for license information.
 * For commercial licenses see https://www.tiny.cloud/
 */

import AbsoluteLayout from './AbsoluteLayout';

/**
 * This layout manager works similar to the CSS flex box.
 *
 * @setting {String} direction row|row-reverse|column|column-reverse
 * @setting {Number} flex A positive-number to flex by.
 * @setting {String} align start|end|center|stretch
 * @setting {String} pack start|end|justify
 *
 * @class tinymce.ui.FlexLayout
 * @extends tinymce.ui.AbsoluteLayout
 */

export default AbsoluteLayout.extend({
  /**
   * Recalculates the positions of the controls in the specified container.
   *
   * @method recalc
   * @param {tinymce.ui.Container} container Container instance to recalc.
   */
  recalc (container) {
    // A ton of variables, needs to be in the same scope for performance
    let i, l, items, contLayoutRect, contPaddingBox, contSettings, align, pack, spacing, totalFlex, availableSpace, direction;
    let ctrl, ctrlLayoutRect, ctrlSettings, flex;
    const maxSizeItems = [];
    let size, maxSize, ratio, rect, pos, maxAlignEndPos;
    let sizeName, minSizeName, posName, maxSizeName, beforeName, innerSizeName, deltaSizeName, contentSizeName;
    let alignAxisName, alignInnerSizeName, alignSizeName, alignMinSizeName, alignBeforeName, alignAfterName;
    let alignDeltaSizeName, alignContentSizeName;
    const max = Math.max, min = Math.min;

    // Get container items, properties and settings
    items = container.items().filter(':visible');
    contLayoutRect = container.layoutRect();
    contPaddingBox = container.paddingBox;
    contSettings = container.settings;
    direction = container.isRtl() ? (contSettings.direction || 'row-reversed') : contSettings.direction;
    align = contSettings.align;
    pack = container.isRtl() ? (contSettings.pack || 'end') : contSettings.pack;
    spacing = contSettings.spacing || 0;

    if (direction === 'row-reversed' || direction === 'column-reverse') {
      items = items.set(items.toArray().reverse());
      direction = direction.split('-')[0];
    }

    // Setup axis variable name for row/column direction since the calculations is the same
    if (direction === 'column') {
      posName = 'y';
      sizeName = 'h';
      minSizeName = 'minH';
      maxSizeName = 'maxH';
      innerSizeName = 'innerH';
      beforeName = 'top';
      deltaSizeName = 'deltaH';
      contentSizeName = 'contentH';

      alignBeforeName = 'left';
      alignSizeName = 'w';
      alignAxisName = 'x';
      alignInnerSizeName = 'innerW';
      alignMinSizeName = 'minW';
      alignAfterName = 'right';
      alignDeltaSizeName = 'deltaW';
      alignContentSizeName = 'contentW';
    } else {
      posName = 'x';
      sizeName = 'w';
      minSizeName = 'minW';
      maxSizeName = 'maxW';
      innerSizeName = 'innerW';
      beforeName = 'left';
      deltaSizeName = 'deltaW';
      contentSizeName = 'contentW';

      alignBeforeName = 'top';
      alignSizeName = 'h';
      alignAxisName = 'y';
      alignInnerSizeName = 'innerH';
      alignMinSizeName = 'minH';
      alignAfterName = 'bottom';
      alignDeltaSizeName = 'deltaH';
      alignContentSizeName = 'contentH';
    }

    // Figure out total flex, availableSpace and collect any max size elements
    availableSpace = contLayoutRect[innerSizeName] - contPaddingBox[beforeName] - contPaddingBox[beforeName];
    maxAlignEndPos = totalFlex = 0;
    for (i = 0, l = items.length; i < l; i++) {
      ctrl = items[i];
      ctrlLayoutRect = ctrl.layoutRect();
      ctrlSettings = ctrl.settings;
      flex = ctrlSettings.flex;
      availableSpace -= (i < l - 1 ? spacing : 0);

      if (flex > 0) {
        totalFlex += flex;

        // Flexed item has a max size then we need to check if we will hit that size
        if (ctrlLayoutRect[maxSizeName]) {
          maxSizeItems.push(ctrl);
        }

        ctrlLayoutRect.flex = flex;
      }

      availableSpace -= ctrlLayoutRect[minSizeName];

      // Calculate the align end position to be used to check for overflow/underflow
      size = contPaddingBox[alignBeforeName] + ctrlLayoutRect[alignMinSizeName] + contPaddingBox[alignAfterName];
      if (size > maxAlignEndPos) {
        maxAlignEndPos = size;
      }
    }

    // Calculate minW/minH
    rect = {};
    if (availableSpace < 0) {
      rect[minSizeName] = contLayoutRect[minSizeName] - availableSpace + contLayoutRect[deltaSizeName];
    } else {
      rect[minSizeName] = contLayoutRect[innerSizeName] - availableSpace + contLayoutRect[deltaSizeName];
    }

    rect[alignMinSizeName] = maxAlignEndPos + contLayoutRect[alignDeltaSizeName];

    rect[contentSizeName] = contLayoutRect[innerSizeName] - availableSpace;
    rect[alignContentSizeName] = maxAlignEndPos;
    rect.minW = min(rect.minW, contLayoutRect.maxW);
    rect.minH = min(rect.minH, contLayoutRect.maxH);
    rect.minW = max(rect.minW, contLayoutRect.startMinWidth);
    rect.minH = max(rect.minH, contLayoutRect.startMinHeight);

    // Resize container container if minSize was changed
    if (contLayoutRect.autoResize && (rect.minW !== contLayoutRect.minW || rect.minH !== contLayoutRect.minH)) {
      rect.w = rect.minW;
      rect.h = rect.minH;

      container.layoutRect(rect);
      this.recalc(container);

      // Forced recalc for example if items are hidden/shown
      if (container._lastRect === null) {
        const parentCtrl = container.parent();
        if (parentCtrl) {
          parentCtrl._lastRect = null;
          parentCtrl.recalc();
        }
      }

      return;
    }

    // Handle max size elements, check if they will become to wide with current options
    ratio = availableSpace / totalFlex;
    for (i = 0, l = maxSizeItems.length; i < l; i++) {
      ctrl = maxSizeItems[i];
      ctrlLayoutRect = ctrl.layoutRect();
      maxSize = ctrlLayoutRect[maxSizeName];
      size = ctrlLayoutRect[minSizeName] + ctrlLayoutRect.flex * ratio;

      if (size > maxSize) {
        availableSpace -= (ctrlLayoutRect[maxSizeName] - ctrlLayoutRect[minSizeName]);
        totalFlex -= ctrlLayoutRect.flex;
        ctrlLayoutRect.flex = 0;
        ctrlLayoutRect.maxFlexSize = maxSize;
      } else {
        ctrlLayoutRect.maxFlexSize = 0;
      }
    }

    // Setup new ratio, target layout rect, start position
    ratio = availableSpace / totalFlex;
    pos = contPaddingBox[beforeName];
    rect = {};

    // Handle pack setting moves the start position to end, center
    if (totalFlex === 0) {
      if (pack === 'end') {
        pos = availableSpace + contPaddingBox[beforeName];
      } else if (pack === 'center') {
        pos = Math.round(
          (contLayoutRect[innerSizeName] / 2) - ((contLayoutRect[innerSizeName] - availableSpace) / 2)
        ) + contPaddingBox[beforeName];

        if (pos < 0) {
          pos = contPaddingBox[beforeName];
        }
      } else if (pack === 'justify') {
        pos = contPaddingBox[beforeName];
        spacing = Math.floor(availableSpace / (items.length - 1));
      }
    }

    // Default aligning (start) the other ones needs to be calculated while doing the layout
    rect[alignAxisName] = contPaddingBox[alignBeforeName];

    // Start laying out controls
    for (i = 0, l = items.length; i < l; i++) {
      ctrl = items[i];
      ctrlLayoutRect = ctrl.layoutRect();
      size = ctrlLayoutRect.maxFlexSize || ctrlLayoutRect[minSizeName];

      // Align the control on the other axis
      if (align === 'center') {
        rect[alignAxisName] = Math.round((contLayoutRect[alignInnerSizeName] / 2) - (ctrlLayoutRect[alignSizeName] / 2));
      } else if (align === 'stretch') {
        rect[alignSizeName] = max(
          ctrlLayoutRect[alignMinSizeName] || 0,
          contLayoutRect[alignInnerSizeName] - contPaddingBox[alignBeforeName] - contPaddingBox[alignAfterName]
        );
        rect[alignAxisName] = contPaddingBox[alignBeforeName];
      } else if (align === 'end') {
        rect[alignAxisName] = contLayoutRect[alignInnerSizeName] - ctrlLayoutRect[alignSizeName] - contPaddingBox.top;
      }

      // Calculate new size based on flex
      if (ctrlLayoutRect.flex > 0) {
        size += ctrlLayoutRect.flex * ratio;
      }

      rect[sizeName] = size;
      rect[posName] = pos;
      ctrl.layoutRect(rect);

      // Recalculate containers
      if (ctrl.recalc) {
        ctrl.recalc();
      }

      // Move x/y position
      pos += size + spacing;
    }
  }
});