import AttributeCompression from "../../Core/AttributeCompression.js";
import BoundingSphere from "../../Core/BoundingSphere.js";
import Cartesian3 from "../../Core/Cartesian3.js";
import Cartographic from "../../Core/Cartographic.js";
import Check from "../../Core/Check.js";
import ComponentDatatype from "../../Core/ComponentDatatype.js";
import defaultValue from "../../Core/defaultValue.js";
import defined from "../../Core/defined.js";
import Ellipsoid from "../../Core/Ellipsoid.js";
import IndexDatatype from "../../Core/IndexDatatype.js";
import IntersectionTests from "../../Core/IntersectionTests.js";
import Ray from "../../Core/Ray.js";
import Matrix4 from "../../Core/Matrix4.js";
import Transforms from "../../Core/Transforms.js";
import VerticalExaggeration from "../../Core/VerticalExaggeration.js";
import AttributeType from "../AttributeType.js";
import SceneMode from "../SceneMode.js";
import VertexAttributeSemantic from "../VertexAttributeSemantic.js";
import ModelUtility from "./ModelUtility.js";

const scratchV0 = new Cartesian3();
const scratchV1 = new Cartesian3();
const scratchV2 = new Cartesian3();
const scratchNodeComputedTransform = new Matrix4();
const scratchModelMatrix = new Matrix4();
const scratchcomputedModelMatrix = new Matrix4();
const scratchPickCartographic = new Cartographic();
const scratchBoundingSphere = new BoundingSphere();

/**
 * Find an intersection between a ray and the model surface that was rendered. The ray must be given in world coordinates.
 *
 * @param {Model} model The model to pick.
 * @param {Ray} ray The ray to test for intersection.
 * @param {FrameState} frameState The frame state.
 * @param {number} [verticalExaggeration=1.0] A scalar used to exaggerate the height of a position relative to the ellipsoid. If the value is 1.0 there will be no effect.
 * @param {number} [relativeHeight=0.0] The ellipsoid height relative to which a position is exaggerated. If the value is 0.0 the position will be exaggerated relative to the ellipsoid surface.
 * @param {Ellipsoid} [ellipsoid=Ellipsoid.WGS84] The ellipsoid to which the exaggerated position is relative.
 * @param {Cartesian3|undefined} [result] The intersection or <code>undefined</code> if none was found.
 * @returns {Cartesian3|undefined} The intersection or <code>undefined</code> if none was found.
 *
 * @private
 */
export default function pickModel(
  model,
  ray,
  frameState,
  verticalExaggeration,
  relativeHeight,
  ellipsoid,
  result
) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.object("model", model);
  Check.typeOf.object("ray", ray);
  Check.typeOf.object("frameState", frameState);
  //>>includeEnd('debug');

  if (!model._ready || frameState.mode === SceneMode.MORPHING) {
    return;
  }

  let minT = Number.MAX_VALUE;
  const sceneGraph = model.sceneGraph;

  const nodes = sceneGraph._runtimeNodes;
  for (let i = 0; i < nodes.length; i++) {
    const runtimeNode = nodes[i];
    const node = runtimeNode.node;

    let nodeComputedTransform = Matrix4.clone(
      runtimeNode.computedTransform,
      scratchNodeComputedTransform
    );
    let modelMatrix = Matrix4.clone(
      sceneGraph.computedModelMatrix,
      scratchModelMatrix
    );

    const instances = node.instances;
    if (defined(instances)) {
      if (instances.transformInWorldSpace) {
        // Replicate the multiplication order in LegacyInstancingStageVS.
        modelMatrix = Matrix4.multiplyTransformation(
          model.modelMatrix,
          sceneGraph.components.transform,
          modelMatrix
        );

        nodeComputedTransform = Matrix4.multiplyTransformation(
          sceneGraph.axisCorrectionMatrix,
          runtimeNode.computedTransform,
          nodeComputedTransform
        );
      }
    }

    let computedModelMatrix = Matrix4.multiplyTransformation(
      modelMatrix,
      nodeComputedTransform,
      scratchcomputedModelMatrix
    );

    if (frameState.mode !== SceneMode.SCENE3D) {
      computedModelMatrix = Transforms.basisTo2D(
        frameState.mapProjection,
        computedModelMatrix,
        computedModelMatrix
      );
    }

    const transforms = [];
    if (defined(instances)) {
      const transformsCount = instances.attributes[0].count;
      const instanceComponentDatatype =
        instances.attributes[0].componentDatatype;

      const transformElements = 12;
      let transformsTypedArray = runtimeNode.transformsTypedArray;
      if (!defined(transformsTypedArray)) {
        const instanceTransformsBuffer = runtimeNode.instancingTransformsBuffer;
        if (defined(instanceTransformsBuffer) && frameState.context.webgl2) {
          transformsTypedArray = ComponentDatatype.createTypedArray(
            instanceComponentDatatype,
            transformsCount * transformElements
          );
          instanceTransformsBuffer.getBufferData(transformsTypedArray);
        }
      }

      if (defined(transformsTypedArray)) {
        for (let i = 0; i < transformsCount; i++) {
          const index = i * transformElements;

          const transform = new Matrix4(
            transformsTypedArray[index],
            transformsTypedArray[index + 1],
            transformsTypedArray[index + 2],
            transformsTypedArray[index + 3],
            transformsTypedArray[index + 4],
            transformsTypedArray[index + 5],
            transformsTypedArray[index + 6],
            transformsTypedArray[index + 7],
            transformsTypedArray[index + 8],
            transformsTypedArray[index + 9],
            transformsTypedArray[index + 10],
            transformsTypedArray[index + 11],
            0,
            0,
            0,
            1
          );

          if (instances.transformInWorldSpace) {
            Matrix4.multiplyTransformation(
              transform,
              nodeComputedTransform,
              transform
            );
            Matrix4.multiplyTransformation(modelMatrix, transform, transform);
          } else {
            Matrix4.multiplyTransformation(
              transform,
              computedModelMatrix,
              transform
            );
          }
          transforms.push(transform);
        }
      }
    }

    if (transforms.length === 0) {
      transforms.push(computedModelMatrix);
    }

    const primitivesLength = runtimeNode.runtimePrimitives.length;
    for (let j = 0; j < primitivesLength; j++) {
      const runtimePrimitive = runtimeNode.runtimePrimitives[j];
      const primitive = runtimePrimitive.primitive;

      if (defined(runtimePrimitive.boundingSphere) && !defined(instances)) {
        const boundingSphere = BoundingSphere.transform(
          runtimePrimitive.boundingSphere,
          computedModelMatrix,
          scratchBoundingSphere
        );
        const boundsIntersection = IntersectionTests.raySphere(
          ray,
          boundingSphere
        );
        if (!defined(boundsIntersection)) {
          continue;
        }
      }

      const positionAttribute = ModelUtility.getAttributeBySemantic(
        primitive,
        VertexAttributeSemantic.POSITION
      );
      const byteOffset = positionAttribute.byteOffset;
      const byteStride = positionAttribute.byteStride;
      const vertexCount = positionAttribute.count;

      if (!defined(primitive.indices)) {
        // Point clouds
        continue;
      }

      let indices = primitive.indices.typedArray;
      if (!defined(indices)) {
        const indicesBuffer = primitive.indices.buffer;
        const indicesCount = primitive.indices.count;
        const indexDatatype = primitive.indices.indexDatatype;
        if (defined(indicesBuffer) && frameState.context.webgl2) {
          if (indexDatatype === IndexDatatype.UNSIGNED_BYTE) {
            indices = new Uint8Array(indicesCount);
          } else if (indexDatatype === IndexDatatype.UNSIGNED_SHORT) {
            indices = new Uint16Array(indicesCount);
          } else if (indexDatatype === IndexDatatype.UNSIGNED_INT) {
            indices = new Uint32Array(indicesCount);
          }

          indicesBuffer.getBufferData(indices);
        }
      }

      let vertices = positionAttribute.typedArray;
      let componentDatatype = positionAttribute.componentDatatype;
      let attributeType = positionAttribute.type;

      const quantization = positionAttribute.quantization;
      if (defined(quantization)) {
        componentDatatype = positionAttribute.quantization.componentDatatype;
        attributeType = positionAttribute.quantization.type;
      }

      const numComponents = AttributeType.getNumberOfComponents(attributeType);
      const bytes = ComponentDatatype.getSizeInBytes(componentDatatype);
      const isInterleaved =
        !defined(vertices) &&
        defined(byteStride) &&
        byteStride !== numComponents * bytes;

      let elementStride = numComponents;
      let offset = 0;
      if (isInterleaved) {
        elementStride = byteStride / bytes;
        offset = byteOffset / bytes;
      }
      const elementCount = vertexCount * elementStride;

      if (!defined(vertices)) {
        const verticesBuffer = positionAttribute.buffer;

        if (defined(verticesBuffer) && frameState.context.webgl2) {
          vertices = ComponentDatatype.createTypedArray(
            componentDatatype,
            elementCount
          );
          verticesBuffer.getBufferData(
            vertices,
            isInterleaved ? 0 : byteOffset,
            0,
            elementCount
          );
        }

        if (quantization && positionAttribute.normalized) {
          vertices = AttributeCompression.dequantize(
            vertices,
            componentDatatype,
            attributeType,
            vertexCount
          );
        }
      }

      if (!defined(indices) || !defined(vertices)) {
        return;
      }

      ellipsoid = defaultValue(ellipsoid, Ellipsoid.WGS84);
      verticalExaggeration = defaultValue(verticalExaggeration, 1.0);
      relativeHeight = defaultValue(relativeHeight, 0.0);

      const indicesLength = indices.length;
      for (let i = 0; i < indicesLength; i += 3) {
        const i0 = indices[i];
        const i1 = indices[i + 1];
        const i2 = indices[i + 2];

        for (const instanceTransform of transforms) {
          const v0 = getVertexPosition(
            vertices,
            i0,
            offset,
            elementStride,
            quantization,
            instanceTransform,
            verticalExaggeration,
            relativeHeight,
            ellipsoid,
            scratchV0
          );
          const v1 = getVertexPosition(
            vertices,
            i1,
            offset,
            elementStride,
            quantization,
            instanceTransform,
            verticalExaggeration,
            relativeHeight,
            ellipsoid,
            scratchV1
          );
          const v2 = getVertexPosition(
            vertices,
            i2,
            offset,
            elementStride,
            quantization,
            instanceTransform,
            verticalExaggeration,
            relativeHeight,
            ellipsoid,
            scratchV2
          );

          const t = IntersectionTests.rayTriangleParametric(
            ray,
            v0,
            v1,
            v2,
            defaultValue(model.backFaceCulling, true)
          );

          if (defined(t)) {
            if (t < minT && t >= 0.0) {
              minT = t;
            }
          }
        }
      }
    }
  }

  if (minT === Number.MAX_VALUE) {
    return undefined;
  }

  result = Ray.getPoint(ray, minT, result);
  if (frameState.mode !== SceneMode.SCENE3D) {
    Cartesian3.fromElements(result.y, result.z, result.x, result);

    const projection = frameState.mapProjection;
    const ellipsoid = projection.ellipsoid;

    const cartographic = projection.unproject(result, scratchPickCartographic);
    ellipsoid.cartographicToCartesian(cartographic, result);
  }

  return result;
}

function getVertexPosition(
  vertices,
  index,
  offset,
  numElements,
  quantization,
  instanceTransform,
  verticalExaggeration,
  relativeHeight,
  ellipsoid,
  result
) {
  const i = offset + index * numElements;
  result.x = vertices[i];
  result.y = vertices[i + 1];
  result.z = vertices[i + 2];

  if (defined(quantization)) {
    if (quantization.octEncoded) {
      result = AttributeCompression.octDecodeInRange(
        result,
        quantization.normalizationRange,
        result
      );

      if (quantization.octEncodedZXY) {
        const x = result.x;
        result.x = result.z;
        result.z = result.y;
        result.y = x;
      }
    } else {
      result = Cartesian3.multiplyComponents(
        result,
        quantization.quantizedVolumeStepSize,
        result
      );

      result = Cartesian3.add(
        result,
        quantization.quantizedVolumeOffset,
        result
      );
    }
  }

  result = Matrix4.multiplyByPoint(instanceTransform, result, result);

  if (verticalExaggeration !== 1.0) {
    VerticalExaggeration.getPosition(
      result,
      ellipsoid,
      verticalExaggeration,
      relativeHeight,
      result
    );
  }

  return result;
}
