// David Eberly, Geometric Tools, Redmond WA 98052
// Copyright (c) 1998-2020
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt
// https://www.geometrictools.com/License/Boost/LICENSE_1_0.txt
// Version: 4.0.2019.08.13

#pragma once

#include <Mathematics/FIQuery.h>
#include <Mathematics/TIQuery.h>
#include <Mathematics/IntrConvexPolygonHyperplane.h>
#include <Mathematics/Triangle.h>
#include <Mathematics/OrientedBox.h>
#include <Mathematics/Vector3.h>

// The test-intersection query is based on the document
// https://www.geometrictools.com/Documentation/MethodOfSeparatingAxes.pdf
// The find-intersection query clips the triangle against the faces of
// the oriented box.

namespace WwiseGTE
{
    template <typename Real>
    class TIQuery<Real, Triangle3<Real>, OrientedBox3<Real>>
    {
    public:
        struct Result
        {
            bool intersect;
        };

        Result operator()(Triangle3<Real> const& triangle, OrientedBox3<Real> const& box)
        {
            Result result;

            Real min0, max0, min1, max1;
            Vector3<Real> D, edge[3];

            // Test direction of triangle normal.
            edge[0] = triangle.v[1] - triangle.v[0];
            edge[1] = triangle.v[2] - triangle.v[0];
            D = Cross(edge[0], edge[1]);
            min0 = Dot(D, triangle.v[0]);
            max0 = min0;
            GetProjection(D, box, min1, max1);
            if (max1 < min0 || max0 < min1)
            {
                result.intersect = false;
                return result;
            }

            // Test direction of box faces.
            for (int i = 0; i < 3; ++i)
            {
                D = box.axis[i];
                GetProjection(D, triangle, min0, max0);
                Real DdC = Dot(D, box.center);
                min1 = DdC - box.extent[i];
                max1 = DdC + box.extent[i];
                if (max1 < min0 || max0 < min1)
                {
                    result.intersect = false;
                    return result;
                }
            }

            // Test direction of triangle-box edge cross products.
            edge[2] = edge[1] - edge[0];
            for (int i0 = 0; i0 < 3; ++i0)
            {
                for (int i1 = 0; i1 < 3; ++i1)
                {
                    D = Cross(edge[i0], box.axis[i1]);
                    GetProjection(D, triangle, min0, max0);
                    GetProjection(D, box, min1, max1);
                    if (max1 < min0 || max0 < min1)
                    {
                        result.intersect = false;
                        return result;
                    }
                }
            }

            result.intersect = true;
            return result;
        }

    private:
        void GetProjection(Vector3<Real> const& axis, Triangle3<Real> const& triangle, Real& imin, Real& imax)
        {
            Real dot[3] =
            {
                Dot(axis, triangle.v[0]),
                Dot(axis, triangle.v[1]),
                Dot(axis, triangle.v[2])
            };

            imin = dot[0];
            imax = imin;

            if (dot[1] < imin)
            {
                imin = dot[1];
            }
            else if (dot[1] > imax)
            {
                imax = dot[1];
            }

            if (dot[2] < imin)
            {
                imin = dot[2];
            }
            else if (dot[2] > imax)
            {
                imax = dot[2];
            }
        }

        void GetProjection(Vector3<Real> const& axis, OrientedBox3<Real> const& box, Real& imin, Real& imax)
        {
            Real origin = Dot(axis, box.center);
            Real maximumExtent =
                std::fabs(box.extent[0] * Dot(axis, box.axis[0])) +
                std::fabs(box.extent[1] * Dot(axis, box.axis[1])) +
                std::fabs(box.extent[2] * Dot(axis, box.axis[2]));

            imin = origin - maximumExtent;
            imax = origin + maximumExtent;
        }
    };

    template <typename Real>
    class FIQuery<Real, Triangle3<Real>, OrientedBox3<Real>>
    {
    public:
        struct Result
        {
            std::vector<Vector3<Real>> insidePolygon;
            std::vector<std::vector<Vector3<Real>>> outsidePolygons;
        };

        Result operator()(Triangle3<Real> const& triangle, OrientedBox3<Real> const& box)
        {
            Result result;

            // Start with the triangle and clip it against each face of the
            // box.  The largest number of vertices for the polygon of
            // intersection is 7.
            result.insidePolygon.resize(3);
            for (int i = 0; i < 3; ++i)
            {
                result.insidePolygon[i] = triangle.v[i];
            }

            typedef FIQuery<Real, std::vector<Vector<3, Real>>, Hyperplane<3, Real>> PPQuery;

            Plane3<Real> plane;
            PPQuery ppQuery;
            typename PPQuery::Result ppResult;
            for (int dir = -1; dir <= 1; dir += 2)
            {
                for (int side = 0; side < 3; ++side)
                {
                    // Create a plane for the box face that points inside the box.
                    plane.normal = ((Real)dir) * box.axis[side];
                    plane.constant = Dot(plane.normal, box.center) - box.extent[side];

                    ppResult = ppQuery(result.insidePolygon, plane);
                    switch (ppResult.configuration)
                    {
                    case PPQuery::Configuration::SPLIT:
                        result.insidePolygon = ppResult.positivePolygon;
                        result.outsidePolygons.push_back(ppResult.negativePolygon);
                        break;
                    case PPQuery::Configuration::POSITIVE_SIDE_VERTEX:
                    case PPQuery::Configuration::POSITIVE_SIDE_EDGE:
                    case PPQuery::Configuration::POSITIVE_SIDE_STRICT:
                        // The result.insidePolygon is already
                        // ppResult.positivePolygon, but to make it clear,
                        // assign it here.
                        result.insidePolygon = ppResult.positivePolygon;
                        break;
                    case PPQuery::Configuration::NEGATIVE_SIDE_VERTEX:
                    case PPQuery::Configuration::NEGATIVE_SIDE_EDGE:
                    case PPQuery::Configuration::NEGATIVE_SIDE_STRICT:
                        result.insidePolygon.clear();
                        result.outsidePolygons.push_back(ppResult.negativePolygon);
                        return result;
                    case PPQuery::Configuration::CONTAINED:
                        // A triangle coplanar with a box face will be
                        // processed as if it is inside the box.
                        result.insidePolygon = ppResult.intersection;
                        break;
                    default:
                        result.insidePolygon.clear();
                        result.outsidePolygons.clear();
                        break;
                    }
                }
            }

            return result;
        }
    };
}