/*  Copyright (C) 2017 Eric Wasylishen

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program 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 General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

    See file, 'COPYING', for details.
*/

#pragma once

#include <common/qvec.hh>
#include <common/iterators.hh>
#include <array>

/**!
 * touching a side/edge/corner is considered touching
 */
template<class V, size_t N>
class aabb
{
public:
    using value_type = qvec<V, N>;

    class intersection_t
    {
    public:
        bool valid;
        aabb bbox;

        constexpr intersection_t()
            : valid(false),
              bbox(value_type{}, value_type{})
        {
        }

        constexpr intersection_t(const aabb &i)
            : valid(true),
              bbox(i)
        {
        }

        constexpr bool operator==(const intersection_t &other) const
        {
            return valid == other.valid && bbox == other.bbox;
        }

        constexpr operator bool() const { return valid; }
    };

private:
    template<typename V2, size_t N2>
    friend class aabb;

    std::array<value_type, 2> m_corners;

    constexpr void fix()
    {
        for (size_t i = 0; i < N; i++) {
            if (m_corners[1][i] < m_corners[0][i]) {
                m_corners[1][i] = m_corners[0][i];
            }
        }
    }

public:
    constexpr aabb()
        : m_corners({value_type{std::numeric_limits<V>::max()}, value_type{std::numeric_limits<V>::lowest()}})
    {
    }

    constexpr aabb(const value_type &mins, const value_type &maxs)
        : m_corners({mins, maxs})
    {
        fix();
    }

    constexpr aabb(const value_type &points)
        : aabb(points, points)
    {
    }

    template<typename V2>
    constexpr aabb(const aabb<V2, N> &other)
        : aabb(other.m_corners[0], other.m_corners[1])
    {
    }

    template<typename Iter, std::enable_if_t<is_iterator_v<Iter>, int> = 0>
    constexpr aabb(Iter start, Iter end)
        : aabb()
    {
        for (auto it = start; it != end; it++) {
            *this += *it;
        }
    }

    constexpr bool operator==(const aabb &other) const { return m_corners == other.m_corners; }

    constexpr const value_type &mins() const { return m_corners[0]; }

    constexpr const value_type &maxs() const { return m_corners[1]; }

    constexpr aabb translate(const value_type &vec) const { return {mins() + vec, maxs() + vec}; }

    template<typename F>
    constexpr bool disjoint(const aabb<F, N> &other, const F &epsilon = 0) const
    {
        for (size_t i = 0; i < N; i++) {
            if (maxs()[i] < (other.mins()[i] - epsilon) || mins()[i] > (other.maxs()[i] + epsilon)) {
                return true;
            }
        }
        return false;
    }

    template<typename F>
    constexpr bool disjoint_or_touching(const aabb<F, N> &other, const F &epsilon = 0) const
    {
        for (size_t i = 0; i < N; i++) {
            if (maxs()[i] <= (other.mins()[i] - epsilon) || mins()[i] >= (other.maxs()[i] + epsilon)) {
                return true;
            }
        }
        return false;
    }

    constexpr bool contains(const aabb &other) const
    {
        for (size_t i = 0; i < N; i++) {
            if (other.mins()[i] < mins()[i] || other.maxs()[i] > maxs()[i]) {
                return false;
            }
        }
        return true;
    }

    constexpr bool containsPoint(const value_type &p) const
    {
        for (size_t i = 0; i < N; i++) {
            if (!(p[i] >= mins()[i] && p[i] <= maxs()[i])) {
                return false;
            }
        }
        return true;
    }

    constexpr aabb expand(const value_type &pt) const
    {
        auto corners = m_corners;
        for (size_t i = 0; i < N; i++) {
            corners[0][i] = min(corners[0][i], pt[i]);
            corners[1][i] = max(corners[1][i], pt[i]);
        }
        return {corners[0], corners[1]};
    }

    constexpr aabb operator+(const value_type &pt) const { return expand(pt); }

    constexpr aabb operator+(const aabb &other) const { return unionWith(other); }

    constexpr aabb unionWith(const aabb &other) const { return expand(other.mins()).expand(other.maxs()); }

    // in-place expansions

    constexpr aabb &expand_in_place(const value_type &pt)
    {
        for (size_t i = 0; i < N; i++) {
            m_corners[0][i] = min(m_corners[0][i], pt[i]);
            m_corners[1][i] = max(m_corners[1][i], pt[i]);
        }

        return *this;
    }

    constexpr aabb &operator+=(const value_type &pt) { return expand_in_place(pt); }

    constexpr aabb &operator+=(const aabb &other) { return unionWith_in_place(other); }

    constexpr aabb &unionWith_in_place(const aabb &other)
    {
        for (size_t i = 0; i < N; i++) {
            m_corners[0][i] = min({m_corners[0][i], other.mins()[i], other.maxs()[i]});
            m_corners[1][i] = max({m_corners[1][i], other.mins()[i], other.maxs()[i]});
        }

        return *this;
    }

    constexpr intersection_t intersectWith(const aabb &other) const
    {
        auto corners = m_corners;
        for (size_t i = 0; i < N; i++) {
            corners[0][i] = max(corners[0][i], other.mins()[i]);
            corners[1][i] = min(corners[1][i], other.maxs()[i]);
            if (corners[0][i] > corners[1][i]) {
                // empty intersection
                return {};
            }
        }
        return {{corners[0], corners[1]}};
    }

    constexpr value_type size() const { return maxs() - mins(); }

    constexpr bool valid() const
    {
        value_type our_size = size();

        if (our_size[0] < static_cast<V>(0) || our_size[1] < static_cast<V>(0) || our_size[2] < static_cast<V>(0)) {
            return false;
        }
        return true;
    }

    constexpr aabb grow(const value_type &size) const { return {mins() - size, maxs() + size}; }

    constexpr value_type &operator[](const size_t &index) { return m_corners[index]; }

    constexpr const value_type &operator[](const size_t &index) const { return m_corners[index]; }

    constexpr value_type centroid() const { return (mins() + maxs()) * 0.5; }

    constexpr V volume() const
    {
        auto s = size();
        return s[0] * s[1] * s[2];
    }

    constexpr auto begin() { return m_corners.begin(); }
    constexpr auto end() { return m_corners.end(); }

    constexpr auto begin() const { return m_corners.begin(); }
    constexpr auto end() const { return m_corners.end(); }

    // stream support
    auto stream_data() { return std::tie(m_corners); }
};

template<class V>
inline std::array<qplane3<V>, 6> aabb_planes(const aabb<V, 3> &bbox)
{
    return {
        qplane3<V>{qvec<V, 3>(1, 0, 0), bbox.maxs()[0]}, // +X
        qplane3<V>{qvec<V, 3>(-1, 0, 0), -bbox.mins()[0]}, // -X

        qplane3<V>{qvec<V, 3>(0, 1, 0), bbox.maxs()[1]}, // +Y
        qplane3<V>{qvec<V, 3>(0, -1, 0), -bbox.mins()[1]}, // -Y

        qplane3<V>{qvec<V, 3>(0, 0, 1), bbox.maxs()[2]}, // +Z
        qplane3<V>{qvec<V, 3>(0, 0, -1), -bbox.mins()[2]}, // -Z
    };
}

// Fmt support
template<class T, size_t Dim>
struct fmt::formatter<aabb<T, Dim>> : formatter<qvec<T, Dim>>
{
    template<typename FormatContext>
    auto format(const aabb<T, Dim> &b, FormatContext &ctx) -> decltype(ctx.out())
    {
        fmt::format_to(ctx.out(), "{{mins: ");
        fmt::formatter<qvec<T, Dim>>::format(b.mins(), ctx);
        fmt::format_to(ctx.out(), ", maxs: ");
        fmt::formatter<qvec<T, Dim>>::format(b.maxs(), ctx);
        fmt::format_to(ctx.out(), "}}");
        return ctx.out();
    }
};

using aabb3d = aabb<vec_t, 3>;
using aabb2d = aabb<vec_t, 2>;

using aabb3f = aabb<float, 3>;
using aabb2f = aabb<float, 2>;
