// Created on: 1992-10-19
// Created by: Laurent PAINNOT
// Copyright (c) 1992-1999 Matra Datavision
// 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.

#ifndef _Extrema_GGExtPC_HeaderFile
#define _Extrema_GGExtPC_HeaderFile

#include <Standard.hxx>
#include <Standard_DefineAlloc.hxx>

#include <ElCLib.hxx>
#include <GeomAbs_CurveType.hxx>
#include <NCollection_Array1.hxx>
#include <Precision.hxx>
#include <Standard_Failure.hxx>
#include <Standard_OutOfRange.hxx>
#include <StdFail_NotDone.hxx>
#include <NCollection_HArray1.hxx>
#include <NCollection_Sequence.hxx>

//! Generic class for computing extremal distances between a point and a curve.
//!
//! This template class provides comprehensive extremum search functionality,
//! handling different curve types (lines, circles, ellipses, parabolas,
//! hyperbolas, Bezier, BSpline, and general curves) with optimized algorithms.
//!
//! @tparam TheCurve       The curve type (e.g., Adaptor3d_Curve, Adaptor2d_Curve2d)
//! @tparam TheCurveTool   The curve tool providing static methods
//! @tparam TheExtPElC     The elementary curve extremum class
//! @tparam ThePoint       The point type (e.g., gp_Pnt, gp_Pnt2d)
//! @tparam TheVector      The vector type (e.g., gp_Vec, gp_Vec2d)
//! @tparam ThePOnC        The point-on-curve type
//! @tparam TheSequenceOfPOnC The sequence of points on curve
//! @tparam TheEPC         The general extremum point-curve class
template <typename TheCurve,
          typename TheCurveTool,
          typename TheExtPElC,
          typename ThePoint,
          typename TheVector,
          typename ThePOnC,
          typename TheSequenceOfPOnC,
          typename TheEPC>
class Extrema_GGExtPC
{
public:
  DEFINE_STANDARD_ALLOC

  //! Default constructor.
  Extrema_GGExtPC()
      : myC(nullptr),
        mydone(false),
        mydist1(RealLast()),
        mydist2(RealLast()),
        mytolu(0.0),
        mytolf(0.0),
        mysample(17),
        myintuinf(Precision::Infinite()),
        myintusup(Precision::Infinite()),
        myuinf(Precision::Infinite()),
        myusup(Precision::Infinite()),
        type(GeomAbs_OtherCurve)
  {
  }

  //! Calculates all extremum distances between point P and curve C
  //! within the specified parameter range.
  //! @param theP    The point
  //! @param theC    The curve
  //! @param theUinf Lower bound of parameter range
  //! @param theUsup Upper bound of parameter range
  //! @param theTolF Tolerance on function value (default 1.0e-10)
  Extrema_GGExtPC(const ThePoint& theP,
                  const TheCurve& theC,
                  const double    theUinf,
                  const double    theUsup,
                  const double    theTolF = 1.0e-10)
  {
    Initialize(theC, theUinf, theUsup, theTolF);
    Perform(theP);
  }

  //! Calculates all extremum distances between point P and curve C
  //! using the full curve parameter range.
  //! @param theP    The point
  //! @param theC    The curve
  //! @param theTolF Tolerance on function value (default 1.0e-10)
  Extrema_GGExtPC(const ThePoint& theP, const TheCurve& theC, const double theTolF = 1.0e-10)
  {
    Initialize(theC,
               TheCurveTool::FirstParameter(theC),
               TheCurveTool::LastParameter(theC),
               theTolF);
    Perform(theP);
  }

  //! Initializes the algorithm with curve and parameter range.
  //! @param theC    The curve
  //! @param theUinf Lower bound of parameter range
  //! @param theUsup Upper bound of parameter range
  //! @param theTolF Tolerance on function value (default 1.0e-10)
  void Initialize(const TheCurve& theC,
                  const double    theUinf,
                  const double    theUsup,
                  const double    theTolF = 1.0e-10)
  {
    myC       = const_cast<TheCurve*>(&theC);
    myintuinf = myuinf = theUinf;
    myintusup = myusup = theUsup;
    mytolf             = theTolF;
    mytolu             = TheCurveTool::Resolution(*myC, Precision::Confusion());
    type               = TheCurveTool::GetType(theC);
    mydone             = false;
    mydist1            = RealLast();
    mydist2            = RealLast();
    mysample           = 17;
  }

  //! Performs the extremum computation for the given point.
  //! @param theP The point to find extrema from
  void Perform(const ThePoint& theP)
  {
    mySqDist.Clear();
    mypoint.Clear();
    myismin.Clear();
    int    i, NbExt, n;
    double U;
    mysample             = 17;
    constexpr double t3d = Precision::Confusion();

    if (Precision::IsInfinite(myuinf))
      mydist1 = RealLast();
    else
    {
      Pf      = TheCurveTool::Value(*myC, myuinf);
      mydist1 = theP.SquareDistance(Pf);
    }

    if (Precision::IsInfinite(myusup))
      mydist2 = RealLast();
    else
    {
      Pl      = TheCurveTool::Value(*myC, myusup);
      mydist2 = theP.SquareDistance(Pl);
    }

    TheCurve& aCurve = *myC;

    switch (type)
    {
      case GeomAbs_Circle: {
        myExtPElC.Perform(theP, TheCurveTool::Circle(aCurve), t3d, myuinf, myusup);
        break;
      }
      case GeomAbs_Ellipse: {
        myExtPElC.Perform(theP, TheCurveTool::Ellipse(aCurve), t3d, myuinf, myusup);
        break;
      }
      case GeomAbs_Parabola: {
        myExtPElC.Perform(theP, TheCurveTool::Parabola(aCurve), t3d, myuinf, myusup);
        break;
      }
      case GeomAbs_Hyperbola: {
        myExtPElC.Perform(theP, TheCurveTool::Hyperbola(aCurve), t3d, myuinf, myusup);
        break;
      }
      case GeomAbs_Line: {
        myExtPElC.Perform(theP, TheCurveTool::Line(aCurve), t3d, myuinf, myusup);
        break;
      }
      case GeomAbs_BezierCurve: {
        myintuinf = myuinf;
        myintusup = myusup;
        mysample  = (TheCurveTool::Bezier(aCurve))->NbPoles() * 2;
        myExtPC.Initialize(aCurve);
        IntervalPerform(theP);
        return;
      }
      case GeomAbs_BSplineCurve: {
        auto                              aBSpline  = TheCurveTool::BSpline(aCurve);
        const int                         aFirstIdx = aBSpline->FirstUKnotIndex();
        const int                         aLastIdx  = aBSpline->LastUKnotIndex();
        const NCollection_Array1<double>& aKnots    = aBSpline->Knots();

        double       aPeriodJump = 0.0;
        const double aTolCoeff   = (myusup - myuinf) * Precision::PConfusion();
        if (TheCurveTool::IsPeriodic(aCurve))
        {
          int aPeriodShift = int((myuinf - aKnots(aFirstIdx)) / TheCurveTool::Period(aCurve));
          if (myuinf < aKnots(aFirstIdx) - aTolCoeff)
            aPeriodShift--;
          aPeriodJump = TheCurveTool::Period(aCurve) * aPeriodShift;
        }

        int anIdx;
        int aFirstUsedKnot = aFirstIdx, aLastUsedKnot = aLastIdx;
        for (anIdx = aFirstIdx; anIdx <= aLastIdx; anIdx++)
        {
          double aKnot = aKnots(anIdx) + aPeriodJump;
          if (myuinf >= aKnot - aTolCoeff)
            aFirstUsedKnot = anIdx;
          else
            break;
        }
        for (anIdx = aLastIdx; anIdx >= aFirstIdx; anIdx--)
        {
          double aKnot = aKnots(anIdx) + aPeriodJump;
          if (myusup <= aKnot + aTolCoeff)
            aLastUsedKnot = anIdx;
          else
            break;
        }

        if (aFirstUsedKnot == aLastUsedKnot)
        {
          aFirstUsedKnot = aFirstIdx;
          aLastUsedKnot  = aFirstIdx + 1;
        }

        mysample = aBSpline->Degree() + 1;

        if (mysample == 2)
        {
          ThePoint aPmin;
          double   tmin = 0., distmin = RealLast();
          double   aMin1 = 0., aMin2 = 0.;
          myExtPC.Initialize(aCurve);
          for (anIdx = aFirstUsedKnot; anIdx < aLastUsedKnot; anIdx++)
          {
            double aF = aKnots(anIdx) + aPeriodJump, aL = aKnots(anIdx + 1) + aPeriodJump;

            if (anIdx == aFirstUsedKnot)
              aF = myuinf;
            else if (anIdx == aLastUsedKnot - 1)
              aL = myusup;

            ThePoint aP1, aP2;
            TheCurveTool::D0(aCurve, aF, aP1);
            TheCurveTool::D0(aCurve, aL, aP2);
            TheVector aBase1(theP, aP1), aBase2(theP, aP2);
            TheVector aV(aP2, aP1);
            double    aVal1 = aV.Dot(aBase1);
            double    aVal2 = aV.Dot(aBase2);
            if (anIdx == aFirstUsedKnot)
              aMin1 = theP.SquareDistance(aP1);
            else
            {
              aMin1 = aMin2;
              if (distmin > aMin1)
              {
                distmin = aMin1;
                tmin    = aF;
                aPmin   = aP1;
              }
            }
            aMin2             = theP.SquareDistance(aP2);
            double aMinSqDist = std::min(aMin1, aMin2);
            double aMinDer    = std::min(std::abs(aVal1), std::abs(aVal2));
            if (!(Precision::IsInfinite(aVal1) || Precision::IsInfinite(aVal2)))
            {
              if (aVal1 * aVal2 <= 0.0 || aMinSqDist < 100. * Precision::SquareConfusion()
                  || 2. * aMinDer < Precision::Confusion())
              {
                myintuinf = aF;
                myintusup = aL;
                IntervalPerform(theP);
              }
            }
          }
          if (!Precision::IsInfinite(distmin))
          {
            bool isToAdd = true;
            NbExt        = mypoint.Length();
            for (i = 1; i <= NbExt && isToAdd; i++)
            {
              double t = mypoint.Value(i).Parameter();
              isToAdd  = (distmin < mySqDist(i)) && (std::abs(t - tmin) > mytolu);
            }
            if (isToAdd)
            {
              ThePOnC PC(tmin, aPmin);
              mySqDist.Append(distmin);
              myismin.Append(true);
              mypoint.Append(PC);
            }
          }
        }
        else
        {
          int                        aValIdx = 1;
          NCollection_Array1<double> aVal(1, (mysample) * (aLastUsedKnot - aFirstUsedKnot) + 1);
          NCollection_Array1<double> aParam(1, (mysample) * (aLastUsedKnot - aFirstUsedKnot) + 1);
          for (anIdx = aFirstUsedKnot; anIdx < aLastUsedKnot; anIdx++)
          {
            double aF = aKnots(anIdx) + aPeriodJump, aL = aKnots(anIdx + 1) + aPeriodJump;

            if (anIdx == aFirstUsedKnot)
              aF = myuinf;
            if (anIdx == aLastUsedKnot - 1)
              aL = myusup;

            double aStep = (aL - aF) / mysample;
            for (int aPntIdx = 0; aPntIdx < mysample; aPntIdx++)
            {
              double aCurrentParam = aF + aStep * aPntIdx;
              aVal(aValIdx)   = TheCurveTool::Value(aCurve, aCurrentParam).SquareDistance(theP);
              aParam(aValIdx) = aCurrentParam;
              aValIdx++;
            }
          }
          aVal(aValIdx)   = TheCurveTool::Value(aCurve, myusup).SquareDistance(theP);
          aParam(aValIdx) = myusup;

          myExtPC.Initialize(aCurve);

          for (anIdx = aVal.Lower() + 1; anIdx < aVal.Upper(); anIdx++)
          {
            if (aVal(anIdx) <= Precision::SquareConfusion())
            {
              mySqDist.Append(aVal(anIdx));
              myismin.Append(true);
              mypoint.Append(ThePOnC(aParam(anIdx), TheCurveTool::Value(aCurve, aParam(anIdx))));
            }
            if ((aVal(anIdx) >= aVal(anIdx + 1) && aVal(anIdx) >= aVal(anIdx - 1))
                || (aVal(anIdx) <= aVal(anIdx + 1) && aVal(anIdx) <= aVal(anIdx - 1)))
            {
              myintuinf = aParam(anIdx - 1);
              myintusup = aParam(anIdx + 1);
              IntervalPerform(theP);
            }
          }

          if (mydist1 > Precision::SquareConfusion() && !Precision::IsPositiveInfinite(mydist1))
          {
            ThePoint  aP1, aP2;
            TheVector aV1, aV2;
            TheCurveTool::D1(aCurve, aParam.Value(aParam.Lower()), aP1, aV1);
            TheCurveTool::D1(aCurve, aParam.Value(aParam.Lower() + 1), aP2, aV2);
            TheVector aBase1(theP, aP1), aBase2(theP, aP2);
            double    aVal1 = aV1.Dot(aBase1);
            double    aVal2 = aV2.Dot(aBase2);
            if (!(Precision::IsInfinite(aVal1) || Precision::IsInfinite(aVal2)))
            {
              if (aVal1 * aVal2 <= 0.0 || aBase1.Dot(aBase2) <= 0.0
                  || 2.0 * std::abs(aVal1) < Precision::Confusion())
              {
                myintuinf = aParam(aVal.Lower());
                myintusup = aParam(aVal.Lower() + 1);
                IntervalPerform(theP);
              }
            }
          }

          if (mydist2 > Precision::SquareConfusion() && !Precision::IsPositiveInfinite(mydist2))
          {
            ThePoint  aP1, aP2;
            TheVector aV1, aV2;
            TheCurveTool::D1(aCurve, aParam.Value(aParam.Upper() - 1), aP1, aV1);
            TheCurveTool::D1(aCurve, aParam.Value(aParam.Upper()), aP2, aV2);
            TheVector aBase1(theP, aP1), aBase2(theP, aP2);
            double    aVal1 = aV1.Dot(aBase1);
            double    aVal2 = aV2.Dot(aBase2);

            if (!(Precision::IsInfinite(aVal1) || Precision::IsInfinite(aVal2)))
            {
              if (aVal1 * aVal2 <= 0.0 || aBase1.Dot(aBase2) <= 0.0
                  || 2.0 * std::abs(aVal2) < Precision::Confusion())
              {
                myintuinf = aParam(aVal.Upper() - 1);
                myintusup = aParam(aVal.Upper());
                IntervalPerform(theP);
              }
            }
          }
        }
        mydone = true;
        break;
      }
      default: {
        constexpr int                            aMaxSample   = 17;
        bool                                     IntExtIsDone = false;
        bool                                     IntIsNotValid;
        occ::handle<NCollection_HArray1<double>> theHInter;
        n = TheCurveTool::NbIntervals(aCurve, GeomAbs_C2);
        if (n > 1)
        {
          theHInter = new NCollection_HArray1<double>(1, n + 1);
          TheCurveTool::Intervals(aCurve, theHInter->ChangeArray1(), GeomAbs_C2);
        }
        else
        {
          theHInter = TheCurveTool::DeflCurvIntervals(aCurve);
          n         = theHInter->Length() - 1;
        }
        mysample      = std::max(mysample / n, aMaxSample);
        double maxint = 0.;
        for (i = 1; i <= n; ++i)
        {
          double dt = theHInter->Value(i + 1) - theHInter->Value(i);
          if (maxint < dt)
            maxint = dt;
        }
        bool      isPeriodic = TheCurveTool::IsPeriodic(aCurve);
        TheVector V1;
        ThePoint  PP;
        double    s1 = 0.0;
        double    s2 = 0.0;
        myExtPC.Initialize(aCurve);
        for (i = 1; i <= n; i++)
        {
          myintuinf = theHInter->Value(i);
          myintusup = theHInter->Value(i + 1);
          mysample  = std::max(RealToInt(aMaxSample * (myintusup - myintuinf) / maxint), 3);

          double anInfToCheck = myintuinf;
          double aSupToCheck  = myintusup;

          if (isPeriodic)
          {
            double aPeriod = TheCurveTool::Period(aCurve);
            anInfToCheck   = ElCLib::InPeriod(myintuinf, myuinf, myuinf + aPeriod);
            aSupToCheck    = myintusup + (anInfToCheck - myintuinf);
          }
          IntIsNotValid = (myuinf > aSupToCheck) || (myusup < anInfToCheck);

          if (IntIsNotValid)
            continue;

          if (myuinf >= anInfToCheck)
            anInfToCheck = myuinf;
          if (myusup <= aSupToCheck)
            aSupToCheck = myusup;
          if ((aSupToCheck - anInfToCheck) <= mytolu)
            continue;

          if (i != 1)
          {
            TheCurveTool::D1(aCurve, myintuinf, PP, V1);
            s1 = (TheVector(theP, PP)) * V1;
            if (s1 * s2 < 0.0)
            {
              mySqDist.Append(PP.SquareDistance(theP));
              myismin.Append((s1 < 0.0));
              mypoint.Append(ThePOnC(myintuinf, PP));
            }
          }
          if (i != n)
          {
            TheCurveTool::D1(aCurve, myintusup, PP, V1);
            s2 = (TheVector(theP, PP)) * V1;
          }

          IntervalPerform(theP);
          IntExtIsDone = IntExtIsDone || mydone;
        }

        mydone = IntExtIsDone;
        break;
      }
    }

    // Postprocessing
    if (type == GeomAbs_BSplineCurve || type == GeomAbs_OffsetCurve || type == GeomAbs_OtherCurve)
    {
      if (mydist1 < Precision::SquareConfusion() || mydist2 < Precision::SquareConfusion())
      {
        bool isFirstAdded = false;
        bool isLastAdded  = false;
        int  aNbPoints    = mypoint.Length();
        for (i = 1; i <= aNbPoints; i++)
        {
          U = mypoint.Value(i).Parameter();
          if (std::abs(U - myuinf) < mytolu)
            isFirstAdded = true;
          else if (std::abs(myusup - U) < mytolu)
            isLastAdded = true;
        }
        if (!isFirstAdded && mydist1 < Precision::SquareConfusion())
        {
          mySqDist.Prepend(mydist1);
          myismin.Prepend(true);
          mypoint.Prepend(ThePOnC(myuinf, Pf));
        }
        if (!isLastAdded && mydist2 < Precision::SquareConfusion())
        {
          mySqDist.Append(mydist2);
          myismin.Append(true);
          mypoint.Append(ThePOnC(myusup, Pl));
        }
        mydone = true;
      }
    }
    else
    {
      mydone = myExtPElC.IsDone();
      if (mydone)
      {
        NbExt = myExtPElC.NbExt();
        for (i = 1; i <= NbExt; i++)
        {
          ThePOnC PC = myExtPElC.Point(i);
          U          = PC.Parameter();
          if (TheCurveTool::IsPeriodic(aCurve))
          {
            U = ElCLib::InPeriod(U, myuinf, myuinf + TheCurveTool::Period(aCurve));
          }
          if ((U >= myuinf - mytolu) && (U <= myusup + mytolu))
          {
            PC.SetValues(U, myExtPElC.Point(i).Value());
            mySqDist.Append(myExtPElC.SquareDistance(i));
            myismin.Append(myExtPElC.IsMin(i));
            mypoint.Append(PC);
          }
        }
      }
    }
  }

  //! Returns true if the distances are found.
  bool IsDone() const { return mydone; }

  //! Returns the Nth extremum square distance.
  //! @param theN Index of the extremum (1-based)
  //! @return Square distance value
  double SquareDistance(const int theN) const
  {
    if ((theN < 1) || (theN > NbExt()))
      throw Standard_OutOfRange();
    return mySqDist.Value(theN);
  }

  //! Returns the number of extremum distances.
  //! @return Number of extrema found
  int NbExt() const
  {
    if (!IsDone())
      throw StdFail_NotDone();
    return mySqDist.Length();
  }

  //! Returns true if the Nth extremum distance is a minimum.
  //! @param theN Index of the extremum (1-based)
  //! @return true if minimum, false if maximum
  bool IsMin(const int theN) const
  {
    if ((theN < 1) || (theN > NbExt()))
      throw Standard_OutOfRange();
    return myismin.Value(theN);
  }

  //! Returns the point of the Nth extremum distance.
  //! @param theN Index of the extremum (1-based)
  //! @return The point on curve
  const ThePOnC& Point(const int theN) const
  {
    if ((theN < 1) || (theN > NbExt()))
      throw Standard_OutOfRange();
    return mypoint.Value(theN);
  }

  //! Returns the distances at curve endpoints.
  //! @param[out] theDist1 Square distance to first point
  //! @param[out] theDist2 Square distance to last point
  //! @param[out] theP1 First point on curve
  //! @param[out] theP2 Last point on curve
  void TrimmedSquareDistances(double&   theDist1,
                              double&   theDist2,
                              ThePoint& theP1,
                              ThePoint& theP2) const
  {
    theDist1 = mydist1;
    theDist2 = mydist2;
    theP1    = Pf;
    theP2    = Pl;
  }

protected:
  //! Performs extremum search on the current interval.
  void IntervalPerform(const ThePoint& theP)
  {
    int    i;
    double U;
    myExtPC.Initialize(mysample, myintuinf, myintusup, mytolu, mytolf);
    myExtPC.Perform(theP);
    mydone = myExtPC.IsDone();
    if (mydone)
    {
      int NbExt = myExtPC.NbExt();
      for (i = 1; i <= NbExt; i++)
      {
        ThePOnC PC = myExtPC.Point(i);
        U          = PC.Parameter();
        if (TheCurveTool::IsPeriodic(*myC))
        {
          U = ElCLib::InPeriod(U, myuinf, myuinf + TheCurveTool::Period(*myC));
        }
        if ((U >= myuinf - mytolu) && (U <= myusup + mytolu))
        {
          AddSol(U, PC.Value(), myExtPC.SquareDistance(i), myExtPC.IsMin(i));
        }
      }
    }
  }

  //! Adds a solution if not already present.
  void AddSol(const double theU, const ThePoint& theP, const double theSqDist, const bool isMin)
  {
    int i, NbExt = mypoint.Length();
    for (i = 1; i <= NbExt; i++)
    {
      double t = mypoint.Value(i).Parameter();
      if (std::abs(t - theU) <= mytolu)
      {
        return;
      }
    }
    ThePOnC PC(theU, theP);
    mySqDist.Append(theSqDist);
    myismin.Append(isMin);
    mypoint.Append(PC);
  }

private:
  TheCurve*                    myC;
  ThePoint                     Pf;
  ThePoint                     Pl;
  TheExtPElC                   myExtPElC;
  TheSequenceOfPOnC            mypoint;
  bool                         mydone;
  double                       mydist1;
  double                       mydist2;
  TheEPC                       myExtPC;
  double                       mytolu;
  double                       mytolf;
  int                          mysample;
  double                       myintuinf;
  double                       myintusup;
  double                       myuinf;
  double                       myusup;
  GeomAbs_CurveType            type;
  NCollection_Sequence<bool>   myismin;
  NCollection_Sequence<double> mySqDist;
};

#endif // _Extrema_GGExtPC_HeaderFile
