#ifndef BOUNDING_BOX_HPP_
#define BOUNDING_BOX_HPP_

#include <Serialize/Utils.hpp>
#include <iostream>
#include <optional>
#include <Utilities/Utilities.hpp>
#include <Side/Side.hpp>
#include <Serialize/Serialize.hpp>

/**
 * @brief Represents a bounding box.
 */
template<typename TCoord>
struct BoundingBox
{
    TCoord x0;
    TCoord y0;
    TCoord z0;
    TCoord x1;
    TCoord y1;
    TCoord z1;

    inline bool rangeOverlaps(TCoord a0, TCoord a1, TCoord b0, TCoord b1) const
    {
        return a0 < b1 && b0 < a1;
    }

    inline bool overlaps(BoundingBox other) const
    {
        return rangeOverlaps(x0, x1, other.x0, other.x1)
            && rangeOverlaps(y0, y1, other.y0, other.y1)
            && rangeOverlaps(z0, z1, other.z0, other.z1);
    }

    inline BoundingBox<TCoord> operator&(const BoundingBox<TCoord> &other) const
    {
        BoundingBox<TCoord> intersection;

        intersection.x0 = std::max(x0, other.x0);
        intersection.x1 = std::min(x1, other.x1);
        intersection.y0 = std::max(y0, other.y0);
        intersection.y1 = std::min(y1, other.y1);
        intersection.z0 = std::max(z0, other.z0);
        intersection.z1 = std::min(z1, other.z1);

        if (intersection.x1 <= intersection.x0 || intersection.y1 <= intersection.y0 || intersection.z1 <= intersection.z0)
        {
            // No intersection: return a zero-sized box.
            return BoundingBox<TCoord>{};
        }

        return intersection;
    }

    template<typename TDest>
    explicit operator BoundingBox<TDest>() const
    {
        return BoundingBox<TDest>
        {
            (TDest) x0, (TDest) y0, (TDest) z0,
            (TDest) x1, (TDest) y1, (TDest) z1
        };
    }

    inline const TCoord& getSideValue(Side side) const
    {
        switch (side.asNumber())
        {
        case 0: return x0;
        case 1: return x1;
        case 2: return y0;
        case 3: return y1;
        case 4: return z0;
        case 5: return z1;
        default: crash("BoundingBox::getSideValue() given invalid side number");
        }
    }

    inline TCoord& getSideValue(Side side)
    {
        switch (side.asNumber())
        {
        case 0: return x0;
        case 1: return x1;
        case 2: return y0;
        case 3: return y1;
        case 4: return z0;
        case 5: return z1;
        default: crash("BoundingBox::getSideValue() given invalid side number");
        }
    }

    inline TCoord getVolume() const
    {
        return (z1 - z0) * (y1 - y0) * (x1 - x0);
    };

    inline std::optional<Side> getSideIntersectedByRay(glm::dvec3 pos, glm::dvec3 dir)
    {
        std::optional<Side> closestSide = std::nullopt;
        double closestDist = 0.0f;

        for (const Side &side : Side::all)
        {
            double distToThisSide = ((double) getSideValue(side) - pos[side.axis()]) / dir[side.axis()];
            glm::vec3 planeIntersection = pos + dir * distToThisSide;

            int dim;
            bool intersectsWall = true;
            for (dim=0; dim<3; dim++)
            {
                if (dim == side.axis())
                {
                    continue;
                }

                if (
                    planeIntersection[dim] < (double) getSideValue(Side(dim << 1)) ||
                    planeIntersection[dim] > (double) getSideValue(Side((dim << 1) | 1))
                )
                {
                    intersectsWall = false;
                }
            }

            if (!intersectsWall)
            {
                continue;
            }

            if (!closestSide.has_value() || distToThisSide < closestDist)
            {
                closestDist = distToThisSide;
                closestSide = side;
            }
        }

        return closestSide;
    }

    bool operator==(const BoundingBox<TCoord> &other) const
    {
        return x0 == other.x0 && x1 == other.x1 &&
            y0 == other.y0 && y1 == other.y1 &&
            z0 == other.z0 && z1 == other.z1;
    }
};

template<typename T>
static inline std::ostream& operator<<(std::ostream &os, BoundingBox<T> bb)
{
    os << "([" << bb.x0 << ".." << bb.x1 << "], [" << bb.y0 << ".." << bb.y1 << "], [" << bb.z0 << ".." << bb.z1 << "])";
    return os;
}

template<typename T, glm::qualifier Q>
static inline BoundingBox<T> operator+(BoundingBox<T> box, glm::vec<3, T, Q> offset)
{
    BoundingBox<T> result;
    result.x0 = box.x0 + offset[0];
    result.x1 = box.x1 + offset[0];
    result.y0 = box.y0 + offset[1];
    result.y1 = box.y1 + offset[1];
    result.z0 = box.z0 + offset[2];
    result.z1 = box.z1 + offset[2];

    return result;
}

template<typename T>
DERIVE_STRUCT_SERIALIZER_PARTIAL(
    SINGLE_ARG(BoundingBox<T>),
    self.x0, self.y0, self.z0,
    self.x1, self.y1, self.z1
);

#endif