import BoundingRectangle from "../Core/BoundingRectangle.js";
import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Color from "../Core/Color.js";
import defaultValue from "../Core/defaultValue.js";
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
import DistanceDisplayCondition from "../Core/DistanceDisplayCondition.js";
import NearFarScalar from "../Core/NearFarScalar.js";
import Billboard from "./Billboard.js";
import HeightReference from "./HeightReference.js";
import HorizontalOrigin from "./HorizontalOrigin.js";
import LabelStyle from "./LabelStyle.js";
import SDFSettings from "./SDFSettings.js";
import VerticalOrigin from "./VerticalOrigin.js";

var fontInfoCache = {};
var fontInfoCacheLength = 0;
var fontInfoCacheMaxSize = 256;
var defaultBackgroundColor = new Color(0.165, 0.165, 0.165, 0.8);
var defaultBackgroundPadding = new Cartesian2(7, 5);

var textTypes = Object.freeze({
  LTR: 0,
  RTL: 1,
  WEAK: 2,
  BRACKETS: 3,
});

function rebindAllGlyphs(label) {
  if (!label._rebindAllGlyphs && !label._repositionAllGlyphs) {
    // only push label if it's not already been marked dirty
    label._labelCollection._labelsToUpdate.push(label);
  }
  label._rebindAllGlyphs = true;
}

function repositionAllGlyphs(label) {
  if (!label._rebindAllGlyphs && !label._repositionAllGlyphs) {
    // only push label if it's not already been marked dirty
    label._labelCollection._labelsToUpdate.push(label);
  }
  label._repositionAllGlyphs = true;
}

function getCSSValue(element, property) {
  return document.defaultView
    .getComputedStyle(element, null)
    .getPropertyValue(property);
}

function parseFont(label) {
  var fontInfo = fontInfoCache[label._font];
  if (!defined(fontInfo)) {
    var div = document.createElement("div");
    div.style.position = "absolute";
    div.style.opacity = 0;
    div.style.font = label._font;
    document.body.appendChild(div);

    var lineHeight = parseFloat(getCSSValue(div, "line-height"));
    if (isNaN(lineHeight)) {
      // line-height isn't a number, i.e. 'normal'; apply default line-spacing
      lineHeight = undefined;
    }

    fontInfo = {
      family: getCSSValue(div, "font-family"),
      size: getCSSValue(div, "font-size").replace("px", ""),
      style: getCSSValue(div, "font-style"),
      weight: getCSSValue(div, "font-weight"),
      lineHeight: lineHeight,
    };

    document.body.removeChild(div);
    if (fontInfoCacheLength < fontInfoCacheMaxSize) {
      fontInfoCache[label._font] = fontInfo;
      fontInfoCacheLength++;
    }
  }
  label._fontFamily = fontInfo.family;
  label._fontSize = fontInfo.size;
  label._fontStyle = fontInfo.style;
  label._fontWeight = fontInfo.weight;
  label._lineHeight = fontInfo.lineHeight;
}

/**
 * A Label draws viewport-aligned text positioned in the 3D scene.  This constructor
 * should not be used directly, instead create labels by calling {@link LabelCollection#add}.
 *
 * @alias Label
 * @internalConstructor
 * @class
 *
 * @exception {DeveloperError} translucencyByDistance.far must be greater than translucencyByDistance.near
 * @exception {DeveloperError} pixelOffsetScaleByDistance.far must be greater than pixelOffsetScaleByDistance.near
 * @exception {DeveloperError} distanceDisplayCondition.far must be greater than distanceDisplayCondition.near
 *
 * @see LabelCollection
 * @see LabelCollection#add
 *
 * @demo {@link https://sandcastle.cesium.com/index.html?src=Labels.html|Cesium Sandcastle Labels Demo}
 */
function Label(options, labelCollection) {
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);

  //>>includeStart('debug', pragmas.debug);
  if (
    defined(options.disableDepthTestDistance) &&
    options.disableDepthTestDistance < 0.0
  ) {
    throw new DeveloperError(
      "disableDepthTestDistance must be greater than 0.0."
    );
  }
  //>>includeEnd('debug');

  var translucencyByDistance = options.translucencyByDistance;
  var pixelOffsetScaleByDistance = options.pixelOffsetScaleByDistance;
  var scaleByDistance = options.scaleByDistance;
  var distanceDisplayCondition = options.distanceDisplayCondition;
  if (defined(translucencyByDistance)) {
    //>>includeStart('debug', pragmas.debug);
    if (translucencyByDistance.far <= translucencyByDistance.near) {
      throw new DeveloperError(
        "translucencyByDistance.far must be greater than translucencyByDistance.near."
      );
    }
    //>>includeEnd('debug');
    translucencyByDistance = NearFarScalar.clone(translucencyByDistance);
  }
  if (defined(pixelOffsetScaleByDistance)) {
    //>>includeStart('debug', pragmas.debug);
    if (pixelOffsetScaleByDistance.far <= pixelOffsetScaleByDistance.near) {
      throw new DeveloperError(
        "pixelOffsetScaleByDistance.far must be greater than pixelOffsetScaleByDistance.near."
      );
    }
    //>>includeEnd('debug');
    pixelOffsetScaleByDistance = NearFarScalar.clone(
      pixelOffsetScaleByDistance
    );
  }
  if (defined(scaleByDistance)) {
    //>>includeStart('debug', pragmas.debug);
    if (scaleByDistance.far <= scaleByDistance.near) {
      throw new DeveloperError(
        "scaleByDistance.far must be greater than scaleByDistance.near."
      );
    }
    //>>includeEnd('debug');
    scaleByDistance = NearFarScalar.clone(scaleByDistance);
  }
  if (defined(distanceDisplayCondition)) {
    //>>includeStart('debug', pragmas.debug);
    if (distanceDisplayCondition.far <= distanceDisplayCondition.near) {
      throw new DeveloperError(
        "distanceDisplayCondition.far must be greater than distanceDisplayCondition.near."
      );
    }
    //>>includeEnd('debug');
    distanceDisplayCondition = DistanceDisplayCondition.clone(
      distanceDisplayCondition
    );
  }

  this._renderedText = undefined;
  this._text = undefined;
  this._show = defaultValue(options.show, true);
  this._font = defaultValue(options.font, "30px sans-serif");
  this._fillColor = Color.clone(defaultValue(options.fillColor, Color.WHITE));
  this._outlineColor = Color.clone(
    defaultValue(options.outlineColor, Color.BLACK)
  );
  this._outlineWidth = defaultValue(options.outlineWidth, 1.0);
  this._showBackground = defaultValue(options.showBackground, false);
  this._backgroundColor = Color.clone(
    defaultValue(options.backgroundColor, defaultBackgroundColor)
  );
  this._backgroundPadding = Cartesian2.clone(
    defaultValue(options.backgroundPadding, defaultBackgroundPadding)
  );
  this._style = defaultValue(options.style, LabelStyle.FILL);
  this._verticalOrigin = defaultValue(
    options.verticalOrigin,
    VerticalOrigin.BASELINE
  );
  this._horizontalOrigin = defaultValue(
    options.horizontalOrigin,
    HorizontalOrigin.LEFT
  );
  this._pixelOffset = Cartesian2.clone(
    defaultValue(options.pixelOffset, Cartesian2.ZERO)
  );
  this._eyeOffset = Cartesian3.clone(
    defaultValue(options.eyeOffset, Cartesian3.ZERO)
  );
  this._position = Cartesian3.clone(
    defaultValue(options.position, Cartesian3.ZERO)
  );
  this._scale = defaultValue(options.scale, 1.0);
  this._id = options.id;
  this._translucencyByDistance = translucencyByDistance;
  this._pixelOffsetScaleByDistance = pixelOffsetScaleByDistance;
  this._scaleByDistance = scaleByDistance;
  this._heightReference = defaultValue(
    options.heightReference,
    HeightReference.NONE
  );
  this._distanceDisplayCondition = distanceDisplayCondition;
  this._disableDepthTestDistance = options.disableDepthTestDistance;

  this._labelCollection = labelCollection;
  this._glyphs = [];
  this._backgroundBillboard = undefined;
  this._batchIndex = undefined; // Used only by Vector3DTilePoints and BillboardCollection

  this._rebindAllGlyphs = true;
  this._repositionAllGlyphs = true;

  this._actualClampedPosition = undefined;
  this._removeCallbackFunc = undefined;
  this._mode = undefined;

  this._clusterShow = true;

  this.text = defaultValue(options.text, "");

  this._relativeSize = 1.0;

  parseFont(this);

  this._updateClamping();
}

Object.defineProperties(Label.prototype, {
  /**
   * Determines if this label will be shown.  Use this to hide or show a label, instead
   * of removing it and re-adding it to the collection.
   * @memberof Label.prototype
   * @type {Boolean}
   * @default true
   */
  show: {
    get: function () {
      return this._show;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (!defined(value)) {
        throw new DeveloperError("value is required.");
      }
      //>>includeEnd('debug');

      if (this._show !== value) {
        this._show = value;

        var glyphs = this._glyphs;
        for (var i = 0, len = glyphs.length; i < len; i++) {
          var billboard = glyphs[i].billboard;
          if (defined(billboard)) {
            billboard.show = value;
          }
        }
        var backgroundBillboard = this._backgroundBillboard;
        if (defined(backgroundBillboard)) {
          backgroundBillboard.show = value;
        }
      }
    },
  },

  /**
   * Gets or sets the Cartesian position of this label.
   * @memberof Label.prototype
   * @type {Cartesian3}
   */
  position: {
    get: function () {
      return this._position;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (!defined(value)) {
        throw new DeveloperError("value is required.");
      }
      //>>includeEnd('debug');

      var position = this._position;
      if (!Cartesian3.equals(position, value)) {
        Cartesian3.clone(value, position);

        var glyphs = this._glyphs;
        for (var i = 0, len = glyphs.length; i < len; i++) {
          var billboard = glyphs[i].billboard;
          if (defined(billboard)) {
            billboard.position = value;
          }
        }
        var backgroundBillboard = this._backgroundBillboard;
        if (defined(backgroundBillboard)) {
          backgroundBillboard.position = value;
        }

        this._updateClamping();
      }
    },
  },

  /**
   * Gets or sets the height reference of this billboard.
   * @memberof Label.prototype
   * @type {HeightReference}
   * @default HeightReference.NONE
   */
  heightReference: {
    get: function () {
      return this._heightReference;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (!defined(value)) {
        throw new DeveloperError("value is required.");
      }
      //>>includeEnd('debug');

      if (value !== this._heightReference) {
        this._heightReference = value;

        var glyphs = this._glyphs;
        for (var i = 0, len = glyphs.length; i < len; i++) {
          var billboard = glyphs[i].billboard;
          if (defined(billboard)) {
            billboard.heightReference = value;
          }
        }
        var backgroundBillboard = this._backgroundBillboard;
        if (defined(backgroundBillboard)) {
          backgroundBillboard.heightReference = value;
        }

        repositionAllGlyphs(this);

        this._updateClamping();
      }
    },
  },

  /**
   * Gets or sets the text of this label.
   * @memberof Label.prototype
   * @type {String}
   */
  text: {
    get: function () {
      return this._text;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (!defined(value)) {
        throw new DeveloperError("value is required.");
      }
      //>>includeEnd('debug');

      if (this._text !== value) {
        this._text = value;
        this._renderedText = Label.enableRightToLeftDetection
          ? reverseRtl(value)
          : value;
        rebindAllGlyphs(this);
      }
    },
  },

  /**
   * Gets or sets the font used to draw this label. Fonts are specified using the same syntax as the CSS 'font' property.
   * @memberof Label.prototype
   * @type {String}
   * @default '30px sans-serif'
   * @see {@link http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#text-styles|HTML canvas 2D context text styles}
   */
  font: {
    get: function () {
      return this._font;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (!defined(value)) {
        throw new DeveloperError("value is required.");
      }
      //>>includeEnd('debug');

      if (this._font !== value) {
        this._font = value;
        rebindAllGlyphs(this);
        parseFont(this);
      }
    },
  },

  /**
   * Gets or sets the fill color of this label.
   * @memberof Label.prototype
   * @type {Color}
   * @default Color.WHITE
   * @see {@link http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#fill-and-stroke-styles|HTML canvas 2D context fill and stroke styles}
   */
  fillColor: {
    get: function () {
      return this._fillColor;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (!defined(value)) {
        throw new DeveloperError("value is required.");
      }
      //>>includeEnd('debug');

      var fillColor = this._fillColor;
      if (!Color.equals(fillColor, value)) {
        Color.clone(value, fillColor);
        rebindAllGlyphs(this);
      }
    },
  },

  /**
   * Gets or sets the outline color of this label.
   * @memberof Label.prototype
   * @type {Color}
   * @default Color.BLACK
   * @see {@link http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#fill-and-stroke-styles|HTML canvas 2D context fill and stroke styles}
   */
  outlineColor: {
    get: function () {
      return this._outlineColor;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (!defined(value)) {
        throw new DeveloperError("value is required.");
      }
      //>>includeEnd('debug');

      var outlineColor = this._outlineColor;
      if (!Color.equals(outlineColor, value)) {
        Color.clone(value, outlineColor);
        rebindAllGlyphs(this);
      }
    },
  },

  /**
   * Gets or sets the outline width of this label.
   * @memberof Label.prototype
   * @type {Number}
   * @default 1.0
   * @see {@link http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#fill-and-stroke-styles|HTML canvas 2D context fill and stroke styles}
   */
  outlineWidth: {
    get: function () {
      return this._outlineWidth;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (!defined(value)) {
        throw new DeveloperError("value is required.");
      }
      //>>includeEnd('debug');

      if (this._outlineWidth !== value) {
        this._outlineWidth = value;
        rebindAllGlyphs(this);
      }
    },
  },

  /**
   * Determines if a background behind this label will be shown.
   * @memberof Label.prototype
   * @default false
   * @type {Boolean}
   */
  showBackground: {
    get: function () {
      return this._showBackground;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (!defined(value)) {
        throw new DeveloperError("value is required.");
      }
      //>>includeEnd('debug');

      if (this._showBackground !== value) {
        this._showBackground = value;
        rebindAllGlyphs(this);
      }
    },
  },

  /**
   * Gets or sets the background color of this label.
   * @memberof Label.prototype
   * @type {Color}
   * @default new Color(0.165, 0.165, 0.165, 0.8)
   */
  backgroundColor: {
    get: function () {
      return this._backgroundColor;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (!defined(value)) {
        throw new DeveloperError("value is required.");
      }
      //>>includeEnd('debug');

      var backgroundColor = this._backgroundColor;
      if (!Color.equals(backgroundColor, value)) {
        Color.clone(value, backgroundColor);

        var backgroundBillboard = this._backgroundBillboard;
        if (defined(backgroundBillboard)) {
          backgroundBillboard.color = backgroundColor;
        }
      }
    },
  },

  /**
   * Gets or sets the background padding, in pixels, of this label.  The <code>x</code> value
   * controls horizontal padding, and the <code>y</code> value controls vertical padding.
   * @memberof Label.prototype
   * @type {Cartesian2}
   * @default new Cartesian2(7, 5)
   */
  backgroundPadding: {
    get: function () {
      return this._backgroundPadding;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (!defined(value)) {
        throw new DeveloperError("value is required.");
      }
      //>>includeEnd('debug');

      var backgroundPadding = this._backgroundPadding;
      if (!Cartesian2.equals(backgroundPadding, value)) {
        Cartesian2.clone(value, backgroundPadding);
        repositionAllGlyphs(this);
      }
    },
  },

  /**
   * Gets or sets the style of this label.
   * @memberof Label.prototype
   * @type {LabelStyle}
   * @default LabelStyle.FILL
   */
  style: {
    get: function () {
      return this._style;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (!defined(value)) {
        throw new DeveloperError("value is required.");
      }
      //>>includeEnd('debug');

      if (this._style !== value) {
        this._style = value;
        rebindAllGlyphs(this);
      }
    },
  },

  /**
   * Gets or sets the pixel offset in screen space from the origin of this label.  This is commonly used
   * to align multiple labels and billboards at the same position, e.g., an image and text.  The
   * screen space origin is the top, left corner of the canvas; <code>x</code> increases from
   * left to right, and <code>y</code> increases from top to bottom.
   * <br /><br />
   * <div align='center'>
   * <table border='0' cellpadding='5'><tr>
   * <td align='center'><code>default</code><br/><img src='Images/Label.setPixelOffset.default.png' width='250' height='188' /></td>
   * <td align='center'><code>l.pixeloffset = new Cartesian2(25, 75);</code><br/><img src='Images/Label.setPixelOffset.x50y-25.png' width='250' height='188' /></td>
   * </tr></table>
   * The label's origin is indicated by the yellow point.
   * </div>
   * @memberof Label.prototype
   * @type {Cartesian2}
   * @default Cartesian2.ZERO
   */
  pixelOffset: {
    get: function () {
      return this._pixelOffset;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (!defined(value)) {
        throw new DeveloperError("value is required.");
      }
      //>>includeEnd('debug');

      var pixelOffset = this._pixelOffset;
      if (!Cartesian2.equals(pixelOffset, value)) {
        Cartesian2.clone(value, pixelOffset);

        var glyphs = this._glyphs;
        for (var i = 0, len = glyphs.length; i < len; i++) {
          var glyph = glyphs[i];
          if (defined(glyph.billboard)) {
            glyph.billboard.pixelOffset = value;
          }
        }
        var backgroundBillboard = this._backgroundBillboard;
        if (defined(backgroundBillboard)) {
          backgroundBillboard.pixelOffset = value;
        }
      }
    },
  },

  /**
   * Gets or sets near and far translucency properties of a Label based on the Label's distance from the camera.
   * A label's translucency will interpolate between the {@link NearFarScalar#nearValue} and
   * {@link NearFarScalar#farValue} while the camera distance falls within the lower and upper bounds
   * of the specified {@link NearFarScalar#near} and {@link NearFarScalar#far}.
   * Outside of these ranges the label's translucency remains clamped to the nearest bound.  If undefined,
   * translucencyByDistance will be disabled.
   * @memberof Label.prototype
   * @type {NearFarScalar}
   *
   * @example
   * // Example 1.
   * // Set a label's translucencyByDistance to 1.0 when the
   * // camera is 1500 meters from the label and disappear as
   * // the camera distance approaches 8.0e6 meters.
   * text.translucencyByDistance = new Cesium.NearFarScalar(1.5e2, 1.0, 8.0e6, 0.0);
   *
   * @example
   * // Example 2.
   * // disable translucency by distance
   * text.translucencyByDistance = undefined;
   */
  translucencyByDistance: {
    get: function () {
      return this._translucencyByDistance;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (defined(value) && value.far <= value.near) {
        throw new DeveloperError(
          "far distance must be greater than near distance."
        );
      }
      //>>includeEnd('debug');

      var translucencyByDistance = this._translucencyByDistance;
      if (!NearFarScalar.equals(translucencyByDistance, value)) {
        this._translucencyByDistance = NearFarScalar.clone(
          value,
          translucencyByDistance
        );

        var glyphs = this._glyphs;
        for (var i = 0, len = glyphs.length; i < len; i++) {
          var glyph = glyphs[i];
          if (defined(glyph.billboard)) {
            glyph.billboard.translucencyByDistance = value;
          }
        }
        var backgroundBillboard = this._backgroundBillboard;
        if (defined(backgroundBillboard)) {
          backgroundBillboard.translucencyByDistance = value;
        }
      }
    },
  },

  /**
   * Gets or sets near and far pixel offset scaling properties of a Label based on the Label's distance from the camera.
   * A label's pixel offset will be scaled between the {@link NearFarScalar#nearValue} and
   * {@link NearFarScalar#farValue} while the camera distance falls within the lower and upper bounds
   * of the specified {@link NearFarScalar#near} and {@link NearFarScalar#far}.
   * Outside of these ranges the label's pixel offset scaling remains clamped to the nearest bound.  If undefined,
   * pixelOffsetScaleByDistance will be disabled.
   * @memberof Label.prototype
   * @type {NearFarScalar}
   *
   * @example
   * // Example 1.
   * // Set a label's pixel offset scale to 0.0 when the
   * // camera is 1500 meters from the label and scale pixel offset to 10.0 pixels
   * // in the y direction the camera distance approaches 8.0e6 meters.
   * text.pixelOffset = new Cesium.Cartesian2(0.0, 1.0);
   * text.pixelOffsetScaleByDistance = new Cesium.NearFarScalar(1.5e2, 0.0, 8.0e6, 10.0);
   *
   * @example
   * // Example 2.
   * // disable pixel offset by distance
   * text.pixelOffsetScaleByDistance = undefined;
   */
  pixelOffsetScaleByDistance: {
    get: function () {
      return this._pixelOffsetScaleByDistance;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (defined(value) && value.far <= value.near) {
        throw new DeveloperError(
          "far distance must be greater than near distance."
        );
      }
      //>>includeEnd('debug');

      var pixelOffsetScaleByDistance = this._pixelOffsetScaleByDistance;
      if (!NearFarScalar.equals(pixelOffsetScaleByDistance, value)) {
        this._pixelOffsetScaleByDistance = NearFarScalar.clone(
          value,
          pixelOffsetScaleByDistance
        );

        var glyphs = this._glyphs;
        for (var i = 0, len = glyphs.length; i < len; i++) {
          var glyph = glyphs[i];
          if (defined(glyph.billboard)) {
            glyph.billboard.pixelOffsetScaleByDistance = value;
          }
        }
        var backgroundBillboard = this._backgroundBillboard;
        if (defined(backgroundBillboard)) {
          backgroundBillboard.pixelOffsetScaleByDistance = value;
        }
      }
    },
  },

  /**
   * Gets or sets near and far scaling properties of a Label based on the label's distance from the camera.
   * A label's scale will interpolate between the {@link NearFarScalar#nearValue} and
   * {@link NearFarScalar#farValue} while the camera distance falls within the lower and upper bounds
   * of the specified {@link NearFarScalar#near} and {@link NearFarScalar#far}.
   * Outside of these ranges the label's scale remains clamped to the nearest bound.  If undefined,
   * scaleByDistance will be disabled.
   * @memberof Label.prototype
   * @type {NearFarScalar}
   *
   * @example
   * // Example 1.
   * // Set a label's scaleByDistance to scale by 1.5 when the
   * // camera is 1500 meters from the label and disappear as
   * // the camera distance approaches 8.0e6 meters.
   * label.scaleByDistance = new Cesium.NearFarScalar(1.5e2, 1.5, 8.0e6, 0.0);
   *
   * @example
   * // Example 2.
   * // disable scaling by distance
   * label.scaleByDistance = undefined;
   */
  scaleByDistance: {
    get: function () {
      return this._scaleByDistance;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (defined(value) && value.far <= value.near) {
        throw new DeveloperError(
          "far distance must be greater than near distance."
        );
      }
      //>>includeEnd('debug');

      var scaleByDistance = this._scaleByDistance;
      if (!NearFarScalar.equals(scaleByDistance, value)) {
        this._scaleByDistance = NearFarScalar.clone(value, scaleByDistance);

        var glyphs = this._glyphs;
        for (var i = 0, len = glyphs.length; i < len; i++) {
          var glyph = glyphs[i];
          if (defined(glyph.billboard)) {
            glyph.billboard.scaleByDistance = value;
          }
        }
        var backgroundBillboard = this._backgroundBillboard;
        if (defined(backgroundBillboard)) {
          backgroundBillboard.scaleByDistance = value;
        }
      }
    },
  },

  /**
   * Gets and sets the 3D Cartesian offset applied to this label in eye coordinates.  Eye coordinates is a left-handed
   * coordinate system, where <code>x</code> points towards the viewer's right, <code>y</code> points up, and
   * <code>z</code> points into the screen.  Eye coordinates use the same scale as world and model coordinates,
   * which is typically meters.
   * <br /><br />
   * An eye offset is commonly used to arrange multiple label or objects at the same position, e.g., to
   * arrange a label above its corresponding 3D model.
   * <br /><br />
   * Below, the label is positioned at the center of the Earth but an eye offset makes it always
   * appear on top of the Earth regardless of the viewer's or Earth's orientation.
   * <br /><br />
   * <div align='center'>
   * <table border='0' cellpadding='5'><tr>
   * <td align='center'><img src='Images/Billboard.setEyeOffset.one.png' width='250' height='188' /></td>
   * <td align='center'><img src='Images/Billboard.setEyeOffset.two.png' width='250' height='188' /></td>
   * </tr></table>
   * <code>l.eyeOffset = new Cartesian3(0.0, 8000000.0, 0.0);</code><br /><br />
   * </div>
   * @memberof Label.prototype
   * @type {Cartesian3}
   * @default Cartesian3.ZERO
   */
  eyeOffset: {
    get: function () {
      return this._eyeOffset;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (!defined(value)) {
        throw new DeveloperError("value is required.");
      }
      //>>includeEnd('debug');

      var eyeOffset = this._eyeOffset;
      if (!Cartesian3.equals(eyeOffset, value)) {
        Cartesian3.clone(value, eyeOffset);

        var glyphs = this._glyphs;
        for (var i = 0, len = glyphs.length; i < len; i++) {
          var glyph = glyphs[i];
          if (defined(glyph.billboard)) {
            glyph.billboard.eyeOffset = value;
          }
        }
        var backgroundBillboard = this._backgroundBillboard;
        if (defined(backgroundBillboard)) {
          backgroundBillboard.eyeOffset = value;
        }
      }
    },
  },

  /**
   * Gets or sets the horizontal origin of this label, which determines if the label is drawn
   * to the left, center, or right of its anchor position.
   * <br /><br />
   * <div align='center'>
   * <img src='Images/Billboard.setHorizontalOrigin.png' width='648' height='196' /><br />
   * </div>
   * @memberof Label.prototype
   * @type {HorizontalOrigin}
   * @default HorizontalOrigin.LEFT
   * @example
   * // Use a top, right origin
   * l.horizontalOrigin = Cesium.HorizontalOrigin.RIGHT;
   * l.verticalOrigin = Cesium.VerticalOrigin.TOP;
   */
  horizontalOrigin: {
    get: function () {
      return this._horizontalOrigin;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (!defined(value)) {
        throw new DeveloperError("value is required.");
      }
      //>>includeEnd('debug');

      if (this._horizontalOrigin !== value) {
        this._horizontalOrigin = value;
        repositionAllGlyphs(this);
      }
    },
  },

  /**
   * Gets or sets the vertical origin of this label, which determines if the label is
   * to the above, below, or at the center of its anchor position.
   * <br /><br />
   * <div align='center'>
   * <img src='Images/Billboard.setVerticalOrigin.png' width='695' height='175' /><br />
   * </div>
   * @memberof Label.prototype
   * @type {VerticalOrigin}
   * @default VerticalOrigin.BASELINE
   * @example
   * // Use a top, right origin
   * l.horizontalOrigin = Cesium.HorizontalOrigin.RIGHT;
   * l.verticalOrigin = Cesium.VerticalOrigin.TOP;
   */
  verticalOrigin: {
    get: function () {
      return this._verticalOrigin;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (!defined(value)) {
        throw new DeveloperError("value is required.");
      }
      //>>includeEnd('debug');

      if (this._verticalOrigin !== value) {
        this._verticalOrigin = value;

        var glyphs = this._glyphs;
        for (var i = 0, len = glyphs.length; i < len; i++) {
          var glyph = glyphs[i];
          if (defined(glyph.billboard)) {
            glyph.billboard.verticalOrigin = value;
          }
        }
        var backgroundBillboard = this._backgroundBillboard;
        if (defined(backgroundBillboard)) {
          backgroundBillboard.verticalOrigin = value;
        }

        repositionAllGlyphs(this);
      }
    },
  },

  /**
   * Gets or sets the uniform scale that is multiplied with the label's size in pixels.
   * A scale of <code>1.0</code> does not change the size of the label; a scale greater than
   * <code>1.0</code> enlarges the label; a positive scale less than <code>1.0</code> shrinks
   * the label.
   * <br /><br />
   * Applying a large scale value may pixelate the label.  To make text larger without pixelation,
   * use a larger font size when calling {@link Label#font} instead.
   * <br /><br />
   * <div align='center'>
   * <img src='Images/Label.setScale.png' width='400' height='300' /><br/>
   * From left to right in the above image, the scales are <code>0.5</code>, <code>1.0</code>,
   * and <code>2.0</code>.
   * </div>
   * @memberof Label.prototype
   * @type {Number}
   * @default 1.0
   */
  scale: {
    get: function () {
      return this._scale;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (!defined(value)) {
        throw new DeveloperError("value is required.");
      }
      //>>includeEnd('debug');

      if (this._scale !== value) {
        this._scale = value;

        var glyphs = this._glyphs;
        for (var i = 0, len = glyphs.length; i < len; i++) {
          var glyph = glyphs[i];
          if (defined(glyph.billboard)) {
            glyph.billboard.scale = value * this._relativeSize;
          }
        }
        var backgroundBillboard = this._backgroundBillboard;
        if (defined(backgroundBillboard)) {
          backgroundBillboard.scale = value * this._relativeSize;
        }

        repositionAllGlyphs(this);
      }
    },
  },

  /**
   * Gets the total scale of the label, which is the label's scale multiplied by the computed relative size
   * of the desired font compared to the generated glyph size.
   * @memberof Label.prototype
   * @type {Number}
   * @default 1.0
   */
  totalScale: {
    get: function () {
      return this._scale * this._relativeSize;
    },
  },

  /**
   * Gets or sets the condition specifying at what distance from the camera that this label will be displayed.
   * @memberof Label.prototype
   * @type {DistanceDisplayCondition}
   * @default undefined
   */
  distanceDisplayCondition: {
    get: function () {
      return this._distanceDisplayCondition;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      if (defined(value) && value.far <= value.near) {
        throw new DeveloperError("far must be greater than near");
      }
      //>>includeEnd('debug');
      if (
        !DistanceDisplayCondition.equals(value, this._distanceDisplayCondition)
      ) {
        this._distanceDisplayCondition = DistanceDisplayCondition.clone(
          value,
          this._distanceDisplayCondition
        );

        var glyphs = this._glyphs;
        for (var i = 0, len = glyphs.length; i < len; i++) {
          var glyph = glyphs[i];
          if (defined(glyph.billboard)) {
            glyph.billboard.distanceDisplayCondition = value;
          }
        }
        var backgroundBillboard = this._backgroundBillboard;
        if (defined(backgroundBillboard)) {
          backgroundBillboard.distanceDisplayCondition = value;
        }
      }
    },
  },

  /**
   * Gets or sets the distance from the camera at which to disable the depth test to, for example, prevent clipping against terrain.
   * When set to zero, the depth test is always applied. When set to Number.POSITIVE_INFINITY, the depth test is never applied.
   * @memberof Label.prototype
   * @type {Number}
   */
  disableDepthTestDistance: {
    get: function () {
      return this._disableDepthTestDistance;
    },
    set: function (value) {
      if (this._disableDepthTestDistance !== value) {
        //>>includeStart('debug', pragmas.debug);
        if (defined(value) && value < 0.0) {
          throw new DeveloperError(
            "disableDepthTestDistance must be greater than 0.0."
          );
        }
        //>>includeEnd('debug');
        this._disableDepthTestDistance = value;

        var glyphs = this._glyphs;
        for (var i = 0, len = glyphs.length; i < len; i++) {
          var glyph = glyphs[i];
          if (defined(glyph.billboard)) {
            glyph.billboard.disableDepthTestDistance = value;
          }
        }
        var backgroundBillboard = this._backgroundBillboard;
        if (defined(backgroundBillboard)) {
          backgroundBillboard.disableDepthTestDistance = value;
        }
      }
    },
  },

  /**
   * Gets or sets the user-defined value returned when the label is picked.
   * @memberof Label.prototype
   * @type {*}
   */
  id: {
    get: function () {
      return this._id;
    },
    set: function (value) {
      if (this._id !== value) {
        this._id = value;

        var glyphs = this._glyphs;
        for (var i = 0, len = glyphs.length; i < len; i++) {
          var glyph = glyphs[i];
          if (defined(glyph.billboard)) {
            glyph.billboard.id = value;
          }
        }
        var backgroundBillboard = this._backgroundBillboard;
        if (defined(backgroundBillboard)) {
          backgroundBillboard.id = value;
        }
      }
    },
  },

  /**
   * @private
   */
  pickId: {
    get: function () {
      if (this._glyphs.length === 0 || !defined(this._glyphs[0].billboard)) {
        return undefined;
      }
      return this._glyphs[0].billboard.pickId;
    },
  },

  /**
   * Keeps track of the position of the label based on the height reference.
   * @memberof Label.prototype
   * @type {Cartesian3}
   * @private
   */
  _clampedPosition: {
    get: function () {
      return this._actualClampedPosition;
    },
    set: function (value) {
      this._actualClampedPosition = Cartesian3.clone(
        value,
        this._actualClampedPosition
      );

      var glyphs = this._glyphs;
      for (var i = 0, len = glyphs.length; i < len; i++) {
        var glyph = glyphs[i];
        if (defined(glyph.billboard)) {
          // Set all the private values here, because we already clamped to ground
          //  so we don't want to do it again for every glyph
          glyph.billboard._clampedPosition = value;
        }
      }
      var backgroundBillboard = this._backgroundBillboard;
      if (defined(backgroundBillboard)) {
        backgroundBillboard._clampedPosition = value;
      }
    },
  },

  /**
   * Determines whether or not this label will be shown or hidden because it was clustered.
   * @memberof Label.prototype
   * @type {Boolean}
   * @default true
   * @private
   */
  clusterShow: {
    get: function () {
      return this._clusterShow;
    },
    set: function (value) {
      if (this._clusterShow !== value) {
        this._clusterShow = value;

        var glyphs = this._glyphs;
        for (var i = 0, len = glyphs.length; i < len; i++) {
          var glyph = glyphs[i];
          if (defined(glyph.billboard)) {
            glyph.billboard.clusterShow = value;
          }
        }
        var backgroundBillboard = this._backgroundBillboard;
        if (defined(backgroundBillboard)) {
          backgroundBillboard.clusterShow = value;
        }
      }
    },
  },
});

Label.prototype._updateClamping = function () {
  Billboard._updateClamping(this._labelCollection, this);
};

/**
 * Computes the screen-space position of the label's origin, taking into account eye and pixel offsets.
 * The screen space origin is the top, left corner of the canvas; <code>x</code> increases from
 * left to right, and <code>y</code> increases from top to bottom.
 *
 * @param {Scene} scene The scene the label is in.
 * @param {Cartesian2} [result] The object onto which to store the result.
 * @returns {Cartesian2} The screen-space position of the label.
 *
 *
 * @example
 * console.log(l.computeScreenSpacePosition(scene).toString());
 *
 * @see Label#eyeOffset
 * @see Label#pixelOffset
 */
Label.prototype.computeScreenSpacePosition = function (scene, result) {
  //>>includeStart('debug', pragmas.debug);
  if (!defined(scene)) {
    throw new DeveloperError("scene is required.");
  }
  //>>includeEnd('debug');

  if (!defined(result)) {
    result = new Cartesian2();
  }

  var labelCollection = this._labelCollection;
  var modelMatrix = labelCollection.modelMatrix;
  var actualPosition = defined(this._actualClampedPosition)
    ? this._actualClampedPosition
    : this._position;

  var windowCoordinates = Billboard._computeScreenSpacePosition(
    modelMatrix,
    actualPosition,
    this._eyeOffset,
    this._pixelOffset,
    scene,
    result
  );
  return windowCoordinates;
};

/**
 * Gets a label's screen space bounding box centered around screenSpacePosition.
 * @param {Label} label The label to get the screen space bounding box for.
 * @param {Cartesian2} screenSpacePosition The screen space center of the label.
 * @param {BoundingRectangle} [result] The object onto which to store the result.
 * @returns {BoundingRectangle} The screen space bounding box.
 *
 * @private
 */
Label.getScreenSpaceBoundingBox = function (
  label,
  screenSpacePosition,
  result
) {
  var x = 0;
  var y = 0;
  var width = 0;
  var height = 0;
  var scale = label.totalScale;

  var backgroundBillboard = label._backgroundBillboard;
  if (defined(backgroundBillboard)) {
    x = screenSpacePosition.x + backgroundBillboard._translate.x;
    y = screenSpacePosition.y - backgroundBillboard._translate.y;
    width = backgroundBillboard.width * scale;
    height = backgroundBillboard.height * scale;

    if (
      label.verticalOrigin === VerticalOrigin.BOTTOM ||
      label.verticalOrigin === VerticalOrigin.BASELINE
    ) {
      y -= height;
    } else if (label.verticalOrigin === VerticalOrigin.CENTER) {
      y -= height * 0.5;
    }
  } else {
    x = Number.POSITIVE_INFINITY;
    y = Number.POSITIVE_INFINITY;
    var maxX = 0;
    var maxY = 0;
    var glyphs = label._glyphs;
    var length = glyphs.length;
    for (var i = 0; i < length; ++i) {
      var glyph = glyphs[i];
      var billboard = glyph.billboard;
      if (!defined(billboard)) {
        continue;
      }

      var glyphX = screenSpacePosition.x + billboard._translate.x;
      var glyphY = screenSpacePosition.y - billboard._translate.y;
      var glyphWidth = glyph.dimensions.width * scale;
      var glyphHeight = glyph.dimensions.height * scale;

      if (
        label.verticalOrigin === VerticalOrigin.BOTTOM ||
        label.verticalOrigin === VerticalOrigin.BASELINE
      ) {
        glyphY -= glyphHeight;
      } else if (label.verticalOrigin === VerticalOrigin.CENTER) {
        glyphY -= glyphHeight * 0.5;
      }

      if (label._verticalOrigin === VerticalOrigin.TOP) {
        glyphY += SDFSettings.PADDING * scale;
      } else if (
        label._verticalOrigin === VerticalOrigin.BOTTOM ||
        label._verticalOrigin === VerticalOrigin.BASELINE
      ) {
        glyphY -= SDFSettings.PADDING * scale;
      }

      x = Math.min(x, glyphX);
      y = Math.min(y, glyphY);
      maxX = Math.max(maxX, glyphX + glyphWidth);
      maxY = Math.max(maxY, glyphY + glyphHeight);
    }

    width = maxX - x;
    height = maxY - y;
  }

  if (!defined(result)) {
    result = new BoundingRectangle();
  }

  result.x = x;
  result.y = y;
  result.width = width;
  result.height = height;

  return result;
};

/**
 * Determines if this label equals another label.  Labels are equal if all their properties
 * are equal.  Labels in different collections can be equal.
 *
 * @param {Label} other The label to compare for equality.
 * @returns {Boolean} <code>true</code> if the labels are equal; otherwise, <code>false</code>.
 */
Label.prototype.equals = function (other) {
  return (
    this === other ||
    (defined(other) &&
      this._show === other._show &&
      this._scale === other._scale &&
      this._outlineWidth === other._outlineWidth &&
      this._showBackground === other._showBackground &&
      this._style === other._style &&
      this._verticalOrigin === other._verticalOrigin &&
      this._horizontalOrigin === other._horizontalOrigin &&
      this._heightReference === other._heightReference &&
      this._renderedText === other._renderedText &&
      this._font === other._font &&
      Cartesian3.equals(this._position, other._position) &&
      Color.equals(this._fillColor, other._fillColor) &&
      Color.equals(this._outlineColor, other._outlineColor) &&
      Color.equals(this._backgroundColor, other._backgroundColor) &&
      Cartesian2.equals(this._backgroundPadding, other._backgroundPadding) &&
      Cartesian2.equals(this._pixelOffset, other._pixelOffset) &&
      Cartesian3.equals(this._eyeOffset, other._eyeOffset) &&
      NearFarScalar.equals(
        this._translucencyByDistance,
        other._translucencyByDistance
      ) &&
      NearFarScalar.equals(
        this._pixelOffsetScaleByDistance,
        other._pixelOffsetScaleByDistance
      ) &&
      NearFarScalar.equals(this._scaleByDistance, other._scaleByDistance) &&
      DistanceDisplayCondition.equals(
        this._distanceDisplayCondition,
        other._distanceDisplayCondition
      ) &&
      this._disableDepthTestDistance === other._disableDepthTestDistance &&
      this._id === other._id)
  );
};

/**
 * Returns true if this object was destroyed; otherwise, false.
 * <br /><br />
 * If this object was destroyed, it should not be used; calling any function other than
 * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
 *
 * @returns {Boolean} True if this object was destroyed; otherwise, false.
 */
Label.prototype.isDestroyed = function () {
  return false;
};

/**
 * Determines whether or not run the algorithm, that match the text of the label to right-to-left languages
 * @memberof Label
 * @type {Boolean}
 * @default false
 *
 * @example
 * // Example 1.
 * // Set a label's rightToLeft before init
 * Cesium.Label.enableRightToLeftDetection = true;
 * var myLabelEntity = viewer.entities.add({
 *   label: {
 *     id: 'my label',
 *     text: 'זה טקסט בעברית \n ועכשיו יורדים שורה',
 *   }
 * });
 *
 * @example
 * // Example 2.
 * var myLabelEntity = viewer.entities.add({
 *   label: {
 *     id: 'my label',
 *     text: 'English text'
 *   }
 * });
 * // Set a label's rightToLeft after init
 * Cesium.Label.enableRightToLeftDetection = true;
 * myLabelEntity.text = 'טקסט חדש';
 */
Label.enableRightToLeftDetection = false;

function convertTextToTypes(text, rtlChars) {
  var ltrChars = /[a-zA-Z0-9]/;
  var bracketsChars = /[()[\]{}<>]/;
  var parsedText = [];
  var word = "";
  var lastType = textTypes.LTR;
  var currentType = "";
  var textLength = text.length;
  for (var textIndex = 0; textIndex < textLength; ++textIndex) {
    var character = text.charAt(textIndex);
    if (rtlChars.test(character)) {
      currentType = textTypes.RTL;
    } else if (ltrChars.test(character)) {
      currentType = textTypes.LTR;
    } else if (bracketsChars.test(character)) {
      currentType = textTypes.BRACKETS;
    } else {
      currentType = textTypes.WEAK;
    }

    if (textIndex === 0) {
      lastType = currentType;
    }

    if (lastType === currentType && currentType !== textTypes.BRACKETS) {
      word += character;
    } else {
      if (word !== "") {
        parsedText.push({ Type: lastType, Word: word });
      }
      lastType = currentType;
      word = character;
    }
  }
  parsedText.push({ Type: currentType, Word: word });
  return parsedText;
}

function reverseWord(word) {
  return word.split("").reverse().join("");
}

function spliceWord(result, pointer, word) {
  return result.slice(0, pointer) + word + result.slice(pointer);
}

function reverseBrackets(bracket) {
  switch (bracket) {
    case "(":
      return ")";
    case ")":
      return "(";
    case "[":
      return "]";
    case "]":
      return "[";
    case "{":
      return "}";
    case "}":
      return "{";
    case "<":
      return ">";
    case ">":
      return "<";
  }
}

//To add another language, simply add its Unicode block range(s) to the below regex.
var hebrew = "\u05D0-\u05EA";
var arabic = "\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF";
var rtlChars = new RegExp("[" + hebrew + arabic + "]");

/**
 *
 * @param {String} value the text to parse and reorder
 * @returns {String} the text as rightToLeft direction
 * @private
 */
function reverseRtl(value) {
  var texts = value.split("\n");
  var result = "";
  for (var i = 0; i < texts.length; i++) {
    var text = texts[i];
    // first character of the line is a RTL character, so need to manage different cases
    var rtlDir = rtlChars.test(text.charAt(0));
    var parsedText = convertTextToTypes(text, rtlChars);

    var splicePointer = 0;
    var line = "";
    for (var wordIndex = 0; wordIndex < parsedText.length; ++wordIndex) {
      var subText = parsedText[wordIndex];
      var reverse =
        subText.Type === textTypes.BRACKETS
          ? reverseBrackets(subText.Word)
          : reverseWord(subText.Word);
      if (rtlDir) {
        if (subText.Type === textTypes.RTL) {
          line = reverse + line;
          splicePointer = 0;
        } else if (subText.Type === textTypes.LTR) {
          line = spliceWord(line, splicePointer, subText.Word);
          splicePointer += subText.Word.length;
        } else if (
          subText.Type === textTypes.WEAK ||
          subText.Type === textTypes.BRACKETS
        ) {
          // current word is weak, last one was bracket
          if (
            subText.Type === textTypes.WEAK &&
            parsedText[wordIndex - 1].Type === textTypes.BRACKETS
          ) {
            line = reverse + line;
          }
          // current word is weak or bracket, last one was rtl
          else if (parsedText[wordIndex - 1].Type === textTypes.RTL) {
            line = reverse + line;
            splicePointer = 0;
          }
          // current word is weak or bracket, there is at least one more word
          else if (parsedText.length > wordIndex + 1) {
            // next word is rtl
            if (parsedText[wordIndex + 1].Type === textTypes.RTL) {
              line = reverse + line;
              splicePointer = 0;
            } else {
              line = spliceWord(line, splicePointer, subText.Word);
              splicePointer += subText.Word.length;
            }
          }
          // current word is weak or bracket, and it the last in this line
          else {
            line = spliceWord(line, 0, reverse);
          }
        }
      }
      // ltr line, rtl word
      else if (subText.Type === textTypes.RTL) {
        line = spliceWord(line, splicePointer, reverse);
      }
      // ltr line, ltr word
      else if (subText.Type === textTypes.LTR) {
        line += subText.Word;
        splicePointer = line.length;
      }
      // ltr line, weak or bracket word
      else if (
        subText.Type === textTypes.WEAK ||
        subText.Type === textTypes.BRACKETS
      ) {
        // not first word in line
        if (wordIndex > 0) {
          // last word was rtl
          if (parsedText[wordIndex - 1].Type === textTypes.RTL) {
            // there is at least one more word
            if (parsedText.length > wordIndex + 1) {
              // next word is rtl
              if (parsedText[wordIndex + 1].Type === textTypes.RTL) {
                line = spliceWord(line, splicePointer, reverse);
              } else {
                line += subText.Word;
                splicePointer = line.length;
              }
            } else {
              line += subText.Word;
            }
          } else {
            line += subText.Word;
            splicePointer = line.length;
          }
        } else {
          line += subText.Word;
          splicePointer = line.length;
        }
      }
    }

    result += line;
    if (i < texts.length - 1) {
      result += "\n";
    }
  }
  return result;
}
export default Label;
