// loaders.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

// This file is derived from the Cesium code base under Apache 2 license
// See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md

// TODO - Dynamic screen space error provides an optimization when looking at
// tilesets from above

/* eslint-disable */
// @ts-nocheck
import {Matrix4, Vector3, clamp} from '@math.gl/core';

const scratchPositionNormal = new Vector3();
const scratchCartographic = new Vector3();
const scratchMatrix = new Matrix4();
const scratchCenter = new Vector3();
const scratchPosition = new Vector3();
const scratchDirection = new Vector3();

// eslint-disable-next-line max-statements, complexity
export function calculateDynamicScreenSpaceError(root, {camera, mapProjection}, options = {}) {
  const {dynamicScreenSpaceErrorHeightFalloff = 0.25, dynamicScreenSpaceErrorDensity = 0.00278} =
    options;

  let up;
  let direction;
  let height;
  let minimumHeight;
  let maximumHeight;

  const tileBoundingVolume = root.contentBoundingVolume;

  if (tileBoundingVolume instanceof TileBoundingRegion) {
    up = Cartesian3.normalize(camera.positionWC, scratchPositionNormal);
    direction = camera.directionWC;
    height = camera.positionCartographic.height;
    minimumHeight = tileBoundingVolume.minimumHeight;
    maximumHeight = tileBoundingVolume.maximumHeight;
  } else {
    // Transform camera position and direction into the local coordinate system of the tileset
    const transformLocal = Matrix4.inverseTransformation(root.computedTransform, scratchMatrix);
    const ellipsoid = mapProjection.ellipsoid;
    const boundingVolume = tileBoundingVolume.boundingVolume;
    const centerLocal = Matrix4.multiplyByPoint(
      transformLocal,
      boundingVolume.center,
      scratchCenter
    );
    if (Cartesian3.magnitude(centerLocal) > ellipsoid.minimumRadius) {
      // The tileset is defined in WGS84. Approximate the minimum and maximum height.
      const centerCartographic = Cartographic.fromCartesian(
        centerLocal,
        ellipsoid,
        scratchCartographic
      );
      up = Cartesian3.normalize(camera.positionWC, scratchPositionNormal);
      direction = camera.directionWC;
      height = camera.positionCartographic.height;
      minimumHeight = 0.0;
      maximumHeight = centerCartographic.height * 2.0;
    } else {
      // The tileset is defined in local coordinates (z-up)
      const positionLocal = Matrix4.multiplyByPoint(
        transformLocal,
        camera.positionWC,
        scratchPosition
      );
      up = Cartesian3.UNIT_Z;
      direction = Matrix4.multiplyByPointAsVector(
        transformLocal,
        camera.directionWC,
        scratchDirection
      );
      direction = Cartesian3.normalize(direction, direction);
      height = positionLocal.z;
      if (tileBoundingVolume instanceof TileOrientedBoundingBox) {
        // Assuming z-up, the last component stores the half-height of the box
        const boxHeight = root._header.boundingVolume.box[11];
        minimumHeight = centerLocal.z - boxHeight;
        maximumHeight = centerLocal.z + boxHeight;
      } else if (tileBoundingVolume instanceof TileBoundingSphere) {
        const radius = boundingVolume.radius;
        minimumHeight = centerLocal.z - radius;
        maximumHeight = centerLocal.z + radius;
      }
    }
  }

  // The range where the density starts to lessen. Start at the quarter height of the tileset.
  const heightFalloff = dynamicScreenSpaceErrorHeightFalloff;
  const heightClose = minimumHeight + (maximumHeight - minimumHeight) * heightFalloff;
  const heightFar = maximumHeight;

  const t = clamp((height - heightClose) / (heightFar - heightClose), 0.0, 1.0);

  // Increase density as the camera tilts towards the horizon
  const dot = Math.abs(Cartesian3.dot(direction, up));

  let horizonFactor = 1.0 - dot;

  // Weaken the horizon factor as the camera height increases, implying the camera is further away from the tileset.
  // The goal is to increase density for the "street view", not when viewing the tileset from a distance.
  horizonFactor = horizonFactor * (1.0 - t);

  return dynamicScreenSpaceErrorDensity * horizonFactor;
}

export function fog(distanceToCamera, density) {
  const scalar = distanceToCamera * density;
  return 1.0 - Math.exp(-(scalar * scalar));
}

export function getDynamicScreenSpaceError(tileset, distanceToCamera) {
  if (tileset.dynamicScreenSpaceError && tileset.dynamicScreenSpaceErrorComputedDensity) {
    const density = tileset.dynamicScreenSpaceErrorComputedDensity;
    const factor = tileset.dynamicScreenSpaceErrorFactor;
    // TODO: Refined screen space error that minimizes tiles in non-first-person
    const dynamicError = fog(distanceToCamera, density) * factor;
    return dynamicError;
  }

  return 0;
}

export function getTiles3DScreenSpaceError(tile, frameState, useParentLodMetric) {
  const tileset = tile.tileset;
  const parentLodMetricValue = (tile.parent && tile.parent.lodMetricValue) || tile.lodMetricValue;
  const lodMetricValue = useParentLodMetric ? parentLodMetricValue : tile.lodMetricValue;

  // Leaf tiles do not have any error so save the computation
  if (lodMetricValue === 0.0) {
    return 0.0;
  }

  // TODO: Orthographic Frustum needs special treatment?
  // this._getOrthograhicScreenSpaceError();

  // Avoid divide by zero when viewer is inside the tile
  const distance = Math.max(tile._distanceToCamera, 1e-7);
  const {height, sseDenominator} = frameState;
  const {viewDistanceScale} = tileset.options;
  let error = (lodMetricValue * height * (viewDistanceScale || 1.0)) / (distance * sseDenominator);

  error -= getDynamicScreenSpaceError(tileset, distance);

  return error;
}
