// loaders.gl
// SPDX-License-Identifier: MIT AND Apache-2.0
// Copyright 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

import {Vector3, Matrix3, Matrix4, Quaternion} from '@math.gl/core';
import {Ellipsoid} from '@math.gl/geospatial';
import {GL} from '@loaders.gl/math'; // 'math.gl/geometry';
import Tile3DFeatureTable from '../classes/tile-3d-feature-table';
import Tile3DBatchTable from '../classes/tile-3d-batch-table';

import {parse3DTileHeaderSync} from './helpers/parse-3d-tile-header';
import {parse3DTileTablesHeaderSync, parse3DTileTablesSync} from './helpers/parse-3d-tile-tables';
import {parse3DTileGLTFViewSync, extractGLTF} from './helpers/parse-3d-tile-gltf-view';
import {Tiles3DLoaderOptions} from '../../tiles-3d-loader';
import {LoaderContext} from '@loaders.gl/loader-utils';
import {Tiles3DTileContent} from '../../types';

export async function parseInstancedModel3DTile(
  tile: Tiles3DTileContent,
  arrayBuffer: ArrayBuffer,
  byteOffset: number,
  options?: Tiles3DLoaderOptions,
  context?: LoaderContext
): Promise<number> {
  byteOffset = parseInstancedModel(tile, arrayBuffer, byteOffset, options, context);
  await extractGLTF(tile, tile.gltfFormat || 0, options, context);
  return byteOffset;
}

function parseInstancedModel(
  tile: Tiles3DTileContent,
  arrayBuffer: ArrayBuffer,
  byteOffset: number,
  options?: Tiles3DLoaderOptions,
  context?: LoaderContext
): number {
  byteOffset = parse3DTileHeaderSync(tile, arrayBuffer, byteOffset);
  if (tile.version !== 1) {
    throw new Error(`Instanced 3D Model version ${tile.version} is not supported`);
  }

  byteOffset = parse3DTileTablesHeaderSync(tile, arrayBuffer, byteOffset);

  const view = new DataView(arrayBuffer);

  tile.gltfFormat = view.getUint32(byteOffset, true);
  byteOffset += 4;

  // PARSE FEATURE TABLE
  byteOffset = parse3DTileTablesSync(tile, arrayBuffer, byteOffset, options);

  byteOffset = parse3DTileGLTFViewSync(tile, arrayBuffer, byteOffset, options);

  // TODO - Is the feature table sometimes optional or can check be moved into table header parser?
  if (!tile?.header?.featureTableJsonByteLength || tile.header.featureTableJsonByteLength === 0) {
    throw new Error('i3dm parser: featureTableJsonByteLength is zero.');
  }

  const featureTable = new Tile3DFeatureTable(tile.featureTableJson, tile.featureTableBinary);

  const instancesLength = featureTable.getGlobalProperty('INSTANCES_LENGTH');
  featureTable.featuresLength = instancesLength;

  if (!Number.isFinite(instancesLength)) {
    throw new Error('i3dm parser: INSTANCES_LENGTH must be defined');
  }

  tile.eastNorthUp = featureTable.getGlobalProperty('EAST_NORTH_UP');
  tile.rtcCenter = featureTable.getGlobalProperty('RTC_CENTER', GL.FLOAT, 3);

  const batchTable = new Tile3DBatchTable(
    tile.batchTableJson,
    tile.batchTableBinary,
    instancesLength
  );

  extractInstancedAttributes(tile, featureTable, batchTable, instancesLength);

  return byteOffset;
}

// eslint-disable-next-line max-statements, complexity
function extractInstancedAttributes(
  tile: Tiles3DTileContent,
  featureTable: Tile3DFeatureTable,
  batchTable: Tile3DBatchTable,
  instancesLength: number
) {
  const instances = new Array(instancesLength);
  const instancePosition = new Vector3();
  const instanceNormalRight = new Vector3();
  const instanceNormalUp = new Vector3();
  const instanceNormalForward = new Vector3();
  const instanceRotation = new Matrix3();
  const instanceQuaternion = new Quaternion();
  const instanceScale = new Vector3();
  const instanceTranslationRotationScale = {};
  const instanceTransform = new Matrix4();
  const scratch1 = [];
  const scratch2 = [];
  const scratch3 = [];
  const scratch4 = [];

  for (let i = 0; i < instancesLength; i++) {
    let position;

    // Get the instance position
    if (featureTable.hasProperty('POSITION')) {
      position = featureTable.getProperty('POSITION', GL.FLOAT, 3, i, instancePosition);
    } else if (featureTable.hasProperty('POSITION_QUANTIZED')) {
      position = featureTable.getProperty(
        'POSITION_QUANTIZED',
        GL.UNSIGNED_SHORT,
        3,
        i,
        instancePosition
      );

      const quantizedVolumeOffset = featureTable.getGlobalProperty(
        'QUANTIZED_VOLUME_OFFSET',
        GL.FLOAT,
        3
      );
      if (!quantizedVolumeOffset) {
        throw new Error(
          'i3dm parser: QUANTIZED_VOLUME_OFFSET must be defined for quantized positions.'
        );
      }

      const quantizedVolumeScale = featureTable.getGlobalProperty(
        'QUANTIZED_VOLUME_SCALE',
        GL.FLOAT,
        3
      );
      if (!quantizedVolumeScale) {
        throw new Error(
          'i3dm parser: QUANTIZED_VOLUME_SCALE must be defined for quantized positions.'
        );
      }

      const MAX_UNSIGNED_SHORT = 65535.0;
      for (let j = 0; j < 3; j++) {
        position[j] =
          (position[j] / MAX_UNSIGNED_SHORT) * quantizedVolumeScale[j] + quantizedVolumeOffset[j];
      }
    }

    if (!position) {
      throw new Error('i3dm: POSITION or POSITION_QUANTIZED must be defined for each instance.');
    }

    instancePosition.copy(position);
    // @ts-expect-error
    instanceTranslationRotationScale.translation = instancePosition;

    // Get the instance rotation
    tile.normalUp = featureTable.getProperty('NORMAL_UP', GL.FLOAT, 3, i, scratch1);
    tile.normalRight = featureTable.getProperty('NORMAL_RIGHT', GL.FLOAT, 3, i, scratch2);

    const hasCustomOrientation = false;
    if (tile.normalUp) {
      if (!tile.normalRight) {
        throw new Error('i3dm: Custom orientation requires both NORMAL_UP and NORMAL_RIGHT.');
      }
      // Vector3.unpack(normalUp, 0, instanceNormalUp);
      // Vector3.unpack(normalRight, 0, instanceNormalRight);
      tile.hasCustomOrientation = true;
    } else {
      tile.octNormalUp = featureTable.getProperty(
        'NORMAL_UP_OCT32P',
        GL.UNSIGNED_SHORT,
        2,
        i,
        scratch1
      );
      tile.octNormalRight = featureTable.getProperty(
        'NORMAL_RIGHT_OCT32P',
        GL.UNSIGNED_SHORT,
        2,
        i,
        scratch2
      );

      if (tile.octNormalUp) {
        if (!tile.octNormalRight) {
          throw new Error(
            'i3dm: oct-encoded orientation requires NORMAL_UP_OCT32P and NORMAL_RIGHT_OCT32P'
          );
        }

        throw new Error('i3dm: oct-encoded orientation not implemented');
        /*
        AttributeCompression.octDecodeInRange(octNormalUp[0], octNormalUp[1], 65535, instanceNormalUp);
        AttributeCompression.octDecodeInRange(octNormalRight[0], octNormalRight[1], 65535, instanceNormalRight);
        hasCustomOrientation = true;
        */
      } else if (tile.eastNorthUp) {
        Ellipsoid.WGS84.eastNorthUpToFixedFrame(instancePosition, instanceTransform);
        instanceTransform.getRotationMatrix3(instanceRotation);
      } else {
        instanceRotation.identity();
      }
    }

    if (hasCustomOrientation) {
      instanceNormalForward.copy(instanceNormalRight).cross(instanceNormalUp).normalize();
      instanceRotation.setColumn(0, instanceNormalRight);
      instanceRotation.setColumn(1, instanceNormalUp);
      instanceRotation.setColumn(2, instanceNormalForward);
    }

    instanceQuaternion.fromMatrix3(instanceRotation);
    // @ts-expect-error
    instanceTranslationRotationScale.rotation = instanceQuaternion;

    // Get the instance scale
    instanceScale.set(1.0, 1.0, 1.0);
    const scale = featureTable.getProperty('SCALE', GL.FLOAT, 1, i, scratch3);
    if (Number.isFinite(scale)) {
      instanceScale.multiplyByScalar(scale);
    }
    const nonUniformScale = featureTable.getProperty('SCALE_NON_UNIFORM', GL.FLOAT, 3, i, scratch1);
    if (nonUniformScale) {
      instanceScale.scale(nonUniformScale);
    }

    // @ts-expect-error
    instanceTranslationRotationScale.scale = instanceScale;

    // Get the batchId
    let batchId = featureTable.getProperty('BATCH_ID', GL.UNSIGNED_SHORT, 1, i, scratch4);
    if (batchId === undefined) {
      // If BATCH_ID semantic is undefined, batchId is just the instance number
      batchId = i;
    }

    // @ts-expect-error
    const rotationMatrix = new Matrix4().fromQuaternion(instanceTranslationRotationScale.rotation);

    // Create the model matrix and the instance
    instanceTransform.identity();
    // @ts-expect-error
    instanceTransform.translate(instanceTranslationRotationScale.translation);
    instanceTransform.multiplyRight(rotationMatrix);
    // @ts-expect-error
    instanceTransform.scale(instanceTranslationRotationScale.scale);

    const modelMatrix = instanceTransform.clone();
    instances[i] = {
      modelMatrix,
      batchId
    };
  }

  tile.instances = instances;
}
