'use strict';

var util = require('../../../../../zrender@6.0.0/node_modules/zrender/lib/core/util.js');
var BoundingRect = require('../../../../../zrender@6.0.0/node_modules/zrender/lib/core/BoundingRect.js');
var number = require('./number.js');
var format = require('./format.js');
var log = require('./log.js');
var CoordinateSystem = require('../core/CoordinateSystem.js');

var each = util.each;
/**
 * @public
 */
var LOCATION_PARAMS = ['left', 'right', 'top', 'bottom', 'width', 'height'];
/**
 * @public
 */
var HV_NAMES = [['width', 'left', 'right'], ['height', 'top', 'bottom']];
function boxLayout(orient, group, gap, maxWidth, maxHeight) {
  var x = 0;
  var y = 0;
  if (maxWidth == null) {
    maxWidth = Infinity;
  }
  if (maxHeight == null) {
    maxHeight = Infinity;
  }
  var currentLineMaxSize = 0;
  group.eachChild(function (child, idx) {
    var rect = child.getBoundingRect();
    var nextChild = group.childAt(idx + 1);
    var nextChildRect = nextChild && nextChild.getBoundingRect();
    var nextX;
    var nextY;
    if (orient === 'horizontal') {
      var moveX = rect.width + (nextChildRect ? -nextChildRect.x + rect.x : 0);
      nextX = x + moveX;
      // Wrap when width exceeds maxWidth or meet a `newline` group
      // FIXME compare before adding gap?
      if (nextX > maxWidth || child.newline) {
        x = 0;
        nextX = moveX;
        y += currentLineMaxSize + gap;
        currentLineMaxSize = rect.height;
      } else {
        // FIXME: consider rect.y is not `0`?
        currentLineMaxSize = Math.max(currentLineMaxSize, rect.height);
      }
    } else {
      var moveY = rect.height + (nextChildRect ? -nextChildRect.y + rect.y : 0);
      nextY = y + moveY;
      // Wrap when width exceeds maxHeight or meet a `newline` group
      if (nextY > maxHeight || child.newline) {
        x += currentLineMaxSize + gap;
        y = 0;
        nextY = moveY;
        currentLineMaxSize = rect.width;
      } else {
        currentLineMaxSize = Math.max(currentLineMaxSize, rect.width);
      }
    }
    if (child.newline) {
      return;
    }
    child.x = x;
    child.y = y;
    child.markRedraw();
    orient === 'horizontal' ? x = nextX + gap : y = nextY + gap;
  });
}
/**
 * VBox or HBox layouting
 * @param {string} orient
 * @param {module:zrender/graphic/Group} group
 * @param {number} gap
 * @param {number} [width=Infinity]
 * @param {number} [height=Infinity]
 */
var box = boxLayout;
/**
 * VBox layouting
 * @param {module:zrender/graphic/Group} group
 * @param {number} gap
 * @param {number} [width=Infinity]
 * @param {number} [height=Infinity]
 */
var vbox = util.curry(boxLayout, 'vertical');
/**
 * HBox layouting
 * @param {module:zrender/graphic/Group} group
 * @param {number} gap
 * @param {number} [width=Infinity]
 * @param {number} [height=Infinity]
 */
var hbox = util.curry(boxLayout, 'horizontal');
function getBoxLayoutParams(boxLayoutModel, ignoreParent) {
  return {
    left: boxLayoutModel.getShallow('left', ignoreParent),
    top: boxLayoutModel.getShallow('top', ignoreParent),
    right: boxLayoutModel.getShallow('right', ignoreParent),
    bottom: boxLayoutModel.getShallow('bottom', ignoreParent),
    width: boxLayoutModel.getShallow('width', ignoreParent),
    height: boxLayoutModel.getShallow('height', ignoreParent)
  };
}
function getViewRectAndCenterForCircleLayout(seriesModel, api) {
  var layoutRef = createBoxLayoutReference(seriesModel, api, {
    enableLayoutOnlyByCenter: true
  });
  var boxLayoutParams = seriesModel.getBoxLayoutParams();
  var viewRect;
  var center;
  if (layoutRef.type === BoxLayoutReferenceType.point) {
    center = layoutRef.refPoint;
    // `viewRect` is required in `pie/labelLayout.ts`.
    viewRect = getLayoutRect(boxLayoutParams, {
      width: api.getWidth(),
      height: api.getHeight()
    });
  } else {
    // layoutRef.type === layout.BoxLayoutReferenceType.rect
    var centerOption = seriesModel.get('center');
    var centerOptionArr = util.isArray(centerOption) ? centerOption : [centerOption, centerOption];
    viewRect = getLayoutRect(boxLayoutParams, layoutRef.refContainer);
    center = layoutRef.boxCoordFrom === CoordinateSystem.BoxCoordinateSystemCoordFrom.coord2 ? layoutRef.refPoint // option `series.center` has been used as coord.
    : [number.parsePercent(centerOptionArr[0], viewRect.width) + viewRect.x, number.parsePercent(centerOptionArr[1], viewRect.height) + viewRect.y];
  }
  return {
    viewRect: viewRect,
    center: center
  };
}
function getCircleLayout(seriesModel, api) {
  // center can be string or number when coordinateSystem is specified
  var _a = getViewRectAndCenterForCircleLayout(seriesModel, api),
    viewRect = _a.viewRect,
    center = _a.center;
  var radius = seriesModel.get('radius');
  if (!util.isArray(radius)) {
    radius = [0, radius];
  }
  var width = number.parsePercent(viewRect.width, api.getWidth());
  var height = number.parsePercent(viewRect.height, api.getHeight());
  var size = Math.min(width, height);
  var r0 = number.parsePercent(radius[0], size / 2);
  var r = number.parsePercent(radius[1], size / 2);
  return {
    cx: center[0],
    cy: center[1],
    r0: r0,
    r: r,
    viewRect: viewRect
  };
}
/**
 * Parse position info.
 */
function getLayoutRect(positionInfo, containerRect,
// This is the space from the `containerRect` to the returned bounding rect.
// Commonly used in option `legend.padding`, `timeline.padding`, `title.padding`,
//  `visualMap.padding`, ...
// [NOTICE]:
//  It's named `margin`, because it's the space that outside the bounding rect. But from
//  the perspective of the the caller, it's commonly used as the `padding` of a component,
//  because conventionally background color covers this space.
// [BEHAVIOR]:
//  - If width/height is specified, `margin` does not effect them.
//  - Otherwise, they are calculated based on the rect that `containerRect` shrinked by `margin`.
//  - left/right/top/bottom are based on the rect that `containerRect` shrinked by `margin`.
margin) {
  margin = format.normalizeCssArray(margin || 0);
  var containerWidth = containerRect.width;
  var containerHeight = containerRect.height;
  var left = number.parsePercent(positionInfo.left, containerWidth);
  var top = number.parsePercent(positionInfo.top, containerHeight);
  var right = number.parsePercent(positionInfo.right, containerWidth);
  var bottom = number.parsePercent(positionInfo.bottom, containerHeight);
  var width = number.parsePercent(positionInfo.width, containerWidth);
  var height = number.parsePercent(positionInfo.height, containerHeight);
  var verticalMargin = margin[2] + margin[0];
  var horizontalMargin = margin[1] + margin[3];
  var aspect = positionInfo.aspect;
  // If width is not specified, calculate width from left and right
  if (isNaN(width)) {
    width = containerWidth - right - horizontalMargin - left;
  }
  if (isNaN(height)) {
    height = containerHeight - bottom - verticalMargin - top;
  }
  if (aspect != null) {
    // If width and height are not given
    // 1. Graph should not exceeds the container
    // 2. Aspect must be keeped
    // 3. Graph should take the space as more as possible
    // FIXME
    // Margin is not considered, because there is no case that both
    // using margin and aspect so far.
    if (isNaN(width) && isNaN(height)) {
      // PENDING: if only `left` or `right` is defined, perhaps it's more preferable to
      // calculate size based on `containerWidth - left` or `containerWidth - left` here,
      // but for backward compatibility we do not change it.
      if (aspect > containerWidth / containerHeight) {
        width = containerWidth * 0.8;
      } else {
        height = containerHeight * 0.8;
      }
    }
    // Calculate width or height with given aspect
    if (isNaN(width)) {
      width = aspect * height;
    }
    if (isNaN(height)) {
      height = width / aspect;
    }
  }
  // If left is not specified, calculate left from right and width
  if (isNaN(left)) {
    left = containerWidth - right - width - horizontalMargin;
  }
  if (isNaN(top)) {
    top = containerHeight - bottom - height - verticalMargin;
  }
  // Align left and top
  switch (positionInfo.left || positionInfo.right) {
    case 'center':
      left = containerWidth / 2 - width / 2 - margin[3];
      break;
    case 'right':
      left = containerWidth - width - horizontalMargin;
      break;
  }
  switch (positionInfo.top || positionInfo.bottom) {
    case 'middle':
    case 'center':
      top = containerHeight / 2 - height / 2 - margin[0];
      break;
    case 'bottom':
      top = containerHeight - height - verticalMargin;
      break;
  }
  // If something is wrong and left, top, width, height are calculated as NaN
  left = left || 0;
  top = top || 0;
  if (isNaN(width)) {
    // Width may be NaN if only one value is given except width
    width = containerWidth - horizontalMargin - left - (right || 0);
  }
  if (isNaN(height)) {
    // Height may be NaN if only one value is given except height
    height = containerHeight - verticalMargin - top - (bottom || 0);
  }
  var rect = new BoundingRect.default((containerRect.x || 0) + left + margin[3], (containerRect.y || 0) + top + margin[0], width, height);
  rect.margin = margin;
  return rect;
}
/**
 * PENDING:
 *  when preserveAspect: 'cover' and aspect is near Infinity
 *  or when preserveAspect: 'contain' and aspect is near 0,
 *  the result width or height is near Inifity. It's logically correct,
 *  Therefore currently we do not handle it, until bad cases arise.
 */
function applyPreserveAspect(component, layoutRect,
// That is, `width / height`.
// Assume `aspect` is positive.
aspect) {
  var preserveAspect = component.getShallow('preserveAspect', true);
  if (!preserveAspect) {
    return layoutRect;
  }
  var actualAspect = layoutRect.width / layoutRect.height;
  if (Math.abs(Math.atan(aspect) - Math.atan(actualAspect)) < 1e-9) {
    return layoutRect;
  }
  var preserveAspectAlign = component.getShallow('preserveAspectAlign', true);
  var preserveAspectVerticalAlign = component.getShallow('preserveAspectVerticalAlign', true);
  var layoutOptInner = {
    width: layoutRect.width,
    height: layoutRect.height
  };
  var isCover = preserveAspect === 'cover';
  if (actualAspect > aspect && !isCover || actualAspect < aspect && isCover) {
    layoutOptInner.width = layoutRect.height * aspect;
    preserveAspectAlign === 'left' ? layoutOptInner.left = 0 : preserveAspectAlign === 'right' ? layoutOptInner.right = 0 : layoutOptInner.left = 'center';
  } else {
    layoutOptInner.height = layoutRect.width / aspect;
    preserveAspectVerticalAlign === 'top' ? layoutOptInner.top = 0 : preserveAspectVerticalAlign === 'bottom' ? layoutOptInner.bottom = 0 : layoutOptInner.top = 'middle';
  }
  return getLayoutRect(layoutOptInner, layoutRect);
}
var BoxLayoutReferenceType = {
  rect: 1,
  point: 2
};
/**
 * Uniformly calculate layout reference (rect or center) based on either:
 *  - viewport:
 *      - Get `refContainer` as `{x: 0, y: 0, width: api.getWidth(), height: api.getHeight()}`
 *  - coordinate system, which can serve in several ways:
 *      - Use `dataToPoint` to get the `refPoint`, such as, in cartesian2d coord sys.
 *      - Use `dataToLayout` to get the `refContainer`, such as, in matrix coord sys.
 */
function createBoxLayoutReference(model, api, opt) {
  var refContainer;
  var refPoint;
  var layoutRefType;
  var boxCoordSys = model.boxCoordinateSystem;
  var boxCoordFrom;
  if (boxCoordSys) {
    var _a = CoordinateSystem.getCoordForBoxCoordSys(model),
      coord = _a.coord,
      from = _a.from;
    // Do not use `clamp` in `dataToLayout` and `dataToPoint`, because:
    //  1. Should support overflow (such as, by dataZoom), where NaN should be in the result.
    //  2. Be consistent with the way used in `series.data`
    if (boxCoordSys.dataToLayout) {
      layoutRefType = BoxLayoutReferenceType.rect;
      boxCoordFrom = from;
      var result = boxCoordSys.dataToLayout(coord);
      refContainer = result.contentRect || result.rect;
    } else if (opt && opt.enableLayoutOnlyByCenter && boxCoordSys.dataToPoint) {
      layoutRefType = BoxLayoutReferenceType.point;
      boxCoordFrom = from;
      refPoint = boxCoordSys.dataToPoint(coord);
    } else {
      if (process.env.NODE_ENV !== 'production') {
        log.error(model.type + "[" + model.componentIndex + "]" + (" layout based on " + boxCoordSys.type + " is not supported."));
      }
    }
  }
  if (layoutRefType == null) {
    layoutRefType = BoxLayoutReferenceType.rect;
  }
  if (layoutRefType === BoxLayoutReferenceType.rect) {
    if (!refContainer) {
      refContainer = {
        x: 0,
        y: 0,
        width: api.getWidth(),
        height: api.getHeight()
      };
    }
    refPoint = [refContainer.x + refContainer.width / 2, refContainer.y + refContainer.height / 2];
  }
  return {
    type: layoutRefType,
    refContainer: refContainer,
    refPoint: refPoint,
    boxCoordFrom: boxCoordFrom
  };
}
/**
 * Position a zr element in viewport
 *  Group position is specified by either
 *  {left, top}, {right, bottom}
 *  If all properties exists, right and bottom will be igonred.
 *
 * Logic:
 *     1. Scale (against origin point in parent coord)
 *     2. Rotate (against origin point in parent coord)
 *     3. Translate (with el.position by this method)
 * So this method only fixes the last step 'Translate', which does not affect
 * scaling and rotating.
 *
 * If be called repeatedly with the same input el, the same result will be gotten.
 *
 * Return true if the layout happened.
 *
 * @param el Should have `getBoundingRect` method.
 * @param positionInfo
 * @param positionInfo.left
 * @param positionInfo.top
 * @param positionInfo.right
 * @param positionInfo.bottom
 * @param positionInfo.width Only for opt.boundingModel: 'raw'
 * @param positionInfo.height Only for opt.boundingModel: 'raw'
 * @param containerRect
 * @param margin
 * @param opt
 * @param opt.hv Only horizontal or only vertical. Default to be [1, 1]
 * @param opt.boundingMode
 *        Specify how to calculate boundingRect when locating.
 *        'all': Position the boundingRect that is transformed and uioned
 *               both itself and its descendants.
 *               This mode simplies confine the elements in the bounding
 *               of their container (e.g., using 'right: 0').
 *        'raw': Position the boundingRect that is not transformed and only itself.
 *               This mode is useful when you want a element can overflow its
 *               container. (Consider a rotated circle needs to be located in a corner.)
 *               In this mode positionInfo.width/height can only be number.
 */
function positionElement(el, positionInfo, containerRect, margin, opt, out) {
  var h = !opt || !opt.hv || opt.hv[0];
  var v = !opt || !opt.hv || opt.hv[1];
  var boundingMode = opt && opt.boundingMode || 'all';
  out = out || el;
  out.x = el.x;
  out.y = el.y;
  if (!h && !v) {
    return false;
  }
  var rect;
  if (boundingMode === 'raw') {
    rect = el.type === 'group' ? new BoundingRect.default(0, 0, +positionInfo.width || 0, +positionInfo.height || 0) : el.getBoundingRect();
  } else {
    rect = el.getBoundingRect();
    if (el.needLocalTransform()) {
      var transform = el.getLocalTransform();
      // Notice: raw rect may be inner object of el,
      // which should not be modified.
      rect = rect.clone();
      rect.applyTransform(transform);
    }
  }
  // The real width and height can not be specified but calculated by the given el.
  var layoutRect = getLayoutRect(util.defaults({
    width: rect.width,
    height: rect.height
  }, positionInfo), containerRect, margin);
  // Because 'tranlate' is the last step in transform
  // (see zrender/core/Transformable#getLocalTransform),
  // we can just only modify el.position to get final result.
  var dx = h ? layoutRect.x - rect.x : 0;
  var dy = v ? layoutRect.y - rect.y : 0;
  if (boundingMode === 'raw') {
    out.x = dx;
    out.y = dy;
  } else {
    out.x += dx;
    out.y += dy;
  }
  if (out === el) {
    el.markRedraw();
  }
  return true;
}
/**
 * @param option Contains some of the properties in HV_NAMES.
 * @param hvIdx 0: horizontal; 1: vertical.
 */
function sizeCalculable(option, hvIdx) {
  return option[HV_NAMES[hvIdx][0]] != null || option[HV_NAMES[hvIdx][1]] != null && option[HV_NAMES[hvIdx][2]] != null;
}
function fetchLayoutMode(ins) {
  var layoutMode = ins.layoutMode || ins.constructor.layoutMode;
  return util.isObject(layoutMode) ? layoutMode : layoutMode ? {
    type: layoutMode
  } : null;
}
/**
 * Consider Case:
 * When default option has {left: 0, width: 100}, and we set {right: 0}
 * through setOption or media query, using normal zrUtil.merge will cause
 * {right: 0} does not take effect.
 *
 * @example
 * ComponentModel.extend({
 *     init: function () {
 *         ...
 *         let inputPositionParams = layout.getLayoutParams(option);
 *         this.mergeOption(inputPositionParams);
 *     },
 *     mergeOption: function (newOption) {
 *         newOption && zrUtil.merge(thisOption, newOption, true);
 *         layout.mergeLayoutParam(thisOption, newOption);
 *     }
 * });
 *
 * @param targetOption
 * @param newOption
 * @param opt
 */
function mergeLayoutParam(targetOption, newOption, opt) {
  var ignoreSize = opt && opt.ignoreSize;
  !util.isArray(ignoreSize) && (ignoreSize = [ignoreSize, ignoreSize]);
  var hResult = merge(HV_NAMES[0], 0);
  var vResult = merge(HV_NAMES[1], 1);
  copy(HV_NAMES[0], targetOption, hResult);
  copy(HV_NAMES[1], targetOption, vResult);
  function merge(names, hvIdx) {
    var newParams = {};
    var newValueCount = 0;
    var merged = {};
    var mergedValueCount = 0;
    var enoughParamNumber = 2;
    each(names, function (name) {
      merged[name] = targetOption[name];
    });
    each(names, function (name) {
      // Consider case: newOption.width is null, which is
      // set by user for removing width setting.
      util.hasOwn(newOption, name) && (newParams[name] = merged[name] = newOption[name]);
      hasValue(newParams, name) && newValueCount++;
      hasValue(merged, name) && mergedValueCount++;
    });
    if (ignoreSize[hvIdx]) {
      // Only one of left/right is premitted to exist.
      if (hasValue(newOption, names[1])) {
        merged[names[2]] = null;
      } else if (hasValue(newOption, names[2])) {
        merged[names[1]] = null;
      }
      return merged;
    }
    // Case: newOption: {width: ..., right: ...},
    // or targetOption: {right: ...} and newOption: {width: ...},
    // There is no conflict when merged only has params count
    // little than enoughParamNumber.
    if (mergedValueCount === enoughParamNumber || !newValueCount) {
      return merged;
    }
    // Case: newOption: {width: ..., right: ...},
    // Than we can make sure user only want those two, and ignore
    // all origin params in targetOption.
    else if (newValueCount >= enoughParamNumber) {
      return newParams;
    } else {
      // Chose another param from targetOption by priority.
      for (var i = 0; i < names.length; i++) {
        var name_1 = names[i];
        if (!util.hasOwn(newParams, name_1) && util.hasOwn(targetOption, name_1)) {
          newParams[name_1] = targetOption[name_1];
          break;
        }
      }
      return newParams;
    }
  }
  function hasValue(obj, name) {
    return obj[name] != null && obj[name] !== 'auto';
  }
  function copy(names, target, source) {
    each(names, function (name) {
      target[name] = source[name];
    });
  }
}
/**
 * Retrieve 'left', 'right', 'top', 'bottom', 'width', 'height' from object.
 */
function getLayoutParams(source) {
  return copyLayoutParams({}, source);
}
/**
 * Retrieve 'left', 'right', 'top', 'bottom', 'width', 'height' from object.
 * @param {Object} source
 * @return {Object} Result contains those props.
 */
function copyLayoutParams(target, source) {
  source && target && each(LOCATION_PARAMS, function (name) {
    util.hasOwn(source, name) && (target[name] = source[name]);
  });
  return target;
}

exports.BoxLayoutReferenceType = BoxLayoutReferenceType;
exports.HV_NAMES = HV_NAMES;
exports.LOCATION_PARAMS = LOCATION_PARAMS;
exports.applyPreserveAspect = applyPreserveAspect;
exports.box = box;
exports.copyLayoutParams = copyLayoutParams;
exports.createBoxLayoutReference = createBoxLayoutReference;
exports.fetchLayoutMode = fetchLayoutMode;
exports.getBoxLayoutParams = getBoxLayoutParams;
exports.getCircleLayout = getCircleLayout;
exports.getLayoutParams = getLayoutParams;
exports.getLayoutRect = getLayoutRect;
exports.hbox = hbox;
exports.mergeLayoutParam = mergeLayoutParam;
exports.positionElement = positionElement;
exports.sizeCalculable = sizeCalculable;
exports.vbox = vbox;
