/*
    This file is part of Msc-generator.
    Copyright (C) 2008-2024 Zoltan Turanyi
    Distributed under GNU Affero General Public License.

    Msc-generator is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Msc-generator is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Msc-generator.  If not, see <http://www.gnu.org/licenses/>.
*/

/** @file contour_distance.h Declares class DistanceType and some helpers.
 * @ingroup contour_files
 */

#if !defined(CONTOUR_DISTANCE_H)
#define CONTOUR_DISTANCE_H

#include "contour_basics.h"

namespace contour {

/** @addtogroup contour_internal
 * @{
 */


/** Returns true if `r` is in [0..1]
 */
template <typename real>
constexpr bool between01(real r) noexcept
{
    return r>=0 && r<=1;
}

/** Returns true if `r` is in [0..1] or just outside by max SMALL_NUM.
 */
template <typename real>
constexpr bool between01_approximate_inclusive(real r) noexcept
{
    return !(test_smaller(r, 0) || test_smaller(1, r));
}

/** Returns true if `n` is not significantly outside [0,1] (by max SMALL_NUM).
  If n is close to 0 or 1 we return exactly 0 or 1.
 */
template <typename real>
constexpr bool between01_adjust(real &n) noexcept
{
    if (test_smaller(1,n) || test_smaller(n,0)) //if n>1 or n~=1 or n<<0
        return false;
    if (!test_smaller(0, n))
        n = 0; //if n~=0;
    else if (!test_smaller(n, 1))
        n = 1; //if n~=1;
    return true;
}

/** Convenience function to take the square. */
template <typename real> constexpr real sqr(real a) noexcept { return a*a; }

/** An `fmod(a, b)`, that always return a value in `[0..b)` even if `a<0`
*/
template<typename real> constexpr real fmod_negative_safe(real a, real b) noexcept { _ASSERT(b>0); return a>=0 ? fmod(a, b) : b - fmod(-a, b); }

/** Converts between degrees and radians.
 */
constexpr double deg2rad(double degree) noexcept
{
    return fmod_negative_safe(degree, 360.)*(M_PI/180);
}

/** Converts between radians and degrees.
 */
constexpr double rad2deg(double degree) noexcept 
{
	return fmod_negative_safe(degree, 2*M_PI)*(180/M_PI);
}

/** Describes how three points can relate to each other.
*/
enum ETriangleDirType
{
    ALL_EQUAL,       ///<All three are identical.
    A_EQUAL_B,       ///<Two of them are identical.
    A_EQUAL_C,       ///<Two of them are identical.
    B_EQUAL_C,       ///<Two of them are identical.
    IN_LINE,         ///<Three separate points on a line.
    CLOCKWISE,       ///<`A`->`B`->`C` define a non-degenerate clockwise triangle.
    COUNTERCLOCKWISE ///<`A`->`B`->`C` define a non-degenerate counterclockwise triangle.
};

ETriangleDirType triangle_dir(XY a, XY b, XY c) noexcept;

double angle(const XY &base, const XY &A, const XY &B) noexcept;
double angle_to_horizontal(const XY &base, const XY &A) noexcept;

/** Convert from a fake angle to degrees.
 */
inline double angle2degree(double angle) noexcept {
    return (angle>=2) ? 360 - acos(angle-3)*(180./M_PI) : acos(1-angle)*(180./M_PI);
}
/** Convert from a fake angle to radians.
 */
inline double angle2radian(double angle) noexcept {
    return (angle>=2) ? 2*M_PI - acos(angle-3) : acos(1-angle);
}
/** Convert from degrees to fake angle.
 */
inline double degree2angle(double degree) noexcept {
    const double rad = fmod_negative_safe(degree, 360.)/(180./M_PI);
    return (rad>=M_PI) ? 3 + cos(2*M_PI-rad) : 1 - cos(rad);
}
/** Convert from radians to fake angle.
 */
inline double radian2angle(double rad) noexcept {
    rad = fmod_negative_safe(rad, 2*M_PI);
    return (rad>=M_PI) ? 3 + cos(2*M_PI-rad) : 1 - cos(rad);
}

/** Returns the position of point 'p' on the straight M->N segment.
*/
double point2pos_straight(const XY &M, const XY&N, const XY &p) noexcept;

/** Returns +1 if P is on the right side of A->B, -1 on left side
* and 0 if on the line. */
constexpr int WhichSectionSide(const XY&A, const XY&B, const XY&P) noexcept
{
    return fsign((B-A).Rotate90CW().DotProduct(P-A));
}

/** A structure containing a position inside a path. */
struct PathPos
{
    size_t edge;  ///<The index of the edge containing the position
    double pos;   ///<The position within the edge, in between [0..1]. Note that since edges do not necessarily connect pos(e,1) may not equal pos(e+1,0) as with contours.
    constexpr bool operator <(const PathPos& o) const noexcept { return std::tie(edge, pos) < std::tie(o.edge, o.pos); }
    constexpr bool operator <=(const PathPos &o) const noexcept { return operator<(o) || operator==(o); }
    /** Two PathPos are equal also if they both refer to the same vertex.
     * E.g, (edge=4, pos=1) == (edge=5, pos=0). */
    bool operator ==(const PathPos &o) const { 
        if (edge==o.edge) return pos == o.pos;
        if (edge+1==o.edge) return pos==1 && o.pos==0;
        if (edge==o.edge+1) return pos==0 && o.pos==1;
        return false;
    }
    /** Two PathPos are equal also if they both refer to the same vertex.
    * E.g, (edge=4, pos=1) == (edge=5, pos=0). */
    constexpr bool test_equal(const PathPos &o) const
    {
        if (edge==o.edge) return contour::test_equal(pos, o.pos);
        if (edge+1==o.edge) return pos==1 && o.pos==0;
        if (edge==o.edge+1) return pos==0 && o.pos==1;
        return false;
    }
    std::string Dump() const {
        char buff[128];
        snprintf(buff, sizeof(buff), "(%zd:%g)", edge, pos);
        return buff;
    }
};

/** Returns true if the order of the 3 positions is correct.
 * We assume a non-cyclic path.*/
inline bool between(const PathPos& a, const PathPos& b, const PathPos& c) noexcept { return a <= b && b <= c; }
/** Returns true if the order of the 3 positions is correct.
 * We assume a cyclic path, thus if we start from a, warp to the beginning of the path,
 * hit b and then hit c, we are also good. Or we start from a, hit b then warp and hit c.*/
inline bool between_warp(const PathPos& a, const PathPos& b, const PathPos& c) noexcept { 
    return between(a, b, c) || between(b, c, a) || between(c, a, b);
}



/** Helper class to collect data during distance calculation.
 *
 * We distinguish between case when an element is inside a contour or outside.
 * The former is represented by a negative distance.
 * We use this class to store the smallest distance so far during a pairwise comparison
 * of contour edges.
 * We store two flags to see if we had points inside a contour or outside.
 * If both were seen, the two contours may cross each other.
 * Here 'inside' does not mean 'in a hole', that counts as outside.
 */
struct DistanceType
{
    //friend class SimpleContour;
    //friend class HoledSimpleContour;
    bool was_inside;    ///<True if there was one point of the other shape inside us.
    bool was_outside;   ///<True if there was one point of the other shape inside us.
    double distance;    ///<The smallest distance so far. (negative if one inside the other)
    XY point_on_me;     ///<This of my points was closest to the other shape.
    XY point_on_other;  ///<This of the other's points was closest to us.
    PathPos pos_on_me;  ///<The position on me
    PathPos pos_on_other;///<The position on the other
    DistanceType() noexcept {Reset();}
    void Reset() noexcept {was_inside = was_outside = false; distance = MaxVal(distance);}  ///<Make the distance infinite (and invalid)
    DistanceType &SwapPoints() noexcept { std::swap(point_on_me, point_on_other); std::swap(pos_on_me, pos_on_other); return *this; }            ///<Swap the two sides (points and positions)
    bool Merge(double d, const XY &point_on_me, const XY &point_on_other, 
               const PathPos &pos_on_me, const PathPos &pos_on_other) noexcept; ///<Check if `d` is smaller than `distance` and if so, replace. Set inside or outside flag depending on the sign of `d`.
    bool Merge(const DistanceType &o) noexcept;                                 ///<Check if `o` is smaller and replace if so.
    bool ConsiderBB(double bbdist) const noexcept {return fabs(distance) > fabs(bbdist);} ///<Return true if `bbdist` is smaller in absolute value.
    bool IsValid() const noexcept {return distance != MaxVal(distance);}  ///<Return true if any distance is merged in.
    bool IsZero() const noexcept {return distance==0;}                    ///<Return if distance is zero.
    void ClearInOut() noexcept {was_inside = was_outside = false;}        ///<Clear flags of previous history.
    void MergeInOut(double dist) noexcept {(dist<0 ? was_inside : was_outside) = true;} ///<Set the inside or outside flag depending on `dist`. Do not change `distance`.
    void MakeAllInside() noexcept {was_outside = false; was_inside = true; distance = -fabs(distance);}  ///<Force all history to be in the inside.
    void MakeAllOutside() noexcept {was_outside = true; was_inside = false; distance = fabs(distance);}  ///<Force all history to be in the inside.
    void SwapInOut() noexcept {std::swap(was_inside, was_outside); distance *= -1;}  ///<Change in/outside history.
};

/** @} */ //addtogroup contour_internal

/** Takes the smaller of this or the data provided.
 * @returns true if the data provided *was* smaller and we did an update.*/
inline bool DistanceType::Merge(double d, const XY &p_on_me, const XY &p_on_other,
                                const PathPos &po_on_me, const PathPos &po_on_other) noexcept
{
    if (IsZero() || fabs(d) == MaxVal(d)) return false;
    if (d>0) was_outside = true;
    else if (d<0) was_inside = true;
    if (fabs(distance) <= fabs(d)) return false;
    distance = d;
    point_on_me = p_on_me;
    point_on_other = p_on_other;
    pos_on_me = po_on_me;
    pos_on_other= po_on_other;
    return true;
}

/** Takes the smaller of this or the data provided.
 * @returns true if the data provided *was* smaller and we did an update.*/
inline bool DistanceType::Merge(const DistanceType &o) noexcept
{
    if (o.was_inside) was_inside = true;
    if (o.was_outside) was_outside = true;
    return Merge(o.distance, o.point_on_me, o.point_on_other, o.pos_on_me, o.pos_on_other);
}



} //namespace

#endif //CONTOUR_DISTANCE_H