import BoundingRectangle from "../Core/BoundingRectangle.js";
import Cartesian2 from "../Core/Cartesian2.js";
import Color from "../Core/Color.js";
import defaultValue from "../Core/defaultValue.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
import Matrix4 from "../Core/Matrix4.js";
import writeTextToCanvas from "../Core/writeTextToCanvas.js";
import bitmapSDF from "bitmap-sdf";
import BillboardCollection from "./BillboardCollection.js";
import BlendOption from "./BlendOption.js";
import HeightReference from "./HeightReference.js";
import HorizontalOrigin from "./HorizontalOrigin.js";
import Label from "./Label.js";
import LabelStyle from "./LabelStyle.js";
import SDFSettings from "./SDFSettings.js";
import TextureAtlas from "./TextureAtlas.js";
import VerticalOrigin from "./VerticalOrigin.js";
import GraphemeSplitter from "grapheme-splitter";
// A glyph represents a single character in a particular label.  It may or may
// not have a billboard, depending on whether the texture info has an index into
// the the label collection's texture atlas.  Invisible characters have no texture, and
// no billboard.  However, it always has a valid dimensions object.
function Glyph() {
  this.textureInfo = undefined;
  this.dimensions = undefined;
  this.billboard = undefined;
}
// GlyphTextureInfo represents a single character, drawn in a particular style,
// shared and reference counted across all labels.  It may or may not have an
// index into the label collection's texture atlas, depending on whether the character
// has both width and height, but it always has a valid dimensions object.
function GlyphTextureInfo(labelCollection, index, dimensions) {
  this.labelCollection = labelCollection;
  this.index = index;
  this.dimensions = dimensions;
}
// Traditionally, leading is %20 of the font size.
const defaultLineSpacingPercent = 1.2;
const whitePixelCanvasId = "ID_WHITE_PIXEL";
const whitePixelSize = new Cartesian2(4, 4);
const whitePixelBoundingRegion = new BoundingRectangle(1, 1, 1, 1);
function addWhitePixelCanvas(textureAtlas) {
  const canvas = document.createElement("canvas");
  canvas.width = whitePixelSize.x;
  canvas.height = whitePixelSize.y;
  const context2D = canvas.getContext("2d");
  context2D.fillStyle = "#fff";
  context2D.fillRect(0, 0, canvas.width, canvas.height);
  // Canvas operations take a frame to draw. Use the asynchronous add function which resolves a promise and allows the draw to complete,
  // but there's no need to wait on the promise before operation can continue
  textureAtlas.addImage(whitePixelCanvasId, canvas);
}
// reusable object for calling writeTextToCanvas
const writeTextToCanvasParameters = {};
function createGlyphCanvas(
  character,
  font,
  fillColor,
  outlineColor,
  outlineWidth,
  style,
  verticalOrigin
) {
  writeTextToCanvasParameters.font = font;
  writeTextToCanvasParameters.fillColor = fillColor;
  writeTextToCanvasParameters.strokeColor = outlineColor;
  writeTextToCanvasParameters.strokeWidth = outlineWidth;
  // Setting the padding to something bigger is necessary to get enough space for the outlining.
  writeTextToCanvasParameters.padding = SDFSettings.PADDING;
  if (verticalOrigin === VerticalOrigin.CENTER) {
    writeTextToCanvasParameters.textBaseline = "middle";
  } else if (verticalOrigin === VerticalOrigin.TOP) {
    writeTextToCanvasParameters.textBaseline = "top";
  } else {
    // VerticalOrigin.BOTTOM and VerticalOrigin.BASELINE
    writeTextToCanvasParameters.textBaseline = "bottom";
  }
  writeTextToCanvasParameters.fill =
    style === LabelStyle.FILL || style === LabelStyle.FILL_AND_OUTLINE;
  writeTextToCanvasParameters.stroke =
    style === LabelStyle.OUTLINE || style === LabelStyle.FILL_AND_OUTLINE;
  writeTextToCanvasParameters.backgroundColor = Color.BLACK;
  return writeTextToCanvas(character, writeTextToCanvasParameters);
}
function unbindGlyph(labelCollection, glyph) {
  glyph.textureInfo = undefined;
  glyph.dimensions = undefined;
  const billboard = glyph.billboard;
  if (defined(billboard)) {
    billboard.show = false;
    billboard.image = undefined;
    if (defined(billboard._removeCallbackFunc)) {
      billboard._removeCallbackFunc();
      billboard._removeCallbackFunc = undefined;
    }
    labelCollection._spareBillboards.push(billboard);
    glyph.billboard = undefined;
  }
}
function addGlyphToTextureAtlas(textureAtlas, id, canvas, glyphTextureInfo) {
  glyphTextureInfo.index = textureAtlas.addImageSync(id, canvas);
}
const splitter = new GraphemeSplitter();
function rebindAllGlyphs(labelCollection, label) {
  const text = label._renderedText;
  const graphemes = splitter.splitGraphemes(text);
  const textLength = graphemes.length;
  const glyphs = label._glyphs;
  const glyphsLength = glyphs.length;
  let glyph;
  let glyphIndex;
  let textIndex;
  // Compute a font size scale relative to the sdf font generated size.
  label._relativeSize = label._fontSize / SDFSettings.FONT_SIZE;
  // if we have more glyphs than needed, unbind the extras.
  if (textLength < glyphsLength) {
    for (glyphIndex = textLength; glyphIndex < glyphsLength; ++glyphIndex) {
      unbindGlyph(labelCollection, glyphs[glyphIndex]);
    }
  }
  // presize glyphs to match the new text length
  glyphs.length = textLength;
  const showBackground =
    label._showBackground && text.split("\n").join("").length > 0;
  let backgroundBillboard = label._backgroundBillboard;
  const backgroundBillboardCollection =
    labelCollection._backgroundBillboardCollection;
  if (!showBackground) {
    if (defined(backgroundBillboard)) {
      backgroundBillboardCollection.remove(backgroundBillboard);
      label._backgroundBillboard = backgroundBillboard = undefined;
    }
  } else {
    if (!defined(backgroundBillboard)) {
      backgroundBillboard = backgroundBillboardCollection.add({
        collection: labelCollection,
        image: whitePixelCanvasId,
        imageSubRegion: whitePixelBoundingRegion,
      });
      label._backgroundBillboard = backgroundBillboard;
    }
    backgroundBillboard.color = label._backgroundColor;
    backgroundBillboard.show = label._show;
    backgroundBillboard.position = label._position;
    backgroundBillboard.eyeOffset = label._eyeOffset;
    backgroundBillboard.pixelOffset = label._pixelOffset;
    backgroundBillboard.horizontalOrigin = HorizontalOrigin.LEFT;
    backgroundBillboard.verticalOrigin = label._verticalOrigin;
    backgroundBillboard.heightReference = label._heightReference;
    backgroundBillboard.scale = label.totalScale;
    backgroundBillboard.pickPrimitive = label;
    backgroundBillboard.id = label._id;
    backgroundBillboard.translucencyByDistance = label._translucencyByDistance;
    backgroundBillboard.pixelOffsetScaleByDistance =
      label._pixelOffsetScaleByDistance;
    backgroundBillboard.scaleByDistance = label._scaleByDistance;
    backgroundBillboard.distanceDisplayCondition =
      label._distanceDisplayCondition;
    backgroundBillboard.disableDepthTestDistance =
      label._disableDepthTestDistance;
  }
  const glyphTextureCache = labelCollection._glyphTextureCache;
  // walk the text looking for new characters (creating new glyphs for each)
  // or changed characters (rebinding existing glyphs)
  for (textIndex = 0; textIndex < textLength; ++textIndex) {
    const character = graphemes[textIndex];
    const verticalOrigin = label._verticalOrigin;
    const id = JSON.stringify([
      character,
      label._fontFamily,
      label._fontStyle,
      label._fontWeight,
      +verticalOrigin,
    ]);
    let glyphTextureInfo = glyphTextureCache[id];
    if (!defined(glyphTextureInfo)) {
      const glyphFont = `${label._fontStyle} ${label._fontWeight} ${SDFSettings.FONT_SIZE}px ${label._fontFamily}`;
      const canvas = createGlyphCanvas(
        character,
        glyphFont,
        Color.WHITE,
        Color.WHITE,
        0.0,
        LabelStyle.FILL,
        verticalOrigin
      );
      glyphTextureInfo = new GlyphTextureInfo(
        labelCollection,
        -1,
        canvas.dimensions
      );
      glyphTextureCache[id] = glyphTextureInfo;
      if (canvas.width > 0 && canvas.height > 0) {
        const sdfValues = bitmapSDF(canvas, {
          cutoff: SDFSettings.CUTOFF,
          radius: SDFSettings.RADIUS,
        });
        // Context is originally created in writeTextToCanvas()
        const ctx = canvas.getContext("2d");
        const canvasWidth = canvas.width;
        const canvasHeight = canvas.height;
        const imgData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
        for (let i = 0; i < canvasWidth; i++) {
          for (let j = 0; j < canvasHeight; j++) {
            const baseIndex = j * canvasWidth + i;
            const alpha = sdfValues[baseIndex] * 255;
            const imageIndex = baseIndex * 4;
            imgData.data[imageIndex + 0] = alpha;
            imgData.data[imageIndex + 1] = alpha;
            imgData.data[imageIndex + 2] = alpha;
            imgData.data[imageIndex + 3] = alpha;
          }
        }
        ctx.putImageData(imgData, 0, 0);
        if (character !== " ") {
          addGlyphToTextureAtlas(
            labelCollection._textureAtlas,
            id,
            canvas,
            glyphTextureInfo
          );
        }
      }
    }
    glyph = glyphs[textIndex];
    if (defined(glyph)) {
      // clean up leftover information from the previous glyph
      if (glyphTextureInfo.index === -1) {
        // no texture, and therefore no billboard, for this glyph.
        // so, completely unbind glyph.
        unbindGlyph(labelCollection, glyph);
      } else if (defined(glyph.textureInfo)) {
        // we have a texture and billboard.  If we had one before, release
        // our reference to that texture info, but reuse the billboard.
        glyph.textureInfo = undefined;
      }
    } else {
      // create a glyph object
      glyph = new Glyph();
      glyphs[textIndex] = glyph;
    }
    glyph.textureInfo = glyphTextureInfo;
    glyph.dimensions = glyphTextureInfo.dimensions;
    // if we have a texture, configure the existing billboard, or obtain one
    if (glyphTextureInfo.index !== -1) {
      let billboard = glyph.billboard;
      const spareBillboards = labelCollection._spareBillboards;
      if (!defined(billboard)) {
        if (spareBillboards.length > 0) {
          billboard = spareBillboards.pop();
        } else {
          billboard = labelCollection._billboardCollection.add({
            collection: labelCollection,
          });
          billboard._labelDimensions = new Cartesian2();
          billboard._labelTranslate = new Cartesian2();
        }
        glyph.billboard = billboard;
      }
      billboard.show = label._show;
      billboard.position = label._position;
      billboard.eyeOffset = label._eyeOffset;
      billboard.pixelOffset = label._pixelOffset;
      billboard.horizontalOrigin = HorizontalOrigin.LEFT;
      billboard.verticalOrigin = label._verticalOrigin;
      billboard.heightReference = label._heightReference;
      billboard.scale = label.totalScale;
      billboard.pickPrimitive = label;
      billboard.id = label._id;
      billboard.image = id;
      billboard.translucencyByDistance = label._translucencyByDistance;
      billboard.pixelOffsetScaleByDistance = label._pixelOffsetScaleByDistance;
      billboard.scaleByDistance = label._scaleByDistance;
      billboard.distanceDisplayCondition = label._distanceDisplayCondition;
      billboard.disableDepthTestDistance = label._disableDepthTestDistance;
      billboard._batchIndex = label._batchIndex;
      billboard.outlineColor = label.outlineColor;
      if (label.style === LabelStyle.FILL_AND_OUTLINE) {
        billboard.color = label._fillColor;
        billboard.outlineWidth = label.outlineWidth;
      } else if (label.style === LabelStyle.FILL) {
        billboard.color = label._fillColor;
        billboard.outlineWidth = 0.0;
      } else if (label.style === LabelStyle.OUTLINE) {
        billboard.color = Color.TRANSPARENT;
        billboard.outlineWidth = label.outlineWidth;
      }
    }
  }
  // changing glyphs will cause the position of the
  // glyphs to change, since different characters have different widths
  label._repositionAllGlyphs = true;
}
function calculateWidthOffset(lineWidth, horizontalOrigin, backgroundPadding) {
  if (horizontalOrigin === HorizontalOrigin.CENTER) {
    return -lineWidth / 2;
  } else if (horizontalOrigin === HorizontalOrigin.RIGHT) {
    return -(lineWidth + backgroundPadding.x);
  }
  return backgroundPadding.x;
}
// reusable Cartesian2 instances
const glyphPixelOffset = new Cartesian2();
const scratchBackgroundPadding = new Cartesian2();
function repositionAllGlyphs(label) {
  const glyphs = label._glyphs;
  const text = label._renderedText;
  let glyph;
  let dimensions;
  let lastLineWidth = 0;
  let maxLineWidth = 0;
  const lineWidths = [];
  let maxGlyphDescent = Number.NEGATIVE_INFINITY;
  let maxGlyphY = 0;
  let numberOfLines = 1;
  let glyphIndex;
  const glyphLength = glyphs.length;
  const backgroundBillboard = label._backgroundBillboard;
  const backgroundPadding = Cartesian2.clone(
    defined(backgroundBillboard) ? label._backgroundPadding : Cartesian2.ZERO,
    scratchBackgroundPadding
  );
  // We need to scale the background padding, which is specified in pixels by the inverse of the relative size so it is scaled properly.
  backgroundPadding.x /= label._relativeSize;
  backgroundPadding.y /= label._relativeSize;
  for (glyphIndex = 0; glyphIndex < glyphLength; ++glyphIndex) {
    if (text.charAt(glyphIndex) === "\n") {
      lineWidths.push(lastLineWidth);
      ++numberOfLines;
      lastLineWidth = 0;
    } else {
      glyph = glyphs[glyphIndex];
      dimensions = glyph.dimensions;
      maxGlyphY = Math.max(maxGlyphY, dimensions.height - dimensions.descent);
      maxGlyphDescent = Math.max(maxGlyphDescent, dimensions.descent);
      //Computing the line width must also account for the kerning that occurs between letters.
      lastLineWidth += dimensions.width - dimensions.minx;
      if (glyphIndex < glyphLength - 1) {
        lastLineWidth += glyphs[glyphIndex + 1].dimensions.minx;
      }
      maxLineWidth = Math.max(maxLineWidth, lastLineWidth);
    }
  }
  lineWidths.push(lastLineWidth);
  const maxLineHeight = maxGlyphY + maxGlyphDescent;
  const scale = label.totalScale;
  const horizontalOrigin = label._horizontalOrigin;
  const verticalOrigin = label._verticalOrigin;
  let lineIndex = 0;
  let lineWidth = lineWidths[lineIndex];
  let widthOffset = calculateWidthOffset(
    lineWidth,
    horizontalOrigin,
    backgroundPadding
  );
  const lineSpacing =
    (defined(label._lineHeight)
      ? label._lineHeight
      : defaultLineSpacingPercent * label._fontSize) / label._relativeSize;
  const otherLinesHeight = lineSpacing * (numberOfLines - 1);
  let totalLineWidth = maxLineWidth;
  let totalLineHeight = maxLineHeight + otherLinesHeight;
  if (defined(backgroundBillboard)) {
    totalLineWidth += backgroundPadding.x * 2;
    totalLineHeight += backgroundPadding.y * 2;
    backgroundBillboard._labelHorizontalOrigin = horizontalOrigin;
  }
  glyphPixelOffset.x = widthOffset * scale;
  glyphPixelOffset.y = 0;
  let firstCharOfLine = true;
  let lineOffsetY = 0;
  for (glyphIndex = 0; glyphIndex < glyphLength; ++glyphIndex) {
    if (text.charAt(glyphIndex) === "\n") {
      ++lineIndex;
      lineOffsetY += lineSpacing;
      lineWidth = lineWidths[lineIndex];
      widthOffset = calculateWidthOffset(
        lineWidth,
        horizontalOrigin,
        backgroundPadding
      );
      glyphPixelOffset.x = widthOffset * scale;
      firstCharOfLine = true;
    } else {
      glyph = glyphs[glyphIndex];
      dimensions = glyph.dimensions;
      if (verticalOrigin === VerticalOrigin.TOP) {
        glyphPixelOffset.y =
          dimensions.height - maxGlyphY - backgroundPadding.y;
        glyphPixelOffset.y += SDFSettings.PADDING;
      } else if (verticalOrigin === VerticalOrigin.CENTER) {
        glyphPixelOffset.y =
          (otherLinesHeight + dimensions.height - maxGlyphY) / 2;
      } else if (verticalOrigin === VerticalOrigin.BASELINE) {
        glyphPixelOffset.y = otherLinesHeight;
        glyphPixelOffset.y -= SDFSettings.PADDING;
      } else {
        // VerticalOrigin.BOTTOM
        glyphPixelOffset.y =
          otherLinesHeight + maxGlyphDescent + backgroundPadding.y;
        glyphPixelOffset.y -= SDFSettings.PADDING;
      }
      glyphPixelOffset.y =
        (glyphPixelOffset.y - dimensions.descent - lineOffsetY) * scale;
      // Handle any offsets for the first character of the line since the bounds might not be right on the bottom left pixel.
      if (firstCharOfLine) {
        glyphPixelOffset.x -= SDFSettings.PADDING * scale;
        firstCharOfLine = false;
      }
      if (defined(glyph.billboard)) {
        glyph.billboard._setTranslate(glyphPixelOffset);
        glyph.billboard._labelDimensions.x = totalLineWidth;
        glyph.billboard._labelDimensions.y = totalLineHeight;
        glyph.billboard._labelHorizontalOrigin = horizontalOrigin;
      }
      //Compute the next x offset taking into account the kerning performed
      //on both the current letter as well as the next letter to be drawn
      //as well as any applied scale.
      if (glyphIndex < glyphLength - 1) {
        const nextGlyph = glyphs[glyphIndex + 1];
        glyphPixelOffset.x +=
          (dimensions.width - dimensions.minx + nextGlyph.dimensions.minx) *
          scale;
      }
    }
  }
  if (defined(backgroundBillboard) && text.split("\n").join("").length > 0) {
    if (horizontalOrigin === HorizontalOrigin.CENTER) {
      widthOffset = -maxLineWidth / 2 - backgroundPadding.x;
    } else if (horizontalOrigin === HorizontalOrigin.RIGHT) {
      widthOffset = -(maxLineWidth + backgroundPadding.x * 2);
    } else {
      widthOffset = 0;
    }
    glyphPixelOffset.x = widthOffset * scale;
    if (verticalOrigin === VerticalOrigin.TOP) {
      glyphPixelOffset.y = maxLineHeight - maxGlyphY - maxGlyphDescent;
    } else if (verticalOrigin === VerticalOrigin.CENTER) {
      glyphPixelOffset.y = (maxLineHeight - maxGlyphY) / 2 - maxGlyphDescent;
    } else if (verticalOrigin === VerticalOrigin.BASELINE) {
      glyphPixelOffset.y = -backgroundPadding.y - maxGlyphDescent;
    } else {
      // VerticalOrigin.BOTTOM
      glyphPixelOffset.y = 0;
    }
    glyphPixelOffset.y = glyphPixelOffset.y * scale;
    backgroundBillboard.width = totalLineWidth;
    backgroundBillboard.height = totalLineHeight;
    backgroundBillboard._setTranslate(glyphPixelOffset);
    backgroundBillboard._labelTranslate = Cartesian2.clone(
      glyphPixelOffset,
      backgroundBillboard._labelTranslate
    );
  }
  if (label.heightReference === HeightReference.CLAMP_TO_GROUND) {
    for (glyphIndex = 0; glyphIndex < glyphLength; ++glyphIndex) {
      glyph = glyphs[glyphIndex];
      const billboard = glyph.billboard;
      if (defined(billboard)) {
        billboard._labelTranslate = Cartesian2.clone(
          glyphPixelOffset,
          billboard._labelTranslate
        );
      }
    }
  }
}
function destroyLabel(labelCollection, label) {
  const glyphs = label._glyphs;
  for (let i = 0, len = glyphs.length; i < len; ++i) {
    unbindGlyph(labelCollection, glyphs[i]);
  }
  if (defined(label._backgroundBillboard)) {
    labelCollection._backgroundBillboardCollection.remove(
      label._backgroundBillboard
    );
    label._backgroundBillboard = undefined;
  }
  label._labelCollection = undefined;
  if (defined(label._removeCallbackFunc)) {
    label._removeCallbackFunc();
  }
  destroyObject(label);
}
/**
 * A renderable collection of labels.  Labels are viewport-aligned text positioned in the 3D scene.
 * Each label can have a different font, color, scale, etc.
 * 
 * 

* Draws the bounding sphere for each draw command in the primitive. *
* * @type {boolean} * * @default false */ this.debugShowBoundingVolume = defaultValue( options.debugShowBoundingVolume, false ); /** * The label blending option. The default is used for rendering both opaque and translucent labels. * However, if either all of the labels are completely opaque or all are completely translucent, * setting the technique to BlendOption.OPAQUE or BlendOption.TRANSLUCENT can improve * performance by up to 2x. * @type {BlendOption} * @default BlendOption.OPAQUE_AND_TRANSLUCENT */ this.blendOption = defaultValue( options.blendOption, BlendOption.OPAQUE_AND_TRANSLUCENT ); } Object.defineProperties(LabelCollection.prototype, { /** * Returns the number of labels in this collection. This is commonly used with * {@link LabelCollection#get} to iterate over all the labels * in the collection. * @memberof LabelCollection.prototype * @type {number} */ length: { get: function () { return this._labels.length; }, }, }); /** * Creates and adds a label with the specified initial properties to the collection. * The added label is returned so it can be modified or removed from the collection later. * * @param {object} [options] A template describing the label's properties as shown in Example 1. * @returns {Label} The label that was added to the collection. * * @performance Callingadd is expected constant time.  However, the collection's vertex buffer
 * is rewritten; this operations is O(n) and also incurs
 * CPU to GPU overhead.  For best performance, add as many billboards as possible before
 * calling update.
 *
 * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
 *
 *
 * @example
 * // Example 1:  Add a label, specifying all the default values.
 * const l = labels.add({
 *   show : true,
 *   position : Cesium.Cartesian3.ZERO,
 *   text : '',
 *   font : '30px sans-serif',
 *   fillColor : Cesium.Color.WHITE,
 *   outlineColor : Cesium.Color.BLACK,
 *   outlineWidth : 1.0,
 *   showBackground : false,
 *   backgroundColor : new Cesium.Color(0.165, 0.165, 0.165, 0.8),
 *   backgroundPadding : new Cesium.Cartesian2(7, 5),
 *   style : Cesium.LabelStyle.FILL,
 *   pixelOffset : Cesium.Cartesian2.ZERO,
 *   eyeOffset : Cesium.Cartesian3.ZERO,
 *   horizontalOrigin : Cesium.HorizontalOrigin.LEFT,
 *   verticalOrigin : Cesium.VerticalOrigin.BASELINE,
 *   scale : 1.0,
 *   translucencyByDistance : undefined,
 *   pixelOffsetScaleByDistance : undefined,
 *   heightReference : HeightReference.NONE,
 *   distanceDisplayCondition : undefined
 * });
 *
 * @example
 * // Example 2:  Specify only the label's cartographic position,
 * // text, and font.
 * const l = labels.add({
 *   position : Cesium.Cartesian3.fromRadians(longitude, latitude, height),
 *   text : 'Hello World',
 *   font : '24px Helvetica',
 * });
 *
 * @see LabelCollection#remove
 * @see LabelCollection#removeAll
 */
LabelCollection.prototype.add = function (options) {
  const label = new Label(options, this);
  this._labels.push(label);
  this._labelsToUpdate.push(label);
  return label;
};
/**
 * Removes a label from the collection.  Once removed, a label is no longer usable.
 *
 * @param {Label} label The label to remove.
 * @returns {boolean} true if the label was removed; false if the label was not found in the collection.
 *
 * @performance Calling remove is expected constant time.  However, the collection's vertex buffer
 * is rewritten - an O(n) operation that also incurs CPU to GPU overhead.  For
 * best performance, remove as many labels as possible before calling update.
 * If you intend to temporarily hide a label, it is usually more efficient to call
 * {@link Label#show} instead of removing and re-adding the label.
 *
 * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
 *
 *
 * @example
 * const l = labels.add(...);
 * labels.remove(l);  // Returns true
 *
 * @see LabelCollection#add
 * @see LabelCollection#removeAll
 * @see Label#show
 */
LabelCollection.prototype.remove = function (label) {
  if (defined(label) && label._labelCollection === this) {
    const index = this._labels.indexOf(label);
    if (index !== -1) {
      this._labels.splice(index, 1);
      destroyLabel(this, label);
      return true;
    }
  }
  return false;
};
/**
 * Removes all labels from the collection.
 *
 * @performance O(n).  It is more efficient to remove all the labels
 * from a collection and then add new ones than to create a new collection entirely.
 *
 * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
 *
 *
 * @example
 * labels.add(...);
 * labels.add(...);
 * labels.removeAll();
 *
 * @see LabelCollection#add
 * @see LabelCollection#remove
 */
LabelCollection.prototype.removeAll = function () {
  const labels = this._labels;
  for (let i = 0, len = labels.length; i < len; ++i) {
    destroyLabel(this, labels[i]);
  }
  labels.length = 0;
};
/**
 * Check whether this collection contains a given label.
 *
 * @param {Label} label The label to check for.
 * @returns {boolean} true if this collection contains the label, false otherwise.
 *
 * @see LabelCollection#get
 *
 */
LabelCollection.prototype.contains = function (label) {
  return defined(label) && label._labelCollection === this;
};
/**
 * Returns the label in the collection at the specified index.  Indices are zero-based
 * and increase as labels are added.  Removing a label shifts all labels after
 * it to the left, changing their indices.  This function is commonly used with
 * {@link LabelCollection#length} to iterate over all the labels
 * in the collection.
 *
 * @param {number} index The zero-based index of the billboard.
 *
 * @returns {Label} The label at the specified index.
 *
 * @performance Expected constant time.  If labels were removed from the collection and
 * {@link Scene#render} was not called, an implicit O(n)
 * operation is performed.
 *
 * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
 *
 *
 * @example
 * // Toggle the show property of every label in the collection
 * const len = labels.length;
 * for (let i = 0; i < len; ++i) {
 *   const l = billboards.get(i);
 *   l.show = !l.show;
 * }
 *
 * @see LabelCollection#length
 */
LabelCollection.prototype.get = function (index) {
  //>>includeStart('debug', pragmas.debug);
  if (!defined(index)) {
    throw new DeveloperError("index is required.");
  }
  //>>includeEnd('debug');
  return this._labels[index];
};
/**
 * @private
 *
 */
LabelCollection.prototype.update = function (frameState) {
  if (!this.show) {
    return;
  }
  const billboardCollection = this._billboardCollection;
  const backgroundBillboardCollection = this._backgroundBillboardCollection;
  billboardCollection.modelMatrix = this.modelMatrix;
  billboardCollection.debugShowBoundingVolume = this.debugShowBoundingVolume;
  backgroundBillboardCollection.modelMatrix = this.modelMatrix;
  backgroundBillboardCollection.debugShowBoundingVolume = this.debugShowBoundingVolume;
  const context = frameState.context;
  if (!defined(this._textureAtlas)) {
    this._textureAtlas = new TextureAtlas({
      context: context,
    });
    billboardCollection.textureAtlas = this._textureAtlas;
  }
  if (!defined(this._backgroundTextureAtlas)) {
    this._backgroundTextureAtlas = new TextureAtlas({
      context: context,
      initialSize: whitePixelSize,
    });
    backgroundBillboardCollection.textureAtlas = this._backgroundTextureAtlas;
    addWhitePixelCanvas(this._backgroundTextureAtlas);
  }
  const len = this._labelsToUpdate.length;
  for (let i = 0; i < len; ++i) {
    const label = this._labelsToUpdate[i];
    if (label.isDestroyed()) {
      continue;
    }
    const preUpdateGlyphCount = label._glyphs.length;
    if (label._rebindAllGlyphs) {
      rebindAllGlyphs(this, label);
      label._rebindAllGlyphs = false;
    }
    if (label._repositionAllGlyphs) {
      repositionAllGlyphs(label);
      label._repositionAllGlyphs = false;
    }
    const glyphCountDifference = label._glyphs.length - preUpdateGlyphCount;
    this._totalGlyphCount += glyphCountDifference;
  }
  const blendOption =
    backgroundBillboardCollection.length > 0
      ? BlendOption.TRANSLUCENT
      : this.blendOption;
  billboardCollection.blendOption = blendOption;
  backgroundBillboardCollection.blendOption = blendOption;
  billboardCollection._highlightColor = this._highlightColor;
  backgroundBillboardCollection._highlightColor = this._highlightColor;
  this._labelsToUpdate.length = 0;
  backgroundBillboardCollection.update(frameState);
  billboardCollection.update(frameState);
};
/**
 * Returns true if this object was destroyed; otherwise, false.
 * isDestroyed will result in a {@link DeveloperError} exception.
 *
 * @returns {boolean} True if this object was destroyed; otherwise, false.
 *
 * @see LabelCollection#destroy
 */
LabelCollection.prototype.isDestroyed = function () {
  return false;
};
/**
 * Destroys the WebGL resources held by this object.  Destroying an object allows for deterministic
 * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
 * isDestroyed will result in a {@link DeveloperError} exception.  Therefore,
 * assign the return value (undefined) to the object as done in the example.
 *
 * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
 *
 *
 * @example
 * labels = labels && labels.destroy();
 *
 * @see LabelCollection#isDestroyed
 */
LabelCollection.prototype.destroy = function () {
  this.removeAll();
  this._billboardCollection = this._billboardCollection.destroy();
  this._textureAtlas = this._textureAtlas && this._textureAtlas.destroy();
  this._backgroundBillboardCollection = this._backgroundBillboardCollection.destroy();
  this._backgroundTextureAtlas =
    this._backgroundTextureAtlas && this._backgroundTextureAtlas.destroy();
  return destroyObject(this);
};
export default LabelCollection;