
/*
 * Nekohook: Free, fast and modular cheat software
 * Copyright (C) 2018 Rebekah Rowe
 *
 * 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 3 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, see <https://www.gnu.org/licenses/>.
 */

#pragma once

#include <array>
#include <algorithm>
#include <glm/detail/qualifier.hpp>
#include <glm/fwd.hpp>
#include <stdexcept>

#include <glm/glm.hpp>
#include <glm/gtc/constants.hpp>
#include <glm/geometric.hpp>

namespace qubel::geo {

namespace internal {

#if true // NEKOHOOK_GFX == opengl
template <int _dim_length, typename T>
using BVec = glm::vec<_dim_length, T>;
template <typename T>
using BVec2 = glm::vec<2, T>;
template <typename T>
using BVec3 = glm::vec<3, T>;
using Vec2 = glm::vec2;
using Vec3 = glm::vec3;
using DVec3 = glm::dvec3;
using IVec2 = glm::ivec2;
using UVec2 = glm::uvec2;

constexpr float F_PI = glm::pi<float>();

template <class... T>
auto Distance(T... in) { return glm::distance(in...); }

template <typename T>
auto GetMaskOfVec2(const T& src, glm::bvec2 mask) {
    T ret = { 0, 0 };
    if (mask.x)
        ret.x = src.x;
    if (mask.y)
        ret.y = src.y;
    return ret;
}
#endif

template <typename T, int _length>
class BoxBase;

template <typename T>
class BoxBase<T, 2> {
    static_assert(T::length() == 2);

protected:
    BoxBase() { }
    BoxBase(T _first, T _second)
        : first(_first)
        , second(_second) { }

public:
    // TODO, rotation around origin?

    // Data
    union {
        T first, origin;
    };
    union {
        T second, size;
    };

    T GetMax() const { return this->origin + this->size; }
    T GetSize() const { return this->size; }

    T GetPoint(int idx) const {
        switch (idx) {
        case 0:
            return this->origin;
        case 1:
            return { this->origin.x + this->size.x, this->origin.y };
        case 2:
            return { this->origin.x, this->origin.y + this->size.y };
        case 3:
            return this->origin + this->size;
        default:
            throw std::logic_error("Unknown point idx: " + std::to_string(idx));
        }
    }
    std::array<T, 4> GetPoints() const {
        T max = this->GetMax();
        return {
            this->origin,
            { max.x, this->origin.y },
            { this->origin.x, max.y },
            max
        };
    }

    bool Contains(T in) const {
        if (in.x > this->origin.x && in.y > this->origin.y) {
            T max = this->GetMax();
            return (in.x < max.x && in.x < max.y);
        }
        return false;
    }

    bool SegmentIntersects(T src, T dest) const {
        if ((dest.x > this->min.x && src.x > this->min.x) && (dest.y > this->min.y && src.y > this->min.y)) {
            T max = this->GetMax();
            return (dest.x < max.x && src.x < max.x) || (dest.y < max.y && src.y < max.y);
        }
        return false;
    }
    enum class Direction {
        kUp,
        kDown,
        kLeft,
        kRight,
        kPosX = kRight,
        kPosY = kDown,
        kNegX = kLeft,
        kNegY = kUp
    };
    void Expand(Direction dir, int amt) { // TODO: Bitmongering(nopey)
        switch (dir) {
        case Direction::kUp:
            this->origin.y -= amt;
        case Direction::kDown:
            this->size.y += amt;
            break;
        case Direction::kLeft:
            this->origin.x -= amt;
        case Direction::kRight:
            this->size.x += amt;
            break;
        default:
            assert(false);
        }
    }
    void ExpandTo(T v) {
        if (this->origin.x > v.x)
            this->Expand(Direction::kNegX, this->origin.x - v.x);
        if (this->origin.y > v.y)
            this->Expand(Direction::kNegY, this->origin.y - v.y);
        T t_max = this->GetMax();
        if (t_max.x < v.x)
            this->Expand(Direction::kPosX, v.x - this->origin.x);
        if (t_max.y < v.y)
            this->Expand(Direction::kPosY, v.y - this->origin.y);
    }
    // TODO: Center around
};

template <typename T>
class BoxBase<T, 3> {
    static_assert(T::length() == 3);

protected:
    BoxBase() { }
    BoxBase(T _first, T _second)
        : first(_first)
        , second(_second) { }

public:
    // Data
    union {
        T first, min;
    };
    union {
        T second, max;
    };

    auto operator[](const int idx) const {
        return this->data.at(idx);
    }

    // Utility functions
    T GetSize() const { return this->max - this->min; }
    T& GetMax() const { return this->max; }

    T GetPoint(int idx) const {
        switch (idx) {
        case 0:
            return this->min;
        case 1:
            return { this->max.x, this->min.y, this->min.z };
        case 2:
            return { this->max.x, this->max.y, this->min.z };
        case 3:
            return { this->min.x, this->max.y, this->max.z };
        case 4:
            return { this->min.x, this->min.y, this->max.z };
        case 5:
            return { this->max.x, this->min.y, this->max.z };
        case 6:
            return { this->min.x, this->max.y, this->min.z };
        case 7:
            return this->max;
        default:
            throw std::logic_error("GetPoint() is only available for 2 and 3 axis vectors");
        }
    }
    std::array<T, 8> GetPoints() const {
        return {
            this->min,
            { this->max.x, this->min.y, this->min.z },
            { this->max.x, this->max.y, this->min.z },
            { this->min.x, this->max.y, this->max.z },
            { this->min.x, this->min.y, this->max.z },
            { this->max.x, this->min.y, this->max.z },
            { this->min.x, this->max.y, this->min.z },
            this->max
        };
    }

    bool Contains(T i) const {
        if (i.x > this->min.x || i.y > this->min.y || i.z > this->min.z)
            if (i.x < this->max.x || i.y < this->max.y || i.z < this->max.z)
                return false;
        return true;
    }

    // Credits to cathook
    bool LineIntersects(T src, T dst) const {
        if (dst.x < this->min.x && src.x < this->min.x)
            return false;
        if (dst.y < this->min.y && src.y < this->min.y)
            return false;
        if (dst.z < this->min.z && src.z < this->min.z)
            return false;
        if (dst.x > this->max.x && src.x > this->max.x)
            return false;
        if (dst.y > this->max.y && src.y > this->max.y)
            return false;
        if (dst.z > this->max.z && src.z > this->max.z)
            return false;
        return true;
    }

    enum Direction {
        kUp,
        kDown,
        kLeft,
        kRight,
        kForward,
        kBackward,
        kPosX = kUp,
        kPosY = kLeft,
        kPosZ = kForward,
        kNegX = kDown,
        kNegY = kRight,
        kNegZ = kBackward
    };
    void Expand(Direction dir, int amt) {
        switch (dir) {
        case Direction::kUp:
            this->max.y += amt;
            break;
        case Direction::kDown:
            this->min.y -= amt;
            break;
        case Direction::kLeft:
            this->min.x += amt;
            break;
        case Direction::kRight:
            this->min.x -= amt;
            break;
        case Direction::kForward:
            this->min.z += amt;
            break;
        case Direction::kBackward:
            this->max.z -= amt;
            break;
        default:
            assert(false);
        }
    }
};

template <typename T>
class Box : public BoxBase<T, T::length()> {
    using Parent = BoxBase<T, T::length()>;

public:
    using Direction = typename Parent::Direction;

    Box() { }
    Box(T _first, T _second)
        : Parent(_first, _second) { }
    Box(std::pair<T, T> v)
        : Box(v.first, v.second) { }
    template <typename TT>
    Box(TT v)
        : Box(v.first, v.second) {
        static_assert(!std::is_same_v<std::pair<T, T>, TT>);
    }

    template <typename TT>
    Box<T>& operator=(TT v) { *this = Box<T>(v.first, v.second); }

    // Unary arithmetic operators
    bool operator==(T v) const {
        return this->first == v.first && this->second == v.second;
    }
    bool operator!=(T v) const {
        return !(*this == v);
    }
    Box<T> operator+(T v) const {
        return { this->min + v, this->max + v };
    }
    Box<T> operator-(T v) const {
        return { this->min - v, this->max - v };
    }

    Box<T> operator*(typename T::value_type v) const {
        T center = this->GetCenter();
        T size = (this->GetSize() * v) / typename T::value_type(2);
        return { center - size, center + size };
    }
    Box<T> operator/(typename T::value_type v) const {
        T center = this->GetCenter();
        T delta = (this->GetSize() / v) / typename T::value_type(2);

        return { center - delta, center + delta };
    }
    Box<T>& operator/=(typename T::value_type v) {
        return *this = *this / v;
    }

    // Utility functions
    T GetCenter() const { return (this->first + this->second) / typename T::value_type(2); }
    void Expand(std::initializer_list<Direction> dirs, int amt) {
        auto end = dirs.end();
        for (auto i = dirs.begin(); i != end; i++)
            this->Expand(*i, amt);
    }
    void Shrink(Direction dir, int amt) { this->Expand(dir, -amt); }
};

template <int T_length>
class AngleBase;

template <>
class AngleBase<1> {
    float data;

protected:
    template <typename... T>
    AngleBase(T... args)
        : data(args...) { }

public:
    operator float() const {
        return this->data;
    }
    operator float&() {
        return this->data;
    }
};

template <>
class AngleBase<2> : public Vec2 {
    using Vec2::Vec2;
    using Parent = Vec2;

protected:
    template <typename... T>
    AngleBase(T... args)
        : Vec2(args...) {
        static_assert(!(std::is_same_v<Vec2, T> || ...));
    }

public:
    using Vec2::operator=;
    template <class T>
    AngleBase operator-(T in) { return *this - Vec2(in); }
    template <class T>
    AngleBase operator+(T in) { return *this + Vec2(in); }

    AngleBase<2>& Clamp();
    AngleBase<2> Clamp() const;
    AngleBase<2> GetDelta(const AngleBase<2>& other_angles) const;
    float GetFov(const Vec3& view_src, const Vec3& dest_point) const;
    float GetFov(const AngleBase<2>& pointed_angle) const;
    static AngleBase<2> PointTo(const Vec3& src_point, const Vec3& dest_point);
};

template <int T_Length>
using Angle = AngleBase<T_Length>;
using Angle1 = Angle<1>;
using Angle2 = Angle<2>;

template <typename T_Origin, typename T_Angle>
class Ray {
private:
    const T_Origin origin;
    const T_Angle angle;

public:
    Ray(T_Origin origin, T_Angle angle)
        : origin(origin)
        , angle(angle) { }
    std::pair<T_Origin, T_Origin> Cast(float dist);
};

template <typename T>
class Segment : public std::pair<T, T> {
public:
    using Parent = std::pair<T, T>;
    using Parent::Parent;
    using Parent::operator=;
    template <typename TT>
    Segment(Ray<T, TT> r, float dist = 8192.0f)
        : Parent(r.Cast(dist)) { }
    Segment(const std::pair<T, T>& p)
        : Parent(p) { }
    Segment(std::pair<T, T>&& p)
        : Parent(std::move(p)) { }
    Segment(std::pair<T, T> p)
        : Parent(std::move(p)) { }
};

} // namespace internal

using Vec3 = internal::Vec3;

using Vec2 = internal::Vec2;

// idk if these should be in here, but its too useful not to...
using DVec3 = internal::DVec3;
using IVec2 = internal::IVec2;
using UVec2 = internal::UVec2;
///

template <class... T>
auto Distance(T... in) { return internal::Distance(in...); }

template <class T>
auto GetMaskOfVec2(const T& src, glm::bvec2 mask) { return internal::GetMaskOfVec2(src, mask); }

template <class T>
using Box = internal::Box<T>;

template <int T>
using Angle = internal::AngleBase<T>;

template <typename T_Origin, typename T_Angle>
using Ray = internal::Ray<T_Origin, T_Angle>;

template <typename T>
using Segment = internal::Segment<T>;
using Segment2 = internal::Segment<Vec2>;
using Segment3 = internal::Segment<Vec3>;

template <class T_Vec, class T_Float>
using Sphereoid = std::pair<T_Vec, T_Float>;

} // namespace qubel::geo
