/*
 *  ARControllerNFT_simd.ts
 *  JSARToolKitNFT
 *
 *  This file is part of JSARToolKitNFT - WebARKit.
 *
 *  JSARToolKitNFT is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  JSARToolKitNFT is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with JSARToolKitNFT.  If not, see <http://www.gnu.org/licenses/>.
 *
 *  As a special exception, the copyright holders of this library give you
 *  permission to link this library with independent modules to produce an
 *  executable, regardless of the license terms of these independent modules, and to
 *  copy and distribute the resulting executable under terms of your choice,
 *  provided that you also meet, for each linked independent module, the terms and
 *  conditions of the license of that module. An independent module is a module
 *  which is neither derived from nor based on this library. If you modify this
 *  library, you may extend this exception to your version of the library, but you
 *  are not obligated to do so. If you do not wish to do so, delete this exception
 *  statement from your version.
 *
 *  Copyright 2020 WebARKit.
 *
 *  Author(s): Walter Perdan @kalwalt https://github.com/kalwalt
 *
 */
import {
  INFTMarkerInfo,
  IImageObj,
  INFTMarker,
} from "./abstractions/CommonInterfaces";
import { IARToolkitNFT } from "./abstractions/IARToolkitNFT";
import { ARToolkitNFT } from "./ARToolkitNFT_simd";
import { AbstractARControllerNFT } from "./abstractions/AbstractARControllerNFT";

export class ARControllerNFT implements AbstractARControllerNFT {
  // private declarations
  private id: number;
  private _width: number;
  private _height: number;
  private _cameraParam: string;
  private cameraId: number;

  private artoolkitNFT: IARToolkitNFT;
  private FS: any;
  private StringList: any;

  private listeners: object;
  private nftMarkers: INFTMarker[];

  private transform_mat: Float64Array;
  private transformGL_RH: Float64Array;
  private camera_mat: Float64Array;

  private videoWidth: number;
  private videoHeight: number;
  private videoSize: number;
  private framesize: number;
  private videoLuma: Uint8Array;
  private videoLumaInternal: boolean;
  private grayscaleEnabled: boolean;
  private grayscaleSource: Uint8Array;

  private nftMarkerFound: boolean; // = false
  private nftMarkerFoundTime: number;
  private nftMarkerCount: number; // = 0
  private defaultMarkerWidth: number;

  private _bwpointer: number;

  /**
   * The ARControllerNFT default constructor. It has no params (see above).
   * These properties are initialized:
   * id, width, height, cameraParam, cameraId,
   * cameraLoaded, artoolkitNFT, listeners, nftMarkers, transform_mat,
   * transformGL_RH, videoWidth, videoHeight, videoSize,
   * videoLuma, framesize, camera_mat.
   */
  constructor();
  /**
   * The ARControllerNFT default constructor. It has 2 params (see above).
   * These properties are initialized:
   * id, width, height, cameraParam, cameraId,
   * cameraLoaded, artoolkitNFT, listeners, nftMarkers, transform_mat,
   * transformGL_RH, videoWidth, videoHeight, videoSize,
   * videoLuma, framesize, camera_mat.
   * @param {number} width
   * @param {number} height
   */
  constructor(width: number, height: number);
  /**
   * The ARControllerNFT constructor. It has 4 params (see above).
   * These properties are initialized:
   * id, width, height, cameraParam, cameraId,
   * cameraLoaded, artoolkitNFT, listeners, nftMarkers, transform_mat,
   * transformGL_RH, videoWidth, videoHeight, videoSize,
   * framesize, camera_mat.
   * @param {number} width
   * @param {number} height
   * @param {string} cameraParam
   * @param {boolean} internalLuma
   */
  constructor(
    width: number,
    height: number,
    cameraParam: string,
    internalLuma: boolean,
  );
  constructor(
    width?: number,
    height?: number,
    cameraParam?: string,
    internalLuma?: boolean,
  ) {
    // no point in initializing a member as "undefined"
    // replaced it with -1
    this.id = -1;

    this._width = width;
    this._height = height;

    // this is a replacement for ARCameraParam
    this._cameraParam = cameraParam;
    this.cameraId = -1;

    // toolkit instance
    this.artoolkitNFT;

    // to register observers as event listeners
    this.listeners = {};

    this.nftMarkers = [];

    this.transform_mat = new Float64Array(16);
    this.transformGL_RH = new Float64Array(16);

    this.videoWidth = width;
    this.videoHeight = height;
    this.videoSize = this.videoWidth * this.videoHeight;

    this.framesize = null;
    this.videoLuma = null;
    this.videoLumaInternal = internalLuma;
    this.grayscaleEnabled = false;
    this.camera_mat = null;

    // this is to workaround the introduction of "self" variable
    this.nftMarkerFound = false;
    this.nftMarkerFoundTime = 0;
    this.nftMarkerCount = 0;

    this._bwpointer = null;
    this.defaultMarkerWidth = 1;
  }

  /** The static method **initWithDimensions** is the start of your app.
   *  Define it with the width and height of the video stream
   *  and the camera parameter file path. It return a Promise with the ARControllerNFT object.
   *  Use a thenable to load the NFT marker and all the code stuff.
   *  Example:
   *  ```js
   *    import ARControllerNFT from '@webarkit/jsartoolkit-nft'
   *    ARControllerNFT.initWithDimensions(640, 480, "camera_para.dat", true).then(
   *    (nft) => {
   *      nft.loadNFTMarker();
   *      // other code...
   *    })
   *  ```
   * @param {number} width
   * @param {number} height
   * @param {string} cameraParam
   * @param {boolean} internalLuma
   * @return {Promise<ARControllerNFT>} this
   */
  static async initWithDimensions(
    width: number,
    height: number,
    cameraParam: string,
    internalLuma: boolean,
  ): Promise<ARControllerNFT> {
    // directly init with given width / height
    const arControllerNFT = new ARControllerNFT(
      width,
      height,
      cameraParam,
      internalLuma,
    );
    return await arControllerNFT._initialize();
  }

  /** The static method **initWithImage** is the start of your app.
   *  Define it with an HTML element like a video or a static Image
   *  and the camera parameter file path. As with **initWithDimensions** it return a Promise
   *  with the ARControllerNFT object.
   *  Use a thenable to load the NFT marker and all the code stuff.
   *  Example:
   *  ```js
   *    import ARControllerNFT from '@webarkit/jsartoolkit-nft'
   *    const image = document.getElementById('image')
   *    ARControllerNFT.initWithImage(image, "camera_para.dat", true).then(
   *    (nft) => {
   *      nft.loadNFTMarker();
   *      // other code...
   *    })
   *  ```
   * @param {image} image
   * @param {string} cameraParam
   * @param {boolean} internalLuma
   * @return {Promise<ARControllerNFT>} this
   */
  static async initWithImage(
    image: IImageObj,
    cameraParam: string,
    internalLuma: boolean,
  ): Promise<ARControllerNFT> {
    const width = image.videoWidth || image.width;
    const height = image.videoHeight || image.height;
    const arControllerNFT = new ARControllerNFT(
      width,
      height,
      cameraParam,
      internalLuma,
    );
    return await arControllerNFT._initialize();
  }

  /** The static method **customInit** is the start of your app.
   *  This method is only for advanced users.
   *  Define it with the width and height of the video stream,
   *  the camera parameter file path and the callback function where you define custom behaviours.
   *  As with **initWithDimensions** it return a Promise
   *  with the ARControllerNFT object.
   *  Use a thenable to load the NFT marker and all the code stuff.
   *  Example:
   *  ```js
   *    import ARControllerNFT from '@webarkit/jsartoolkit-nft'
   *    ARControllerNFT.customInit(
   *    640,
   *    480,
   *    "camera_para.dat",
   *    true,
   *    function() { // your code here }
   *    ).then(
   *    (nft) => {
   *      nft.loadNFTMarker();
   *      // other code...
   *    })
   *  ```
   * @param {number} width
   * @param {number} height
   * @param {string} cameraParam
   * @param {boolean} internalLuma
   * @param {function} callback
   * @return {Promise<ARControllerNFT>} this
   */
  static async customInit(
    width: number,
    height: number,
    cameraParam: string,
    internalLuma: boolean,
    callback: () => void,
  ): Promise<ARControllerNFT> {
    const arControllerNFT = new ARControllerNFT(
      width,
      height,
      cameraParam,
      internalLuma,
    );
    callback();
    return await arControllerNFT._initialize();
  }

  // getters and setters
  set width(width: number) {
    this._width = width;
  }

  get width() {
    return this._width;
  }

  set height(height: number) {
    this._height = height;
  }

  get height() {
    return this._height;
  }

  set cameraParam(cameraParam: string) {
    this._cameraParam = cameraParam;
  }

  get cameraParam() {
    return this._cameraParam;
  }

  /**
   * This is one of the most important method inside ARControllerNFT. It detect the marker
   * and dispatch internally with the getNFTMarker event listener the NFTMarkerInfo
   * struct object of the tracked NFT Markers.
   * @param {image} image data
   * @return {void}
   */
  process(image: IImageObj): void {
    this._copyImageToHeap(image);

    let k, o: INFTMarker;

    // get NFT markers
    for (k in this.converter().nftMarkers) {
      o = this.converter().nftMarkers[k];
      o.inPrevious = o.inCurrent;
      o.inCurrent = false;
    }

    // detect NFT markers
    let nftMarkerCount = this.nftMarkerCount;
    this.detectNFTMarker();

    // in ms
    const MARKER_LOST_TIME = 200;

    for (let i = 0; i < nftMarkerCount; i++) {
      let nftMarkerInfo: IARToolkitNFT["NFTMarkerInfo"] = this.getNFTMarker(i);

      let markerType = ARToolkitNFT.NFT_MARKER;

      if (nftMarkerInfo.found) {
        this.nftMarkerFound = <boolean>(<unknown>i);
        this.nftMarkerFoundTime = Date.now();

        let visible: INFTMarker = this.trackNFTMarkerId(i);
        visible.matrix.set(nftMarkerInfo.pose);
        visible.inCurrent = true;
        this.transMatToGLMat(visible.matrix, this.transform_mat);
        this.transformGL_RH = this.arglCameraViewRHf(this.transform_mat);
        this.dispatchEvent({
          name: "getNFTMarker",
          target: this,
          data: {
            index: i,
            type: markerType,
            marker: nftMarkerInfo,
            matrix: this.transform_mat,
            matrixGL_RH: this.transformGL_RH,
          },
        });
      } else if (this.nftMarkerFound === <boolean>(<unknown>i)) {
        // for now this marker found/lost events handling is for one marker at a time
        if (Date.now() - this.nftMarkerFoundTime > MARKER_LOST_TIME) {
          this.nftMarkerFound = false;
          this.dispatchEvent({
            name: "lostNFTMarker",
            target: this,
            data: {
              index: i,
              type: markerType,
              marker: nftMarkerInfo,
              matrix: this.transform_mat,
              matrixGL_RH: this.transformGL_RH,
            },
          });
        }
      }
    }
  }

  /**
   * Detects the NFT markers in the process() function,
   * with the given tracked id.
   * @return {number}
   */
  detectNFTMarker(): number {
    return this.artoolkitNFT.detectNFTMarker();
  }

  /**
   * Adds the given NFT marker ID to the index of tracked IDs.
   * Sets the markerWidth for the pattern marker to markerWidth.
   * Used by process() to implement continuous tracking,
   * keeping track of the marker's transformation matrix
   * and customizable marker widths.
   * @param {number} id ID of the NFT marker to track.
   * @param {number} markerWidth The width of the marker to track.
   * @return {Object} The marker tracking object.
   */
  trackNFTMarkerId(id: number, markerWidth?: number): INFTMarker {
    let obj: INFTMarker = this.converter().nftMarkers[id];
    if (!obj) {
      this.converter().nftMarkers[id] = obj = {
        inPrevious: false,
        inCurrent: false,
        matrix: new Float64Array(12),
        matrixGL_RH: new Float64Array(12),
        markerWidth: markerWidth || this.defaultMarkerWidth,
      };
    }
    if (markerWidth) {
      obj.markerWidth = markerWidth;
    }
    return obj;
  }

  // marker detection routines
  // ----------------------------------------------------------------------------

  /**
   * Get the NFT marker info struct for the given NFT marker index in detected markers.
   * The returned object is the global artoolkitNFT.NFTMarkerInfo object and will be overwritten
   * by subsequent calls.
   * Returns undefined if no marker was found.
   * A markerIndex of -1 is used to access the global custom marker.
   * @param {number} markerIndex The index of the NFT marker to query.
   * @return {Object} The NFTMarkerInfo struct.
   */
  getNFTMarker(markerIndex: number): INFTMarkerInfo {
    return this.artoolkitNFT.getNFTMarker(markerIndex);
  }

  /**
   * **GetNFTData** will return the width. height and dpi of the NFT marker.
   * @param id the internal id (this.id)
   * @param index the index of the NFT marker, in case you have multi NFT markers.
   * @returns {object}
   */
  getNFTData(index: number) {
    return this.artoolkitNFT.getNFTData(index);
  }

  // event handling
  //----------------------------------------------------------------------------

  /**
   * Add an event listener on this ARControllerNFT for the named event, calling the callback function
   * whenever that event is dispatched.
   * Possible events are:
   * - getNFTMarker - dispatched whenever process() finds a NFT marker
   * - lostNFTMarker - dispatched whenever process() lost a visible NFT marker
   * - load - dispatched when the ARControllerNFT is ready to use (useful if passing in a camera URL in the constructor)
   * @param {string} name Name of the event to listen to.
   * @param {function} callback Callback function to call when an event with the given name is dispatched.
   */
  addEventListener(name: string, callback: object): void {
    if (!this.converter().listeners[name]) {
      this.converter().listeners[name] = [];
    }
    this.converter().listeners[name].push(callback);
  }

  /**
   * Remove an event listener from the named event.
   * @param {string} name Name of the event to stop listening to.
   * @param {function} callback Callback function to remove from the listeners of the named event.
   */
  removeEventListener(name: string, callback: object): void {
    if (this.converter().listeners[name]) {
      let index = this.converter().listeners[name].indexOf(callback);
      if (index > -1) {
        this.converter().listeners[name].splice(index, 1);
      }
    }
  }

  /**
   * Dispatches the given event to all registered listeners on event.name.
   * @param {Object} event Event to dispatch.
   */
  dispatchEvent(event: { name: string; target: any; data?: object }): void {
    let listeners = this.converter().listeners[event.name];
    if (listeners) {
      for (let i = 0; i < listeners.length; i++) {
        listeners[i].call(this, event);
      }
    }
  }

  // debug stuff
  //----------------------------------------------------------------------------

  /**
   * Sets up for debugging AR detection.
   */
  debugSetup(): void {
    this.setDebugMode(true);
    this._bwpointer = this.getProcessingImage();
  }

  /**
   * Converts the given 3x4 marker transformation matrix in the 12-element transMat array
   * into a 4x4 WebGL matrix and writes the result into the 16-element glMat array.
   * If scale parameter is given, scales the transform of the glMat by the scale parameter.
   * @param {Float64Array} transMat The 3x4 marker transformation matrix.
   * @param {Float64Array} glMat The 4x4 GL transformation matrix.
   * @param {number} scale The scale for the transform.
   * @return {Float64Array} the modified matrix
   */
  transMatToGLMat(
    transMat: Float64Array,
    glMat: Float64Array,
    scale?: number,
  ): Float64Array {
    if (glMat == undefined) {
      glMat = new Float64Array(16);
    }

    glMat[0 + 0 * 4] = transMat[0]; // R1C1
    glMat[0 + 1 * 4] = transMat[1]; // R1C2
    glMat[0 + 2 * 4] = transMat[2];
    glMat[0 + 3 * 4] = transMat[3];
    glMat[1 + 0 * 4] = transMat[4]; // R2
    glMat[1 + 1 * 4] = transMat[5];
    glMat[1 + 2 * 4] = transMat[6];
    glMat[1 + 3 * 4] = transMat[7];
    glMat[2 + 0 * 4] = transMat[8]; // R3
    glMat[2 + 1 * 4] = transMat[9];
    glMat[2 + 2 * 4] = transMat[10];
    glMat[2 + 3 * 4] = transMat[11];
    glMat[3 + 0 * 4] = 0.0;
    glMat[3 + 1 * 4] = 0.0;
    glMat[3 + 2 * 4] = 0.0;
    glMat[3 + 3 * 4] = 1.0;

    if (scale != undefined && scale !== 0.0) {
      glMat[12] *= scale;
      glMat[13] *= scale;
      glMat[14] *= scale;
    }
    return glMat;
  }

  /**
   * Converts the given 4x4 openGL matrix in the 16-element transMat array
   * into a 4x4 OpenGL Right-Hand-View matrix and writes the result into the 16-element glMat array.
   * If scale parameter is given, scales the transform of the glMat by the scale parameter.
   * @param {Float64Array} glMatrix The 4x4 marker transformation matrix.
   * @param {Float64Array} [glRhMatrix] The 4x4 GL right hand transformation matrix.
   * @param {number} [scale] The scale for the transform.
   * @return {Float64Array} the modified gl matrix
   */
  arglCameraViewRHf(
    glMatrix: Float64Array,
    glRhMatrix?: Float64Array,
    scale?: number,
  ): Float64Array {
    let m_modelview;
    if (glRhMatrix == undefined) {
      m_modelview = new Float64Array(16);
    } else {
      m_modelview = glRhMatrix;
    }

    // x
    m_modelview[0] = glMatrix[0];
    m_modelview[4] = glMatrix[4];
    m_modelview[8] = glMatrix[8];
    m_modelview[12] = glMatrix[12];
    // y
    m_modelview[1] = -glMatrix[1];
    m_modelview[5] = -glMatrix[5];
    m_modelview[9] = -glMatrix[9];
    m_modelview[13] = -glMatrix[13];
    // z
    m_modelview[2] = -glMatrix[2];
    m_modelview[6] = -glMatrix[6];
    m_modelview[10] = -glMatrix[10];
    m_modelview[14] = -glMatrix[14];

    // 0 0 0 1
    m_modelview[3] = 0;
    m_modelview[7] = 0;
    m_modelview[11] = 0;
    m_modelview[15] = 1;

    if (scale != undefined && scale !== 0.0) {
      m_modelview[12] *= scale;
      m_modelview[13] *= scale;
      m_modelview[14] *= scale;
    }

    glRhMatrix = m_modelview;

    return glRhMatrix;
  }

  /**
   * Returns the 16-element WebGL transformation matrix used by ARControllerNFT.process to
   * pass marker WebGL matrices to event listeners.
   * Unique to each ARControllerNFT.
   * @return {Float64Array} The 16-element WebGL transformation matrix used by the ARControllerNFT.
   */
  getTransformationMatrix(): Float64Array {
    return this.transform_mat;
  }

  /**
   * Returns the projection matrix computed from camera parameters for the ARControllerNFT.
   * @return {Float64Array} The 16-element WebGL camera matrix for the ARControllerNFT camera parameters.
   */
  getCameraMatrix(): Float64Array {
    return this.camera_mat;
  }

  // Setter / Getter Proxies
  //----------------------------------------------------------------------------

  /**
   * Enables or disables debug mode in the tracker. When enabled, a black and white debug
   * image is generated during marker detection. The debug image is useful for visualising
   * the binarization process and choosing a threshold value.
   * @param {boolean} mode true to enable debug mode, false to disable debug mode
   * @see getDebugMode()
   */
  setDebugMode(mode: boolean): number {
    return this.artoolkitNFT.setDebugMode(mode);
  }

  /**
   * Returns whether debug mode is currently enabled.
   * @return {boolean} true when debug mode is enabled, false when debug mode is disabled
   * @see  setDebugMode()
   */
  getDebugMode(): boolean {
    return this.artoolkitNFT.getDebugMode();
  }

  /**
   * Returns the Emscripten HEAP offset to the debug processing image used by ARToolKit.
   * @return {number} HEAP offset to the debug processing image.
   */
  getProcessingImage(): number {
    return this.artoolkitNFT.getProcessingImage();
  }

  /**
   * Sets the logging level to use by ARToolKit.
   * @param {number} mode type for the log level.
   */
  setLogLevel(mode: boolean): number {
    return this.artoolkitNFT.setLogLevel(mode);
  }

  /**
   * Gets the logging level used by ARToolKit.
   * @return {number} return the log level in use.
   */
  getLogLevel(): number {
    return this.artoolkitNFT.getLogLevel();
  }

  /**
   * Sets the value of the near plane of the camera.
   * @param {number} value the value of the near plane
   * @return {number} 0 (void)
   */
  setProjectionNearPlane(value: number): void {
    return this.artoolkitNFT.setProjectionNearPlane(value);
  }

  /**
   * Gets the value of the near plane of the camera with the give id.
   * @return {number} the value of the near plane.
   */
  getProjectionNearPlane(): number {
    return this.artoolkitNFT.getProjectionNearPlane();
  }

  /**
   * Sets the value of the far plane of the camera.
   * @param {number} value the value of the far plane
   * @return {number} 0 (void)
   */
  setProjectionFarPlane(value: number): void {
    return this.artoolkitNFT.setProjectionFarPlane(value);
  }

  /**
   * Gets the value of the far plane of the camera with the give id.
   * @return {number} the value of the far plane.
   */
  getProjectionFarPlane(): number {
    return this.artoolkitNFT.getProjectionFarPlane();
  }

  /**
   * Set the labeling threshold mode (auto/manual).
   * @param {number} mode An integer specifying the mode. One of:
   * AR_LABELING_THRESH_MODE_MANUAL,
   * AR_LABELING_THRESH_MODE_AUTO_MEDIAN,
   * AR_LABELING_THRESH_MODE_AUTO_OTSU,
   * AR_LABELING_THRESH_MODE_AUTO_ADAPTIVE,
   * AR_LABELING_THRESH_MODE_AUTO_BRACKETING
   */
  setThresholdMode(mode: number): number {
    return this.artoolkitNFT.setThresholdMode(mode);
  }

  /**
   * Gets the current threshold mode used for image binarization.
   * @return {number} The current threshold mode
   * @see getVideoThresholdMode()
   */
  getThresholdMode(): number {
    return this.artoolkitNFT.getThresholdMode();
  }

  /**
   * Set the labeling threshold.
   * This function forces sets the threshold value.
   * The default value is AR_DEFAULT_LABELING_THRESH which is 100.
   * The current threshold mode is not affected by this call.
   * Typically, this function is used when labeling threshold mode
   * is AR_LABELING_THRESH_MODE_MANUAL.
   * The threshold value is not relevant if threshold mode is
   * AR_LABELING_THRESH_MODE_AUTO_ADAPTIVE.
   * Background: The labeling threshold is the value which
   * the AR library uses to differentiate between black and white
   * portions of an ARToolKit marker. Since the actual brightness,
   * contrast, and gamma of incoming images can vary signficantly
   * between different cameras and lighting conditions, this
   * value typically needs to be adjusted dynamically to a
   * suitable midpoint between the observed values for black
   * and white portions of the markers in the image.
   * @param {number} threshold An integer in the range [0,255] (inclusive).
   */
  setThreshold(threshold: number): number {
    return this.artoolkitNFT.setThreshold(threshold);
  }

  /**
   * Get the current labeling threshold.
   * This function queries the current labeling threshold. For,
   * AR_LABELING_THRESH_MODE_AUTO_MEDIAN, AR_LABELING_THRESH_MODE_AUTO_OTSU,
   * and AR_LABELING_THRESH_MODE_AUTO_BRACKETING
   * the threshold value is only valid until the next auto-update.
   * The current threshold mode is not affected by this call.
   * The threshold value is not relevant if threshold mode is
   * AR_LABELING_THRESH_MODE_AUTO_ADAPTIVE.
   * @return {number} The current threshold value.
   */
  getThreshold(): number {
    return this.artoolkitNFT.getThreshold();
  }

  /**
   * Loads an NFT marker from the given URL or data string.
   * This method is asynchronous and returns a Promise that resolves to an array of marker IDs.
   *
   * Example usage:
   * ```typescript
   * import ARControllerNFT from '@webarkit/jsartoolkit-nft';
   *
   * const arController = await ARControllerNFT.initWithDimensions(640, 480, "camera_para.dat");
   * arController.loadNFTMarker("path/to/marker.dat", (id) => {
   *   console.log("Marker loaded with ID:", id);
   * }, (err) => {
   *   console.error("Failed to load marker:", err);
   * });
   * ```
   * @param {string} urlOrData - The URL or data string of the NFT marker to load.
   * @param {function} onSuccess - Callback function to call when the marker is successfully loaded. Receives the marker ID as an argument.
   * @param {function} onError - Callback function to call when there is an error loading the marker. Receives the error code as an argument.
   * @return {Promise<number[]>} A Promise that resolves to an array of marker IDs.
   */
  async loadNFTMarker(
    urlOrData: string,
    onSuccess: (ids: number) => void,
    onError: (err: number) => void,
  ): Promise<number[]> {
    let nft = await this.artoolkitNFT.addNFTMarkers(
      [urlOrData],
      (ids: number[]) => {
        this.nftMarkerCount += ids.length;
        onSuccess(ids[0]);
      },
      onError,
    );
    return nft;
  }

  /**
   * Loads an array of NFT markers from the given URLs or data strings.
   * This method is asynchronous and returns a Promise that resolves to an array of marker IDs.
   *
   * Example usage:
   * ```typescript
   * import ARControllerNFT from '@webarkit/jsartoolkit-nft';
   *
   * const arController = await ARControllerNFT.initWithDimensions(640, 480, "camera_para.dat");
   * arController.loadNFTMarkers(["path/to/marker1.dat", "path/to/marker2.dat"], (ids) => {
   *   console.log("Markers loaded with IDs:", ids);
   * }, (err) => {
   *   console.error("Failed to load markers:", err);
   * });
   * ```
   * @param {Array<string>} urlOrData - The array of URLs or data strings of the NFT markers to load.
   * @param {function} onSuccess - Callback function to call when the markers are successfully loaded. Receives an array of marker IDs as an argument.
   * @param {function} onError - Callback function to call when there is an error loading the markers. Receives the error code as an argument.
   * @return {Promise<number[]>} A Promise that resolves to an array of marker IDs.
   */
  async loadNFTMarkers(
    urlOrData: Array<string>,
    onSuccess: (ids: number[]) => void,
    onError: (err: number) => void,
  ): Promise<number[]> {
    let nft = await this.artoolkitNFT.addNFTMarkers(
      urlOrData,
      (ids: number[]) => {
        this.nftMarkerCount += ids.length;
        onSuccess(ids);
      },
      onError,
    );
    return nft;
  }

  /**
   * Set the image processing mode.
   * When the image processing mode is AR_IMAGE_PROC_FRAME_IMAGE,
   * ARToolKit processes all pixels in each incoming image
   * to locate markers. When the mode is AR_IMAGE_PROC_FIELD_IMAGE,
   * ARToolKit processes pixels in only every second pixel row and
   * column. This is useful both for handling images from interlaced
   * video sources (where alternate lines are assembled from alternate
   * fields and thus have one field time-difference, resulting in a
   * "comb" effect) such as Digital Video cameras.
   * The effective reduction by 75% in the pixels processed also
   * has utility in accelerating tracking by effectively reducing
   * the image size to one quarter size, at the cost of pose accuraccy.
   * @param {number} mode
   * Options for this field are:
   * AR_IMAGE_PROC_FRAME_IMAGE
   * AR_IMAGE_PROC_FIELD_IMAGE
   * The default mode is AR_IMAGE_PROC_FRAME_IMAGE.
   */
  setImageProcMode(mode: number): number {
    return this.artoolkitNFT.setImageProcMode(mode);
  }

  /**
   * Get the image processing mode.
   * See arSetImageProcMode() for a complete description.
   * @return {number} The current image processing mode.
   */
  getImageProcMode(): number {
    return this.artoolkitNFT.getImageProcMode();
  }

  /**
   * Set the custom gray data (videoLuma) in case you want to add additional
   * trasnformation to gray data: for example gaussianblur or boxblur
   * with external libs.
   * @param data Uint8Array
   */
  setGrayData(data: Uint8Array) {
    this.grayscaleEnabled = true;
    this.grayscaleSource = data;
  }

  // private accessors
  // ----------------------------------------------------------------------------
  /**
   * Used internally by ARControllerNFT, it permit to add methods to this.
   * @return {any} ARControllerNFT
   */
  private converter(): any {
    return this;
  }

  /**
   * This function init the ARControllerNFT with the necessary parmeters and variables.
   * Don't call directly this but instead instantiate a new ARControllerNFT.
   * @return {ARControllerNFT} The initialized ARControllerNFT instance
   */
  private async _initialize() {
    // initialize the toolkit
    this.artoolkitNFT = await new ARToolkitNFT().init();

    this.FS = this.artoolkitNFT.FS;
    this.StringList = this.artoolkitNFT.StringList;

    console.log("[ARControllerNFT]", "ARToolkitNFT initialized");

    // load the camera
    this.cameraId = await this.artoolkitNFT.loadCamera(this.cameraParam);
    console.log(
      "[ARControllerNFT]",
      "Camera params loaded with ID",
      this.cameraId,
    );

    // setup
    this.id = this.artoolkitNFT.setup(this.width, this.height, this.cameraId);
    console.log("[ARControllerNFT]", "Got ID from setup", this.id);

    this._initNFT();

    this.framesize = this._width * this._height;

    this.videoLuma = new Uint8Array(this.framesize);

    this.camera_mat = this.artoolkitNFT.getCameraLens();

    this.setProjectionNearPlane(0.1);
    this.setProjectionFarPlane(1000);

    setTimeout(() => {
      this.dispatchEvent({
        name: "load",
        target: this,
      });
    }, 1);

    return this;
  }

  /**
   * Init the necessary kpm handle for NFT and the settings for the CPU.
   * @return {number} 0 (void)
   */
  private _initNFT() {
    this.artoolkitNFT.setupAR2();
  }

  /**
   * Copy the Image data to the HEAP for the debugSetup function.
   * @return {number} 0 (void)
   */
  private _copyImageToHeap(sourceImage: IImageObj) {
    if (!sourceImage) {
      // default to preloaded image
      console.error("Error: no provided imageData to ARControllerNFT");
      return;
    }

    // this is of type Uint8ClampedArray:
    // The Uint8ClampedArray typed array represents an array of 8-bit unsigned
    // integers clamped to 0-255
    // @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8ClampedArray
    let data: Uint8ClampedArray;

    if (sourceImage.data) {
      // directly use source image
      data = sourceImage.data;
    }

    // Here we have access to the unmodified video image. We now need to add the videoLuma chanel to be able to serve the underlying ARTK API
    if (this.videoLuma && !this.videoLumaInternal) {
      if (this.grayscaleEnabled == false) {
        let q = 0;

        // Create luma from video data assuming Pixelformat AR_PIXEL_FORMAT_RGBA
        // see (ARToolKitJS.cpp L: 43)
        for (let p = 0; p < this.videoSize; p++) {
          let r = data[q + 0],
            g = data[q + 1],
            b = data[q + 2];
          // @see https://stackoverflow.com/a/596241/5843642
          this.videoLuma[p] = (r + r + r + b + g + g + g + g) >> 3;
          q += 4;
        }
      } else if (this.grayscaleEnabled == true) {
        this.videoLuma = this.grayscaleSource;
      }
    }

    if (this.videoLuma) {
      this.artoolkitNFT.passVideoData(
        data,
        this.videoLuma,
        this.videoLumaInternal,
      );
      return true;
    }

    return false;
  }
}
