import { vec3, mat4, quat } from 'gl-matrix';
import CoincidentTopologyHelper from './Mapper/CoincidentTopologyHelper.js';
import vtkAbstractImageMapper from './AbstractImageMapper.js';
import { m as macro } from '../../macros2.js';
import vtkPoints from '../../Common/Core/Points.js';
import vtkPolyLine from '../../Common/DataModel/PolyLine.js';
import { ProjectionMode } from './ImageCPRMapper/Constants.js';

const {
  vtkErrorMacro
} = macro;
const {
  staticOffsetAPI,
  otherStaticMethods
} = CoincidentTopologyHelper;

// ----------------------------------------------------------------------------
// vtkImageCPRMapper methods
// ----------------------------------------------------------------------------

function vtkImageCPRMapper(publicAPI, model) {
  // Set our className
  model.classHierarchy.push('vtkImageCPRMapper');
  const superClass = {
    ...publicAPI
  };

  /**
   * Public methods
   */
  publicAPI.getBounds = () => {
    const imageWidth = publicAPI.getWidth();
    const imageHeight = publicAPI.getHeight();
    return [0, imageWidth, 0, imageHeight, 0, 0];
  };
  publicAPI.getOrientationDataArray = () => {
    const pointData = publicAPI.getInputData(1)?.getPointData();
    if (!pointData) {
      return null;
    }
    if (model.orientationArrayName !== null) {
      return pointData.getArrayByName(model.orientationArrayName) || null;
    }
    return pointData.getArrayByName('Orientation') || pointData.getArrayByName('Direction') || pointData.getVectors() || pointData.getTensors() || pointData.getNormals() || null;
  };
  publicAPI.getOrientedCenterline = () => {
    const inputPolydata = publicAPI.getInputData(1);
    if (!inputPolydata) {
      // No polydata: return previous centerline
      // Don't reset centerline as it could have been set using setOrientedCenterline
      return model._orientedCenterline;
    }

    // Get dependencies of centerline
    const orientationDataArray = publicAPI.getOrientationDataArray();
    const linesDataArray = inputPolydata.getLines();
    const pointsDataArray = inputPolydata.getPoints();
    if (!model.useUniformOrientation && !orientationDataArray) {
      vtkErrorMacro('Failed to create oriented centerline from polydata: no orientation');
      publicAPI._resetOrientedCenterline();
      return model._orientedCenterline;
    }

    // If centerline didn't change, don't recompute
    const centerlineTime = model._orientedCenterline.getMTime();
    if (centerlineTime >= publicAPI.getMTime() && centerlineTime > linesDataArray.getMTime() && centerlineTime > pointsDataArray.getMTime() && (model.useUniformOrientation || centerlineTime > orientationDataArray.getMTime())) {
      return model._orientedCenterline;
    }

    // Get points of the centerline
    const linesData = linesDataArray.getData();
    if (linesData.length <= 0) {
      // No polyline
      publicAPI._resetOrientedCenterline();
      return model._orientedCenterline;
    }
    const nPoints = linesData[0];
    if (nPoints <= 1) {
      // Empty centerline
      publicAPI._resetOrientedCenterline();
      return model._orientedCenterline;
    }
    const pointIndices = linesData.subarray(1, 1 + nPoints);

    // Get orientations of the centerline
    const orientations = new Array(nPoints);
    // Function to convert from mat4, mat3, quat or vec3 to quaternion
    let convert = () => null;
    const numComps = model.useUniformOrientation ? model.uniformOrientation.length : orientationDataArray.getNumberOfComponents();
    switch (numComps) {
      case 16:
        convert = (outQuat, inMat) => {
          mat4.getRotation(outQuat, inMat);
          quat.normalize(outQuat, outQuat);
        };
        break;
      case 9:
        convert = (outQuat, inMat) => {
          quat.fromMat3(outQuat, inMat);
          quat.normalize(outQuat, outQuat);
        };
        break;
      case 4:
        convert = quat.copy;
        break;
      case 3:
        convert = (a, b) => quat.rotationTo(a, model.tangentDirection, b);
        break;
      default:
        vtkErrorMacro('Orientation doesnt match mat4, mat3, quat or vec3');
        publicAPI._resetOrientedCenterline();
        return model._orientedCenterline;
    }
    // Function to get orientation from point index
    let getOrientation = () => null;
    if (model.useUniformOrientation) {
      const outQuat = new Float64Array(4);
      convert(outQuat, model.uniformOrientation);
      getOrientation = () => outQuat;
    } else {
      const temp = new Float64Array(16);
      getOrientation = i => {
        const outQuat = new Float64Array(4);
        orientationDataArray.getTuple(i, temp);
        convert(outQuat, temp);
        return outQuat;
      };
    }
    // Fill the orientation array
    for (let i = 0; i < nPoints; ++i) {
      const pointIdx = pointIndices[i];
      orientations[i] = getOrientation(pointIdx);
    }

    // Done recomputing
    model._orientedCenterline.initialize(pointsDataArray, pointIndices);
    model._orientedCenterline.setOrientations(orientations);
    return model._orientedCenterline;
  };
  publicAPI.setOrientedCenterline = centerline => {
    if (model._orientedCenterline !== centerline) {
      model._orientedCenterline = centerline;
      return true;
    }
    return false;
  };
  publicAPI._resetOrientedCenterline = () => {
    model._orientedCenterline.initialize(vtkPoints.newInstance());
    model._orientedCenterline.setOrientations([]);
  };
  publicAPI.getMTime = () => {
    let mTime = superClass.getMTime();
    if (!model._orientedCenterline) {
      return mTime;
    }
    mTime = Math.max(mTime, model._orientedCenterline.getMTime());
    return mTime;
  };
  publicAPI.getHeight = () => {
    const accHeights = publicAPI.getOrientedCenterline().getDistancesToFirstPoint();
    if (accHeights.length === 0) {
      return 0;
    }
    return accHeights[accHeights.length - 1];
  };
  publicAPI.getCenterlinePositionAndOrientation = distance => {
    const centerline = publicAPI.getOrientedCenterline();
    const subId = centerline.findPointIdAtDistanceFromFirstPoint(distance);
    if (subId < 0) {
      return {};
    }
    const distances = centerline.getDistancesToFirstPoint();
    const pcoords = [(distance - distances[subId]) / (distances[subId + 1] - distances[subId])];
    const weights = new Array(2);
    const position = new Array(3);
    centerline.evaluateLocation(subId, pcoords, position, weights);
    const orientation = new Array(4);
    if (!centerline.evaluateOrientation(subId, pcoords, orientation, weights)) {
      // No orientation
      return {
        position
      };
    }
    return {
      position,
      orientation
    };
  };
  publicAPI.getCenterlineTangentDirections = () => {
    const centerline = publicAPI.getOrientedCenterline();
    const directionsTime = model._centerlineTangentDirectionsTime.getMTime();
    if (directionsTime < centerline.getMTime()) {
      const orientations = centerline.getOrientations();
      model._centerlineTangentDirections = new Float32Array(3 * orientations.length);
      const localDirection = new Array(3);
      for (let i = 0; i < orientations.length; ++i) {
        vec3.transformQuat(localDirection, model.tangentDirection, orientations[i]);
        model._centerlineTangentDirections.set(localDirection, 3 * i);
      }
      model._centerlineTangentDirectionsTime.modified();
    }
    return model._centerlineTangentDirections;
  };
  publicAPI.getUniformDirection = () => vec3.transformQuat(new Array(3), model.tangentDirection, model.uniformOrientation);
  publicAPI.getDirectionMatrix = () => {
    const tangent = model.tangentDirection;
    const bitangent = model.bitangentDirection;
    const normal = model.normalDirection;
    return new Float64Array([tangent[0], tangent[1], tangent[2], bitangent[0], bitangent[1], bitangent[2], normal[0], normal[1], normal[2]]);
  };
  publicAPI.setDirectionMatrix = mat => {
    if (mat4.equals(mat, publicAPI.getDirectionMatrix())) {
      return false;
    }
    model.tangentDirection = [mat[0], mat[1], mat[2]];
    model.bitangentDirection = [mat[3], mat[4], mat[5]];
    model.normalDirection = [mat[6], mat[7], mat[8]];
    publicAPI.modified();
    return true;
  };

  // Check if the rendering can occur
  publicAPI.preRenderCheck = () => {
    if (!publicAPI.getInputData(0)) {
      vtkErrorMacro('No image data input');
      return false;
    }
    return true;
  };
  publicAPI.useStraightenedMode = () => {
    publicAPI.setCenterPoint(null);
    publicAPI.setUseUniformOrientation(false);
    publicAPI.getOrientedCenterline().setDistanceFunction(vec3.dist);
  };
  publicAPI.useStretchedMode = centerPoint => {
    const centerline = publicAPI.getOrientedCenterline();
    // Set center point
    if (!centerPoint) {
      // Get the first point of the centerline if there is one
      const centerlinePoints = centerline.getPoints();
      const newCenterPoint = centerlinePoints.getNumberOfTuples() > 0 ? centerlinePoints.getPoint(0) : [0, 0, 0];
      publicAPI.setCenterPoint(newCenterPoint);
    } else {
      publicAPI.setCenterPoint(centerPoint);
    }
    // Enable uniform orientation
    publicAPI.setUseUniformOrientation(true);
    // Change distance function
    centerline.setDistanceFunction((a, b) => {
      const direction = publicAPI.getUniformDirection();
      const vec = vec3.subtract([], a, b);
      const d2 = vec3.squaredLength(vec);
      const x = vec3.dot(direction, vec);
      return Math.sqrt(d2 - x * x);
    });
  };
  publicAPI.isProjectionEnabled = () => model.projectionSlabNumberOfSamples > 1;
  publicAPI.setCenterlineData = centerlineData => publicAPI.setInputData(centerlineData, 1);
  publicAPI.setCenterlineConnection = centerlineConnection => publicAPI.setInputConnection(centerlineConnection, 1);
  publicAPI.setImageData = imageData => publicAPI.setInputData(imageData, 0);
  publicAPI.setImageConnection = imageData => publicAPI.setInputConnection(imageData, 0);
  publicAPI.getIsOpaque = () => true;

  // One can also call setOrientedCenterline and not provide a polydata centerline to input 1
  model._orientedCenterline = vtkPolyLine.newInstance();
  publicAPI._resetOrientedCenterline();
}

// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------

const DEFAULT_VALUES = {
  width: 10,
  uniformOrientation: [0, 0, 0, 1],
  useUniformOrientation: false,
  centerPoint: null,
  preferSizeOverAccuracy: false,
  orientationArrayName: null,
  tangentDirection: [1, 0, 0],
  bitangentDirection: [0, 1, 0],
  normalDirection: [0, 0, 1],
  projectionSlabThickness: 1,
  projectionSlabNumberOfSamples: 1,
  projectionMode: ProjectionMode.MAX
};

// ----------------------------------------------------------------------------

function extend(publicAPI, model) {
  let initialValues = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
  Object.assign(model, DEFAULT_VALUES, initialValues);

  // Inheritance
  vtkAbstractImageMapper.extend(publicAPI, model, initialValues);

  // Two inputs: one for the ImageData and one for the PolyData (centerline)
  macro.algo(publicAPI, model, 2, 0);
  model._centerlineTangentDirectionsTime = {};
  macro.obj(model._centerlineTangentDirectionsTime, {
    mtime: 0
  });

  // Setters and getters
  macro.setGet(publicAPI, model, ['width', 'uniformOrientation', 'useUniformOrientation', 'centerPoint', 'preferSizeOverAccuracy', 'orientationArrayName', 'tangentDirection', 'bitangentDirection', 'normalDirection', 'projectionSlabThickness', 'projectionSlabNumberOfSamples', 'projectionMode']);
  CoincidentTopologyHelper.implementCoincidentTopologyMethods(publicAPI, model);

  // Object methods
  vtkImageCPRMapper(publicAPI, model);
}

// ----------------------------------------------------------------------------

const newInstance = macro.newInstance(extend, 'vtkImageCPRMapper');

// ----------------------------------------------------------------------------

var index = {
  newInstance,
  extend,
  ...staticOffsetAPI,
  ...otherStaticMethods
};

export { index as default, extend, newInstance };
