/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */


/**
 * @fileoverview A thicker wrapper around graphics groups.
 */


goog.provide('goog.graphics.ext.Group');

goog.require('goog.array');
goog.require('goog.graphics.ext.Element');
goog.requireType('goog.graphics.GroupElement');



/**
 * Wrapper for a graphics group.
 * @param {goog.graphics.ext.Group} group Parent for this element. Can
 *     be null if this is a Graphics instance.
 * @param {goog.graphics.GroupElement=} opt_wrapper The thin wrapper
 *     to wrap. If omitted, a new group will be created. Must be included
 *     when group is null.
 * @constructor
 * @extends {goog.graphics.ext.Element}
 */
goog.graphics.ext.Group = function(group, opt_wrapper) {
  'use strict';
  opt_wrapper = opt_wrapper ||
      group.getGraphicsImplementation().createGroup(group.getWrapper());
  goog.graphics.ext.Element.call(this, group, opt_wrapper);

  /**
   * Array of child elements this group contains.
   * @type {Array<goog.graphics.ext.Element>}
   * @private
   */
  this.children_ = [];
};
goog.inherits(goog.graphics.ext.Group, goog.graphics.ext.Element);


/**
 * Add an element to the group.  This should be treated as package local, as
 * it is called by the draw* methods.
 * @param {!goog.graphics.ext.Element} element The element to add.
 * @param {boolean=} opt_chain Whether this addition is part of a longer set
 *     of element additions.
 */
goog.graphics.ext.Group.prototype.addChild = function(element, opt_chain) {
  'use strict';
  if (!goog.array.contains(this.children_, element)) {
    this.children_.push(element);
  }

  const transformed = this.growToFit_(element);

  if (element.isParentDependent()) {
    element.parentTransform();
  }

  if (!opt_chain && element.isPendingTransform()) {
    element.reset();
  }

  if (transformed) {
    this.reset();
  }
};


/**
 * Remove an element from the group.
 * @param {goog.graphics.ext.Element} element The element to remove.
 */
goog.graphics.ext.Group.prototype.removeChild = function(element) {
  'use strict';
  goog.array.remove(this.children_, element);

  // TODO(robbyw): shape.fireEvent('delete')

  this.getGraphicsImplementation().removeElement(element.getWrapper());
};


/**
 * Calls the given function on each of this component's children in order.  If
 * `opt_obj` is provided, it will be used as the 'this' object in the
 * function when called.  The function should take two arguments:  the child
 * component and its 0-based index.  The return value is ignored.
 * @param {Function} f The function to call for every child component; should
 *    take 2 arguments (the child and its index).
 * @param {Object=} opt_obj Used as the 'this' object in f when called.
 */
goog.graphics.ext.Group.prototype.forEachChild = function(f, opt_obj) {
  'use strict';
  if (this.children_) {
    this.children_.forEach(f, opt_obj);
  }
};


/**
 * @return {goog.graphics.GroupElement} The underlying thin wrapper.
 * @override
 */
goog.graphics.ext.Group.prototype.getWrapper;


/**
 * Reset the element.
 * @override
 */
goog.graphics.ext.Group.prototype.reset = function() {
  'use strict';
  goog.graphics.ext.Group.superClass_.reset.call(this);

  this.updateChildren();
};


/**
 * Called from the parent class, this method resets any pre-computed positions
 * and sizes.
 * @protected
 * @override
 */
goog.graphics.ext.Group.prototype.redraw = function() {
  'use strict';
  this.getWrapper().setSize(this.getWidth(), this.getHeight());
  this.transformChildren();
};


/**
 * Transform the children that need to be transformed.
 * @protected
 */
goog.graphics.ext.Group.prototype.transformChildren = function() {
  'use strict';
  this.forEachChild(function(child) {
    'use strict';
    if (child.isParentDependent()) {
      child.parentTransform();
    }
  });
};


/**
 * As part of the reset process, update child elements.
 */
goog.graphics.ext.Group.prototype.updateChildren = function() {
  'use strict';
  this.forEachChild(function(child) {
    'use strict';
    if (child.isParentDependent() || child.isPendingTransform()) {
      child.reset();
    } else if (child.updateChildren) {
      child.updateChildren();
    }
  });
};


/**
 * When adding an element, grow this group's bounds to fit it.
 * @param {!goog.graphics.ext.Element} element The added element.
 * @return {boolean} Whether the size of this group changed.
 * @private
 */
goog.graphics.ext.Group.prototype.growToFit_ = function(element) {
  'use strict';
  let transformed = false;

  const x = element.getMaxX();
  if (x > this.getWidth()) {
    this.setMinWidth(x);
    transformed = true;
  }

  const y = element.getMaxY();
  if (y > this.getHeight()) {
    this.setMinHeight(y);
    transformed = true;
  }

  return transformed;
};


/**
 * @return {number} The width of the element's coordinate space.
 */
goog.graphics.ext.Group.prototype.getCoordinateWidth = function() {
  'use strict';
  return this.getWidth();
};


/**
 * @return {number} The height of the element's coordinate space.
 */
goog.graphics.ext.Group.prototype.getCoordinateHeight = function() {
  'use strict';
  return this.getHeight();
};


/**
 * Remove all drawing elements from the group.
 */
goog.graphics.ext.Group.prototype.clear = function() {
  'use strict';
  while (this.children_.length) {
    this.removeChild(this.children_[0]);
  }
};
