// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Class definition for a rounded corner panel.
 * @supported IE 6.0+, Safari 2.0+, Firefox 1.5+, Opera 9.2+.
 * @see ../demos/roundedpanel.html
 */

goog.provide('goog.ui.BaseRoundedPanel');
goog.provide('goog.ui.CssRoundedPanel');
goog.provide('goog.ui.GraphicsRoundedPanel');
goog.provide('goog.ui.RoundedPanel');
goog.provide('goog.ui.RoundedPanel.Corner');

goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.dom.classlist');
goog.require('goog.graphics');
goog.require('goog.graphics.Path');
goog.require('goog.graphics.SolidFill');
goog.require('goog.graphics.Stroke');
goog.require('goog.math');
goog.require('goog.math.Coordinate');
goog.require('goog.style');
goog.require('goog.ui.Component');
goog.require('goog.userAgent');


/**
 * Factory method that returns an instance of a BaseRoundedPanel.
 * @param {number} radius The radius of the rounded corner(s), in pixels.
 * @param {number} borderWidth The thickness of the border, in pixels.
 * @param {string} borderColor The border color of the panel.
 * @param {string=} opt_backgroundColor The background color of the panel.
 * @param {number=} opt_corners The corners of the panel to be rounded. Any
 *     corners not specified will be rendered as square corners. Will default
 *     to all square corners if not specified.
 * @param {goog.dom.DomHelper=} opt_domHelper The DOM helper object for the
 *     document we want to render in.
 * @return {!goog.ui.BaseRoundedPanel} An instance of a
 *     goog.ui.BaseRoundedPanel subclass.
 * TODO(user): deprecate this class, which has <5 usages and only really
 *            matters for IE8, and then only stylistically.
 */
goog.ui.RoundedPanel.create = function(
    radius, borderWidth, borderColor, opt_backgroundColor, opt_corners,
    opt_domHelper) {
  // This variable checks for the presence of Safari 3.0+ or Gecko 1.9+,
  // which can leverage special CSS styles to create rounded corners.
  var isCssReady =
      (goog.userAgent.WEBKIT && goog.userAgent.isVersionOrHigher('500')) ||
      (goog.userAgent.GECKO && goog.userAgent.isVersionOrHigher('1.9a')) ||
      goog.userAgent.EDGE;

  if (isCssReady) {
    // Safari 3.0+ and Firefox 3.0+ support this instance.
    return new goog.ui.CssRoundedPanel(
        radius, borderWidth, borderColor, opt_backgroundColor, opt_corners,
        opt_domHelper);
  } else {
    return new goog.ui.GraphicsRoundedPanel(
        radius, borderWidth, borderColor, opt_backgroundColor, opt_corners,
        opt_domHelper);
  }
};


/**
 * Enum for specifying which corners to render.
 * @enum {number}
 */
goog.ui.RoundedPanel.Corner = {
  NONE: 0,
  BOTTOM_LEFT: 2,
  TOP_LEFT: 4,
  LEFT: 6,  // BOTTOM_LEFT | TOP_LEFT
  TOP_RIGHT: 8,
  TOP: 12,  // TOP_LEFT | TOP_RIGHT
  BOTTOM_RIGHT: 1,
  BOTTOM: 3,  // BOTTOM_LEFT | BOTTOM_RIGHT
  RIGHT: 9,   // TOP_RIGHT | BOTTOM_RIGHT
  ALL: 15     // TOP | BOTTOM
};


/**
 * CSS class name suffixes for the elements comprising the RoundedPanel.
 * @enum {string}
 * @private
 */
goog.ui.RoundedPanel.Classes_ = {
  BACKGROUND: goog.getCssName('goog-roundedpanel-background'),
  PANEL: goog.getCssName('goog-roundedpanel'),
  CONTENT: goog.getCssName('goog-roundedpanel-content')
};



/**
 * Base class for the hierarchy of RoundedPanel classes. Do not
 * instantiate directly. Instead, call goog.ui.RoundedPanel.create().
 * The HTML structure for the RoundedPanel is:
 * <pre>
 * - div (Contains the background and content. Class name: goog-roundedpanel)
 *   - div (Contains the background/rounded corners. Class name:
 *       goog-roundedpanel-bg)
 *   - div (Contains the content. Class name: goog-roundedpanel-content)
 * </pre>
 * @param {number} radius The radius of the rounded corner(s), in pixels.
 * @param {number} borderWidth The thickness of the border, in pixels.
 * @param {string} borderColor The border color of the panel.
 * @param {string=} opt_backgroundColor The background color of the panel.
 * @param {number=} opt_corners The corners of the panel to be rounded. Any
 *     corners not specified will be rendered as square corners. Will default
 *     to all square corners if not specified.
 * @param {goog.dom.DomHelper=} opt_domHelper The DOM helper object for the
 *     document we want to render in.
 * @extends {goog.ui.Component}
 * @constructor
 */
goog.ui.BaseRoundedPanel = function(
    radius, borderWidth, borderColor, opt_backgroundColor, opt_corners,
    opt_domHelper) {
  goog.ui.Component.call(this, opt_domHelper);

  /**
   * The radius of the rounded corner(s), in pixels.
   * @type {number}
   * @private
   */
  this.radius_ = radius;

  /**
   * The thickness of the border, in pixels.
   * @type {number}
   * @private
   */
  this.borderWidth_ = borderWidth;

  /**
   * The border color of the panel.
   * @type {string}
   * @private
   */
  this.borderColor_ = borderColor;

  /**
   * The background color of the panel.
   * @type {?string}
   * @private
   */
  this.backgroundColor_ = opt_backgroundColor || null;

  /**
   * The corners of the panel to be rounded; defaults to
   * goog.ui.RoundedPanel.Corner.NONE
   * @type {number}
   * @private
   */
  this.corners_ = opt_corners || goog.ui.RoundedPanel.Corner.NONE;
};
goog.inherits(goog.ui.BaseRoundedPanel, goog.ui.Component);
goog.tagUnsealableClass(goog.ui.BaseRoundedPanel);


/**
 * The element containing the rounded corners and background.
 * @type {Element}
 * @private
 */
goog.ui.BaseRoundedPanel.prototype.backgroundElement_;


/**
 * The element containing the actual content.
 * @type {Element}
 * @private
 */
goog.ui.BaseRoundedPanel.prototype.contentElement_;


/**
 * This method performs all the necessary DOM manipulation to create the panel.
 * Overrides {@link goog.ui.Component#decorateInternal}.
 * @param {Element} element The element to decorate.
 * @protected
 * @override
 */
goog.ui.BaseRoundedPanel.prototype.decorateInternal = function(element) {
  goog.ui.BaseRoundedPanel.superClass_.decorateInternal.call(this, element);
  goog.dom.classlist.add(
      goog.asserts.assert(this.getElement()),
      goog.ui.RoundedPanel.Classes_.PANEL);

  // Create backgroundElement_, and add it to the DOM.
  this.backgroundElement_ =
      this.getDomHelper().createElement(goog.dom.TagName.DIV);
  this.backgroundElement_.className = goog.ui.RoundedPanel.Classes_.BACKGROUND;
  this.getElement().appendChild(this.backgroundElement_);

  // Set contentElement_ by finding a child node within element_ with the
  // proper class name. If none exists, create it and add it to the DOM.
  this.contentElement_ = goog.dom.getElementsByTagNameAndClass(
      null, goog.ui.RoundedPanel.Classes_.CONTENT, this.getElement())[0];
  if (!this.contentElement_) {
    this.contentElement_ = this.getDomHelper().createDom(goog.dom.TagName.DIV);
    this.contentElement_.className = goog.ui.RoundedPanel.Classes_.CONTENT;
    this.getElement().appendChild(this.contentElement_);
  }
};


/** @override */
goog.ui.BaseRoundedPanel.prototype.disposeInternal = function() {
  if (this.backgroundElement_) {
    this.getDomHelper().removeNode(this.backgroundElement_);
    this.backgroundElement_ = null;
  }
  this.contentElement_ = null;
  goog.ui.BaseRoundedPanel.superClass_.disposeInternal.call(this);
};


/**
 * Returns the DOM element containing the actual content.
 * @return {Element} The element containing the actual content (null if none).
 * @override
 */
goog.ui.BaseRoundedPanel.prototype.getContentElement = function() {
  return this.contentElement_;
};



/**
 * RoundedPanel class specifically for browsers that support CSS attributes
 * for elements with rounded borders (ex. Safari 3.0+, Firefox 3.0+). Do not
 * instantiate directly. Instead, call goog.ui.RoundedPanel.create().
 * @param {number} radius The radius of the rounded corner(s), in pixels.
 * @param {number} borderWidth The thickness of the border, in pixels.
 * @param {string} borderColor The border color of the panel.
 * @param {string=} opt_backgroundColor The background color of the panel.
 * @param {number=} opt_corners The corners of the panel to be rounded. Any
 *     corners not specified will be rendered as square corners. Will
 *     default to all square corners if not specified.
 * @param {goog.dom.DomHelper=} opt_domHelper The DOM helper object for the
 *     document we want to render in.
 * @extends {goog.ui.BaseRoundedPanel}
 * @constructor
 * @final
 */
goog.ui.CssRoundedPanel = function(
    radius, borderWidth, borderColor, opt_backgroundColor, opt_corners,
    opt_domHelper) {
  goog.ui.BaseRoundedPanel.call(
      this, radius, borderWidth, borderColor, opt_backgroundColor, opt_corners,
      opt_domHelper);
};
goog.inherits(goog.ui.CssRoundedPanel, goog.ui.BaseRoundedPanel);


/**
 * This method performs all the necessary DOM manipulation to create the panel.
 * Overrides {@link goog.ui.Component#decorateInternal}.
 * @param {Element} element The element to decorate.
 * @protected
 * @override
 */
goog.ui.CssRoundedPanel.prototype.decorateInternal = function(element) {
  goog.ui.CssRoundedPanel.superClass_.decorateInternal.call(this, element);

  // Set the border width and background color, if needed.
  this.backgroundElement_.style.border =
      this.borderWidth_ + 'px solid ' + this.borderColor_;
  if (this.backgroundColor_) {
    this.backgroundElement_.style.backgroundColor = this.backgroundColor_;
  }

  // Set radii of the appropriate rounded corners.
  if (this.corners_ == goog.ui.RoundedPanel.Corner.ALL) {
    var styleName = this.getStyle_(goog.ui.RoundedPanel.Corner.ALL);
    this.backgroundElement_.style[styleName] = this.radius_ + 'px';
  } else {
    var topLeftRadius =
        this.corners_ & goog.ui.RoundedPanel.Corner.TOP_LEFT ? this.radius_ : 0;
    var cornerStyle = this.getStyle_(goog.ui.RoundedPanel.Corner.TOP_LEFT);
    this.backgroundElement_.style[cornerStyle] = topLeftRadius + 'px';
    var topRightRadius = this.corners_ & goog.ui.RoundedPanel.Corner.TOP_RIGHT ?
        this.radius_ :
        0;
    cornerStyle = this.getStyle_(goog.ui.RoundedPanel.Corner.TOP_RIGHT);
    this.backgroundElement_.style[cornerStyle] = topRightRadius + 'px';
    var bottomRightRadius =
        this.corners_ & goog.ui.RoundedPanel.Corner.BOTTOM_RIGHT ?
        this.radius_ :
        0;
    cornerStyle = this.getStyle_(goog.ui.RoundedPanel.Corner.BOTTOM_RIGHT);
    this.backgroundElement_.style[cornerStyle] = bottomRightRadius + 'px';
    var bottomLeftRadius =
        this.corners_ & goog.ui.RoundedPanel.Corner.BOTTOM_LEFT ? this.radius_ :
                                                                  0;
    cornerStyle = this.getStyle_(goog.ui.RoundedPanel.Corner.BOTTOM_LEFT);
    this.backgroundElement_.style[cornerStyle] = bottomLeftRadius + 'px';
  }
};


/**
 * This method returns the CSS style based on the corner of the panel, and the
 * user-agent.
 * @param {number} corner The corner whose style name to retrieve.
 * @private
 * @return {string} The CSS style based on the specified corner.
 */
goog.ui.CssRoundedPanel.prototype.getStyle_ = function(corner) {
  // Determine the proper corner to work with.
  var cssCorner, suffixLeft, suffixRight;
  if (goog.userAgent.WEBKIT) {
    suffixLeft = 'Left';
    suffixRight = 'Right';
  } else {
    suffixLeft = 'left';
    suffixRight = 'right';
  }
  switch (corner) {
    case goog.ui.RoundedPanel.Corner.ALL:
      cssCorner = '';
      break;
    case goog.ui.RoundedPanel.Corner.TOP_LEFT:
      cssCorner = 'Top' + suffixLeft;
      break;
    case goog.ui.RoundedPanel.Corner.TOP_RIGHT:
      cssCorner = 'Top' + suffixRight;
      break;
    case goog.ui.RoundedPanel.Corner.BOTTOM_LEFT:
      cssCorner = 'Bottom' + suffixLeft;
      break;
    case goog.ui.RoundedPanel.Corner.BOTTOM_RIGHT:
      cssCorner = 'Bottom' + suffixRight;
      break;
  }

  return goog.userAgent.WEBKIT ? 'WebkitBorder' + cssCorner + 'Radius' :
                                 'MozBorderRadius' + cssCorner;
};



/**
 * RoundedPanel class that uses goog.graphics to create the rounded corners.
 * Do not instantiate directly. Instead, call goog.ui.RoundedPanel.create().
 * @param {number} radius The radius of the rounded corner(s), in pixels.
 * @param {number} borderWidth The thickness of the border, in pixels.
 * @param {string} borderColor The border color of the panel.
 * @param {string=} opt_backgroundColor The background color of the panel.
 * @param {number=} opt_corners The corners of the panel to be rounded. Any
 *     corners not specified will be rendered as square corners. Will
 *     default to all square corners if not specified.
 * @param {goog.dom.DomHelper=} opt_domHelper The DOM helper object for the
 *     document we want to render in.
 * @extends {goog.ui.BaseRoundedPanel}
 * @constructor
 * @final
 */
goog.ui.GraphicsRoundedPanel = function(
    radius, borderWidth, borderColor, opt_backgroundColor, opt_corners,
    opt_domHelper) {
  goog.ui.BaseRoundedPanel.call(
      this, radius, borderWidth, borderColor, opt_backgroundColor, opt_corners,
      opt_domHelper);
};
goog.inherits(goog.ui.GraphicsRoundedPanel, goog.ui.BaseRoundedPanel);


/**
 * A 4-element array containing the circle centers for the arcs in the
 * bottom-left, top-left, top-right, and bottom-right corners, respectively.
 * @type {Array<goog.math.Coordinate>}
 * @private
 */
goog.ui.GraphicsRoundedPanel.prototype.arcCenters_;


/**
 * A 4-element array containing the start coordinates for rendering the arcs
 * in the bottom-left, top-left, top-right, and bottom-right corners,
 * respectively.
 * @type {Array<goog.math.Coordinate>}
 * @private
 */
goog.ui.GraphicsRoundedPanel.prototype.cornerStarts_;


/**
 * A 4-element array containing the arc end angles for the bottom-left,
 * top-left, top-right, and bottom-right corners, respectively.
 * @type {Array<number>}
 * @private
 */
goog.ui.GraphicsRoundedPanel.prototype.endAngles_;


/**
 * Graphics object for rendering the background.
 * @type {goog.graphics.AbstractGraphics}
 * @private
 */
goog.ui.GraphicsRoundedPanel.prototype.graphics_;


/**
 * A 4-element array containing the rounded corner radii for the bottom-left,
 * top-left, top-right, and bottom-right corners, respectively.
 * @type {Array<number>}
 * @private
 */
goog.ui.GraphicsRoundedPanel.prototype.radii_;


/**
 * A 4-element array containing the arc start angles for the bottom-left,
 * top-left, top-right, and bottom-right corners, respectively.
 * @type {Array<number>}
 * @private
 */
goog.ui.GraphicsRoundedPanel.prototype.startAngles_;


/**
 * Thickness constant used as an offset to help determine where to start
 * rendering.
 * @type {number}
 * @private
 */
goog.ui.GraphicsRoundedPanel.BORDER_WIDTH_FACTOR_ = 1 / 2;


/**
 * This method performs all the necessary DOM manipulation to create the panel.
 * Overrides {@link goog.ui.Component#decorateInternal}.
 * @param {Element} element The element to decorate.
 * @protected
 * @override
 */
goog.ui.GraphicsRoundedPanel.prototype.decorateInternal = function(element) {
  goog.ui.GraphicsRoundedPanel.superClass_.decorateInternal.call(this, element);

  // Calculate the points and angles for creating the rounded corners. Then
  // instantiate a Graphics object for drawing purposes.
  var elementSize = goog.style.getSize(this.getElement());
  this.calculateArcParameters_(elementSize);
  this.graphics_ = goog.graphics.createGraphics(
      /** @type {number} */ (elementSize.width),
      /** @type {number} */ (elementSize.height),
      /** @type {number} */ (elementSize.width),
      /** @type {number} */ (elementSize.height), this.getDomHelper());
  this.graphics_.createDom();

  // Create the path, starting from the bottom-right corner, moving clockwise.
  // End with the top-right corner.
  var path = new goog.graphics.Path();
  for (var i = 0; i < 4; i++) {
    if (this.radii_[i]) {
      // If radius > 0, draw an arc, moving to the first point and drawing
      // a line to the others.
      var cx = this.arcCenters_[i].x;
      var cy = this.arcCenters_[i].y;
      var rx = this.radii_[i];
      var ry = rx;
      var fromAngle = this.startAngles_[i];
      var extent = this.endAngles_[i] - fromAngle;
      var startX = cx + goog.math.angleDx(fromAngle, rx);
      var startY = cy + goog.math.angleDy(fromAngle, ry);
      if (i > 0) {
        var currentPoint = path.getCurrentPoint();
        if (!currentPoint || startX != currentPoint[0] ||
            startY != currentPoint[1]) {
          path.lineTo(startX, startY);
        }
      } else {
        path.moveTo(startX, startY);
      }
      path.arcTo(rx, ry, fromAngle, extent);
    } else if (i == 0) {
      // If we're just starting out (ie. i == 0), move to the starting point.
      path.moveTo(this.cornerStarts_[i].x, this.cornerStarts_[i].y);
    } else {
      // Otherwise, draw a line to the starting point.
      path.lineTo(this.cornerStarts_[i].x, this.cornerStarts_[i].y);
    }
  }

  // Close the path, create a stroke object, and fill the enclosed area, if
  // needed. Then render the path.
  path.close();
  var stroke = this.borderWidth_ ?
      new goog.graphics.Stroke(this.borderWidth_, this.borderColor_) :
      null;
  var fill = this.backgroundColor_ ?
      new goog.graphics.SolidFill(this.backgroundColor_, 1) :
      null;
  this.graphics_.drawPath(path, stroke, fill);
  this.graphics_.render(this.backgroundElement_);
};


/** @override */
goog.ui.GraphicsRoundedPanel.prototype.disposeInternal = function() {
  goog.ui.GraphicsRoundedPanel.superClass_.disposeInternal.call(this);
  this.graphics_.dispose();
  delete this.graphics_;
  delete this.radii_;
  delete this.cornerStarts_;
  delete this.arcCenters_;
  delete this.startAngles_;
  delete this.endAngles_;
};


/**
 * Calculates the start coordinates, circle centers, and angles, for the rounded
 * corners at each corner of the panel.
 * @param {goog.math.Size} elementSize The size of element_.
 * @private
 */
goog.ui.GraphicsRoundedPanel.prototype.calculateArcParameters_ = function(
    elementSize) {
  // Initialize the arrays containing the key points and angles.
  this.radii_ = [];
  this.cornerStarts_ = [];
  this.arcCenters_ = [];
  this.startAngles_ = [];
  this.endAngles_ = [];

  // Set the start points, circle centers, and angles for the bottom-right,
  // bottom-left, top-left and top-right corners, in that order.
  var angleInterval = 90;
  var borderWidthOffset =
      this.borderWidth_ * goog.ui.GraphicsRoundedPanel.BORDER_WIDTH_FACTOR_;
  var radius, xStart, yStart, xCenter, yCenter, startAngle, endAngle;
  for (var i = 0; i < 4; i++) {
    var corner = Math.pow(2, i);  // Determines which corner we're dealing with.
    var isLeft = corner & goog.ui.RoundedPanel.Corner.LEFT;
    var isTop = corner & goog.ui.RoundedPanel.Corner.TOP;

    // Calculate the radius and the start coordinates.
    radius = corner & this.corners_ ? this.radius_ : 0;
    switch (corner) {
      case goog.ui.RoundedPanel.Corner.BOTTOM_LEFT:
        xStart = borderWidthOffset + radius;
        yStart = elementSize.height - borderWidthOffset;
        break;
      case goog.ui.RoundedPanel.Corner.TOP_LEFT:
        xStart = borderWidthOffset;
        yStart = radius + borderWidthOffset;
        break;
      case goog.ui.RoundedPanel.Corner.TOP_RIGHT:
        xStart = elementSize.width - radius - borderWidthOffset;
        yStart = borderWidthOffset;
        break;
      case goog.ui.RoundedPanel.Corner.BOTTOM_RIGHT:
        xStart = elementSize.width - borderWidthOffset;
        yStart = elementSize.height - radius - borderWidthOffset;
        break;
    }

    // Calculate the circle centers and start/end angles.
    xCenter = isLeft ? radius + borderWidthOffset :
                       elementSize.width - radius - borderWidthOffset;
    yCenter = isTop ? radius + borderWidthOffset :
                      elementSize.height - radius - borderWidthOffset;
    startAngle = angleInterval * i;
    endAngle = startAngle + angleInterval;

    // Append the radius, angles, and coordinates to their arrays.
    this.radii_[i] = radius;
    this.cornerStarts_[i] = new goog.math.Coordinate(xStart, yStart);
    this.arcCenters_[i] = new goog.math.Coordinate(xCenter, yCenter);
    this.startAngles_[i] = startAngle;
    this.endAngles_[i] = endAngle;
  }
};
