import * as Cesium from "cesium";

const viewer = new Cesium.Viewer("cesiumContainer", {
  selectionIndicator: false,
});

// Add labels clustered at the same location
const numBillboards = 30;
for (let i = 0; i < numBillboards; ++i) {
  const position = Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883);
  viewer.entities.add({
    position: position,
    billboard: {
      image: "../images/facility.gif",
      scale: 2.5,
    },
    label: {
      text: `Label${i}`,
      show: false,
    },
  });
}

const scene = viewer.scene;
const camera = scene.camera;
const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas);

handler.setInputAction(function (movement) {
  // Star burst on left mouse click.
  starBurst(movement.position);
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

handler.setInputAction(function (movement) {
  // Remove the star burst when the mouse exits the circle or show the label of the billboard the mouse is hovering over.
  updateStarBurst(movement.endPosition);
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

camera.moveStart.addEventListener(function () {
  // Reset the star burst on camera move because the lines from the center
  // because the line end points rely on the screen space positions of the billboards.
  undoStarBurst();
});

// State saved across mouse click and move events
const starBurstState = {
  enabled: false,
  pickedEntities: undefined,
  billboardEyeOffsets: undefined,
  labelEyeOffsets: undefined,
  linePrimitive: undefined,
  radius: undefined,
  center: undefined,
  pixelPadding: 10.0,
  angleStart: 0.0,
  angleEnd: Cesium.Math.PI,
  maxDimension: undefined,
};

function offsetBillboard(
  entity,
  entityPosition,
  angle,
  magnitude,
  lines,
  billboardEyeOffsets,
  labelEyeOffsets,
) {
  const x = magnitude * Math.cos(angle);
  const y = magnitude * Math.sin(angle);

  const offset = new Cesium.Cartesian2(x, y);

  const drawingBufferWidth = scene.drawingBufferWidth;
  const drawingBufferHeight = scene.drawingBufferHeight;
  const pixelRatio = scene.pixelRatio;

  const diff = Cesium.Cartesian3.subtract(
    entityPosition,
    camera.positionWC,
    new Cesium.Cartesian3(),
  );
  const distance = Cesium.Cartesian3.dot(camera.directionWC, diff);

  const dimensions = camera.frustum.getPixelDimensions(
    drawingBufferWidth,
    drawingBufferHeight,
    distance,
    pixelRatio,
    new Cesium.Cartesian2(),
  );
  Cesium.Cartesian2.multiplyByScalar(
    offset,
    Cesium.Cartesian2.maximumComponent(dimensions),
    offset,
  );

  let labelOffset;
  const billboardOffset = entity.billboard.eyeOffset;

  const eyeOffset = new Cesium.Cartesian3(offset.x, offset.y, 0.0);
  entity.billboard.eyeOffset = eyeOffset;
  if (Cesium.defined(entity.label)) {
    labelOffset = entity.label.eyeOffset;
    entity.label.eyeOffset = new Cesium.Cartesian3(offset.x, offset.y, -10.0);
  }

  const endPoint = Cesium.Matrix4.multiplyByPoint(
    camera.viewMatrix,
    entityPosition,
    new Cesium.Cartesian3(),
  );
  Cesium.Cartesian3.add(eyeOffset, endPoint, endPoint);
  Cesium.Matrix4.multiplyByPoint(camera.inverseViewMatrix, endPoint, endPoint);
  lines.push(endPoint);

  billboardEyeOffsets.push(billboardOffset);
  labelEyeOffsets.push(labelOffset);
}

function starBurst(mousePosition) {
  if (Cesium.defined(starBurstState.pickedEntities)) {
    return;
  }

  const pickedObjects = scene.drillPick(mousePosition);
  if (!Cesium.defined(pickedObjects) || pickedObjects.length < 2) {
    return;
  }

  const billboardEntities = [];
  let length = pickedObjects.length;
  let i;

  for (i = 0; i < length; ++i) {
    const pickedObject = pickedObjects[i];
    if (pickedObject.primitive instanceof Cesium.Billboard) {
      billboardEntities.push(pickedObject);
    }
  }

  if (billboardEntities.length === 0) {
    return;
  }

  const pickedEntities = (starBurstState.pickedEntities = []);
  const billboardEyeOffsets = (starBurstState.billboardEyeOffsets = []);
  const labelEyeOffsets = (starBurstState.labelEyeOffsets = []);
  const lines = [];
  starBurstState.maxDimension = Number.NEGATIVE_INFINITY;

  const angleStart = starBurstState.angleStart;
  const angleEnd = starBurstState.angleEnd;

  let angle = angleStart;
  let angleIncrease;
  let magnitude;
  let magIncrease;
  let maxDimension;

  // Drill pick gets all of the entities under the mouse pointer.
  // Find the billboards and set their pixel offsets in a circle pattern.
  length = billboardEntities.length;
  i = 0;
  while (i < length) {
    let object = billboardEntities[i];
    if (pickedEntities.length === 0) {
      starBurstState.center = Cesium.Cartesian3.clone(
        object.primitive.position,
      );
    }

    if (!Cesium.defined(angleIncrease)) {
      const width = object.primitive.width;
      const height = object.primitive.height;
      maxDimension =
        Math.max(width, height) * object.primitive.scale +
        starBurstState.pixelPadding;
      magnitude = maxDimension + maxDimension * 0.5;
      magIncrease = magnitude;
      angleIncrease = maxDimension / magnitude;
    }

    offsetBillboard(
      object.id,
      object.primitive.position,
      angle,
      magnitude,
      lines,
      billboardEyeOffsets,
      labelEyeOffsets,
    );
    pickedEntities.push(object);

    const reflectedAngle = angleEnd - angle;
    if (
      i + 1 < length &&
      reflectedAngle - angleIncrease * 0.5 > angle + angleIncrease * 0.5
    ) {
      object = billboardEntities[++i];
      offsetBillboard(
        object.id,
        object.primitive.position,
        reflectedAngle,
        magnitude,
        lines,
        billboardEyeOffsets,
        labelEyeOffsets,
      );
      pickedEntities.push(object);
    }

    angle += angleIncrease;
    if (reflectedAngle - angleIncrease * 0.5 < angle + angleIncrease * 0.5) {
      magnitude += magIncrease;
      angle = angleStart;
      angleIncrease = maxDimension / magnitude;
    }

    ++i;
  }

  // Add lines from the pick center out to the translated billboard.
  const instances = [];
  length = lines.length;
  for (i = 0; i < length; ++i) {
    const pickedEntity = pickedEntities[i];
    starBurstState.maxDimension = Math.max(
      pickedEntity.primitive.width,
      pickedEntity.primitive.height,
      starBurstState.maxDimension,
    );

    instances.push(
      new Cesium.GeometryInstance({
        geometry: new Cesium.SimplePolylineGeometry({
          positions: [starBurstState.center, lines[i]],
          arcType: Cesium.ArcType.NONE,
          granularity: Cesium.Math.PI_OVER_FOUR,
        }),
        attributes: {
          color: Cesium.ColorGeometryInstanceAttribute.fromColor(
            Cesium.Color.WHITE,
          ),
        },
      }),
    );
  }

  starBurstState.linePrimitive = scene.primitives.add(
    new Cesium.Primitive({
      geometryInstances: instances,
      appearance: new Cesium.PerInstanceColorAppearance({
        flat: true,
        translucent: false,
      }),
      asynchronous: false,
    }),
  );

  viewer.selectedEntity = undefined;
  starBurstState.radius = magnitude + magIncrease;
}

function updateStarBurst(mousePosition) {
  if (!Cesium.defined(starBurstState.pickedEntities)) {
    return;
  }

  if (!starBurstState.enabled) {
    // For some reason we get a mousemove event on click, so
    // do not show a label on the first event.
    starBurstState.enabled = true;
    return;
  }

  // Remove the star burst if the mouse exits the screen space circle.
  // If the mouse is inside the circle, show the label of the billboard the mouse is hovering over.
  const screenPosition = Cesium.SceneTransforms.worldToWindowCoordinates(
    scene,
    starBurstState.center,
  );
  const fromCenter = Cesium.Cartesian2.subtract(
    mousePosition,
    screenPosition,
    new Cesium.Cartesian2(),
  );
  const radius = starBurstState.radius;

  if (
    Cesium.Cartesian2.magnitudeSquared(fromCenter) > radius * radius ||
    fromCenter.y >
      3.0 * (starBurstState.maxDimension + starBurstState.pixelPadding)
  ) {
    undoStarBurst();
  } else {
    showLabels(mousePosition);
  }
}

function undoStarBurst() {
  const pickedEntities = starBurstState.pickedEntities;
  if (!Cesium.defined(pickedEntities)) {
    return;
  }

  const billboardEyeOffsets = starBurstState.billboardEyeOffsets;
  const labelEyeOffsets = starBurstState.labelEyeOffsets;

  // Reset billboard and label pixel offsets.
  // Hide overlapping labels.
  for (let i = 0; i < pickedEntities.length; ++i) {
    const entity = pickedEntities[i].id;
    entity.billboard.eyeOffset = billboardEyeOffsets[i];
    if (Cesium.defined(entity.label)) {
      entity.label.eyeOffset = labelEyeOffsets[i];
      entity.label.show = false;
    }
  }

  // Remove lines from the scene.
  // Free resources and reset state.
  scene.primitives.remove(starBurstState.linePrimitive);
  starBurstState.linePrimitive = undefined;
  starBurstState.pickedEntities = undefined;
  starBurstState.billboardEyeOffsets = undefined;
  starBurstState.labelEyeOffsets = undefined;
  starBurstState.radius = undefined;
  starBurstState.enabled = false;
}

let currentObject;

function showLabels(mousePosition) {
  const pickedObjects = scene.drillPick(mousePosition);
  let pickedObject;

  if (Cesium.defined(pickedObjects)) {
    const length = pickedObjects.length;
    for (let i = 0; i < length; ++i) {
      if (pickedObjects[i].primitive instanceof Cesium.Billboard) {
        pickedObject = pickedObjects[i];
        break;
      }
    }
  }

  if (pickedObject !== currentObject) {
    if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id.label)) {
      if (Cesium.defined(currentObject)) {
        currentObject.id.label.show = false;
      }

      currentObject = pickedObject;
      pickedObject.id.label.show = true;
    } else if (Cesium.defined(currentObject)) {
      currentObject.id.label.show = false;
      currentObject = undefined;
    }
  }
}
