// Copyright (c) 1999-2014 OPEN CASCADE SAS
//
// This file is part of Open CASCADE Technology software library.
//
// This library is free software; you can redistribute it and/or modify it under
// the terms of the GNU Lesser General Public License version 2.1 as published
// by the Free Software Foundation, with special exception defined in the file
// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
// distribution for complete text of the license and disclaimer of any warranty.
//
// Alternatively, this file may be used under the terms of Open CASCADE
// commercial license or contractual agreement.

#include <ShapeConstruct_ProjectCurveOnSurface.hxx>

#include <Approx_CurveOnSurface.hxx>
#include <ElCLib.hxx>
#include <Geom2d_BSplineCurve.hxx>
#include <Geom2d_Circle.hxx>
#include <Geom2d_Curve.hxx>
#include <Geom2d_Ellipse.hxx>
#include <Geom2d_Hyperbola.hxx>
#include <Geom2d_Line.hxx>
#include <Geom2d_Parabola.hxx>
#include <Geom2d_TrimmedCurve.hxx>
#include <Geom2dAdaptor.hxx>
#include <Geom2dAPI_Interpolate.hxx>
#include <Geom_BezierSurface.hxx>
#include <Geom_BSplineCurve.hxx>
#include <Geom_BSplineSurface.hxx>
#include <Geom_Curve.hxx>
#include <Geom_OffsetSurface.hxx>
#include <Geom_Plane.hxx>
#include <Geom_RectangularTrimmedSurface.hxx>
#include <Geom_SphericalSurface.hxx>
#include <Geom_Surface.hxx>
#include <Geom_SurfaceOfLinearExtrusion.hxx>
#include <Geom_TrimmedCurve.hxx>
#include <GeomAdaptor_Curve.hxx>
#include <GeomAdaptor_Surface.hxx>
#include <GeomAPI_Interpolate.hxx>
#include <GeomAPI_PointsToBSpline.hxx>
#include <GeomProjLib.hxx>
#include <gp_Pnt2d.hxx>
#include <IntCurve_IntConicConic.hxx>
#include <IntRes2d_Domain.hxx>
#include <NCollection_IncAllocator.hxx>
#include <NCollection_Sequence.hxx>
#include <NCollection_DynamicArray.hxx>
#include <Precision.hxx>
#include <ProjLib_ProjectedCurve.hxx>
#include <ShapeAnalysis.hxx>
#include <ShapeAnalysis_Curve.hxx>
#include <ShapeAnalysis_Surface.hxx>
#include <ShapeExtend.hxx>
#include <Standard_ErrorHandler.hxx>
#include <Standard_Failure.hxx>
#include <Standard_Type.hxx>
#include <gp_Pnt.hxx>
#include <NCollection_Array1.hxx>
#include <NCollection_HArray1.hxx>
#include <Standard_Integer.hxx>

#include <algorithm>

IMPLEMENT_STANDARD_RTTIEXT(ShapeConstruct_ProjectCurveOnSurface, Standard_Transient)

namespace
{
//! Default number of control points for discretization.
constexpr int THE_NCONTROL = 23;

//! Utility class for projecting points onto a surface with B-spline corner cache optimization.
//! For clamped B-spline surfaces, caches corner pole positions and their exact UV parameters
//! to avoid expensive ValueOfUV calls when projected points coincide with surface corners.
//! Only corner poles are cached because B-spline surfaces pass through corners only
//! (when end multiplicities equal degree + 1).
class SurfaceProjectorWithCache
{
public:
  //! Constructor - initializes the projector with a surface.
  //! For B-spline surfaces, builds the corner cache automatically.
  //! @param[in] theSurf the surface analysis object
  SurfaceProjectorWithCache(const occ::handle<ShapeAnalysis_Surface>& theSurf)
      : mySurf(theSurf)
  {
    if (theSurf.IsNull())
    {
      return;
    }

    occ::handle<Geom_BSplineSurface> aBSplineSurf =
      occ::down_cast<Geom_BSplineSurface>(theSurf->Surface());
    if (!aBSplineSurf.IsNull())
    {
      buildCornerCache(aBSplineSurf);
    }
  }

  //! Projects a 3D point onto the surface.
  //! First checks B-spline corner cache, then falls back to ValueOfUV.
  //! @param[in] thePoint the 3D point to project
  //! @param[in] theTol the tolerance for projection
  //! @param[in] theTolSq squared tolerance for corner matching
  //! @return the UV coordinates on the surface
  gp_Pnt2d ValueOfUV(const gp_Pnt& thePoint, const double theTol, const double theTolSq) const
  {
    gp_Pnt2d aResult;
    if (findInCornerCache(thePoint, theTolSq, aResult))
    {
      // Corner UV is exact, but refine for numerical stability
      return mySurf->NextValueOfUV(aResult, thePoint, theTol, theTol);
    }
    return mySurf->ValueOfUV(thePoint, theTol);
  }

  //! Projects a 3D point onto the surface using a hint from previous projection.
  //! First checks B-spline corner cache, then falls back to NextValueOfUV.
  //! @param[in] theHint the UV hint from previous projection
  //! @param[in] thePoint the 3D point to project
  //! @param[in] theTol the tolerance for projection
  //! @param[in] theTolSq squared tolerance for corner matching
  //! @param[in] theStep the step tolerance
  //! @return the UV coordinates on the surface
  gp_Pnt2d NextValueOfUV(const gp_Pnt2d& theHint,
                         const gp_Pnt&   thePoint,
                         const double    theTol,
                         const double    theTolSq,
                         const double    theStep) const
  {
    gp_Pnt2d aResult;
    if (findInCornerCache(thePoint, theTolSq, aResult))
    {
      // Corner UV is exact, but refine for numerical stability
      return mySurf->NextValueOfUV(aResult, thePoint, theTol, theTol);
    }
    return mySurf->NextValueOfUV(theHint, thePoint, theTol, theStep);
  }

  //! Returns the gap from the last projection
  double Gap() const { return mySurf->Gap(); }

private:
  //! Builds cache from B-spline surface corner poles.
  //! For clamped B-splines (multiplicity = degree + 1 at ends), the surface passes
  //! exactly through corner poles, so we can use their UV parameters directly.
  //! @param[in] theSurface the B-spline surface
  void buildCornerCache(const occ::handle<Geom_BSplineSurface>& theSurface)
  {
    const int aNbPolesU = theSurface->NbUPoles();
    const int aNbPolesV = theSurface->NbVPoles();
    const int aDegreeU  = theSurface->UDegree();
    const int aDegreeV  = theSurface->VDegree();

    // Check if surface is clamped (end multiplicities = degree + 1)
    const int aFirstUMult = theSurface->UMultiplicity(1);
    const int aLastUMult  = theSurface->UMultiplicity(theSurface->NbUKnots());
    const int aFirstVMult = theSurface->VMultiplicity(1);
    const int aLastVMult  = theSurface->VMultiplicity(theSurface->NbVKnots());

    const bool isUFirstClamped = (aFirstUMult >= aDegreeU + 1);
    const bool isULastClamped  = (aLastUMult >= aDegreeU + 1);
    const bool isVFirstClamped = (aFirstVMult >= aDegreeV + 1);
    const bool isVLastClamped  = (aLastVMult >= aDegreeV + 1);

    // Get parameter bounds
    const double aUFirst = theSurface->UKnot(1);
    const double aULast  = theSurface->UKnot(theSurface->NbUKnots());
    const double aVFirst = theSurface->VKnot(1);
    const double aVLast  = theSurface->VKnot(theSurface->NbVKnots());

    // Cache corner poles where surface passes through them
    // Corner (1, 1) - UFirst, VFirst
    if (isUFirstClamped && isVFirstClamped)
    {
      myCorners3d.Append(theSurface->Pole(1, 1));
      myCorners2d.Append(gp_Pnt2d(aUFirst, aVFirst));
    }

    // Corner (NbPolesU, 1) - ULast, VFirst
    if (isULastClamped && isVFirstClamped)
    {
      myCorners3d.Append(theSurface->Pole(aNbPolesU, 1));
      myCorners2d.Append(gp_Pnt2d(aULast, aVFirst));
    }

    // Corner (1, NbPolesV) - UFirst, VLast
    if (isUFirstClamped && isVLastClamped)
    {
      myCorners3d.Append(theSurface->Pole(1, aNbPolesV));
      myCorners2d.Append(gp_Pnt2d(aUFirst, aVLast));
    }

    // Corner (NbPolesU, NbPolesV) - ULast, VLast
    if (isULastClamped && isVLastClamped)
    {
      myCorners3d.Append(theSurface->Pole(aNbPolesU, aNbPolesV));
      myCorners2d.Append(gp_Pnt2d(aULast, aVLast));
    }
  }

  //! Finds the closest corner to a 3D point within tolerance.
  //! @param[in] thePoint the 3D point to find
  //! @param[in] theTolSq squared tolerance for matching
  //! @param[out] theUV the UV parameter if found
  //! @return true if a matching corner was found
  bool findInCornerCache(const gp_Pnt& thePoint, const double theTolSq, gp_Pnt2d& theUV) const
  {
    for (int i = 0; i < myCorners3d.Length(); ++i)
    {
      if (myCorners3d(i).SquareDistance(thePoint) < theTolSq)
      {
        theUV = myCorners2d(i);
        return true;
      }
    }
    return false;
  }

private:
  occ::handle<ShapeAnalysis_Surface> mySurf;      //!< Surface to project on
  NCollection_DynamicArray<gp_Pnt>   myCorners3d; //!< 3D positions of B-spline surface corners
  NCollection_DynamicArray<gp_Pnt2d> myCorners2d; //!< UV parameters of B-spline surface corners
};

//=================================================================================================

//! Extracts B-spline curve from a possibly nested trimmed curve.
//! Recursively unwraps trimmed curves to find B-spline basis.
//! @param[in] theCurve the curve to extract from
//! @return the extracted B-spline curve, or null if not a B-spline
occ::handle<Geom_BSplineCurve> extractBSplineCurve(const occ::handle<Geom_Curve>& theCurve)
{
  occ::handle<Geom_Curve> aCurve = theCurve;

  // Recursively unwrap trimmed curves
  while (!aCurve.IsNull() && aCurve->IsKind(STANDARD_TYPE(Geom_TrimmedCurve)))
  {
    aCurve = occ::down_cast<Geom_TrimmedCurve>(aCurve)->BasisCurve();
  }

  return occ::down_cast<Geom_BSplineCurve>(aCurve);
}

//=================================================================================================

//! Adjusts second point to first point considering surface periodicity.
//! For periodic surfaces, adjusts the second point coordinates to be within
//! half-period of the first point.
//! @param[in] theFirstPoint the reference point
//! @param[in,out] theSecondPoint the point to adjust
//! @param[in] theSurf the surface to check for periodicity
void adjustSecondToFirstPoint(const gp_Pnt2d&                  theFirstPoint,
                              gp_Pnt2d&                        theSecondPoint,
                              const occ::handle<Geom_Surface>& theSurf)
{
  if (theSurf->IsUPeriodic())
  {
    const double anUPeriod = theSurf->UPeriod();
    const double aNewU     = ElCLib::InPeriod(theSecondPoint.X(),
                                          theFirstPoint.X() - anUPeriod / 2,
                                          theFirstPoint.X() + anUPeriod / 2);
    theSecondPoint.SetX(aNewU);
  }
  if (theSurf->IsVPeriodic())
  {
    const double aVPeriod = theSurf->VPeriod();
    const double aNewV    = ElCLib::InPeriod(theSecondPoint.Y(),
                                          theFirstPoint.Y() - aVPeriod / 2,
                                          theFirstPoint.Y() + aVPeriod / 2);
    theSecondPoint.SetY(aNewV);
  }
}

//=================================================================================================

//! Fixes possible period jumps in an array of 4 points.
//! Handles the walking period parameter to ensure smooth transition
//! across periodic boundaries.
//! @param[in,out] thePnt array of 4 points to fix
//! @param[in] theIdx coordinate index (1 for U, 2 for V)
//! @param[in] thePeriod the period value
//! @param[in] theSavedPoint index of the reference point (-1 for none)
//! @param[in] theSavedParam the saved parameter value
//! @return true if a period jump was detected and fixed
bool fixPeriodicityTroubles(gp_Pnt2d*    thePnt,
                            const int    theIdx,
                            const double thePeriod,
                            const int    theSavedPoint,
                            const double theSavedParam)
{
  double aSavedParam;
  int    aSavedPoint;
  double aMinParam = 0.0, aMaxParam = thePeriod;

  if (theSavedPoint < 0)
  {
    aSavedParam = 0.5 * thePeriod;
    aSavedPoint = 0;
  }
  else
  {
    aSavedParam = theSavedParam;
    aSavedPoint = theSavedPoint;
    while (aMinParam > aSavedParam)
    {
      aMinParam -= thePeriod;
      aMaxParam -= thePeriod;
    }
    while (aMaxParam < aSavedParam)
    {
      aMinParam += thePeriod;
      aMaxParam += thePeriod;
    }
  }

  double aFixIsoParam = aMinParam;
  bool   isIsoLine    = false;
  if (aMaxParam - aSavedParam < Precision::PConfusion()
      || aSavedParam - aMinParam < Precision::PConfusion())
  {
    aFixIsoParam = aSavedParam;
    isIsoLine    = true;
  }

  // Normalize all coordinates to [aMinParam, aMaxParam)
  for (int i = 0; i < 4; i++)
  {
    double aParam = thePnt[i].Coord(theIdx);
    double aShift = ShapeAnalysis::AdjustToPeriod(aParam, aMinParam, aMaxParam);
    aParam += aShift;

    if (isIsoLine)
    {
      if (aMaxParam - aParam < Precision::PConfusion()
          || aParam - aMinParam < Precision::PConfusion())
      {
        aParam = aFixIsoParam;
      }
    }
    else
    {
      if (aMaxParam - aParam < Precision::PConfusion())
      {
        aParam = aMaxParam;
      }
      if (aParam - aMinParam < Precision::PConfusion())
      {
        aParam = aMinParam;
      }
    }

    thePnt[i].SetCoord(theIdx, aParam);
  }

  // Find possible period jump and increasing or decreasing coordinates vector
  bool   isJump    = false;
  double aPrevDiff = 0.0;
  double aSumDiff  = 1.0;
  for (int i = 0; i < 3; i++)
  {
    double aDiff = thePnt[i + 1].Coord(theIdx) - thePnt[i].Coord(theIdx);
    if (aDiff < -Precision::PConfusion())
    {
      aSumDiff *= -1.0;
    }
    if (aDiff * aPrevDiff < -Precision::PConfusion())
    {
      isJump = true;
    }
    aPrevDiff = aDiff;
  }

  if (!isJump)
  {
    return false;
  }

  if (aSumDiff > 0)
  {
    for (int i = aSavedPoint; i > 0; i--)
    {
      if (thePnt[i].Coord(theIdx) > thePnt[i - 1].Coord(theIdx))
      {
        thePnt[i - 1].SetCoord(theIdx, thePnt[i - 1].Coord(theIdx) + thePeriod);
      }
    }
    for (int i = aSavedPoint; i < 3; i++)
    {
      if (thePnt[i].Coord(theIdx) < thePnt[i + 1].Coord(theIdx))
      {
        thePnt[i + 1].SetCoord(theIdx, thePnt[i + 1].Coord(theIdx) - thePeriod);
      }
    }
  }
  else
  {
    for (int i = aSavedPoint; i > 0; i--)
    {
      if (thePnt[i].Coord(theIdx) < thePnt[i - 1].Coord(theIdx))
      {
        thePnt[i - 1].SetCoord(theIdx, thePnt[i - 1].Coord(theIdx) - thePeriod);
      }
    }
    for (int i = aSavedPoint; i < 3; i++)
    {
      if (thePnt[i].Coord(theIdx) > thePnt[i + 1].Coord(theIdx))
      {
        thePnt[i + 1].SetCoord(theIdx, thePnt[i + 1].Coord(theIdx) + thePeriod);
      }
    }
  }

  return true;
}

//=================================================================================================

//! Checks if B-spline curve has uneven parameterization requiring special handling.
//! Computes the ratio of maximum to minimum parameterization speed across knot intervals.
//! If this ratio exceeds a threshold, the curve should be projected using ProjLib instead
//! of the standard approximation approach.
//! @param[in] theCurve the curve to check (may be trimmed)
//! @param[in] theFirst the first parameter of the working range
//! @param[in] theLast the last parameter of the working range
//! @param[out] theBSpline the extracted B-spline curve (if any)
//! @return true if the curve has uneven parameterization requiring ProjLib
bool isBSplineCurveInvalid(const occ::handle<Geom_Curve>&  theCurve,
                           const double                    theFirst,
                           const double                    theLast,
                           occ::handle<Geom_BSplineCurve>& theBSpline)
{
  theBSpline = extractBSplineCurve(theCurve);
  if (theBSpline.IsNull())
  {
    return false;
  }

  // Compute parametrization speed on each knot interval inside [theFirst, theLast].
  // If quotient = (MaxSpeed / MinSpeed) >= aMaxQuotientCoeff then use PerformByProjLib.
  double aFirstParam = theFirst;
  double aLastParam  = theLast;

  // Find first knot index
  int anIdx = 1;
  for (; anIdx <= theBSpline->NbKnots() && aFirstParam < theLast; anIdx++)
  {
    if (theBSpline->Knot(anIdx) > theFirst)
    {
      break;
    }
  }

  GeomAdaptor_Curve            aC3DAdaptor(theCurve);
  double                       aMinParSpeed = Precision::Infinite();
  NCollection_Sequence<double> aKnotCoeffs;

  for (; anIdx <= theBSpline->NbKnots() && aFirstParam < theLast; anIdx++)
  {
    // Fill current knot interval
    aLastParam     = std::min(theLast, theBSpline->Knot(anIdx));
    int aNbIntPnts = THE_NCONTROL;

    // Adapt number of inner points according to the length of the interval
    // to avoid a lot of calculations on small range of parameters.
    if (anIdx > 1)
    {
      const double aLenThres = 1.e-2;
      const double aLenRatio =
        (aLastParam - aFirstParam) / (theBSpline->Knot(anIdx) - theBSpline->Knot(anIdx - 1));
      if (aLenRatio < aLenThres)
      {
        aNbIntPnts = int(aLenRatio / aLenThres * aNbIntPnts);
        if (aNbIntPnts < 2)
        {
          aNbIntPnts = 2;
        }
      }
    }

    double aStep = (aLastParam - aFirstParam) / (aNbIntPnts - 1);
    gp_Pnt p3d1, p3d2;

    // Start filling from first point
    aC3DAdaptor.D0(aFirstParam, p3d1);

    double aLength3d = 0.0;
    for (int anIntIdx = 1; anIntIdx < aNbIntPnts; anIntIdx++)
    {
      double aParam = aFirstParam + aStep * anIntIdx;
      aC3DAdaptor.D0(aParam, p3d2);
      const double aDist = p3d2.Distance(p3d1);

      aLength3d += aDist;
      p3d1 = p3d2;

      aMinParSpeed = std::min(aMinParSpeed, aDist / aStep);
    }

    const double aCoeff = aLength3d / (aLastParam - aFirstParam);
    if (std::abs(aCoeff) > gp::Resolution())
    {
      aKnotCoeffs.Append(aCoeff);
    }
    aFirstParam = aLastParam;
  }

  double anEvenlyCoeff = 0;
  if (aKnotCoeffs.Size() > 0)
  {
    anEvenlyCoeff = *std::max_element(aKnotCoeffs.begin(), aKnotCoeffs.end())
                    / *std::min_element(aKnotCoeffs.begin(), aKnotCoeffs.end());
  }

  const double aMaxQuotientCoeff = 1500.0;
  return (anEvenlyCoeff > aMaxQuotientCoeff && aMinParSpeed > Precision::Confusion());
}

//=================================================================================================

//! Generates discretization points for a curve.
//! Uses uniform distribution for general curves, and adjusts the number of points
//! for B-splines based on their knot structure to ensure adequate sampling.
//! @param[in] theCurve the curve to discretize
//! @param[in] theFirst the first parameter
//! @param[in] theLast the last parameter
//! @param[in] theNbControlPoints the minimum number of points
//! @param[out] thePoints the generated 3D points (resized as needed)
//! @param[out] theParams the corresponding parameters (resized as needed)
//! @return the actual number of generated points
int generateCurvePoints(const occ::handle<Geom_Curve>&                     theCurve,
                        const double                                       theFirst,
                        const double                                       theLast,
                        const int                                          theNbControlPoints,
                        ShapeConstruct_ProjectCurveOnSurface::ArrayOfPnt&  thePoints,
                        ShapeConstruct_ProjectCurveOnSurface::ArrayOfReal& theParams)
{
  occ::handle<Geom_BSplineCurve> aBSpline = extractBSplineCurve(theCurve);
  int                            aNbPini  = theNbControlPoints;

  if (!aBSpline.IsNull())
  {
    int aUsedKnots = 0;
    for (int i = 1; i < aBSpline->NbKnots(); i++)
    {
      if (aBSpline->Knot(i + 1) > theFirst && aBSpline->Knot(i) < theLast)
      {
        aUsedKnots++;
      }
    }
    int aMinPnt = aUsedKnots * (aBSpline->Degree() + 1);
    while (aNbPini < aMinPnt)
    {
      aNbPini += THE_NCONTROL - 1;
    }
  }

  thePoints.Resize(1, aNbPini, false);
  theParams.Resize(1, aNbPini, false);

  const double aDeltaParam = (theLast - theFirst) / (aNbPini - 1);

  for (int i = 1; i <= aNbPini; ++i)
  {
    double aParam;
    if (i == 1)
    {
      aParam = theFirst;
    }
    else if (i == aNbPini)
    {
      aParam = theLast;
    }
    else
    {
      aParam = theFirst + (i - 1) * aDeltaParam;
    }

    gp_Pnt aPoint;
    theCurve->D0(aParam, aPoint);
    thePoints.SetValue(i, aPoint);
    theParams.SetValue(i, aParam);
  }

  return aNbPini;
}

//=================================================================================================

//! Wrapper for ShapeAnalysis_Surface::ProjectDegenerated.
//! Converts NCollection_Array1 containers to sequences, performs projection,
//! then copies results back. Required because ShapeAnalysis_Surface uses
//! sequence containers.
//! @param[in] theSurf the analysis surface
//! @param[in] theNbPnt the number of points
//! @param[in] thePoints the 3D points array
//! @param[in,out] thePoints2d the 2D points array (adjusted for degenerate regions)
//! @param[in] thePreci the precision
//! @param[in] theDirect true to check from start, false to check from end
void projectDegeneratedPoints(const occ::handle<ShapeAnalysis_Surface>& theSurf,
                              const int                                 theNbPnt,
                              const NCollection_Array1<gp_Pnt>&         thePoints,
                              NCollection_Array1<gp_Pnt2d>&             thePoints2d,
                              const double                              thePreci,
                              const bool                                theDirect)
{
  // Use incremental allocator for sequences when there are enough elements
  // to benefit from pooled memory allocation.
  constexpr int THE_ALLOC_THRESHOLD = 100;

  occ::handle<NCollection_BaseAllocator> anAlloc;
  if (theNbPnt > THE_ALLOC_THRESHOLD)
  {
    anAlloc = new NCollection_IncAllocator(NCollection_IncAllocator::THE_MINIMUM_BLOCK_SIZE);
  }

  // Convert arrays to sequences for ShapeAnalysis_Surface::ProjectDegenerated
  NCollection_Sequence<gp_Pnt>   aPoints3d(anAlloc);
  NCollection_Sequence<gp_Pnt2d> aPoints2d(anAlloc);

  for (int i = 1; i <= theNbPnt; ++i)
  {
    aPoints3d.Append(thePoints(i));
    aPoints2d.Append(thePoints2d(i));
  }

  // Call the method that expects sequences
  theSurf->ProjectDegenerated(theNbPnt, aPoints3d, aPoints2d, thePreci, theDirect);

  // Copy results back to array
  for (int i = 1; i <= theNbPnt; ++i)
  {
    thePoints2d.SetValue(i, aPoints2d.Value(i));
  }
}

} // namespace

//=================================================================================================

ShapeConstruct_ProjectCurveOnSurface::ShapeConstruct_ProjectCurveOnSurface()
    : myPreci(Precision::Confusion()),
      myStatus(ShapeExtend::EncodeStatus(ShapeExtend_OK)),
      myAdjustOverDegen(1)
{
}

//=================================================================================================

void ShapeConstruct_ProjectCurveOnSurface::Init(const occ::handle<Geom_Surface>& theSurf,
                                                const double                     thePreci)
{
  Init(new ShapeAnalysis_Surface(theSurf), thePreci);
}

//=================================================================================================

void ShapeConstruct_ProjectCurveOnSurface::Init(const occ::handle<ShapeAnalysis_Surface>& theSurf,
                                                const double                              thePreci)
{
  SetSurface(theSurf);
  SetPrecision(thePreci);
}

//=================================================================================================

void ShapeConstruct_ProjectCurveOnSurface::SetSurface(const occ::handle<Geom_Surface>& theSurf)
{
  SetSurface(new ShapeAnalysis_Surface(theSurf));
}

//=================================================================================================

void ShapeConstruct_ProjectCurveOnSurface::SetSurface(
  const occ::handle<ShapeAnalysis_Surface>& theSurf)
{
  if (mySurf == theSurf)
  {
    return;
  }
  mySurf  = theSurf;
  myCache = CacheArray();
}

//=================================================================================================

void ShapeConstruct_ProjectCurveOnSurface::SetPrecision(const double thePreci)
{
  myPreci = thePreci;
}

//=================================================================================================

int& ShapeConstruct_ProjectCurveOnSurface::AdjustOverDegenMode()
{
  return myAdjustOverDegen;
}

//=================================================================================================

bool ShapeConstruct_ProjectCurveOnSurface::Status(const ShapeExtend_Status theStatus) const
{
  return ShapeExtend::DecodeStatus(myStatus, theStatus);
}

//=================================================================================================

bool ShapeConstruct_ProjectCurveOnSurface::Perform(const occ::handle<Geom_Curve>& theC3D,
                                                   const double                   theFirst,
                                                   const double                   theLast,
                                                   occ::handle<Geom2d_Curve>&     theC2D,
                                                   const double                   theTolFirst,
                                                   const double                   theTolLast)
{
  myStatus = ShapeExtend::EncodeStatus(ShapeExtend_OK);

  if (mySurf.IsNull() || theC3D.IsNull())
  {
    theC2D.Nullify();
    myStatus |= ShapeExtend::EncodeStatus(ShapeExtend_FAIL1);
    return false;
  }

  // Try analytical projection first
  occ::handle<Geom_Curve> aCurve3DTrim = theC3D;
  if (!theC3D->IsKind(STANDARD_TYPE(Geom_BoundedCurve)))
  {
    aCurve3DTrim = new Geom_TrimmedCurve(theC3D, theFirst, theLast);
  }

  theC2D = projectAnalytic(aCurve3DTrim);
  if (!theC2D.IsNull())
  {
    myStatus |= ShapeExtend::EncodeStatus(ShapeExtend_DONE1);
    return true;
  }

  // Handle problematic B-spline curves with uneven parameterization
  occ::handle<Geom_BSplineCurve> aBSpline;
  if (isBSplineCurveInvalid(theC3D, theFirst, theLast, aBSpline))
  {
    // Use ProjLib for curves with highly uneven parameterization speed
    if (PerformByProjLib(theC3D, theFirst, theLast, theC2D))
    {
      return Status(ShapeExtend_DONE);
    }
  }

  // Generate curve points
  ArrayOfPnt  aPoints;
  ArrayOfReal aParams;
  const int aNbPnt = generateCurvePoints(theC3D, theFirst, theLast, THE_NCONTROL, aPoints, aParams);

  // Approximate pcurve
  ArrayOfPnt2d aPoints2d(1, aNbPnt);
  for (int i = 1; i <= aNbPnt; i++)
  {
    aPoints2d.SetValue(i, gp_Pnt2d(0., 0.));
  }

  approxPCurve(aNbPnt, theC3D, theTolFirst, theTolLast, aPoints, aParams, aPoints2d, theC2D);

  const int aNbPini = aPoints.Length();
  if (!theC2D.IsNull())
  {
    myStatus |= ShapeExtend::EncodeStatus(ShapeExtend_DONE2);
    return true;
  }

  // Interpolate the result
  theC2D = interpolatePCurve(aNbPini, aPoints2d, aParams);

  myStatus |= ShapeExtend::EncodeStatus(theC2D.IsNull() ? ShapeExtend_FAIL1 : ShapeExtend_DONE2);
  return Status(ShapeExtend_DONE);
}

//=================================================================================================

bool ShapeConstruct_ProjectCurveOnSurface::PerformByProjLib(const occ::handle<Geom_Curve>& theC3D,
                                                            const double                   theFirst,
                                                            const double                   theLast,
                                                            occ::handle<Geom2d_Curve>&     theC2D)
{
  theC2D.Nullify();
  if (mySurf.IsNull())
  {
    myStatus = ShapeExtend::EncodeStatus(ShapeExtend_FAIL1);
    return false;
  }

  try
  {
    OCC_CATCH_SIGNALS
    occ::handle<GeomAdaptor_Surface> aGAS = mySurf->Adaptor3d();
    occ::handle<GeomAdaptor_Curve>   aGAC = new GeomAdaptor_Curve(theC3D, theFirst, theLast);
    ProjLib_ProjectedCurve           aProjector(aGAS, aGAC);

    switch (aProjector.GetType())
    {
      case GeomAbs_Line:
        theC2D = new Geom2d_Line(aProjector.Line());
        break;
      case GeomAbs_Circle:
        theC2D = new Geom2d_Circle(aProjector.Circle());
        break;
      case GeomAbs_Ellipse:
        theC2D = new Geom2d_Ellipse(aProjector.Ellipse());
        break;
      case GeomAbs_Parabola:
        theC2D = new Geom2d_Parabola(aProjector.Parabola());
        break;
      case GeomAbs_Hyperbola:
        theC2D = new Geom2d_Hyperbola(aProjector.Hyperbola());
        break;
      case GeomAbs_BSplineCurve:
        theC2D = aProjector.BSpline();
        break;
      default:
        break;
    }

    if (theC2D.IsNull())
    {
      myStatus = ShapeExtend::EncodeStatus(ShapeExtend_FAIL2);
      return false;
    }
    else
    {
      myStatus = ShapeExtend::EncodeStatus(ShapeExtend_DONE1);
      return true;
    }
  }
  catch (Standard_Failure const& anException)
  {
#ifdef OCCT_DEBUG
    std::cout << "Warning: ShapeConstruct_ProjectCurveOnSurface::PerformByProjLib(): Exception: ";
    anException.Print(std::cout);
    std::cout << std::endl;
#endif
    (void)anException;
    myStatus = ShapeExtend::EncodeStatus(ShapeExtend_FAIL3);
    theC2D.Nullify();
  }
  return false;
}

//=================================================================================================

occ::handle<Geom2d_Curve> ShapeConstruct_ProjectCurveOnSurface::projectAnalytic(
  const occ::handle<Geom_Curve>& theC3D) const
{
  occ::handle<Geom2d_Curve> aResult;

  occ::handle<Geom_Surface> aSurf  = mySurf->Surface();
  occ::handle<Geom_Plane>   aPlane = occ::down_cast<Geom_Plane>(aSurf);

  if (aPlane.IsNull())
  {
    occ::handle<Geom_RectangularTrimmedSurface> aRTS =
      occ::down_cast<Geom_RectangularTrimmedSurface>(aSurf);
    if (!aRTS.IsNull())
    {
      aPlane = occ::down_cast<Geom_Plane>(aRTS->BasisSurface());
    }
    else
    {
      occ::handle<Geom_OffsetSurface> anOS = occ::down_cast<Geom_OffsetSurface>(aSurf);
      if (!anOS.IsNull())
      {
        aPlane = occ::down_cast<Geom_Plane>(anOS->BasisSurface());
      }
    }
  }

  if (!aPlane.IsNull())
  {
    occ::handle<Geom_Curve> aProjOnPlane =
      GeomProjLib::ProjectOnPlane(theC3D, aPlane, aPlane->Position().Direction(), true);
    occ::handle<GeomAdaptor_Curve> aHC = new GeomAdaptor_Curve(aProjOnPlane);
    ProjLib_ProjectedCurve         aProj(mySurf->Adaptor3d(), aHC);

    aResult = Geom2dAdaptor::MakeCurve(aProj);
    if (aResult.IsNull())
    {
      return aResult;
    }

    if (aResult->IsKind(STANDARD_TYPE(Geom2d_TrimmedCurve)))
    {
      occ::handle<Geom2d_TrimmedCurve> aTC = occ::down_cast<Geom2d_TrimmedCurve>(aResult);
      aResult                              = aTC->BasisCurve();
    }

    return aResult;
  }

  return aResult;
}

//=================================================================================================

occ::handle<Geom2d_Curve> ShapeConstruct_ProjectCurveOnSurface::getLine(
  const ArrayOfPnt&  thePoints,
  const ArrayOfReal& theParams,
  ArrayOfPnt2d&      thePoints2d,
  const double       theTol,
  bool&              theIsRecompute,
  bool&              theIsFromCache) const
{
  const int aNb = thePoints.Length();
  gp_Pnt    aP[4];
  aP[0] = thePoints(1);
  aP[1] = thePoints(2);
  aP[2] = thePoints(aNb - 1);
  aP[3] = thePoints(aNb);

  gp_Pnt2d aP2d[4];
  int      i = 0;

  double     aTol2       = theTol * theTol;
  double     aTolWorking = theTol;
  const bool isPeriodicU = mySurf->Surface()->IsUPeriodic();
  const bool isPeriodicV = mySurf->Surface()->IsVPeriodic();

  // Protection against bad tolerance shapes
  if (aTol2 > 1.0)
  {
    aTolWorking = Precision::Confusion();
    aTol2       = aTolWorking * aTolWorking;
  }
  if (aTol2 < Precision::SquareConfusion())
  {
    aTol2 = Precision::SquareConfusion();
  }
  const double anOldTol2 = aTol2;

  int      aSavedPointNum = -1;
  gp_Pnt2d aSavedPoint;

  // Create projector with B-spline surface pole cache optimization
  SurfaceProjectorWithCache aProjector(mySurf);

  // Helper lambda to project a point with cache lookup
  auto projectPoint = [&](const gp_Pnt& thePoint, gp_Pnt2d& theResult, const int theIndex) -> void {
    // Try existing endpoint cache first
    const int aNbCache = myCache.Length();
    for (int j = 0; j < aNbCache; ++j)
    {
      const CachePoint& aCachePnt = myCache(j);
      if (aCachePnt.first.SquareDistance(thePoint) < aTol2)
      {
        theResult =
          aProjector.NextValueOfUV(aCachePnt.second, thePoint, aTolWorking, aTol2, aTolWorking);
        aSavedPointNum = theIndex;
        aSavedPoint    = aCachePnt.second;
        if (theIndex == 0)
        {
          theIsFromCache = true;
        }
        return;
      }
    }

    // Fall back to full projection (with B-spline pole cache optimization inside)
    theResult = aProjector.ValueOfUV(thePoint, aTolWorking, aTol2);
  };

  // Project first and last points
  for (; i < 4; i += 3)
  {
    projectPoint(aP[i], aP2d[i], i);

    const double aDist    = aProjector.Gap();
    const double aCurDist = aDist * aDist;
    if (aTol2 < aCurDist)
    {
      aTol2 = aCurDist;
    }
  }

  if (isPeriodicU || isPeriodicV)
  {
    // Compute second and last but one c2d points
    for (i = 1; i < 3; i++)
    {
      projectPoint(aP[i], aP2d[i], i);

      const double aDist    = aProjector.Gap();
      const double aCurDist = aDist * aDist;
      if (aTol2 < aCurDist)
      {
        aTol2 = aCurDist;
      }
    }

    if (isPeriodicU)
    {
      theIsRecompute = fixPeriodicityTroubles(&aP2d[0],
                                              1,
                                              mySurf->Surface()->UPeriod(),
                                              aSavedPointNum,
                                              aSavedPoint.X());
    }

    if (isPeriodicV)
    {
      theIsRecompute = fixPeriodicityTroubles(&aP2d[0],
                                              2,
                                              mySurf->Surface()->VPeriod(),
                                              aSavedPointNum,
                                              aSavedPoint.Y());
    }
  }

  if (theIsRecompute && mySurf->Surface()->IsKind(STANDARD_TYPE(Geom_SphericalSurface)))
  {
    return nullptr;
  }

  thePoints2d.SetValue(1, aP2d[0]);
  thePoints2d.SetValue(aNb, aP2d[3]);

  // Restore old tolerance
  aTol2 = anOldTol2;

  const double dPar = theParams(aNb) - theParams(1);
  if (std::abs(dPar) < Precision::PConfusion())
  {
    return nullptr;
  }

  const gp_Vec2d            aVec0(aP2d[0], aP2d[3]);
  const gp_Vec2d            aVec          = aVec0 / dPar;
  occ::handle<Geom_Surface> aSurf         = mySurf->Surface();
  bool                      isNormalCheck = aSurf->IsCNu(1) && aSurf->IsCNv(1);

  if (isNormalCheck)
  {
    for (i = 1; i <= aNb; i++)
    {
      const gp_XY aCurPoint = aP2d[0].XY() + aVec.XY() * (theParams(i) - theParams(1));
      gp_Pnt      aCurP;
      gp_Vec      aNormalVec, aDu, aDv;
      aSurf->D1(aCurPoint.X(), aCurPoint.Y(), aCurP, aDu, aDv);
      aNormalVec = aDu ^ aDv;
      if (aNormalVec.SquareMagnitude() < Precision::SquareConfusion())
      {
        isNormalCheck = false;
        break;
      }
      const gp_Lin aNormalLine(aCurP, gp_Dir(aNormalVec));
      const double aDist = aNormalLine.Distance(thePoints(i));
      if (aDist > aTolWorking)
      {
        return nullptr;
      }
    }
  }

  if (!isNormalCheck)
  {
    const double aFirstPointDist =
      mySurf->Surface()->Value(aP2d[0].X(), aP2d[0].Y()).SquareDistance(thePoints(1));
    aTol2 = std::max(aTol2, aTol2 * 2 * aFirstPointDist);
    for (i = 2; i < aNb; i++)
    {
      const gp_XY aCurPoint = aP2d[0].XY() + aVec.XY() * (theParams(i) - theParams(1));
      gp_Pnt      aCurP;
      aSurf->D0(aCurPoint.X(), aCurPoint.Y(), aCurP);
      const double aDist1 = aCurP.SquareDistance(thePoints(i));

      if (std::abs(aFirstPointDist - aDist1) > aTol2)
      {
        return nullptr;
      }
    }
  }

  // Check if pcurve can be represented by Geom2d_Line
  const double aLLength = aVec0.Magnitude();
  if (std::abs(aLLength - dPar) <= Precision::PConfusion())
  {
    const gp_XY    aDirL = aVec0.XY() / aLLength;
    const gp_Pnt2d aPL(aP2d[0].XY() - theParams(1) * aDirL);
    return new Geom2d_Line(aPL, gp_Dir2d(aDirL));
  }

  // Create straight bspline
  NCollection_Array1<gp_Pnt2d> aPoles(1, 2);
  aPoles(1) = aP2d[0];
  aPoles(2) = aP2d[3];

  NCollection_Array1<double> aKnots(1, 2);
  aKnots(1) = theParams(1);
  aKnots(2) = theParams(theParams.Length());

  NCollection_Array1<int> aMults(1, 2);
  aMults(1) = 2;
  aMults(2) = 2;

  return new Geom2d_BSplineCurve(aPoles, aKnots, aMults, 1);
}

//=================================================================================================

bool ShapeConstruct_ProjectCurveOnSurface::approxPCurve(const int                      theNbPnt,
                                                        const occ::handle<Geom_Curve>& theC3D,
                                                        const double                   theTolFirst,
                                                        const double                   theTolLast,
                                                        ArrayOfPnt&                    thePoints,
                                                        ArrayOfReal&                   theParams,
                                                        ArrayOfPnt2d&                  thePoints2d,
                                                        occ::handle<Geom2d_Curve>&     theC2D)
{
  // For performance, first try to handle typical case when pcurve is straight
  bool isRecompute     = false;
  bool isFromCacheLine = false;

  theC2D = getLine(thePoints, theParams, thePoints2d, myPreci, isRecompute, isFromCacheLine);
  if (!theC2D.IsNull())
  {
    // Fill cache
    bool aChangeCycle = false;
    if (!myCache.IsEmpty()
        && myCache(0).first.Distance(thePoints(1)) > myCache(0).first.Distance(thePoints(theNbPnt))
        && myCache(0).first.Distance(thePoints(theNbPnt)) < Precision::Confusion())
    {
      aChangeCycle = true;
    }

    myCache.Resize(0, 1, false);
    if (aChangeCycle)
    {
      myCache(0) = CachePoint(thePoints(1), thePoints2d(1));
      myCache(1) = CachePoint(thePoints(theNbPnt), thePoints2d(theNbPnt));
    }
    else
    {
      myCache(0) = CachePoint(thePoints(theNbPnt), thePoints2d(theNbPnt));
      myCache(1) = CachePoint(thePoints(1), thePoints2d(1));
    }
    return true;
  }

  bool isDone = true;

  // Test if the curve 3d is a boundary of the surface
  bool                    isoParam, isoPar2d3d, isoTypeU, p1OnIso, p2OnIso, isoClosed;
  gp_Pnt2d                valueP1, valueP2;
  occ::handle<Geom_Curve> cIso;
  double                  t1, t2;

  occ::handle<Standard_Type> sType      = mySurf->Surface()->DynamicType();
  bool                       isAnalytic = true;
  if (sType == STANDARD_TYPE(Geom_BezierSurface) || sType == STANDARD_TYPE(Geom_BSplineSurface))
  {
    isAnalytic = false;
  }

  double uf, ul, vf, vl;
  mySurf->Surface()->Bounds(uf, ul, vf, vl);
  isoClosed = false;

  ArrayOfReal pout(1, theNbPnt);
  for (int i = 1; i <= theNbPnt; i++)
  {
    pout.SetValue(i, 0.0);
  }

  isoParam = isAnIsoparametric(theNbPnt,
                               thePoints,
                               theParams,
                               isoTypeU,
                               p1OnIso,
                               valueP1,
                               p2OnIso,
                               valueP2,
                               isoPar2d3d,
                               cIso,
                               t1,
                               t2,
                               pout);

  // Projection of the points on surfaces
  gp_Pnt   p3d;
  gp_Pnt2d p2d;
  double   isoValue = 0., isoPar1 = 0., isoPar2 = 0., tPar = 0., tdeb, tfin;
  double   Cf, Cl, parf, parl;

  if (isoParam)
  {
    if (isoTypeU)
    {
      isoValue  = valueP1.X();
      isoPar1   = valueP1.Y();
      isoPar2   = valueP2.Y();
      isoClosed = mySurf->IsVClosed(myPreci);
      parf      = vf;
      parl      = vl;
    }
    else
    {
      isoValue  = valueP1.Y();
      isoPar1   = valueP1.X();
      isoPar2   = valueP2.X();
      isoClosed = mySurf->IsUClosed(myPreci);
      parf      = uf;
      parl      = ul;
    }
    if (!isoPar2d3d && !isAnalytic)
    {
      Cf = cIso->FirstParameter();
      Cl = cIso->LastParameter();
      if (Precision::IsInfinite(Cf))
      {
        Cf = -1000;
      }
      if (Precision::IsInfinite(Cl))
      {
        Cl = +1000;
      }

      tdeb = pout(2);

      if (isoClosed && (isoPar1 == parf || isoPar1 == parl))
      {
        if (std::abs(tdeb - parf) < std::abs(tdeb - parl))
        {
          isoPar1 = parf;
        }
        else
        {
          isoPar1 = parl;
        }
        if (isoTypeU)
        {
          valueP1.SetY(isoPar1);
        }
        else
        {
          valueP1.SetX(isoPar1);
        }
      }
      if (isoClosed && (isoPar2 == parf || isoPar2 == parl))
      {
        tfin = pout(theNbPnt - 1);
        if (std::abs(tfin - parf) < std::abs(tfin - parl))
        {
          isoPar2 = parf;
        }
        else
        {
          isoPar2 = parl;
        }
        if (isoTypeU)
        {
          valueP2.SetY(isoPar2);
        }
        else
        {
          valueP2.SetX(isoPar2);
        }
      }

      if (!isoClosed)
      {
        if ((std::abs(tdeb - isoPar1) > std::abs(tdeb - isoPar2))
            && (std::abs(pout(theNbPnt - 1) - isoPar2) > std::abs(pout(theNbPnt - 1) - isoPar1)))
        {
          gp_Pnt2d valueTmp = valueP1;
          valueP1           = valueP2;
          valueP2           = valueTmp;
          if (isoTypeU)
          {
            isoValue = valueP1.X();
            isoPar1  = valueP1.Y();
            isoPar2  = valueP2.Y();
          }
          else
          {
            isoValue = valueP1.Y();
            isoPar1  = valueP1.X();
            isoPar2  = valueP2.X();
          }
        }
      }
    }
  }

  const double Up           = ul - uf;
  const double Vp           = vl - vf;
  double       gap          = myPreci;
  bool         aChangeCycle = false;

  bool     isFromCache = false;
  gp_Pnt2d aSavedPoint;

  if (!myCache.IsEmpty()
      && myCache(0).first.Distance(thePoints(1)) > myCache(0).first.Distance(thePoints(theNbPnt)))
  {
    if (myCache(0).first.Distance(thePoints(theNbPnt)) < Precision::Confusion())
    {
      aChangeCycle = true;
    }
  }

  bool     needResolveUJump = false;
  bool     needResolveVJump = false;
  gp_Pnt   prevP3d;
  gp_Pnt2d prevP2d;

  for (int ii = 1; ii <= theNbPnt; ii++)
  {
    const int aPntIndex = aChangeCycle ? (theNbPnt - ii + 1) : ii;
    p3d                 = thePoints(aPntIndex);

    if (isoParam)
    {
      if (isoPar2d3d)
      {
        if (isoPar2 > isoPar1)
        {
          tPar = theParams(aPntIndex);
        }
        else
        {
          tPar = t1 + t2 - theParams(aPntIndex);
        }
      }
      else if (!isAnalytic)
      {
        if (aPntIndex == 1)
        {
          tPar = isoPar1;
        }
        else if (aPntIndex == theNbPnt)
        {
          tPar = isoPar2;
        }
        else
        {
          tPar = pout(aPntIndex);
        }
      }

      if (!isoPar2d3d && isAnalytic)
      {
        if (aPntIndex == 1)
        {
          p2d = valueP1;
        }
        else if (aPntIndex == theNbPnt)
        {
          p2d = valueP2;
        }
        else
        {
          p2d = mySurf->NextValueOfUV(p2d, p3d, myPreci, Precision::Confusion() + 1000 * gap);
          gap = mySurf->Gap();
        }
      }
      else
      {
        if (isoTypeU)
        {
          p2d.SetX(isoValue);
          p2d.SetY(tPar);
        }
        else
        {
          p2d.SetX(tPar);
          p2d.SetY(isoValue);
        }
      }
    }
    else
    {
      if ((aPntIndex == 1) && p1OnIso)
      {
        p2d = valueP1;
      }
      else if ((aPntIndex == theNbPnt) && p2OnIso)
      {
        p2d = valueP2;
      }
      else
      {
        if (aPntIndex == 1 || aPntIndex == theNbPnt)
        {
          if (!isRecompute)
          {
            p2d = thePoints2d(aPntIndex);
            gap = mySurf->Gap();
            if (aPntIndex == 1)
            {
              isFromCache = isFromCacheLine;
              aSavedPoint = p2d;
            }
            continue;
          }
          else
          {
            int       j        = 0;
            const int aNbCache = myCache.Length();
            for (; j < aNbCache; ++j)
            {
              const CachePoint& aCachePnt = myCache(j);
              if (aCachePnt.first.SquareDistance(p3d) < myPreci * myPreci)
              {
                p2d = mySurf->NextValueOfUV(aCachePnt.second,
                                            p3d,
                                            myPreci,
                                            Precision::Confusion() + gap);
                if (aPntIndex == 1)
                {
                  isFromCache = true;
                  aSavedPoint = aCachePnt.second;
                }
                break;
              }
            }
            if (j >= aNbCache)
            {
              p2d = mySurf->ValueOfUV(p3d, myPreci);
            }
          }
        }
        else
        {
          p2d = mySurf->NextValueOfUV(p2d, p3d, myPreci, Precision::Confusion() + 1000 * gap);
        }
        gap = mySurf->Gap();
      }
    }
    thePoints2d.SetValue(aPntIndex, p2d);

    if (theNbPnt > 23 && ii > 2 && ii < theNbPnt)
    {
      if (fabs(p2d.X() - prevP2d.X()) > 0.95 * Up && prevP3d.Distance(p3d) < myPreci
          && !mySurf->IsUClosed(myPreci) && mySurf->NbSingularities(myPreci) > 0
          && mySurf->Surface()->IsKind(STANDARD_TYPE(Geom_BSplineSurface)))
      {
        needResolveUJump = true;
      }
      if (fabs(p2d.Y() - prevP2d.Y()) > 0.95 * Vp && prevP3d.Distance(p3d) < myPreci
          && !mySurf->IsVClosed(myPreci) && mySurf->NbSingularities(myPreci) > 0
          && mySurf->Surface()->IsKind(STANDARD_TYPE(Geom_BSplineSurface)))
      {
        needResolveVJump = true;
      }
    }
    prevP3d = p3d;
    prevP2d = p2d;
    if (ii > 1)
    {
      if (aChangeCycle)
      {
        p2d.SetXY(2. * p2d.XY() - thePoints2d(aPntIndex + 1).XY());
      }
      else
      {
        p2d.SetXY(2. * p2d.XY() - thePoints2d(aPntIndex - 1).XY());
      }
    }
  }

  if (!isoPar2d3d)
  {
    projectDegeneratedPoints(mySurf, theNbPnt, thePoints, thePoints2d, myPreci, true);
    projectDegeneratedPoints(mySurf, theNbPnt, thePoints, thePoints2d, myPreci, false);
  }

  // Check extremities for singularities
  const gp_Pnt aPointFirst = thePoints.First();
  const gp_Pnt aPointLast  = thePoints.Last();
  const double aTolFirst   = (theTolFirst < 0) ? Precision::Confusion() : theTolFirst;
  const double aTolLast    = (theTolLast < 0) ? Precision::Confusion() : theTolLast;

  for (int i = 1;; i++)
  {
    double   aPreci, aFirstPar, aLastPar;
    gp_Pnt   aP3d;
    gp_Pnt2d aFirstP2d, aLastP2d;
    bool     IsUiso;
    if (!mySurf->Singularity(i, aPreci, aP3d, aFirstP2d, aLastP2d, aFirstPar, aLastPar, IsUiso))
    {
      break;
    }
    if (aPreci <= Precision::Confusion() && aPointFirst.Distance(aP3d) <= aTolFirst)
    {
      correctExtremity(theC3D, theParams, thePoints2d, true, aFirstP2d, IsUiso);
    }
    if (aPreci <= Precision::Confusion() && aPointLast.Distance(aP3d) <= aTolLast)
    {
      correctExtremity(theC3D, theParams, thePoints2d, false, aFirstP2d, IsUiso);
    }
  }

  // Handle U-closed surfaces
  double       dist2d;
  const double TolOnUPeriod = Precision::Confusion() * Up;
  const double TolOnVPeriod = Precision::Confusion() * Vp;

  if (mySurf->IsUClosed(myPreci) || needResolveUJump)
  {
    double prevX, firstX = thePoints2d(1).X();
    if (!isFromCache)
    {
      while (firstX < uf)
      {
        firstX += Up;
        thePoints2d.ChangeValue(1).SetX(firstX);
      }
      while (firstX > ul)
      {
        firstX -= Up;
        thePoints2d.ChangeValue(1).SetX(firstX);
      }
    }

    if (mySurf->Surface()->IsUPeriodic() && isFromCache)
    {
      double aMinParam = uf, aMaxParam = ul;
      while (aMinParam > aSavedPoint.X())
      {
        aMinParam -= Up;
        aMaxParam -= Up;
      }
      while (aMaxParam < aSavedPoint.X())
      {
        aMinParam += Up;
        aMaxParam += Up;
      }
      double aShift = ShapeAnalysis::AdjustToPeriod(firstX, aMinParam, aMaxParam);
      firstX += aShift;
      thePoints2d.ChangeValue(1).SetX(firstX);
    }
    prevX = firstX;

    double minX = firstX, maxX = firstX;
    bool   ToAdjust = false;

    for (int aPntIter = 2; aPntIter <= thePoints2d.Length(); ++aPntIter)
    {
      double CurX = thePoints2d(aPntIter).X();
      dist2d      = std::abs(CurX - prevX);
      if (dist2d > (Up / 2))
      {
        insertAdditionalPointOrAdjust(ToAdjust,
                                      1,
                                      Up,
                                      TolOnUPeriod,
                                      CurX,
                                      prevX,
                                      theC3D,
                                      aPntIter,
                                      thePoints,
                                      theParams,
                                      thePoints2d);
      }
      prevX = CurX;
      if (minX > CurX)
      {
        minX = CurX;
      }
      else if (maxX < CurX)
      {
        maxX = CurX;
      }
    }

    if (!isFromCache)
    {
      double midX   = 0.5 * (minX + maxX);
      double shiftX = 0.;
      if (midX > ul)
      {
        shiftX = -Up;
      }
      else if (midX < uf)
      {
        shiftX = Up;
      }
      if (shiftX != 0.)
      {
        for (int aPntIter = 1; aPntIter <= thePoints2d.Length(); ++aPntIter)
        {
          thePoints2d.ChangeValue(aPntIter).SetX(thePoints2d(aPntIter).X() + shiftX);
        }
      }
    }
  }

  // Handle V-closed surfaces
  if (mySurf->IsVClosed(myPreci) || needResolveVJump
      || mySurf->Surface()->IsKind(STANDARD_TYPE(Geom_SphericalSurface)))
  {
    double prevY, firstY = thePoints2d(1).Y();
    if (!isFromCache)
    {
      while (firstY < vf)
      {
        firstY += Vp;
        thePoints2d.ChangeValue(1).SetY(firstY);
      }
      while (firstY > vl)
      {
        firstY -= Vp;
        thePoints2d.ChangeValue(1).SetY(firstY);
      }
    }

    if (mySurf->Surface()->IsVPeriodic() && isFromCache)
    {
      double aMinParam = vf, aMaxParam = vl;
      while (aMinParam > aSavedPoint.Y())
      {
        aMinParam -= Vp;
        aMaxParam -= Vp;
      }
      while (aMaxParam < aSavedPoint.Y())
      {
        aMinParam += Vp;
        aMaxParam += Vp;
      }
      double aShift = ShapeAnalysis::AdjustToPeriod(firstY, aMinParam, aMaxParam);
      firstY += aShift;
      thePoints2d.ChangeValue(1).SetY(firstY);
    }
    prevY = firstY;

    double minY = firstY, maxY = firstY;
    bool   ToAdjust = false;

    for (int aPntIter = 2; aPntIter <= thePoints2d.Length(); ++aPntIter)
    {
      double CurY = thePoints2d(aPntIter).Y();
      dist2d      = std::abs(CurY - prevY);
      if (dist2d > (Vp / 2))
      {
        insertAdditionalPointOrAdjust(ToAdjust,
                                      2,
                                      Vp,
                                      TolOnVPeriod,
                                      CurY,
                                      prevY,
                                      theC3D,
                                      aPntIter,
                                      thePoints,
                                      theParams,
                                      thePoints2d);
      }
      prevY = CurY;
      if (minY > CurY)
      {
        minY = CurY;
      }
      else if (maxY < CurY)
      {
        maxY = CurY;
      }
    }

    if (!isFromCache)
    {
      double midY   = 0.5 * (minY + maxY);
      double shiftY = 0.;
      if (midY > vl)
      {
        shiftY = -Vp;
      }
      else if (midY < vf)
      {
        shiftY = Vp;
      }
      if (shiftY != 0.)
      {
        for (int aPntIter = 1; aPntIter <= thePoints2d.Length(); ++aPntIter)
        {
          thePoints2d.ChangeValue(aPntIter).SetY(thePoints2d(aPntIter).Y() + shiftY);
        }
      }
    }
  }

  // Handle V-closed seam adjustment
  if (mySurf->IsVClosed(myPreci) || mySurf->Surface()->IsKind(STANDARD_TYPE(Geom_SphericalSurface)))
  {
    for (int aPntIter = 2; aPntIter <= thePoints2d.Length(); ++aPntIter)
    {
      dist2d = std::abs(thePoints2d(aPntIter).Y() - thePoints2d(aPntIter - 1).Y());
      if (dist2d > (Vp / 2))
      {
        bool prevOnFirst = false;
        bool prevOnLast  = false;
        bool currOnFirst = false;
        bool currOnLast  = false;

        double distPrevVF = std::abs(thePoints2d(aPntIter - 1).Y() - vf);
        double distPrevVL = std::abs(thePoints2d(aPntIter - 1).Y() - vl);
        double distCurrVF = std::abs(thePoints2d(aPntIter).Y() - vf);
        double distCurrVL = std::abs(thePoints2d(aPntIter).Y() - vl);

        double theMin = distPrevVF;
        prevOnFirst   = true;
        if (distPrevVL < theMin)
        {
          theMin      = distPrevVL;
          prevOnFirst = false;
          prevOnLast  = true;
        }
        if (distCurrVF < theMin)
        {
          theMin      = distCurrVF;
          prevOnFirst = false;
          prevOnLast  = false;
          currOnFirst = true;
        }
        if (distCurrVL < theMin)
        {
          prevOnFirst = false;
          prevOnLast  = false;
          currOnFirst = false;
          currOnLast  = true;
        }

        if (prevOnFirst)
        {
          thePoints2d.ChangeValue(aPntIter - 1).SetY(vf);
        }
        else if (prevOnLast)
        {
          thePoints2d.ChangeValue(aPntIter - 1).SetY(vl);
        }
        else if (currOnFirst)
        {
          thePoints2d.ChangeValue(aPntIter).SetY(vf);
        }
        else if (currOnLast)
        {
          thePoints2d.ChangeValue(aPntIter).SetY(vl);
        }
      }
    }
  }

  // Handle AdjustOverDegen
  if (myAdjustOverDegen != -1)
  {
    if (mySurf->IsUClosed(myPreci))
    {
      mySurf->IsDegenerated(gp_Pnt(0, 0, 0), myPreci);
      if (mySurf->NbSingularities(myPreci) > 0)
      {
        double PrevX   = 0.;
        int    OnBound = 0, PrevOnBound = 0;
        int    ind;
        bool   start = true;
        for (ind = 1; ind <= thePoints2d.Length(); ind++)
        {
          double CurX = thePoints2d(ind).X();
          if (mySurf->IsDegenerated(thePoints(ind), Precision::Confusion()))
          {
            continue;
          }
          OnBound =
            (std::abs(std::abs(CurX - 0.5 * (ul + uf)) - Up / 2) <= Precision::PConfusion());
          if (!start && std::abs(std::abs(CurX - PrevX) - Up / 2) <= 0.01 * Up)
          {
            break;
          }
          start       = false;
          PrevX       = CurX;
          PrevOnBound = OnBound;
        }
        if (ind <= thePoints2d.Length())
        {
          PrevX     = (myAdjustOverDegen ? uf : ul);
          double dU = Up / 2 + Precision::PConfusion();
          if (PrevOnBound)
          {
            thePoints2d.ChangeValue(ind - 1).SetX(PrevX);
            for (int j = ind - 2; j > 0; j--)
            {
              double CurX = thePoints2d(j).X();
              while (CurX < PrevX - dU)
              {
                CurX += Up;
                thePoints2d.ChangeValue(j).SetX(CurX);
              }
              while (CurX > PrevX + dU)
              {
                CurX -= Up;
                thePoints2d.ChangeValue(j).SetX(CurX);
              }
            }
          }
          else if (OnBound)
          {
            thePoints2d.ChangeValue(ind).SetX(PrevX);
            for (int j = ind + 1; j <= thePoints2d.Length(); j++)
            {
              double CurX = thePoints2d(j).X();
              while (CurX < PrevX - dU)
              {
                CurX += Up;
                thePoints2d.ChangeValue(j).SetX(CurX);
              }
              while (CurX > PrevX + dU)
              {
                CurX -= Up;
                thePoints2d.ChangeValue(j).SetX(CurX);
              }
            }
          }
          myStatus |= ShapeExtend::EncodeStatus(ShapeExtend_DONE4);
        }
      }
    }
    else if (mySurf->IsVClosed(myPreci))
    {
      mySurf->IsDegenerated(gp_Pnt(0, 0, 0), myPreci);
      if (mySurf->NbSingularities(myPreci) > 0)
      {
        double PrevY   = 0.;
        int    OnBound = 0, PrevOnBound = 0;
        int    ind;
        bool   start = true;
        for (ind = 1; ind <= thePoints2d.Length(); ind++)
        {
          double CurY = thePoints2d(ind).Y();
          if (mySurf->IsDegenerated(thePoints(ind), Precision::Confusion()))
          {
            continue;
          }
          OnBound =
            (std::abs(std::abs(CurY - 0.5 * (vl + vf)) - Vp / 2) <= Precision::PConfusion());
          if (!start && std::abs(std::abs(CurY - PrevY) - Vp / 2) <= 0.01 * Vp)
          {
            break;
          }
          start       = false;
          PrevY       = CurY;
          PrevOnBound = OnBound;
        }
        if (ind <= thePoints2d.Length())
        {
          PrevY     = (myAdjustOverDegen ? vf : vl);
          double dV = Vp / 2 + Precision::PConfusion();
          if (PrevOnBound)
          {
            thePoints2d.ChangeValue(ind - 1).SetY(PrevY);
            for (int j = ind - 2; j > 0; j--)
            {
              double CurY = thePoints2d(j).Y();
              while (CurY < PrevY - dV)
              {
                CurY += Vp;
                thePoints2d.ChangeValue(j).SetY(CurY);
              }
              while (CurY > PrevY + dV)
              {
                CurY -= Vp;
                thePoints2d.ChangeValue(j).SetY(CurY);
              }
            }
          }
          else if (OnBound)
          {
            thePoints2d.ChangeValue(ind).SetY(PrevY);
            for (int j = ind + 1; j <= thePoints2d.Length(); j++)
            {
              double CurY = thePoints2d(j).Y();
              while (CurY < PrevY - dV)
              {
                CurY += Vp;
                thePoints2d.ChangeValue(j).SetY(CurY);
              }
              while (CurY > PrevY + dV)
              {
                CurY -= Vp;
                thePoints2d.ChangeValue(j).SetY(CurY);
              }
            }
          }
          myStatus |= ShapeExtend::EncodeStatus(ShapeExtend_DONE4);
        }
      }
    }
  }

  // Fill cache
  myCache.Resize(0, 1, false);
  if (aChangeCycle)
  {
    myCache(0) = CachePoint(thePoints(1), thePoints2d(1));
    myCache(1) = CachePoint(thePoints.Last(), thePoints2d.Last());
  }
  else
  {
    myCache(0) = CachePoint(thePoints.Last(), thePoints2d.Last());
    myCache(1) = CachePoint(thePoints(1), thePoints2d(1));
  }

  return isDone;
}

//=================================================================================================

void ShapeConstruct_ProjectCurveOnSurface::correctExtremity(const occ::handle<Geom_Curve>& theC3D,
                                                            const ArrayOfReal& theParams,
                                                            ArrayOfPnt2d&      thePoints2d,
                                                            const bool         theIsFirstPoint,
                                                            const gp_Pnt2d&    thePointOnIsoLine,
                                                            const bool         theIsUIso)
{
  const int    NbPnt            = thePoints2d.Length();
  const int    IndCoord         = (theIsUIso) ? 2 : 1;
  const double SingularityCoord = thePointOnIsoLine.Coord(3 - IndCoord);
  gp_Pnt2d     EndPoint         = (theIsFirstPoint) ? thePoints2d(1) : thePoints2d(NbPnt);
  const double FinishCoord      = EndPoint.Coord(3 - IndCoord);

  gp_Dir2d        aDir = (theIsUIso) ? gp::DY2d() : gp::DX2d();
  gp_Lin2d        anIsoLine(EndPoint, aDir);
  IntRes2d_Domain Dom1, Dom2;

  const bool IsPeriodic =
    (theIsUIso) ? mySurf->Surface()->IsVPeriodic() : mySurf->Surface()->IsUPeriodic();

  gp_Pnt2d FirstPointOfLine, SecondPointOfLine;
  double   FinishParam, FirstParam, SecondParam;

  if (theIsFirstPoint)
  {
    FirstPointOfLine  = thePoints2d(3);
    SecondPointOfLine = thePoints2d(2);
    FinishParam       = theParams(1);
    FirstParam        = theParams(3);
    SecondParam       = theParams(2);
  }
  else
  {
    FirstPointOfLine  = thePoints2d(NbPnt - 2);
    SecondPointOfLine = thePoints2d(NbPnt - 1);
    FinishParam       = theParams(NbPnt);
    FirstParam        = theParams(NbPnt - 2);
    SecondParam       = theParams(NbPnt - 1);
  }

  if (SingularityCoord > FinishCoord && SecondPointOfLine.Coord(3 - IndCoord) > FinishCoord)
  {
    return;
  }
  if (SingularityCoord < FinishCoord && SecondPointOfLine.Coord(3 - IndCoord) < FinishCoord)
  {
    return;
  }

  {
    const double aPrevDist =
      std::abs(SecondPointOfLine.Coord(IndCoord) - FirstPointOfLine.Coord(IndCoord));
    const double aCurDist = std::abs(EndPoint.Coord(IndCoord) - SecondPointOfLine.Coord(IndCoord));
    if (aCurDist <= 2 * aPrevDist)
    {
      return;
    }
  }

  gp_Pnt2d FinishPoint = (theIsUIso) ? gp_Pnt2d(FinishCoord, SecondPointOfLine.Y())
                                     : gp_Pnt2d(SecondPointOfLine.X(), FinishCoord);

  for (;;)
  {
    if (std::abs(SecondPointOfLine.Coord(3 - IndCoord) - FinishCoord)
        <= 2 * Precision::PConfusion())
    {
      break;
    }

    gp_Vec2d     aVec(FirstPointOfLine, SecondPointOfLine);
    const double aSqMagnitude = aVec.SquareMagnitude();
    if (aSqMagnitude <= 1.e-32)
    {
      break;
    }
    aDir.SetCoord(aVec.X(), aVec.Y());

    gp_Lin2d               aLine(FirstPointOfLine, aDir);
    IntCurve_IntConicConic Intersector(anIsoLine, Dom1, aLine, Dom2, 1.e-10, 1.e-10);
    if (Intersector.IsDone() && !Intersector.IsEmpty())
    {
      IntRes2d_IntersectionPoint IntPoint = Intersector.Point(1);
      FinishPoint                         = IntPoint.Value();
    }
    else
    {
      FinishPoint = (theIsUIso) ? gp_Pnt2d(FinishCoord, SecondPointOfLine.Y())
                                : gp_Pnt2d(SecondPointOfLine.X(), FinishCoord);
    }

    gp_Pnt2d PrevPoint = FirstPointOfLine;
    FirstPointOfLine   = SecondPointOfLine;
    FirstParam         = SecondParam;
    SecondParam        = (FirstParam + FinishParam) / 2;
    if (std::abs(SecondParam - FirstParam) <= 2 * Precision::PConfusion())
    {
      break;
    }
    gp_Pnt aP3d;
    theC3D->D0(SecondParam, aP3d);
    SecondPointOfLine =
      mySurf->NextValueOfUV(FirstPointOfLine, aP3d, myPreci, Precision::Confusion());
    if (IsPeriodic)
    {
      adjustSecondToFirstPoint(FirstPointOfLine, SecondPointOfLine, mySurf->Surface());
    }

    const double aPrevDist = std::abs(FirstPointOfLine.Coord(IndCoord) - PrevPoint.Coord(IndCoord));
    const double aCurDist =
      std::abs(SecondPointOfLine.Coord(IndCoord) - FirstPointOfLine.Coord(IndCoord));
    if (aCurDist > 2 * aPrevDist)
    {
      break;
    }
  }

  if (theIsFirstPoint)
  {
    thePoints2d.ChangeValue(1) = FinishPoint;
  }
  else
  {
    thePoints2d.ChangeValue(NbPnt) = FinishPoint;
  }
}

//=================================================================================================

void ShapeConstruct_ProjectCurveOnSurface::insertAdditionalPointOrAdjust(
  bool&                          theToAdjust,
  const int                      theIndCoord,
  const double                   thePeriod,
  const double                   theTolOnPeriod,
  double&                        theCurCoord,
  const double                   thePrevCoord,
  const occ::handle<Geom_Curve>& theC3D,
  int&                           theIndex,
  ArrayOfPnt&                    thePoints,
  ArrayOfReal&                   theParams,
  ArrayOfPnt2d&                  thePoints2d)
{
  const double CorrectedCurCoord =
    ElCLib::InPeriod(theCurCoord, thePrevCoord - thePeriod / 2, thePrevCoord + thePeriod / 2);

  if (!theToAdjust)
  {
    const double CurPar  = theParams(theIndex);
    const double PrevPar = theParams(theIndex - 1);
    double       MidPar  = (PrevPar + CurPar) / 2;
    gp_Pnt       MidP3d;
    theC3D->D0(MidPar, MidP3d);
    gp_Pnt2d MidP2d   = mySurf->ValueOfUV(MidP3d, myPreci);
    double   MidCoord = MidP2d.Coord(theIndCoord);
    MidCoord =
      ElCLib::InPeriod(MidCoord, thePrevCoord - thePeriod / 2, thePrevCoord + thePeriod / 2);
    double FirstCoord = thePrevCoord, LastCoord = CorrectedCurCoord;
    if (LastCoord < FirstCoord)
    {
      double tmp = FirstCoord;
      FirstCoord = LastCoord;
      LastCoord  = tmp;
    }
    if (LastCoord - FirstCoord <= theTolOnPeriod)
    {
      theToAdjust = true;
    }
    else if (FirstCoord <= MidCoord && MidCoord <= LastCoord)
    {
      theToAdjust = true;
    }
    else
    {
      bool   Success = true;
      double FirstT  = PrevPar;
      double LastT   = CurPar;
      MidCoord       = MidP2d.Coord(theIndCoord);
      while (std::abs(MidCoord - thePrevCoord) >= thePeriod / 2 - theTolOnPeriod
             || std::abs(theCurCoord - MidCoord) >= thePeriod / 2 - theTolOnPeriod)
      {
        if (MidPar - FirstT <= Precision::PConfusion() || LastT - MidPar <= Precision::PConfusion())
        {
          Success = false;
          break;
        }
        if (std::abs(MidCoord - thePrevCoord) >= thePeriod / 2 - theTolOnPeriod)
        {
          LastT = (FirstT + LastT) / 2;
        }
        else
        {
          FirstT = (FirstT + LastT) / 2;
        }
        MidPar = (FirstT + LastT) / 2;
        theC3D->D0(MidPar, MidP3d);
        MidP2d   = mySurf->ValueOfUV(MidP3d, myPreci);
        MidCoord = MidP2d.Coord(theIndCoord);
      }
      if (Success)
      {
        // Insert additional point - need to resize arrays
        const int    aNewLength = thePoints.Length() + 1;
        ArrayOfPnt   aNewPoints(1, aNewLength);
        ArrayOfReal  aNewParams(1, aNewLength);
        ArrayOfPnt2d aNewPoints2d(1, aNewLength);

        for (int i = 1; i < theIndex; i++)
        {
          aNewPoints.SetValue(i, thePoints(i));
          aNewParams.SetValue(i, theParams(i));
          aNewPoints2d.SetValue(i, thePoints2d(i));
        }
        aNewPoints.SetValue(theIndex, MidP3d);
        aNewParams.SetValue(theIndex, MidPar);
        aNewPoints2d.SetValue(theIndex, MidP2d);
        for (int i = theIndex; i <= thePoints.Length(); i++)
        {
          aNewPoints.SetValue(i + 1, thePoints(i));
          aNewParams.SetValue(i + 1, theParams(i));
          aNewPoints2d.SetValue(i + 1, thePoints2d(i));
        }

        thePoints   = std::move(aNewPoints);
        theParams   = std::move(aNewParams);
        thePoints2d = std::move(aNewPoints2d);
        theIndex++;
      }
      else
      {
        theToAdjust = true;
      }
    }
  }
  if (theToAdjust)
  {
    theCurCoord = CorrectedCurCoord;
    thePoints2d.ChangeValue(theIndex).SetCoord(theIndCoord, theCurCoord);
  }
}

//=================================================================================================

occ::handle<Geom2d_Curve> ShapeConstruct_ProjectCurveOnSurface::interpolatePCurve(
  const int           theNbPnt,
  const ArrayOfPnt2d& thePoints2d,
  const ArrayOfReal&  theParams) const
{
  occ::handle<Geom2d_Curve> aC2D;
  const double              theTolerance2d = myPreci / (100 * theNbPnt);

  try
  {
    OCC_CATCH_SIGNALS

    // Convert to HArrays for Geom2dAPI_Interpolate
    occ::handle<NCollection_HArray1<gp_Pnt2d>> aPnts2d =
      new NCollection_HArray1<gp_Pnt2d>(1, theNbPnt);
    occ::handle<NCollection_HArray1<double>> aParams = new NCollection_HArray1<double>(1, theNbPnt);

    for (int i = 1; i <= theNbPnt; i++)
    {
      aPnts2d->SetValue(i, thePoints2d(i));
      aParams->SetValue(i, theParams(i));
    }

    // Check for coincident points
    ArrayOfPnt2d aTmpPnts2d = thePoints2d;
    ArrayOfReal  aTmpParams = theParams;
    double       aPreci     = theTolerance2d;
    checkPoints2d(aTmpPnts2d, aTmpParams, aPreci);

    if (aTmpPnts2d.Length() < 2)
    {
      return aC2D;
    }

    // Rebuild HArrays if points were removed
    if (aTmpPnts2d.Length() != theNbPnt)
    {
      aPnts2d = new NCollection_HArray1<gp_Pnt2d>(1, aTmpPnts2d.Length());
      aParams = new NCollection_HArray1<double>(1, aTmpParams.Length());
      for (int i = 1; i <= aTmpPnts2d.Length(); i++)
      {
        aPnts2d->SetValue(i, aTmpPnts2d(i));
        aParams->SetValue(i, aTmpParams(i));
      }
    }

    Geom2dAPI_Interpolate myInterPol2d(aPnts2d, aParams, false, aPreci);
    myInterPol2d.Perform();
    if (myInterPol2d.IsDone())
    {
      aC2D = myInterPol2d.Curve();
    }
  }
  catch (Standard_Failure const& anException)
  {
#ifdef OCCT_DEBUG
    std::cout << "Warning: ShapeConstruct_ProjectCurveOnSurface::interpolatePCurve(): Exception: ";
    anException.Print(std::cout);
    std::cout << std::endl;
#endif
    (void)anException;
    aC2D.Nullify();
  }
  return aC2D;
}

//=================================================================================================

occ::handle<Geom2d_Curve> ShapeConstruct_ProjectCurveOnSurface::approximatePCurve(
  const ArrayOfPnt2d& thePoints2d,
  const ArrayOfReal&  theParams) const
{
  const double              theTolerance2d = myPreci;
  occ::handle<Geom2d_Curve> aC2D;

  try
  {
    OCC_CATCH_SIGNALS

    ArrayOfPnt2d aTmpPnts2d = thePoints2d;
    ArrayOfReal  aTmpParams = theParams;
    double       aPreci     = theTolerance2d;
    checkPoints2d(aTmpPnts2d, aTmpParams, aPreci);

    const int numberPnt = aTmpPnts2d.Length();
    if (numberPnt < 2)
    {
      return aC2D;
    }

    NCollection_Array1<gp_Pnt> points3d(1, numberPnt);
    NCollection_Array1<double> params(1, numberPnt);

    for (int i = 1; i <= numberPnt; i++)
    {
      const gp_Pnt2d& pnt2d = aTmpPnts2d(i);
      points3d(i).SetCoord(pnt2d.X(), pnt2d.Y(), 0);
      params(i) = aTmpParams(i);
    }

    GeomAPI_PointsToBSpline               appr(points3d, params, 1, 10, GeomAbs_C1, theTolerance2d);
    const occ::handle<Geom_BSplineCurve>& crv3d = appr.Curve();

    const int                         NbPoles = crv3d->NbPoles();
    const NCollection_Array1<gp_Pnt>& poles3d = crv3d->Poles();
    NCollection_Array1<gp_Pnt2d>      poles2d(1, NbPoles);

    for (int i = 1; i <= NbPoles; i++)
    {
      poles2d(i).SetCoord(poles3d(i).X(), poles3d(i).Y());
    }

    const NCollection_Array1<double>& weights        = crv3d->WeightsArray();
    const NCollection_Array1<double>& knots          = crv3d->Knots();
    const NCollection_Array1<int>&    multiplicities = crv3d->Multiplicities();

    aC2D = new Geom2d_BSplineCurve(poles2d,
                                   weights,
                                   knots,
                                   multiplicities,
                                   crv3d->Degree(),
                                   crv3d->IsPeriodic());
    return aC2D;
  }
  catch (Standard_Failure const& anException)
  {
#ifdef OCCT_DEBUG
    std::cout << "Warning: ShapeConstruct_ProjectCurveOnSurface::approximatePCurve(): Exception: ";
    anException.Print(std::cout);
    std::cout << std::endl;
#endif
    (void)anException;
    aC2D.Nullify();
  }
  return aC2D;
}

//=================================================================================================

void ShapeConstruct_ProjectCurveOnSurface::checkPoints(ArrayOfPnt&  thePoints,
                                                       ArrayOfReal& theParams,
                                                       double&      thePreci) const
{
  const int firstElem = thePoints.Lower();
  const int lastElem  = thePoints.Upper();

  int nbPntDropped = 0;
  int lastValid    = firstElem;

  NCollection_Array1<int> tmpParam(firstElem, lastElem);
  for (int i = firstElem; i <= lastElem; i++)
  {
    tmpParam.SetValue(i, 1);
  }

  double DistMin2 = RealLast();
  gp_Pnt Prev     = thePoints.Value(lastValid);

  for (int i = firstElem + 1; i <= lastElem; i++)
  {
    const gp_Pnt& Curr     = thePoints.Value(i);
    const double  CurDist2 = Prev.SquareDistance(Curr);
    if (CurDist2 < gp::Resolution())
    {
      nbPntDropped++;
      if (i == lastElem)
      {
        tmpParam.SetValue(lastValid, 0);
      }
      else
      {
        tmpParam.SetValue(i, 0);
      }
    }
    else
    {
      if (CurDist2 < DistMin2)
      {
        DistMin2 = CurDist2;
      }
      lastValid = i;
      Prev      = Curr;
    }
  }

  if (DistMin2 < RealLast())
  {
    thePreci = 0.9 * std::sqrt(DistMin2);
  }

  if (nbPntDropped == 0)
  {
    return;
  }

  const int newLast = lastElem - nbPntDropped;
  if ((newLast - firstElem + 1) < 2)
  {
    return;
  }

  ArrayOfPnt  newPnts(firstElem, newLast);
  ArrayOfReal newParams(firstElem, newLast);
  int         newCurr = firstElem;

  for (int i = firstElem; i <= lastElem; i++)
  {
    if (tmpParam.Value(i) == 1)
    {
      newPnts.SetValue(newCurr, thePoints.Value(i));
      newParams.SetValue(newCurr, theParams.Value(i));
      newCurr++;
    }
  }

  thePoints = newPnts;
  theParams = newParams;
}

//=================================================================================================

void ShapeConstruct_ProjectCurveOnSurface::checkPoints2d(ArrayOfPnt2d& thePoints2d,
                                                         ArrayOfReal&  theParams,
                                                         double&       thePreci) const
{
  const int firstElem = thePoints2d.Lower();
  const int lastElem  = thePoints2d.Upper();

  int nbPntDropped = 0;
  int lastValid    = firstElem;

  NCollection_Array1<int> tmpParam(firstElem, lastElem);
  for (int i = firstElem; i <= lastElem; i++)
  {
    tmpParam.SetValue(i, 1);
  }

  double   DistMin2 = RealLast();
  gp_Pnt2d Prev     = thePoints2d.Value(lastValid);

  for (int i = firstElem + 1; i <= lastElem; i++)
  {
    const gp_Pnt2d& Curr     = thePoints2d.Value(i);
    const double    CurDist2 = Prev.SquareDistance(Curr);
    if (CurDist2 < gp::Resolution())
    {
      nbPntDropped++;
      if (i == lastElem)
      {
        tmpParam.SetValue(lastValid, 0);
      }
      else
      {
        tmpParam.SetValue(i, 0);
      }
    }
    else
    {
      if (CurDist2 < DistMin2)
      {
        DistMin2 = CurDist2;
      }
      lastValid = i;
      Prev      = Curr;
    }
  }

  if (DistMin2 < RealLast())
  {
    thePreci = 0.9 * std::sqrt(DistMin2);
  }

  if (nbPntDropped == 0)
  {
    return;
  }

  int newLast = lastElem - nbPntDropped;
  if ((newLast - firstElem + 1) < 2)
  {
    // Create minimal length pcurve
    tmpParam.SetValue(firstElem, 1);
    tmpParam.SetValue(lastElem, 1);
    gp_XY lastPnt = thePoints2d.Value(lastElem).XY();
    lastPnt.Add(gp_XY(thePreci, thePreci));
    thePoints2d.ChangeValue(lastElem).SetXY(lastPnt);
    newLast = firstElem + 1;
  }

  ArrayOfPnt2d newPnts(firstElem, newLast);
  ArrayOfReal  newParams(firstElem, newLast);
  int          newCurr = firstElem;

  for (int i = firstElem; i <= lastElem; i++)
  {
    if (tmpParam.Value(i) == 1)
    {
      newPnts.SetValue(newCurr, thePoints2d.Value(i));
      newParams.SetValue(newCurr, theParams.Value(i));
      newCurr++;
    }
  }

  thePoints2d = std::move(newPnts);
  theParams   = std::move(newParams);
}

//=================================================================================================

bool ShapeConstruct_ProjectCurveOnSurface::isAnIsoparametric(const int                theNbPnt,
                                                             const ArrayOfPnt&        thePoints,
                                                             const ArrayOfReal&       theParams,
                                                             bool&                    theIsTypeU,
                                                             bool&                    theP1OnIso,
                                                             gp_Pnt2d&                theValueP1,
                                                             bool&                    theP2OnIso,
                                                             gp_Pnt2d&                theValueP2,
                                                             bool&                    theIsoPar2d3d,
                                                             occ::handle<Geom_Curve>& theCIso,
                                                             double&                  theT1,
                                                             double&                  theT2,
                                                             ArrayOfReal& theParamsOut) const
{
  try
  {
    OCC_CATCH_SIGNALS

    constexpr double prec = Precision::Confusion();

    bool isoParam = false;
    theIsoPar2d3d = false;

    double U1, U2, V1, V2;
    mySurf->Bounds(U1, U2, V1, V2);

    if (mySurf->Surface()->IsKind(STANDARD_TYPE(Geom_RectangularTrimmedSurface)))
    {
      occ::handle<Geom_RectangularTrimmedSurface> sTrim =
        occ::down_cast<Geom_RectangularTrimmedSurface>(mySurf->Surface());
      sTrim->Bounds(U1, U2, V1, V2);
    }

    gp_Pnt pt;
    int    mpt[2];
    mpt[0] = mpt[1] = 0;
    double t, tpar[2] = {0.0, 0.0}, isoValue = 0.;
    double mindist2;
    double mind2[2];
    mindist2 = mind2[0] = mind2[1] = 4 * prec * prec;

    theP1OnIso          = false;
    theP2OnIso          = false;
    const Bnd_Box* aBox = nullptr;

    for (int j = 1; j <= 4; j++)
    {
      double                  isoVal = 0.;
      bool                    isoU   = false;
      occ::handle<Geom_Curve> cI;
      double                  tt1, tt2;

      if (j == 1)
      {
        if (Precision::IsInfinite(U1))
        {
          continue;
        }
        cI     = mySurf->UIso(U1);
        isoU   = true;
        isoVal = U1;
        aBox   = &mySurf->GetBoxUF();
      }
      else if (j == 2)
      {
        if (Precision::IsInfinite(U2))
        {
          continue;
        }
        cI     = mySurf->UIso(U2);
        isoU   = true;
        isoVal = U2;
        aBox   = &mySurf->GetBoxUL();
      }
      else if (j == 3)
      {
        if (Precision::IsInfinite(V1))
        {
          continue;
        }
        cI     = mySurf->VIso(V1);
        isoU   = false;
        isoVal = V1;
        aBox   = &mySurf->GetBoxVF();
      }
      else if (j == 4)
      {
        if (Precision::IsInfinite(V2))
        {
          continue;
        }
        cI     = mySurf->VIso(V2);
        isoU   = false;
        isoVal = V2;
        aBox   = &mySurf->GetBoxVL();
      }
      if (cI.IsNull())
      {
        continue;
      }

      if (isoU)
      {
        tt1 = V1;
        tt2 = V2;
      }
      else
      {
        tt1 = U1;
        tt2 = U2;
      }

      gp_Pnt ext1, ext2;
      cI->D0(tt1, ext1);
      cI->D0(tt2, ext2);

      gp_Pnt extmi;
      cI->D0((tt1 + tt2) / 2, extmi);
      if (ext1.IsEqual(ext2, prec) && ext1.IsEqual(extmi, prec))
      {
        continue;
      }

      bool PtEQext1 = false;
      bool PtEQext2 = false;

      double currd2[2], tp[2] = {0, 0};
      int    mp[2];

      for (int i = 0; i < 2; i++)
      {
        mp[i]       = 0;
        const int k = (i == 0 ? 1 : theNbPnt);

        currd2[i] = thePoints(k).SquareDistance(ext1);
        if (currd2[i] <= prec * prec && !PtEQext1)
        {
          mp[i]    = 1;
          tp[i]    = tt1;
          PtEQext1 = true;
          continue;
        }

        currd2[i] = thePoints(k).SquareDistance(ext2);
        if (currd2[i] <= prec * prec && !PtEQext2)
        {
          mp[i]    = 2;
          tp[i]    = tt2;
          PtEQext2 = true;
          continue;
        }

        if (mySurf->Surface()->IsKind(STANDARD_TYPE(Geom_SphericalSurface)) && !isoU)
        {
          continue;
        }

        if (aBox->IsOut(thePoints(k)))
        {
          continue;
        }

        double Cf = cI->FirstParameter();
        double Cl = cI->LastParameter();
        if (Precision::IsInfinite(Cf))
        {
          Cf = -1000;
        }
        if (Precision::IsInfinite(Cl))
        {
          Cl = +1000;
        }

        ShapeAnalysis_Curve sac;
        const double        dist = sac.Project(cI, thePoints(k), prec, pt, t, Cf, Cl);
        currd2[i]                = dist * dist;
        if ((dist <= prec) && (t >= Cf) && (t <= Cl))
        {
          mp[i] = 3;
          tp[i] = t;
        }
      }

      if (mp[0] > 0 && mp[1] > 0 && std::abs(tp[0] - tp[1]) < Precision::PConfusion())
      {
        continue;
      }

      if (mp[0] > 0 && (!theP1OnIso || currd2[0] < mind2[0]))
      {
        theP1OnIso = true;
        mind2[0]   = currd2[0];
        if (isoU)
        {
          theValueP1.SetCoord(isoVal, tp[0]);
        }
        else
        {
          theValueP1.SetCoord(tp[0], isoVal);
        }
      }

      if (mp[1] > 0 && (!theP2OnIso || currd2[1] < mind2[1]))
      {
        theP2OnIso = true;
        mind2[1]   = currd2[1];
        if (isoU)
        {
          theValueP2.SetCoord(isoVal, tp[1]);
        }
        else
        {
          theValueP2.SetCoord(tp[1], isoVal);
        }
      }

      if (mp[0] <= 0 || mp[1] <= 0)
      {
        continue;
      }

      const double md2 = currd2[0] + currd2[1];
      if (mindist2 <= md2)
      {
        continue;
      }

      mindist2   = md2;
      mpt[0]     = mp[0];
      mpt[1]     = mp[1];
      tpar[0]    = tp[0];
      tpar[1]    = tp[1];
      theIsTypeU = isoU;
      isoValue   = isoVal;
      theCIso    = cI;
      theT1      = tt1;
      theT2      = tt2;
    }

    if (mpt[0] > 0 && mpt[1] > 0)
    {
      theP1OnIso = theP2OnIso = true;
      if (theIsTypeU)
      {
        theValueP1.SetCoord(isoValue, tpar[0]);
        theValueP2.SetCoord(isoValue, tpar[1]);
      }
      else
      {
        theValueP1.SetCoord(tpar[0], isoValue);
        theValueP2.SetCoord(tpar[1], isoValue);
      }

      if (mpt[0] != 3 && mpt[1] != 3)
      {
        theIsoPar2d3d = true;
        for (int i = 2; i < theNbPnt && theIsoPar2d3d; i++)
        {
          if (tpar[1] > tpar[0])
          {
            t = theParams(i);
          }
          else
          {
            t = theT1 + theT2 - theParams(i);
          }
          theCIso->D0(t, pt);
          if (!thePoints(i).IsEqual(pt, prec))
          {
            theIsoPar2d3d = false;
          }
        }
      }

      if (theIsoPar2d3d)
      {
        isoParam = true;
      }
      else
      {
        double prevParam = tpar[0];
        double Cf, Cl;
        bool   isoByDistance = true;
        Cf                   = theCIso->FirstParameter();
        Cl                   = theCIso->LastParameter();
        if (Precision::IsInfinite(Cf))
        {
          Cf = -1000;
        }
        if (Precision::IsInfinite(Cl))
        {
          Cl = +1000;
        }

        ShapeAnalysis_Curve sac;
        for (int i = 2; i < theNbPnt && isoByDistance; i++)
        {
          const double dist =
            sac.NextProject(prevParam, theCIso, thePoints(i), prec, pt, t, Cf, Cl, false);
          prevParam       = t;
          theParamsOut(i) = t;
          if ((dist > prec) || (t < Cf) || (t > Cl))
          {
            isoByDistance = false;
          }
        }
        if (isoByDistance)
        {
          isoParam = true;
        }
      }
    }
    return isoParam;
  }
  catch (Standard_Failure const& anException)
  {
#ifdef OCCT_DEBUG
    std::cout << "Warning: ShapeConstruct_ProjectCurveOnSurface::isAnIsoparametric(): Exception: ";
    anException.Print(std::cout);
    std::cout << std::endl;
#endif
    (void)anException;
    return false;
  }
}
