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

/**
 * @fileoverview Base class for control renderers.
 * TODO(attila):  If the renderer framework works well, pull it into Component.
 */

goog.provide('goog.ui.ControlRenderer');

goog.require('goog.a11y.aria');
goog.require('goog.a11y.aria.Role');
goog.require('goog.a11y.aria.State');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.dom.classlist');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.style');
goog.require('goog.ui.Component');
goog.require('goog.ui.ControlContent');
goog.require('goog.userAgent');  // circular
goog.requireType('goog.ui.Control');



/**
 * Default renderer for {@link goog.ui.Control}s.  Can be used as-is, but
 * subclasses of Control will probably want to use renderers specifically
 * tailored for them by extending this class.  Controls that use renderers
 * delegate one or more of the following API methods to the renderer:
 * <ul>
 *    <li>`createDom` - renders the DOM for the component
 *    <li>`canDecorate` - determines whether an element can be decorated
 *        by the component
 *    <li>`decorate` - decorates an existing element with the component
 *    <li>`setState` - updates the appearance of the component based on
 *        its state
 *    <li>`getContent` - returns the component's content
 *    <li>`setContent` - sets the component's content
 * </ul>
 * Controls are stateful; renderers, on the other hand, should be stateless and
 * reusable.
 * @constructor
 */
goog.ui.ControlRenderer = function() {};
goog.addSingletonGetter(goog.ui.ControlRenderer);


/**
 * Constructs a new renderer and sets the CSS class that the renderer will use
 * as the base CSS class to apply to all elements rendered by that renderer.
 * An example to use this function using a color palette:
 *
 * <pre>
 * var myCustomRenderer = goog.ui.ControlRenderer.getCustomRenderer(
 *     goog.ui.PaletteRenderer, 'my-special-palette');
 * var newColorPalette = new goog.ui.ColorPalette(
 *     colors, myCustomRenderer, opt_domHelper);
 * </pre>
 *
 * Your CSS can look like this now:
 * <pre>
 * .my-special-palette { }
 * .my-special-palette-table { }
 * .my-special-palette-cell { }
 * etc.
 * </pre>
 *
 * <em>instead</em> of
 * <pre>
 * .CSS_MY_SPECIAL_PALETTE .goog-palette { }
 * .CSS_MY_SPECIAL_PALETTE .goog-palette-table { }
 * .CSS_MY_SPECIAL_PALETTE .goog-palette-cell { }
 * etc.
 * </pre>
 *
 * You would want to use this functionality when you want an instance of a
 * component to have specific styles different than the other components of the
 * same type in your application.  This avoids using descendant selectors to
 * apply the specific styles to this component.
 *
 * @param {Function} ctor The constructor of the renderer you are trying to
 *     create.
 * @param {string} cssClassName The name of the CSS class for this renderer.
 * @return {goog.ui.ControlRenderer} An instance of the desired renderer with
 *     its getCssClass() method overridden to return the supplied custom CSS
 *     class name.
 */
goog.ui.ControlRenderer.getCustomRenderer = function(ctor, cssClassName) {
  'use strict';
  var renderer = new ctor();

  /**
   * Returns the CSS class to be applied to the root element of components
   * rendered using this renderer.
   * @return {string} Renderer-specific CSS class.
   */
  renderer.getCssClass = function() {
    'use strict';
    return cssClassName;
  };

  return renderer;
};


/**
 * Default CSS class to be applied to the root element of components rendered
 * by this renderer.
 * @type {string}
 */
goog.ui.ControlRenderer.CSS_CLASS = goog.getCssName('goog-control');


/**
 * Array of arrays of CSS classes that we want composite classes added and
 * removed for in IE6 and lower as a workaround for lack of multi-class CSS
 * selector support.
 *
 * Subclasses that have accompanying CSS requiring this workaround should define
 * their own static IE6_CLASS_COMBINATIONS constant and override
 * getIe6ClassCombinations to return it.
 *
 * For example, if your stylesheet uses the selector .button.collapse-left
 * (and is compiled to .button_collapse-left for the IE6 version of the
 * stylesheet,) you should include ['button', 'collapse-left'] in this array
 * and the class button_collapse-left will be applied to the root element
 * whenever both button and collapse-left are applied individually.
 *
 * Members of each class name combination will be joined with underscores in the
 * order that they're defined in the array. You should alphabetize them (for
 * compatibility with the CSS compiler) unless you are doing something special.
 * @type {Array<Array<string>>}
 */
goog.ui.ControlRenderer.IE6_CLASS_COMBINATIONS = [];


/**
 * Map of component states to corresponding ARIA attributes.  Since the mapping
 * of component states to ARIA attributes is neither component- nor
 * renderer-specific, this is a static property of the renderer class, and is
 * initialized on first use.
 * @type {Object<goog.ui.Component.State, goog.a11y.aria.State>}
 * @private
 */
goog.ui.ControlRenderer.ariaAttributeMap_;


/**
 * Map of certain ARIA states to ARIA roles that support them. Used for checked
 * and selected Component states because they are used on Components with ARIA
 * roles that do not support the corresponding ARIA state.
 * @private {!Object<goog.a11y.aria.Role, goog.a11y.aria.State>}
 * @const
 */
goog.ui.ControlRenderer.TOGGLE_ARIA_STATE_MAP_ = goog.object.create(
    goog.a11y.aria.Role.BUTTON, goog.a11y.aria.State.PRESSED,
    goog.a11y.aria.Role.CHECKBOX, goog.a11y.aria.State.CHECKED,
    goog.a11y.aria.Role.MENU_ITEM, goog.a11y.aria.State.SELECTED,
    goog.a11y.aria.Role.MENU_ITEM_CHECKBOX, goog.a11y.aria.State.CHECKED,
    goog.a11y.aria.Role.MENU_ITEM_RADIO, goog.a11y.aria.State.CHECKED,
    goog.a11y.aria.Role.RADIO, goog.a11y.aria.State.CHECKED,
    goog.a11y.aria.Role.TAB, goog.a11y.aria.State.SELECTED,
    goog.a11y.aria.Role.TREEITEM, goog.a11y.aria.State.SELECTED);


/**
 * Returns the ARIA role to be applied to the control.
 * See http://wiki/Main/ARIA for more info.
 * @return {goog.a11y.aria.Role|undefined} ARIA role.
 */
goog.ui.ControlRenderer.prototype.getAriaRole = function() {
  'use strict';
  // By default, the ARIA role is unspecified.
  return undefined;
};


/**
 * Returns the control's contents wrapped in a DIV, with the renderer's own
 * CSS class and additional state-specific classes applied to it.
 * @param {goog.ui.Control} control Control to render.
 * @return {Element} Root element for the control.
 */
goog.ui.ControlRenderer.prototype.createDom = function(control) {
  'use strict';
  // Create and return DIV wrapping contents.
  var element = control.getDomHelper().createDom(
      goog.dom.TagName.DIV, this.getClassNames(control).join(' '),
      control.getContent());

  return element;
};


/**
 * Takes the control's root element and returns the parent element of the
 * control's contents.  Since by default controls are rendered as a single
 * DIV, the default implementation returns the element itself.  Subclasses
 * with more complex DOM structures must override this method as needed.
 * @param {Element} element Root element of the control whose content element
 *     is to be returned.
 * @return {Element} The control's content element.
 */
goog.ui.ControlRenderer.prototype.getContentElement = function(element) {
  'use strict';
  return element;
};


/**
 * Updates the control's DOM by adding or removing the specified class name
 * to/from its root element. May add additional combined classes as needed in
 * IE6 and lower. Because of this, subclasses should use this method when
 * modifying class names on the control's root element.
 * @param {goog.ui.Control|Element} control Control instance (or root element)
 *     to be updated.
 * @param {string} className CSS class name to add or remove.
 * @param {boolean} enable Whether to add or remove the class name.
 */
goog.ui.ControlRenderer.prototype.enableClassName = function(
    control, className, enable) {
  'use strict';
  /** @suppress {strictMissingProperties} Added to tighten compiler checks */
  var element = /** @type {Element} */ (
      control.getElement ? control.getElement() : control);
  if (element) {
    var classNames = [className];

    goog.dom.classlist.enableAll(element, classNames, enable);
  }
};


/**
 * Updates the control's DOM by adding or removing the specified extra class
 * name to/from its element.
 * @param {goog.ui.Control} control Control to be updated.
 * @param {string} className CSS class name to add or remove.
 * @param {boolean} enable Whether to add or remove the class name.
 */
goog.ui.ControlRenderer.prototype.enableExtraClassName = function(
    control, className, enable) {
  'use strict';
  // The base class implementation is trivial; subclasses should override as
  // needed.
  this.enableClassName(control, className, enable);
};


/**
 * Returns true if this renderer can decorate the element, false otherwise.
 * The default implementation always returns true.
 * @param {Element} element Element to decorate.
 * @return {boolean} Whether the renderer can decorate the element.
 */
goog.ui.ControlRenderer.prototype.canDecorate = function(element) {
  'use strict';
  return true;
};


/**
 * Default implementation of `decorate` for {@link goog.ui.Control}s.
 * Initializes the control's ID, content, and state based on the ID of the
 * element, its child nodes, and its CSS classes, respectively.  Returns the
 * element.
 * @param {goog.ui.Control} control Control instance to decorate the element.
 * @param {Element} element Element to decorate.
 * @return {Element} Decorated element.
 */
goog.ui.ControlRenderer.prototype.decorate = function(control, element) {
  'use strict';
  // Set the control's ID to the decorated element's DOM ID, if any.
  if (element.id) {
    control.setId(element.id);
  }

  // Set the control's content to the decorated element's content.
  var contentElem = this.getContentElement(element);
  if (contentElem && contentElem.firstChild) {
    control.setContentInternal(
        contentElem.firstChild.nextSibling ?
            goog.array.clone(contentElem.childNodes) :
            contentElem.firstChild);
  } else {
    control.setContentInternal(null);
  }

  // Initialize the control's state based on the decorated element's CSS class.
  // This implementation is optimized to minimize object allocations, string
  // comparisons, and DOM access.
  var state = 0x00;
  var rendererClassName = this.getCssClass();
  var structuralClassName = this.getStructuralCssClass();
  var hasRendererClassName = false;
  var hasStructuralClassName = false;
  var hasCombinedClassName = false;
  var classNames = goog.array.toArray(goog.dom.classlist.get(element));
  classNames.forEach(function(className) {
    'use strict';
    if (!hasRendererClassName && className == rendererClassName) {
      hasRendererClassName = true;
      if (structuralClassName == rendererClassName) {
        hasStructuralClassName = true;
      }
    } else if (!hasStructuralClassName && className == structuralClassName) {
      hasStructuralClassName = true;
    } else {
      state |= this.getStateFromClass(className);
    }
    if (this.getStateFromClass(className) == goog.ui.Component.State.DISABLED) {
      goog.asserts.assertElement(contentElem);
      if (goog.dom.isFocusableTabIndex(contentElem)) {
        goog.dom.setFocusableTabIndex(contentElem, false);
      }
    }
  }, this);
  control.setStateInternal(state);

  // Make sure the element has the renderer's CSS classes applied, as well as
  // any extra class names set on the control.
  if (!hasRendererClassName) {
    classNames.push(rendererClassName);
    if (structuralClassName == rendererClassName) {
      hasStructuralClassName = true;
    }
  }
  if (!hasStructuralClassName) {
    classNames.push(structuralClassName);
  }
  var extraClassNames = control.getExtraClassNames();
  if (extraClassNames) {
    classNames.push.apply(classNames, extraClassNames);
  }

  // Only write to the DOM if new class names had to be added to the element.
  if (!hasRendererClassName || !hasStructuralClassName || extraClassNames ||
      hasCombinedClassName) {
    goog.dom.classlist.set(element, classNames.join(' '));
  }

  return element;
};


/**
 * Initializes the control's DOM by configuring properties that can only be set
 * after the DOM has entered the document.  This implementation sets up BiDi
 * and keyboard focus.  Called from {@link goog.ui.Control#enterDocument}.
 * @param {goog.ui.Control} control Control whose DOM is to be initialized
 *     as it enters the document.
 */
goog.ui.ControlRenderer.prototype.initializeDom = function(control) {
  'use strict';
  // Initialize render direction (BiDi).  We optimize the left-to-right render
  // direction by assuming that elements are left-to-right by default, and only
  // updating their styling if they are explicitly set to right-to-left.
  if (control.isRightToLeft()) {
    this.setRightToLeft(control.getElement(), true);
  }

  // Initialize keyboard focusability (tab index).  We assume that components
  // aren't focusable by default (i.e have no tab index), and only touch the
  // DOM if the component is focusable, enabled, and visible, and therefore
  // needs a tab index.
  if (control.isEnabled()) {
    this.setFocusable(control, control.isVisible());
  }
};


/**
 * Sets the element's ARIA role.
 * @param {Element} element Element to update.
 * @param {?goog.a11y.aria.Role=} opt_preferredRole The preferred ARIA role.
 */
goog.ui.ControlRenderer.prototype.setAriaRole = function(
    element, opt_preferredRole) {
  'use strict';
  var ariaRole = opt_preferredRole || this.getAriaRole();
  if (ariaRole) {
    goog.asserts.assert(
        element, 'The element passed as a first parameter cannot be null.');
    var currentRole = goog.a11y.aria.getRole(element);
    if (ariaRole == currentRole) {
      return;
    }
    goog.a11y.aria.setRole(element, ariaRole);
  }
};


/**
 * Sets the element's ARIA attributes, including distinguishing between
 * universally supported ARIA properties and ARIA states that are only
 * supported by certain ARIA roles. Only attributes which are initialized to be
 * true will be set.
 * @param {!goog.ui.Control} control Control whose ARIA state will be updated.
 * @param {!Element} element Element whose ARIA state is to be updated.
 */
goog.ui.ControlRenderer.prototype.setAriaStates = function(control, element) {
  'use strict';
  goog.asserts.assert(control);
  goog.asserts.assert(element);

  var ariaLabel = control.getAriaLabel();
  if (ariaLabel != null) {
    this.setAriaLabel(element, ariaLabel);
  }

  if (!control.isVisible()) {
    goog.a11y.aria.setState(
        element, goog.a11y.aria.State.HIDDEN, !control.isVisible());
  }
  if (!control.isEnabled()) {
    this.updateAriaState(
        element, goog.ui.Component.State.DISABLED, !control.isEnabled());
  }
  if (control.isSupportedState(goog.ui.Component.State.SELECTED)) {
    this.updateAriaState(
        element, goog.ui.Component.State.SELECTED, control.isSelected());
  }
  if (control.isSupportedState(goog.ui.Component.State.CHECKED)) {
    this.updateAriaState(
        element, goog.ui.Component.State.CHECKED, control.isChecked());
  }
  if (control.isSupportedState(goog.ui.Component.State.OPENED)) {
    this.updateAriaState(
        element, goog.ui.Component.State.OPENED, control.isOpen());
  }
};


/**
 * Sets the element's ARIA label. This should be overriden by subclasses that
 * don't apply the role directly on control.element_.
 * @param {!Element} element Element whose ARIA label is to be updated.
 * @param {string} ariaLabel Label to add to the element.
 */
goog.ui.ControlRenderer.prototype.setAriaLabel = function(element, ariaLabel) {
  'use strict';
  goog.a11y.aria.setLabel(element, ariaLabel);
};


/**
 * Allows or disallows text selection within the control's DOM.
 * @param {Element} element The control's root element.
 * @param {boolean} allow Whether the element should allow text selection.
 */
goog.ui.ControlRenderer.prototype.setAllowTextSelection = function(
    element, allow) {
  'use strict';
  // On all browsers other than IE and Opera, it isn't necessary to recursively
  // apply unselectable styling to the element's children.
  goog.style.setUnselectable(element, !allow, !goog.userAgent.IE);
};


/**
 * Applies special styling to/from the control's element if it is rendered
 * right-to-left, and removes it if it is rendered left-to-right.
 * @param {Element} element The control's root element.
 * @param {boolean} rightToLeft Whether the component is rendered
 *     right-to-left.
 */
goog.ui.ControlRenderer.prototype.setRightToLeft = function(
    element, rightToLeft) {
  'use strict';
  this.enableClassName(
      element, goog.getCssName(this.getStructuralCssClass(), 'rtl'),
      rightToLeft);
};


/**
 * Returns true if the control's key event target supports keyboard focus
 * (based on its `tabIndex` attribute), false otherwise.
 * @param {goog.ui.Control} control Control whose key event target is to be
 *     checked.
 * @return {boolean} Whether the control's key event target is focusable.
 */
goog.ui.ControlRenderer.prototype.isFocusable = function(control) {
  'use strict';
  var keyTarget;
  if (control.isSupportedState(goog.ui.Component.State.FOCUSED) &&
      (keyTarget = control.getKeyEventTarget())) {
    return goog.dom.isFocusableTabIndex(keyTarget);
  }
  return false;
};


/**
 * Updates the control's key event target to make it focusable or non-focusable
 * via its `tabIndex` attribute.  Does nothing if the control doesn't
 * support the `FOCUSED` state, or if it has no key event target.
 * @param {goog.ui.Control} control Control whose key event target is to be
 *     updated.
 * @param {boolean} focusable Whether to enable keyboard focus support on the
 *     control's key event target.
 */
goog.ui.ControlRenderer.prototype.setFocusable = function(control, focusable) {
  'use strict';
  var keyTarget;
  if (control.isSupportedState(goog.ui.Component.State.FOCUSED) &&
      (keyTarget = control.getKeyEventTarget())) {
    if (!focusable && control.isFocused()) {
      // Blur before hiding.  Note that IE calls onblur handlers asynchronously.
      try {
        keyTarget.blur();
      } catch (e) {
        // TODO(user):  Find out why this fails on IE.
      }
      // The blur event dispatched by the key event target element when blur()
      // was called on it should have been handled by the control's handleBlur()
      // method, so at this point the control should no longer be focused.
      // However, blur events are unreliable on IE and FF3, so if at this point
      // the control is still focused, we trigger its handleBlur() method
      // programmatically.
      if (control.isFocused()) {
        control.handleBlur(null);
      }
    }
    // Don't overwrite existing tab index values unless needed.
    if (goog.dom.isFocusableTabIndex(keyTarget) != focusable) {
      goog.dom.setFocusableTabIndex(keyTarget, focusable);
    }
  }
};


/**
 * Shows or hides the element.
 * @param {Element} element Element to update.
 * @param {boolean} visible Whether to show the element.
 */
goog.ui.ControlRenderer.prototype.setVisible = function(element, visible) {
  'use strict';
  // The base class implementation is trivial; subclasses should override as
  // needed.  It should be possible to do animated reveals, for example.
  goog.style.setElementShown(element, visible);
  if (element) {
    goog.a11y.aria.setState(element, goog.a11y.aria.State.HIDDEN, !visible);
  }
};


/**
 * Updates the appearance of the control in response to a state change.
 * @param {goog.ui.Control} control Control instance to update.
 * @param {goog.ui.Component.State} state State to enable or disable.
 * @param {boolean} enable Whether the control is entering or exiting the state.
 */
goog.ui.ControlRenderer.prototype.setState = function(control, state, enable) {
  'use strict';
  var element = control.getElement();
  if (element) {
    var className = this.getClassForState(state);
    if (className) {
      this.enableClassName(control, className, enable);
    }
    this.updateAriaState(element, state, enable);
  }
};


/**
 * Updates the element's ARIA (accessibility) attributes , including
 * distinguishing between universally supported ARIA properties and ARIA states
 * that are only supported by certain ARIA roles.
 * @param {Element} element Element whose ARIA state is to be updated.
 * @param {goog.ui.Component.State} state Component state being enabled or
 *     disabled.
 * @param {boolean} enable Whether the state is being enabled or disabled.
 * @protected
 */
goog.ui.ControlRenderer.prototype.updateAriaState = function(
    element, state, enable) {
  'use strict';
  // Ensure the ARIA attribute map exists.
  if (!goog.ui.ControlRenderer.ariaAttributeMap_) {
    goog.ui.ControlRenderer.ariaAttributeMap_ = goog.object.create(
        goog.ui.Component.State.DISABLED, goog.a11y.aria.State.DISABLED,
        goog.ui.Component.State.SELECTED, goog.a11y.aria.State.SELECTED,
        goog.ui.Component.State.CHECKED, goog.a11y.aria.State.CHECKED,
        goog.ui.Component.State.OPENED, goog.a11y.aria.State.EXPANDED);
  }
  goog.asserts.assert(
      element, 'The element passed as a first parameter cannot be null.');
  var ariaAttr = goog.ui.ControlRenderer.getAriaStateForAriaRole_(
      element, goog.ui.ControlRenderer.ariaAttributeMap_[state]);
  if (ariaAttr) {
    goog.a11y.aria.setState(element, ariaAttr, enable);
  }
};


/**
 * Returns the appropriate ARIA attribute based on ARIA role if the ARIA
 * attribute is an ARIA state.
 * @param {!Element} element The element from which to get the ARIA role for
 * matching ARIA state.
 * @param {goog.a11y.aria.State} attr The ARIA attribute to check to see if it
 * can be applied to the given ARIA role.
 * @return {goog.a11y.aria.State} An ARIA attribute that can be applied to the
 * given ARIA role.
 * @private
 */
goog.ui.ControlRenderer.getAriaStateForAriaRole_ = function(element, attr) {
  'use strict';
  var role = goog.a11y.aria.getRole(element);
  if (!role) {
    return attr;
  }
  role = /** @type {goog.a11y.aria.Role} */ (role);
  var matchAttr = goog.ui.ControlRenderer.TOGGLE_ARIA_STATE_MAP_[role] || attr;
  return goog.ui.ControlRenderer.isAriaState_(attr) ? matchAttr : attr;
};


/**
 * Determines if the given ARIA attribute is an ARIA property or ARIA state.
 * @param {goog.a11y.aria.State} attr The ARIA attribute to classify.
 * @return {boolean} If the ARIA attribute is an ARIA state.
 * @private
 */
goog.ui.ControlRenderer.isAriaState_ = function(attr) {
  'use strict';
  return attr == goog.a11y.aria.State.CHECKED ||
      attr == goog.a11y.aria.State.SELECTED;
};


/**
 * Takes a control's root element, and sets its content to the given text
 * caption or DOM structure.  The default implementation replaces the children
 * of the given element.  Renderers that create more complex DOM structures
 * must override this method accordingly.
 * @param {Element} element The control's root element.
 * @param {goog.ui.ControlContent} content Text caption or DOM structure to be
 *     set as the control's content. The DOM nodes will not be cloned, they
 *     will only moved under the content element of the control.
 */
goog.ui.ControlRenderer.prototype.setContent = function(element, content) {
  'use strict';
  var contentElem = this.getContentElement(element);
  if (contentElem) {
    goog.dom.removeChildren(contentElem);
    if (content) {
      if (typeof content === 'string') {
        goog.dom.setTextContent(contentElem, content);
      } else {
        var childHandler = function(child) {
          'use strict';
          if (child) {
            var doc = goog.dom.getOwnerDocument(contentElem);
            contentElem.appendChild(
                typeof child === 'string' ? doc.createTextNode(child) : child);
          }
        };
        if (Array.isArray(content)) {
          // Array of nodes.
          content.forEach(childHandler);
        } else if (goog.isArrayLike(content) && !('nodeType' in content)) {
          // NodeList. The second condition filters out TextNode which also has
          // length attribute but is not array like. The nodes have to be cloned
          // because childHandler removes them from the list during iteration.
          goog.array.clone(/** @type {!NodeList<?>} */ (content))
              .forEach(childHandler);
        } else {
          // Node or string.
          childHandler(content);
        }
      }
    }
  }
};


/**
 * Returns the element within the component's DOM that should receive keyboard
 * focus (null if none).  The default implementation returns the control's root
 * element.
 * @param {goog.ui.Control} control Control whose key event target is to be
 *     returned.
 * @return {Element} The key event target.
 */
goog.ui.ControlRenderer.prototype.getKeyEventTarget = function(control) {
  'use strict';
  return control.getElement();
};


// CSS class name management.


/**
 * Returns the CSS class name to be applied to the root element of all
 * components rendered or decorated using this renderer.  The class name
 * is expected to uniquely identify the renderer class, i.e. no two
 * renderer classes are expected to share the same CSS class name.
 * @return {string} Renderer-specific CSS class name.
 */
goog.ui.ControlRenderer.prototype.getCssClass = function() {
  'use strict';
  return goog.ui.ControlRenderer.CSS_CLASS;
};


/**
 * Returns an array of combinations of classes to apply combined class names for
 * in IE6 and below. See {@link IE6_CLASS_COMBINATIONS} for more detail. This
 * method doesn't reference {@link IE6_CLASS_COMBINATIONS} so that it can be
 * compiled out, but subclasses should return their IE6_CLASS_COMBINATIONS
 * static constant instead.
 * @return {!Array<Array<string>>} Array of class name combinations.
 */
goog.ui.ControlRenderer.prototype.getIe6ClassCombinations = function() {
  'use strict';
  return [];
};


/**
 * Returns the name of a DOM structure-specific CSS class to be applied to the
 * root element of all components rendered or decorated using this renderer.
 * Unlike the class name returned by {@link #getCssClass}, the structural class
 * name may be shared among different renderers that generate similar DOM
 * structures.  The structural class name also serves as the basis of derived
 * class names used to identify and style structural elements of the control's
 * DOM, as well as the basis for state-specific class names.  The default
 * implementation returns the same class name as {@link #getCssClass}, but
 * subclasses are expected to override this method as needed.
 * @return {string} DOM structure-specific CSS class name (same as the renderer-
 *     specific CSS class name by default).
 */
goog.ui.ControlRenderer.prototype.getStructuralCssClass = function() {
  'use strict';
  return this.getCssClass();
};


/**
 * Returns all CSS class names applicable to the given control, based on its
 * state.  The return value is an array of strings containing
 * <ol>
 *   <li>the renderer-specific CSS class returned by {@link #getCssClass},
 *       followed by
 *   <li>the structural CSS class returned by {@link getStructuralCssClass} (if
 *       different from the renderer-specific CSS class), followed by
 *   <li>any state-specific classes returned by {@link #getClassNamesForState},
 *       followed by
 *   <li>any extra classes returned by the control's `getExtraClassNames`
 *       method and
 *   <li>for IE6 and lower, additional combined classes from
 *       {@link getAppliedCombinedClassNames_}.
 * </ol>
 * Since all controls have at least one renderer-specific CSS class name, this
 * method is guaranteed to return an array of at least one element.
 * @param {goog.ui.Control} control Control whose CSS classes are to be
 *     returned.
 * @return {!Array<string>} Array of CSS class names applicable to the control.
 * @protected
 */
goog.ui.ControlRenderer.prototype.getClassNames = function(control) {
  'use strict';
  var cssClass = this.getCssClass();

  // Start with the renderer-specific class name.
  var classNames = [cssClass];

  // Add structural class name, if different.
  var structuralCssClass = this.getStructuralCssClass();
  if (structuralCssClass != cssClass) {
    classNames.push(structuralCssClass);
  }

  // Add state-specific class names, if any.
  var classNamesForState = this.getClassNamesForState(control.getState());
  classNames.push.apply(classNames, classNamesForState);

  // Add extra class names, if any.
  var extraClassNames = control.getExtraClassNames();
  if (extraClassNames) {
    classNames.push.apply(classNames, extraClassNames);
  }

  return classNames;
};


/**
 * Returns an array of all the combined class names that should be applied based
 * on the given list of classes. Checks the result of
 * {@link getIe6ClassCombinations} for any combinations that have all
 * members contained in classes. If a combination matches, the members are
 * joined with an underscore (in order), and added to the return array.
 *
 * If opt_includedClass is provided, return only the combined classes that have
 * all members contained in classes AND include opt_includedClass as well.
 * opt_includedClass is added to classes as well.
 * @param {IArrayLike<string>} classes Array-like thing of classes to
 *     return matching combined classes for.
 * @param {?string=} opt_includedClass If provided, get only the combined
 *     classes that include this one.
 * @return {!Array<string>} Array of combined class names that should be
 *     applied.
 * @private
 */
goog.ui.ControlRenderer.prototype.getAppliedCombinedClassNames_ = function(
    classes, opt_includedClass) {
  'use strict';
  var toAdd = [];
  if (opt_includedClass) {
    classes = [].concat(classes, [opt_includedClass]);
  }
  this.getIe6ClassCombinations().forEach(function(combo) {
    'use strict';
    if (goog.array.every(combo, goog.partial(goog.array.contains, classes)) &&
        (!opt_includedClass || goog.array.contains(combo, opt_includedClass))) {
      toAdd.push(combo.join('_'));
    }
  });
  return toAdd;
};


/**
 * Takes a bit mask of {@link goog.ui.Component.State}s, and returns an array
 * of the appropriate class names representing the given state, suitable to be
 * applied to the root element of a component rendered using this renderer, or
 * null if no state-specific classes need to be applied.  This default
 * implementation uses the renderer's {@link getClassForState} method to
 * generate each state-specific class.
 * @param {number} state Bit mask of component states.
 * @return {!Array<string>} Array of CSS class names representing the given
 *     state.
 * @protected
 */
goog.ui.ControlRenderer.prototype.getClassNamesForState = function(state) {
  'use strict';
  var classNames = [];
  while (state) {
    // For each enabled state, push the corresponding CSS class name onto
    // the classNames array.
    var mask = state & -state;  // Least significant bit
    classNames.push(
        this.getClassForState(
            /** @type {goog.ui.Component.State} */ (mask)));
    state &= ~mask;
  }
  return classNames;
};


/**
 * Takes a single {@link goog.ui.Component.State}, and returns the
 * corresponding CSS class name (null if none).
 * @param {goog.ui.Component.State} state Component state.
 * @return {string|undefined} CSS class representing the given state (undefined
 *     if none).
 * @protected
 */
goog.ui.ControlRenderer.prototype.getClassForState = function(state) {
  'use strict';
  if (!this.classByState_) {
    this.createClassByStateMap_();
  }
  return this.classByState_[state];
};


/**
 * Takes a single CSS class name which may represent a component state, and
 * returns the corresponding component state (0x00 if none).
 * @param {string} className CSS class name, possibly representing a component
 *     state.
 * @return {goog.ui.Component.State} state Component state corresponding
 *     to the given CSS class (0x00 if none).
 * @protected
 */
goog.ui.ControlRenderer.prototype.getStateFromClass = function(className) {
  'use strict';
  if (!this.stateByClass_) {
    this.createStateByClassMap_();
  }
  var state = parseInt(this.stateByClass_[className], 10);
  return /** @type {goog.ui.Component.State} */ (isNaN(state) ? 0x00 : state);
};


/**
 * Creates the lookup table of states to classes, used during state changes.
 * @private
 */
goog.ui.ControlRenderer.prototype.createClassByStateMap_ = function() {
  'use strict';
  var baseClass = this.getStructuralCssClass();

  // This ensures space-separated css classnames are not allowed, which some
  // ControlRenderers had been doing.  See http://b/13694665.
  var isValidClassName =
      !goog.string.contains(goog.string.normalizeWhitespace(baseClass), ' ');
  goog.asserts.assert(
      isValidClassName,
      'ControlRenderer has an invalid css class: \'' + baseClass + '\'');

  /**
   * Map of component states to state-specific structural class names,
   * used when changing the DOM in response to a state change.  Precomputed
   * and cached on first use to minimize object allocations and string
   * concatenation.
   * @type {Object}
   * @private
   */
  this.classByState_ = goog.object.create(
      goog.ui.Component.State.DISABLED, goog.getCssName(baseClass, 'disabled'),
      goog.ui.Component.State.HOVER, goog.getCssName(baseClass, 'hover'),
      goog.ui.Component.State.ACTIVE, goog.getCssName(baseClass, 'active'),
      goog.ui.Component.State.SELECTED, goog.getCssName(baseClass, 'selected'),
      goog.ui.Component.State.CHECKED, goog.getCssName(baseClass, 'checked'),
      goog.ui.Component.State.FOCUSED, goog.getCssName(baseClass, 'focused'),
      goog.ui.Component.State.OPENED, goog.getCssName(baseClass, 'open'));
};


/**
 * Creates the lookup table of classes to states, used during decoration.
 * @private
 */
goog.ui.ControlRenderer.prototype.createStateByClassMap_ = function() {
  'use strict';
  // We need the classByState_ map so we can transpose it.
  if (!this.classByState_) {
    this.createClassByStateMap_();
  }

  /**
   * Map of state-specific structural class names to component states,
   * used during element decoration.  Precomputed and cached on first use
   * to minimize object allocations and string concatenation.
   * @type {Object}
   * @private
   */
  this.stateByClass_ = goog.object.transpose(this.classByState_);
};
