import { m as macro } from '../../macros2.js';
import vtkBoundingBox from '../../Common/DataModel/BoundingBox.js';
import vtkDataArray from '../../Common/Core/DataArray.js';
import { f as vtkMath } from '../../Common/Core/Math/index.js';
import { AttributeTypes } from '../../Common/DataModel/DataSetAttributes/Constants.js';
import vtkPoints from '../../Common/Core/Points.js';
import vtkPolyData from '../../Common/DataModel/PolyData.js';
import vtkTriangle from '../../Common/DataModel/Triangle.js';

const VertexType = {
  VTK_SIMPLE_VERTEX: 0,
  VTK_FIXED_VERTEX: 1,
  VTK_FEATURE_EDGE_VERTEX: 2,
  VTK_BOUNDARY_EDGE_VERTEX: 3
};

// ----------------------------------------------------------------------------
// vtkWindowedSincPolyDataFilter methods
// ----------------------------------------------------------------------------

function vtkWindowedSincPolyDataFilter(publicAPI, model) {
  // Set our className
  model.classHierarchy.push('vtkWindowedSincPolyDataFilter');
  publicAPI.vtkWindowedSincPolyDataFilterExecute = (inPts, inputPolyData, output) => {
    if (!inPts || model.numberOfIterations <= 0) {
      return inPts;
    }
    const inPtsData = inPts.getData();
    const inVerts = inputPolyData.getVerts().getData();
    const inLines = inputPolyData.getLines().getData();
    const inPolys = inputPolyData.getPolys().getData();
    const inStrips = inputPolyData.getStrips().getData();
    const cosFeatureAngle = Math.cos(vtkMath.radiansFromDegrees(model.featureAngle));
    const cosEdgeAngle = Math.cos(vtkMath.radiansFromDegrees(model.edgeAngle));
    const numPts = inPts.getNumberOfPoints();

    // Perform topological analysis. What we're going to do is build a connectivity
    // array of connected vertices. The outcome will be one of three
    // classifications for a vertex: VTK_SIMPLE_VERTEX, VTK_FIXED_VERTEX. or
    // VTK_EDGE_VERTEX. Simple vertices are smoothed using all connected
    // vertices. FIXED vertices are never smoothed. Edge vertices are smoothed
    // using a subset of the attached vertices.
    const verts = new Array(numPts);
    for (let i = 0; i < numPts; ++i) {
      verts[i] = {
        type: VertexType.VTK_SIMPLE_VERTEX,
        edges: null
      };
    }

    // check vertices first. Vertices are never smoothed_--------------
    let npts = 0;
    for (let i = 0; i < inVerts.length; i += npts + 1) {
      npts = inVerts[i];
      const pts = inVerts.slice(i + 1, i + 1 + npts);
      for (let j = 0; j < pts.length; ++j) {
        verts[pts[j]].type = VertexType.VTK_FIXED_VERTEX;
      }
    }

    // now check lines. Only manifold lines can be smoothed------------
    for (let i = 0; i < inLines.length; i += npts + 1) {
      npts = inLines[i];
      const pts = inLines.slice(i + 1, i + 1 + npts);

      // Check for closed loop which are treated specially. Basically the
      // last point is ignored (set to fixed).
      const closedLoop = pts[0] === pts[npts - 1] && npts > 3;
      for (let j = 0; j < npts; ++j) {
        if (verts[pts[j]].type === VertexType.VTK_SIMPLE_VERTEX) {
          // First point
          if (j === 0) {
            if (!closedLoop) {
              verts[pts[0]].type = VertexType.VTK_FIXED_VERTEX;
            } else {
              verts[pts[0]].type = VertexType.VTK_FEATURE_EDGE_VERTEX;
              verts[pts[0]].edges = [pts[npts - 2], pts[1]];
            }
          }
          // Last point
          else if (j === npts - 1 && !closedLoop) {
            verts[pts[j]].type = VertexType.VTK_FIXED_VERTEX;
          }
          // In between point // is edge vertex (unless already edge vertex!)
          else {
            verts[pts[j]].type = VertexType.VTK_FEATURE_EDGE_VERTEX;
            verts[pts[j]].edges = [pts[j - 1], pts[closedLoop && j === npts - 2 ? 0 : j + 1]];
          }
        } // if simple vertex

        // Vertex has been visited before, need to fix it. Special case
        // when working on closed loop.
        else if (verts[pts[j]].type === VertexType.VTK_FEATURE_EDGE_VERTEX && !(closedLoop && j === npts - 1)) {
          verts[pts[j]].type = VertexType.VTK_FIXED_VERTEX;
          verts[pts[j]].edges = null;
        }
      } // for all points in this line
    } // for all lines

    // now polygons and triangle strips-------------------------------
    const numPolys = inPolys.length;
    const numStrips = inStrips.length;
    if (numPolys > 0 || numStrips > 0) {
      const inMesh = vtkPolyData.newInstance();
      inMesh.setPoints(inputPolyData.getPoints());
      inMesh.setPolys(inputPolyData.getPolys());
      const mesh = inMesh;
      let neighbors = [];
      let nei = 0;
      // const numNeiPts = 0;
      const normal = [];
      const neiNormal = [];

      /* TODO: Add vtkTriangleFilter
      if ( (numStrips = inputPolyData.getStrips().GetNumberOfCells()) > 0 )
      { // convert data to triangles
        inMesh.setStrips(inputPolyData.getStrips());
        const toTris = vtkTriangleFilter.newInstance();
        toTris.setInputData(inMesh);
        toTris.update();
        mesh = toTris.getOutput();
      }
      */

      mesh.buildLinks(); // to do neighborhood searching
      const polys = mesh.getPolys().getData();
      let cellId = 0;
      for (let c = 0; c < polys.length; c += npts + 1, ++cellId) {
        npts = polys[c];
        const pts = polys.slice(c + 1, c + 1 + npts);
        for (let i = 0; i < npts; ++i) {
          const p1 = pts[i];
          const p2 = pts[(i + 1) % npts];
          if (verts[p1].edges === null) {
            verts[p1].edges = [];
          }
          if (verts[p2].edges == null) {
            verts[p2].edges = [];
          }
          neighbors = mesh.getCellEdgeNeighbors(cellId, p1, p2);
          const numNei = neighbors.length; // neighbors->GetNumberOfIds();

          let edge = VertexType.VTK_SIMPLE_VERTEX;
          if (numNei === 0) {
            edge = VertexType.VTK_BOUNDARY_EDGE_VERTEX;
          } else if (numNei >= 2) {
            // non-manifold case, check nonmanifold smoothing state
            if (!model.nonManifoldSmoothing) {
              // check to make sure that this edge hasn't been marked already
              let j = 0;
              for (; j < numNei; ++j) {
                if (neighbors[j] < cellId) {
                  break;
                }
              }
              if (j >= numNei) {
                edge = VertexType.VTK_FEATURE_EDGE_VERTEX;
              }
            }
            /* eslint-disable no-cond-assign */
          } else if (numNei === 1 && (nei = neighbors[0]) > cellId) {
            if (model.featureEdgeSmoothing) {
              // TODO: support polygons
              // vtkPolygon::ComputeNormal(inPts,npts,pts,normal);
              vtkTriangle.computeNormal(inPts.getPoint(pts[0]), inPts.getPoint(pts[1]), inPts.getPoint(pts[2]), normal);
              const {
                cellPointIds
              } = mesh.getCellPoints(nei);
              // vtkPolygon::ComputeNormal(inPts,numNeiPts,neiPts,neiNormal);
              vtkTriangle.computeNormal(inPts.getPoint(cellPointIds[0]), inPts.getPoint(cellPointIds[1]), inPts.getPoint(cellPointIds[2]), neiNormal);
              if (vtkMath.dot(normal, neiNormal) <= cosFeatureAngle) {
                edge = VertexType.VTK_FEATURE_EDGE_VERTEX;
              }
            }
          } // a visited edge; skip rest of analysis
          else {
            /* eslint-disable no-continue */
            continue;
          }
          if (edge && verts[p1].type === VertexType.VTK_SIMPLE_VERTEX) {
            verts[p1].edges = [p2];
            verts[p1].type = edge;
          } else if (edge && verts[p1].type === VertexType.VTK_BOUNDARY_EDGE_VERTEX || edge && verts[p1].type === VertexType.VTK_FEATURE_EDGE_VERTEX || !edge && verts[p1].type === VertexType.VTK_SIMPLE_VERTEX) {
            verts[p1].edges.push(p2);
            if (verts[p1].type && edge === VertexType.VTK_BOUNDARY_EDGE_VERTEX) {
              verts[p1].type = VertexType.VTK_BOUNDARY_EDGE_VERTEX;
            }
          }
          if (edge && verts[p2].type === VertexType.VTK_SIMPLE_VERTEX) {
            verts[p2].edges = [p1];
            verts[p2].type = edge;
          } else if (edge && verts[p2].type === VertexType.VTK_BOUNDARY_EDGE_VERTEX || edge && verts[p2].type === VertexType.VTK_FEATURE_EDGE_VERTEX || !edge && verts[p2].type === VertexType.VTK_SIMPLE_VERTEX) {
            verts[p2].edges.push(p1);
            if (verts[p2].type && edge === VertexType.VTK_BOUNDARY_EDGE_VERTEX) {
              verts[p2].type = VertexType.VTK_BOUNDARY_EDGE_VERTEX;
            }
          }
        }
      }
    } // if strips or polys
    for (let i = 0; i < numPts; ++i) {
      if (verts[i].type === VertexType.VTK_SIMPLE_VERTEX) ; else if (verts[i].type === VertexType.VTK_FIXED_VERTEX) ; else if (verts[i].type === VertexType.VTK_FEATURE_EDGE_VERTEX || verts[i].type === VertexType.VTK_BOUNDARY_EDGE_VERTEX) {
        // see how many edges; if two, what the angle is

        if (!model.boundarySmoothing && verts[i].type === VertexType.VTK_BOUNDARY_EDGE_VERTEX) {
          verts[i].type = VertexType.VTK_FIXED_VERTEX;
        } else if ((npts = verts[i].edges.length) !== 2) {
          // can only smooth edges on 2-manifold surfaces
          verts[i].type = VertexType.VTK_FIXED_VERTEX;
        } // check angle between edges
        else {
          const x1 = inPts.getPoint(verts[i].edges[0]);
          const x2 = inPts.getPoint(i);
          const x3 = inPts.getPoint(verts[i].edges[1]);
          const l1 = [0, 0, 0];
          const l2 = [0, 0, 0];
          for (let k = 0; k < 3; ++k) {
            l1[k] = x2[k] - x1[k];
            l2[k] = x3[k] - x2[k];
          }
          if (vtkMath.normalize(l1) >= 0.0 && vtkMath.normalize(l2) >= 0.0 && vtkMath.dot(l1, l2) < cosEdgeAngle) {
            verts[i].type = VertexType.VTK_FIXED_VERTEX;
          } else if (verts[i].type === VertexType.VTK_FEATURE_EDGE_VERTEX) ; else ;
        } // if along edge
      } // if edge vertex
    } // for all points

    // Perform Windowed Sinc function interpolation
    //
    // console.log('Beginning smoothing iterations...');
    // need 4 vectors of points
    let zero = 0;
    let one = 1;
    let two = 2;
    const three = 3;
    const newPts = [];
    newPts.push(vtkPoints.newInstance());
    newPts[zero].setNumberOfPoints(numPts);
    newPts.push(vtkPoints.newInstance());
    newPts[one].setNumberOfPoints(numPts);
    newPts.push(vtkPoints.newInstance());
    newPts[two].setNumberOfPoints(numPts);
    newPts.push(vtkPoints.newInstance());
    newPts[three].setNumberOfPoints(numPts);

    // Get the center and length of the input dataset
    const inCenter = vtkBoundingBox.getCenter(inputPolyData.getBounds());
    const inLength = vtkBoundingBox.getDiagonalLength(inputPolyData.getBounds());
    if (!model.normalizeCoordinates) {
      // initialize to old coordinates
      // for (let i = 0; i < numPts; ++i) {
      //   newPts[zero].setPoint(i, inPts.subarray(i));
      // }
      const copy = macro.newTypedArray(newPts[zero].getDataType(), inPtsData);
      newPts[zero].setData(copy, 3);
    } else {
      // center the data and scale to be within unit cube [-1, 1]
      // initialize to old coordinates
      const normalizedPoint = [0, 0, 0];
      for (let i = 0; i < numPts; ++i) {
        inPts.getPoint(i, normalizedPoint);
        normalizedPoint[0] = (normalizedPoint[0] - inCenter[0]) / inLength;
        normalizedPoint[1] = (normalizedPoint[1] - inCenter[1]) / inLength;
        normalizedPoint[2] = (normalizedPoint[2] - inCenter[2]) / inLength;
        newPts[zero].setPoint(i, ...normalizedPoint);
      }
    }

    // Smooth with a low pass filter defined as a windowed sinc function.
    // Taubin describes this methodology is the IBM tech report RC-20404
    // (#90237, dated 3/12/96) "Optimal Surface Smoothing as Filter Design"
    // G. Taubin, T. Zhang and G. Golub. (Zhang and Golub are at Stanford
    // University)

    // The formulas here follow the notation of Taubin's TR, i.e.
    // newPts[zero], newPts[one], etc.

    // calculate weights and filter coefficients
    const kPb = model.passBand; // reasonable default for kPb in [0, 2] is 0.1
    const thetaPb = Math.acos(1.0 - 0.5 * kPb); // thetaPb in [0, M_PI/2]

    // vtkDebugMacro(<< "thetaPb = " << thetaPb);

    const w = new Array(model.numberOfIterations + 1);
    const c = new Array(model.numberOfIterations + 1);
    const cprime = new Array(model.numberOfIterations + 1);
    const zerovector = [0, 0, 0];

    // Calculate the weights and the Chebychev coefficients c.
    //

    // Windowed sinc function weights. This is for a Hamming window. Other
    // windowing function could be implemented here.
    for (let i = 0; i <= model.numberOfIterations; ++i) {
      w[i] = 0.54 + 0.46 * Math.cos(i * Math.PI / (model.numberOfIterations + 1));
    }

    // Calculate the optimal sigma (offset or fudge factor for the filter).
    // This is a Newton-Raphson Search.
    let fKpb = 0;
    let fPrimeKpb = 0;
    let done = false;
    let sigma = 0.0;
    for (let j = 0; !done && j < 500; ++j) {
      // Chebyshev coefficients
      c[0] = w[0] * (thetaPb + sigma) / Math.PI;
      for (let i = 1; i <= model.numberOfIterations; ++i) {
        c[i] = 2.0 * w[i] * Math.sin(i * (thetaPb + sigma)) / (i * Math.PI);
      }

      // calculate the Chebyshev coefficients for the derivative of the filter
      cprime[model.numberOfIterations] = 0.0;
      cprime[model.numberOfIterations - 1] = 0.0;
      if (model.numberOfIterations > 1) {
        cprime[model.numberOfIterations - 2] = 2.0 * (model.numberOfIterations - 1) * c[model.numberOfIterations - 1];
      }
      for (let i = model.numberOfIterations - 3; i >= 0; --i) {
        cprime[i] = cprime[i + 2] + 2.0 * (i + 1) * c[i + 1];
      }
      // Evaluate the filter and its derivative at kPb (note the discrepancy
      // of calculating the c's based on thetaPb + sigma and evaluating the
      // filter at kPb (which is equivalent to thetaPb)
      fKpb = 0.0;
      fPrimeKpb = 0.0;
      fKpb += c[0];
      fPrimeKpb += cprime[0];
      for (let i = 1; i <= model.numberOfIterations; ++i) {
        if (i === 1) {
          fKpb += c[i] * (1.0 - 0.5 * kPb);
          fPrimeKpb += cprime[i] * (1.0 - 0.5 * kPb);
        } else {
          fKpb += c[i] * Math.cos(i * Math.acos(1.0 - 0.5 * kPb));
          fPrimeKpb += cprime[i] * Math.cos(i * Math.acos(1.0 - 0.5 * kPb));
        }
      }
      // if fKpb is not close enough to 1.0, then adjust sigma
      if (model.numberOfIterations > 1) {
        if (Math.abs(fKpb - 1.0) >= 1e-3) {
          sigma -= (fKpb - 1.0) / fPrimeKpb; // Newton-Rhapson (want f=1)
        } else {
          done = true;
        }
      } else {
        // Order of Chebyshev is 1. Can't use Newton-Raphson to find an
        // optimal sigma. Object will most likely shrink.
        done = true;
        sigma = 0.0;
      }
    }
    if (Math.abs(fKpb - 1.0) >= 1e-3) {
      console.log('An optimal offset for the smoothing filter could not be found.  Unpredictable smoothing/shrinkage may result.');
    }
    const x = [0, 0, 0];
    const y = [0, 0, 0];
    const deltaX = [0, 0, 0];
    const xNew = [0, 0, 0];
    const x1 = [0, 0, 0];
    const x2 = [0, 0, 0];

    // first iteration
    for (let i = 0; i < numPts; ++i) {
      if (verts[i].edges != null && (npts = verts[i].edges.length) > 0) {
        // point is allowed to move
        newPts[zero].getPoint(i, x); // use current points
        deltaX[0] = 0.0;
        deltaX[1] = 0.0;
        deltaX[2] = 0.0;

        // calculate the negative of the laplacian
        // for all connected points
        for (let j = 0; j < npts; ++j) {
          newPts[zero].getPoint(verts[i].edges[j], y);
          for (let k = 0; k < 3; ++k) {
            deltaX[k] += (x[k] - y[k]) / npts;
          }
        }
        // newPts[one] = newPts[zero] - 0.5 newPts[one]
        for (let k = 0; k < 3; ++k) {
          deltaX[k] = x[k] - 0.5 * deltaX[k];
        }
        newPts[one].setPoint(i, ...deltaX);
        if (verts[i].type === VertexType.VTK_FIXED_VERTEX) {
          newPts[zero].getPoint(i, deltaX);
        } else {
          // calculate newPts[three] = c0 newPts[zero] + c1 newPts[one]
          for (let k = 0; k < 3; ++k) {
            deltaX[k] = c[0] * x[k] + c[1] * deltaX[k];
          }
        }
        newPts[three].setPoint(i, ...deltaX);
      } // if can move point
      else {
        // point is not allowed to move, just use the old point...
        // (zero out the Laplacian)
        newPts[one].setPoint(i, ...zerovector);
        newPts[zero].getPoint(i, deltaX);
        newPts[three].setPoint(i, ...deltaX);
      }
    } // for all points

    // for the rest of the iterations
    const pX0 = [0, 0, 0];
    const pX1 = [0, 0, 0];
    const pX3 = [0, 0, 0];
    let iterationNumber = 2;
    for (; iterationNumber <= model.numberOfIterations; iterationNumber++) {
      for (let i = 0; i < numPts; ++i) {
        npts = verts[i].edges != null ? verts[i].edges.length : 0;
        if (npts > 0) {
          // point is allowed to move
          newPts[zero].getPoint(i, pX0); // use current points
          newPts[one].getPoint(i, pX1);
          deltaX[0] = 0.0;
          deltaX[1] = 0.0;
          deltaX[2] = 0.0;

          // calculate the negative laplacian of x1
          for (let j = 0; j < npts; ++j) {
            newPts[one].getPoint(verts[i].edges[j], y);
            for (let k = 0; k < 3; ++k) {
              deltaX[k] += (pX1[k] - y[k]) / npts;
            }
          } // for all connected points

          // Taubin:  x2 = (x1 - x0) + (x1 - x2)
          for (let k = 0; k < 3; ++k) {
            deltaX[k] = pX1[k] - pX0[k] + pX1[k] - deltaX[k];
          }
          newPts[two].setPoint(i, ...deltaX);

          // smooth the vertex (x3 = x3 + cj x2)
          newPts[three].getPoint(i, pX3);
          for (let k = 0; k < 3; ++k) {
            xNew[k] = pX3[k] + c[iterationNumber] * deltaX[k];
          }
          if (verts[i].type !== VertexType.VTK_FIXED_VERTEX) {
            newPts[three].setPoint(i, ...xNew);
          }
        } // if can move point
        else {
          // point is not allowed to move, just use the old point...
          // (zero out the Laplacian)
          newPts[one].setPoint(i, ...zerovector);
          newPts[two].setPoint(i, ...zerovector);
        }
      } // for all points

      // update the pointers. three is always three. all other pointers
      // shift by one and wrap.
      zero = (1 + zero) % 3;
      one = (1 + one) % 3;
      two = (1 + two) % 3;
    } // for all iterations or until converge

    // move the iteration count back down so that it matches the
    // actual number of iterations executed
    --iterationNumber;

    // set zero to three so the correct set of positions is outputted
    zero = three;

    // console.log('Performed', iterationNumber, 'smoothing passes');
    // if we scaled the data down to the unit cube, then scale data back
    // up to the original space
    if (model.normalizeCoordinates) {
      // Re-position the coordinated
      const repositionedPoint = [0, 0, 0];
      for (let i = 0; i < numPts; ++i) {
        newPts[zero].getPoint(i, repositionedPoint);
        for (let j = 0; j < 3; ++j) {
          repositionedPoint[j] = repositionedPoint[j] * inLength + inCenter[j];
        }
        newPts[zero].setPoint(i, ...repositionedPoint);
      }
    }
    if (model.generateErrorScalars) {
      const newScalars = new Float32Array(numPts);
      for (let i = 0; i < numPts; ++i) {
        inPts.getPoint(i, x1);
        newPts[zero].getPoint(i, x2);
        newScalars[i] = Math.sqrt(Math.distance2BetweenPoints(x1, x2));
      }
      const newScalarsArray = vtkDataArray.newInstance({
        numberOfComponents: 1,
        values: newScalars
      });
      const idx = output.getPointData().addArray(newScalarsArray);
      output.getPointData().setActiveAttribute(idx, AttributeTypes.SCALARS);
    }
    if (model.generateErrorVectors) {
      const newVectors = new Float32Array(3 * numPts);
      for (let i = 0; i < numPts; ++i) {
        inPts.getPoint(i, x1);
        newPts[zero].getPoint(i, x2);
        for (let j = 0; j < 3; ++j) {
          newVectors[3 * i + j] = x2[j] - x1[j];
        }
      }
      const newVectorsArray = vtkDataArray.newInstance({
        numberOfComponents: 3,
        values: newVectors
      });
      output.getPointData().setVectors(newVectorsArray);
    }
    return newPts[zero];
  };
  publicAPI.requestData = (inData, outData) => {
    const numberOfInputs = publicAPI.getNumberOfInputPorts();
    if (!numberOfInputs) {
      return;
    }
    const input = inData[0];
    if (!input) {
      return;
    }
    const output = vtkPolyData.newInstance();
    const outputPoints = publicAPI.vtkWindowedSincPolyDataFilterExecute(input.getPoints(), input, output);
    output.setPointData(input.getPointData());
    output.setCellData(input.getCellData());
    output.setFieldData(input.getFieldData());
    output.setPoints(outputPoints);
    output.setVerts(input.getVerts());
    output.setLines(input.getLines());
    output.setPolys(input.getPolys());
    output.setStrips(input.getStrips());
    outData[0] = output;
  };
}

// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------
const DEFAULT_VALUES = {
  numberOfIterations: 20,
  passBand: 0.1,
  featureAngle: 45.0,
  edgeAngle: 15.0,
  featureEdgeSmoothing: 0,
  boundarySmoothing: 1,
  nonManifoldSmoothing: 0,
  generateErrorScalars: 0,
  generateErrorVectors: 0,
  normalizeCoordinates: 0
};

// ----------------------------------------------------------------------------
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 one input and one output */

  macro.algo(publicAPI, model, 1, 1);

  /* Setters */
  macro.setGet(publicAPI, model, ['numberOfIterations', 'passBand', 'featureAngle', 'edgeAngle', 'featureEdgeSmoothing', 'boundarySmoothing', 'nonManifoldSmoothing', 'generateErrorScalars', 'generateErrorVectors', 'normalizeCoordinates']);

  /* Object specific methods */

  vtkWindowedSincPolyDataFilter(publicAPI, model);
}

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

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

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

var vtkWindowedSincPolyDataFilter$1 = {
  newInstance,
  extend
};

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