/**
 * @license
 * Copyright 2019 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Utilities for element styles.
 * These methods are not specific to Blockly, and could be factored out into
 * a JavaScript framework such as Closure.
 * @author samelh@google.com (Sam El-Husseini)
 */
'use strict';

/**
 * @name Blockly.utils.style
 * @namespace
 */
goog.provide('Blockly.utils.style');

goog.require('Blockly.utils.Coordinate');
goog.require('Blockly.utils.Size');


/**
 * Gets the height and width of an element.
 * Similar to Closure's goog.style.getSize
 * @param {!Element} element Element to get size of.
 * @return {!Blockly.utils.Size} Object with width/height properties.
 */
Blockly.utils.style.getSize = function(element) {
  if (Blockly.utils.style.getStyle_(element, 'display') != 'none') {
    return Blockly.utils.style.getSizeWithDisplay_(element);
  }

  // Evaluate size with a temporary element.
  var style = element.style;
  var originalDisplay = style.display;
  var originalVisibility = style.visibility;
  var originalPosition = style.position;

  style.visibility = 'hidden';
  style.position = 'absolute';
  style.display = 'inline';

  var offsetWidth = /** @type {!HTMLElement} */ (element).offsetWidth;
  var offsetHeight = /** @type {!HTMLElement} */ (element).offsetHeight;

  style.display = originalDisplay;
  style.position = originalPosition;
  style.visibility = originalVisibility;

  return new Blockly.utils.Size(offsetWidth, offsetHeight);
};

/**
 * Gets the height and width of an element when the display is not none.
 * @param {!Element} element Element to get size of.
 * @return {!Blockly.utils.Size} Object with width/height properties.
 * @private
 */
Blockly.utils.style.getSizeWithDisplay_ = function(element) {
  var offsetWidth = /** @type {!HTMLElement} */ (element).offsetWidth;
  var offsetHeight = /** @type {!HTMLElement} */ (element).offsetHeight;
  return new Blockly.utils.Size(offsetWidth, offsetHeight);
};

/**
 * Cross-browser pseudo get computed style. It returns the computed style where
 * available. If not available it tries the cascaded style value (IE
 * currentStyle) and in worst case the inline style value.  It shouldn't be
 * called directly, see http://wiki/Main/ComputedStyleVsCascadedStyle for
 * discussion.
 *
 * Copied from Closure's goog.style.getStyle_
 *
 * @param {!Element} element Element to get style of.
 * @param {string} style Property to get (must be camelCase, not css-style.).
 * @return {string} Style value.
 * @private
 */
Blockly.utils.style.getStyle_ = function(element, style) {
  return Blockly.utils.style.getComputedStyle(element, style) ||
      Blockly.utils.style.getCascadedStyle(element, style) ||
      (element.style && element.style[style]);
};

/**
 * Retrieves a computed style value of a node. It returns empty string if the
 * value cannot be computed (which will be the case in Internet Explorer) or
 * "none" if the property requested is an SVG one and it has not been
 * explicitly set (firefox and webkit).
 *
 * Copied from Closure's goog.style.getComputedStyle
 *
 * @param {!Element} element Element to get style of.
 * @param {string} property Property to get (camel-case).
 * @return {string} Style value.
 */
Blockly.utils.style.getComputedStyle = function(element, property) {
  if (document.defaultView && document.defaultView.getComputedStyle) {
    var styles = document.defaultView.getComputedStyle(element, null);
    if (styles) {
      // element.style[..] is undefined for browser specific styles
      // as 'filter'.
      return styles[property] || styles.getPropertyValue(property) || '';
    }
  }

  return '';
};

/**
 * Gets the cascaded style value of a node, or null if the value cannot be
 * computed (only Internet Explorer can do this).
 *
 * Copied from Closure's goog.style.getCascadedStyle
 *
 * @param {!Element} element Element to get style of.
 * @param {string} style Property to get (camel-case).
 * @return {string} Style value.
 */
Blockly.utils.style.getCascadedStyle = function(element, style) {
  return /** @type {string} */ (
      element.currentStyle ? element.currentStyle[style] : null);
};

/**
 * Returns a Coordinate object relative to the top-left of the HTML document.
 * Similar to Closure's goog.style.getPageOffset
 * @param {!Element} el Element to get the page offset for.
 * @return {!Blockly.utils.Coordinate} The page offset.
 */
Blockly.utils.style.getPageOffset = function(el) {
  var pos = new Blockly.utils.Coordinate(0, 0);
  var box = el.getBoundingClientRect();
  var documentElement = document.documentElement;
  // Must add the scroll coordinates in to get the absolute page offset
  // of element since getBoundingClientRect returns relative coordinates to
  // the viewport.
  var scrollCoord = new Blockly.utils.Coordinate(
      window.pageXOffset || documentElement.scrollLeft,
      window.pageYOffset || documentElement.scrollTop);
  pos.x = box.left + scrollCoord.x;
  pos.y = box.top + scrollCoord.y;

  return pos;
};

/**
 * Calculates the viewport coordinates relative to the document.
 * Similar to Closure's goog.style.getViewportPageOffset
 * @return {!Blockly.utils.Coordinate} The page offset of the viewport.
 */
Blockly.utils.style.getViewportPageOffset = function() {
  var body = document.body;
  var documentElement = document.documentElement;
  var scrollLeft = body.scrollLeft || documentElement.scrollLeft;
  var scrollTop = body.scrollTop || documentElement.scrollTop;
  return new Blockly.utils.Coordinate(scrollLeft, scrollTop);
};

/**
 * Shows or hides an element from the page. Hiding the element is done by
 * setting the display property to "none", removing the element from the
 * rendering hierarchy so it takes up no space. To show the element, the default
 * inherited display property is restored (defined either in stylesheets or by
 * the browser's default style rules).
 * Copied from Closure's goog.style.getViewportPageOffset
 *
 * @param {!Element} el Element to show or hide.
 * @param {*} isShown True to render the element in its default style,
 *     false to disable rendering the element.
 */
Blockly.utils.style.setElementShown = function(el, isShown) {
  el.style.display = isShown ? '' : 'none';
};

/**
 * Returns true if the element is using right to left (RTL) direction.
 * Copied from Closure's goog.style.isRightToLeft
 *
 * @param {!Element} el The element to test.
 * @return {boolean} True for right to left, false for left to right.
 */
Blockly.utils.style.isRightToLeft = function(el) {
  return 'rtl' == Blockly.utils.style.getStyle_(el, 'direction');
};

/**
 * Gets the computed border widths (on all sides) in pixels
 * Copied from Closure's goog.style.getBorderBox
 * @param {!Element} element  The element to get the border widths for.
 * @return {!Object} The computed border widths.
 */
Blockly.utils.style.getBorderBox = function(element) {
  var left = Blockly.utils.style.getComputedStyle(element, 'borderLeftWidth');
  var right = Blockly.utils.style.getComputedStyle(element, 'borderRightWidth');
  var top = Blockly.utils.style.getComputedStyle(element, 'borderTopWidth');
  var bottom = Blockly.utils.style.getComputedStyle(element, 'borderBottomWidth');

  return {
    top: parseFloat(top),
    right: parseFloat(right),
    bottom: parseFloat(bottom),
    left: parseFloat(left)
  };
};

/**
 * Changes the scroll position of `container` with the minimum amount so
 * that the content and the borders of the given `element` become visible.
 * If the element is bigger than the container, its top left corner will be
 * aligned as close to the container's top left corner as possible.
 * Copied from Closure's goog.style.scrollIntoContainerView
 *
 * @param {!Element} element The element to make visible.
 * @param {!Element} container The container to scroll. If not set, then the
 *     document scroll element will be used.
 * @param {boolean=} opt_center Whether to center the element in the container.
 *     Defaults to false.
 */
Blockly.utils.style.scrollIntoContainerView = function(
    element, container, opt_center) {
  var offset =
      Blockly.utils.style.getContainerOffsetToScrollInto(element,
          container, opt_center);
  container.scrollLeft = offset.x;
  container.scrollTop = offset.y;
};

/**
 * Calculate the scroll position of `container` with the minimum amount so
 * that the content and the borders of the given `element` become visible.
 * If the element is bigger than the container, its top left corner will be
 * aligned as close to the container's top left corner as possible.
 * Copied from Closure's goog.style.getContainerOffsetToScrollInto
 *
 * @param {!Element} element The element to make visible.
 * @param {!Element} container The container to scroll. If not set, then the
 *     document scroll element will be used.
 * @param {boolean=} opt_center Whether to center the element in the container.
 *     Defaults to false.
 * @return {!Blockly.utils.Coordinate} The new scroll position of the container,
 *     in form of goog.math.Coordinate(scrollLeft, scrollTop).
 */
Blockly.utils.style.getContainerOffsetToScrollInto = function(
    element, container, opt_center) {
  // Absolute position of the element's border's top left corner.
  var elementPos = Blockly.utils.style.getPageOffset(element);
  // Absolute position of the container's border's top left corner.
  var containerPos = Blockly.utils.style.getPageOffset(container);
  var containerBorder = Blockly.utils.style.getBorderBox(container);
  // Relative pos. of the element's border box to the container's content box.
  var relX = elementPos.x - containerPos.x - containerBorder.left;
  var relY = elementPos.y - containerPos.y - containerBorder.top;
  // How much the element can move in the container, i.e. the difference between
  // the element's bottom-right-most and top-left-most position where it's
  // fully visible.
  var elementSize = Blockly.utils.style.getSizeWithDisplay_(element);
  var spaceX = container.clientWidth - elementSize.width;
  var spaceY = container.clientHeight - elementSize.height;
  var scrollLeft = container.scrollLeft;
  var scrollTop = container.scrollTop;
  if (opt_center) {
    // All browsers round non-integer scroll positions down.
    scrollLeft += relX - spaceX / 2;
    scrollTop += relY - spaceY / 2;
  } else {
    // This formula was designed to give the correct scroll values in the
    // following cases:
    // - element is higher than container (spaceY < 0) => scroll down by relY
    // - element is not higher that container (spaceY >= 0):
    //   - it is above container (relY < 0) => scroll up by abs(relY)
    //   - it is below container (relY > spaceY) => scroll down by relY - spaceY
    //   - it is in the container => don't scroll
    scrollLeft += Math.min(relX, Math.max(relX - spaceX, 0));
    scrollTop += Math.min(relY, Math.max(relY - spaceY, 0));
  }
  return new Blockly.utils.Coordinate(scrollLeft, scrollTop);
};
