import { LazPerf } from 'laz-perf';
import { Las } from 'copc';

/**
 * @typedef {Object} Header - Partial LAS header.
 * @property {number} header.pointDataRecordFormat - Type of point data
 * records contained by the buffer.
 * @property {number} header.pointDataRecordLength - Size (in bytes) of the
 * point data records. If the specified size is larger than implied by the
 * point data record format (see above) the remaining bytes are user-specfic
 * "extra bytes". Those are described by an Extra Bytes VLR.
 * @property {number[]} header.scale - Scale factors (an array `[xScale,
 * yScale, zScale]`) multiplied to the X, Y, Z point record values.
 * @property {number[]} header.offset - Offsets (an array `[xOffset,
 * xOffset, zOffset]`) added to the scaled X, Y, Z point record values.
 */

/**
 * @classdesc
 * Loader for LAS and LAZ (LASZip) point clouds. It uses the copc.js library and
 * the laz-perf decoder under the hood.
 *
 * The laz-perf web assembly module is lazily fetched at runtime when a parsing
 * request is initiated. Location of laz-perf wasm defaults to the unpkg
 * repository.
 */
class LASLoader {
  constructor() {
    this._wasmPath = 'https://cdn.jsdelivr.net/npm/laz-perf@0.0.6/lib/';
    this._wasmPromise = null;
  }
  _initDecoder() {
    if (this._wasmPromise) {
      return this._wasmPromise;
    }
    this._wasmPromise = LazPerf.create({
      locateFile: file => `${this._wasmPath}/${file}`
    });
    return this._wasmPromise;
  }
  _parseView(view, options) {
    const colorDepth = options.colorDepth ?? 16;
    const getPosition = ['X', 'Y', 'Z'].map(view.getter);
    const getIntensity = view.getter('Intensity');
    const getReturnNumber = view.getter('ReturnNumber');
    const getNumberOfReturns = view.getter('NumberOfReturns');
    const getClassification = view.getter('Classification');
    const getPointSourceID = view.getter('PointSourceId');
    const getColor = view.dimensions.Red ? ['Red', 'Green', 'Blue'].map(view.getter) : undefined;
    const getScanAngle = view.getter('ScanAngle');
    const positions = new Float32Array(view.pointCount * 3);
    const intensities = new Uint16Array(view.pointCount);
    const returnNumbers = new Uint8Array(view.pointCount);
    const numberOfReturns = new Uint8Array(view.pointCount);
    const classifications = new Uint8Array(view.pointCount);
    const pointSourceIDs = new Uint16Array(view.pointCount);
    const colors = getColor ? new Uint8Array(view.pointCount * 4) : undefined;
    /*
    As described by the LAS spec, Scan Angle is encoded:
    - as signed char in a valid range from -90 to +90 (degrees) prior to the LAS 1.4 Point Data Record Formats (PDRF) 6
    - as a signed short in a valid range from -30 000 to +30 000. Those values represents scan angles from -180 to +180
      degrees with an increment of 0.006 for PDRF >= 6.
    The copc.js library does the degree convertion and stores it as a `Float32`.
    */
    const scanAngles = new Float32Array(view.pointCount);

    // For precision we take the first point that will be use as origin for a local referentiel.
    const origin = getPosition.map(f => f(0)).map(val => Math.floor(val));
    for (let i = 0; i < view.pointCount; i++) {
      // `getPosition` apply scale and offset transform to the X, Y, Z
      // values. See https://github.com/connormanning/copc.js/blob/master/src/las/extractor.ts.
      const [x, y, z] = getPosition.map(f => f(i));
      positions[i * 3] = x - origin[0];
      positions[i * 3 + 1] = y - origin[1];
      positions[i * 3 + 2] = z - origin[2];
      intensities[i] = getIntensity(i);
      returnNumbers[i] = getReturnNumber(i);
      numberOfReturns[i] = getNumberOfReturns(i);
      if (getColor) {
        // Note that we do not infer color depth as it is expensive
        // (i.e. traverse the whole view to check if there exists a red,
        // green or blue value > 255).
        let [r, g, b] = getColor.map(f => f(i));
        if (colorDepth === 16) {
          r /= 256;
          g /= 256;
          b /= 256;
        }
        colors[i * 4] = r;
        colors[i * 4 + 1] = g;
        colors[i * 4 + 2] = b;
        colors[i * 4 + 3] = 255;
      }
      classifications[i] = getClassification(i);
      pointSourceIDs[i] = getPointSourceID(i);
      scanAngles[i] = getScanAngle(i);
    }
    return {
      position: positions,
      intensity: intensities,
      returnNumber: returnNumbers,
      numberOfReturns,
      classification: classifications,
      pointSourceID: pointSourceIDs,
      color: colors,
      scanAngle: scanAngles,
      origin
    };
  }

  /**
   * Set LazPerf decoder path.
   * @param {string} path - path to `laz-perf.wasm` folder.
   */
  set lazPerf(path) {
    this._wasmPath = path;
    this._wasmPromise = null;
  }

  /**
   * Parses a LAS or LAZ (LASZip) file. Note that this function is
   * **CPU-bound** and shall be parallelised in a dedicated worker.
   * @param {ArrayBuffer} data - Binary data to parse.
   * @param {Object} [options] - Parsing options.
   * @param {8 | 16} [options.colorDepth] - Color depth encoding (in bits).
   * Either 8 or 16 bits. Defaults to 8 bits for LAS 1.2 and 16 bits for later
   * versions (as mandatory by the specification)
   */
  async parseFile(data) {
    let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
    const bytes = new Uint8Array(data);
    const pointData = await Las.PointData.decompressFile(bytes, this._initDecoder());
    const header = Las.Header.parse(bytes);
    const colorDepth = options.colorDepth ?? (header.majorVersion === 1 && header.minorVersion <= 2 ? 8 : 16);
    const getter = async (begin, end) => bytes.slice(begin, end);
    const vlrs = await Las.Vlr.walk(getter, header);
    const ebVlr = Las.Vlr.find(vlrs, 'LASF_Spec', 4);
    const eb = ebVlr && Las.ExtraBytes.parse(await Las.Vlr.fetch(getter, ebVlr));
    const view = Las.View.create(pointData, header, eb);
    const attributes = this._parseView(view, {
      colorDepth
    });
    return {
      header,
      attributes
    };
  }
}
export default LASLoader;