import { vec3 } from 'gl-matrix';
import WebworkerPromise from 'webworker-promise';
import { m as macro } from '../../macros2.js';
import vtkImageData from '../../Common/DataModel/ImageData.js';
import vtkDataArray from '../../Common/Core/DataArray.js';
import vtkPolygon from '../../Common/DataModel/Polygon.js';
import { W as WorkerFactory } from '../../_virtual/rollup-plugin-worker-loader__module_Sources/Filters/General/PaintFilter/PaintFilter.worker.js';

const {
  vtkErrorMacro
} = macro;

// ----------------------------------------------------------------------------
// vtkPaintFilter methods
// ----------------------------------------------------------------------------

function vtkPaintFilter(publicAPI, model) {
  // Set our className
  model.classHierarchy.push('vtkPaintFilter');
  let worker = null;
  let workerPromise = null;
  const history = {};

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

  function resetHistory() {
    history.index = -1;
    history.snapshots = [];
    history.labels = [];
  }
  function pushToHistory(snapshot, label) {
    // Clear any "redo" info
    const spliceIndex = history.index + 1;
    const spliceLength = history.snapshots.length - history.index;
    history.snapshots.splice(spliceIndex, spliceLength);
    history.labels.splice(spliceIndex, spliceLength);

    // Push new snapshot
    history.snapshots.push(snapshot);
    history.labels.push(label);
    history.index++;
  }

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

  publicAPI.startStroke = () => {
    if (model.labelMap) {
      if (!workerPromise) {
        worker = new WorkerFactory();
        workerPromise = new WebworkerPromise(worker);
      }
      workerPromise.exec('start', {
        bufferType: 'Uint8Array',
        dimensions: model.labelMap.getDimensions(),
        slicingMode: model.slicingMode
      });
    }
  };

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

  publicAPI.endStroke = () => {
    let endStrokePromise;
    if (workerPromise) {
      endStrokePromise = workerPromise.exec('end');
      endStrokePromise.then(strokeBuffer => {
        publicAPI.applyBinaryMask(strokeBuffer);
        worker.terminate();
        worker = null;
        workerPromise = null;
      });
    }
    return endStrokePromise;
  };
  publicAPI.applyBinaryMask = maskBuffer => {
    const scalars = model.labelMap.getPointData().getScalars();
    const data = scalars.getData();
    const maskLabelMap = new Uint8Array(maskBuffer);
    let diffCount = 0;
    for (let i = 0; i < maskLabelMap.length; i++) {
      // maskLabelMap is a binary mask
      diffCount += maskLabelMap[i];
    }

    // Format: [ [index, oldLabel], ...]
    // I could use an ArrayBuffer, which would place limits
    // on the values of index/old, but will be more efficient.
    const snapshot = new Array(diffCount);
    const label = model.label;
    let diffIdx = 0;
    if (model.voxelFunc) {
      const bgScalars = model.backgroundImage.getPointData().getScalars();
      const voxel = [];
      for (let i = 0; i < maskLabelMap.length; i++) {
        if (maskLabelMap[i]) {
          bgScalars.getTuple(i, voxel);
          // might not fill up snapshot
          if (model.voxelFunc(voxel, i, label)) {
            snapshot[diffIdx++] = [i, data[i]];
            data[i] = label;
          }
        }
      }
    } else {
      for (let i = 0; i < maskLabelMap.length; i++) {
        if (maskLabelMap[i]) {
          if (data[i] !== label) {
            snapshot[diffIdx++] = [i, data[i]];
            data[i] = label;
          }
        }
      }
    }
    pushToHistory(snapshot, label);
    scalars.setData(data);
    scalars.modified();
    model.labelMap.modified();
    publicAPI.modified();
  };

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

  publicAPI.addPoint = point => {
    if (workerPromise) {
      const worldPt = [point[0], point[1], point[2]];
      const indexPt = [0, 0, 0];
      vec3.transformMat4(indexPt, worldPt, model.maskWorldToIndex);
      indexPt[0] = Math.round(indexPt[0]);
      indexPt[1] = Math.round(indexPt[1]);
      indexPt[2] = Math.round(indexPt[2]);
      const spacing = model.labelMap.getSpacing();
      const radius = spacing.map(s => model.radius / s);
      workerPromise.exec('paint', {
        point: indexPt,
        radius
      });
    }
  };

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

  publicAPI.paintRectangle = (point1, point2) => {
    if (workerPromise) {
      const index1 = [0, 0, 0];
      const index2 = [0, 0, 0];
      vec3.transformMat4(index1, point1, model.maskWorldToIndex);
      vec3.transformMat4(index2, point2, model.maskWorldToIndex);
      index1[0] = Math.round(index1[0]);
      index1[1] = Math.round(index1[1]);
      index1[2] = Math.round(index1[2]);
      index2[0] = Math.round(index2[0]);
      index2[1] = Math.round(index2[1]);
      index2[2] = Math.round(index2[2]);
      workerPromise.exec('paintRectangle', {
        point1: index1,
        point2: index2
      });
    }
  };

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

  publicAPI.paintEllipse = (center, scale3) => {
    if (workerPromise) {
      const realCenter = [0, 0, 0];
      const origin = [0, 0, 0];
      let realScale3 = [0, 0, 0];
      vec3.transformMat4(realCenter, center, model.maskWorldToIndex);
      vec3.transformMat4(origin, origin, model.maskWorldToIndex);
      vec3.transformMat4(realScale3, scale3, model.maskWorldToIndex);
      vec3.subtract(realScale3, realScale3, origin);
      realScale3 = realScale3.map(s => s === 0 ? 0.25 : Math.abs(s));
      workerPromise.exec('paintEllipse', {
        center: realCenter,
        scale3: realScale3
      });
    }
  };

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

  publicAPI.canUndo = () => history.index > -1;

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

  publicAPI.paintPolygon = pointList => {
    if (workerPromise && pointList.length > 0) {
      const polygon = vtkPolygon.newInstance();
      const poly = [];
      for (let i = 0; i < pointList.length / 3; i++) {
        poly.push([pointList[3 * i + 0], pointList[3 * i + 1], pointList[3 * i + 2]]);
      }
      polygon.setPoints(poly);
      if (!polygon.triangulate()) {
        console.log('triangulation failed!');
      }
      const points = polygon.getPointArray();
      const triangleList = new Float32Array(points.length);
      const numPoints = Math.floor(triangleList.length / 3);
      for (let i = 0; i < numPoints; i++) {
        const point = points.slice(3 * i, 3 * i + 3);
        const voxel = triangleList.subarray(3 * i, 3 * i + 3);
        vec3.transformMat4(voxel, point, model.maskWorldToIndex);
      }
      workerPromise.exec('paintTriangles', {
        triangleList
      });
    }
  };

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

  publicAPI.applyLabelMap = labelMap => {
    const currentMapData = model.labelMap.getPointData().getScalars().getData();
    const newMapData = labelMap.getPointData().getScalars().getData();

    // Compute snapshot
    const snapshot = [];
    for (let i = 0; i < newMapData.length; ++i) {
      if (currentMapData[i] !== newMapData[i]) {
        snapshot.push([i, currentMapData[i]]);
      }
    }
    pushToHistory(snapshot, model.label);
    model.labelMap = labelMap;
    publicAPI.modified();
  };

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

  publicAPI.undo = () => {
    if (history.index > -1) {
      const scalars = model.labelMap.getPointData().getScalars();
      const data = scalars.getData();
      const snapshot = history.snapshots[history.index];
      for (let i = 0; i < snapshot.length; i++) {
        if (!snapshot[i]) {
          break;
        }
        const [index, oldLabel] = snapshot[i];
        data[index] = oldLabel;
      }
      history.index--;
      scalars.setData(data);
      scalars.modified();
      model.labelMap.modified();
      publicAPI.modified();
    }
  };

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

  publicAPI.canRedo = () => history.index < history.labels.length - 1;

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

  publicAPI.redo = () => {
    if (history.index < history.labels.length - 1) {
      const scalars = model.labelMap.getPointData().getScalars();
      const data = scalars.getData();
      const redoLabel = history.labels[history.index + 1];
      const snapshot = history.snapshots[history.index + 1];
      for (let i = 0; i < snapshot.length; i++) {
        if (!snapshot[i]) {
          break;
        }
        const [index] = snapshot[i];
        data[index] = redoLabel;
      }
      history.index++;
      scalars.setData(data);
      scalars.modified();
      model.labelMap.modified();
      publicAPI.modified();
    }
  };

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

  const superSetLabelMap = publicAPI.setLabelMap;
  publicAPI.setLabelMap = lm => {
    if (superSetLabelMap(lm)) {
      model.maskWorldToIndex = model.labelMap.getWorldToIndex();
      resetHistory();
      return true;
    }
    return false;
  };

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

  publicAPI.requestData = (inData, outData) => {
    if (!model.backgroundImage) {
      vtkErrorMacro('No background image');
      return;
    }
    if (!model.backgroundImage.getPointData().getScalars()) {
      vtkErrorMacro('Background image has no scalars');
      return;
    }
    if (!model.labelMap) {
      // clone background image properties
      const labelMap = vtkImageData.newInstance(model.backgroundImage.get('spacing', 'origin', 'direction'));
      labelMap.setDimensions(model.backgroundImage.getDimensions());
      labelMap.computeTransforms();

      // right now only support 256 labels
      const values = new Uint8Array(model.backgroundImage.getNumberOfPoints());
      const dataArray = vtkDataArray.newInstance({
        numberOfComponents: 1,
        // labelmap with single component
        values
      });
      labelMap.getPointData().setScalars(dataArray);
      publicAPI.setLabelMap(labelMap);
    }
    if (!model.maskWorldToIndex) {
      model.maskWorldToIndex = model.labelMap.getWorldToIndex();
    }
    const scalars = model.labelMap.getPointData().getScalars();
    if (!scalars) {
      vtkErrorMacro('Mask image has no scalars');
      return;
    }
    model.labelMap.modified();
    outData[0] = model.labelMap;
  };

  // --------------------------------------------------------------------------
  // Initialization
  // --------------------------------------------------------------------------

  resetHistory();
}

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

const DEFAULT_VALUES = {
  backgroundImage: null,
  labelMap: null,
  maskWorldToIndex: null,
  voxelFunc: null,
  radius: 1,
  label: 0,
  slicingMode: null
};

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

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

  // Make this a VTK object
  macro.obj(publicAPI, model);

  // Also make it an algorithm with no input and one output
  macro.algo(publicAPI, model, 0, 1);
  macro.setGet(publicAPI, model, ['backgroundImage', 'labelMap', 'maskWorldToIndex', 'voxelFunc', 'label', 'radius', 'slicingMode']);

  // Object specific methods
  vtkPaintFilter(publicAPI, model);
}

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

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

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

var vtkPaintFilter$1 = {
  newInstance,
  extend
};

export { vtkPaintFilter$1 as default, extend, newInstance };
