#pragma once

#include "IBox.hpp"
#include "../world/position.h"

namespace geometry {
    /// Raycast with distance
    struct Ray {
        camera_pos from;
        glm::vec3 dir;
        float dist;

        // MAYBE: Ray(const glm::mat4& view_matrix) { }
        Ray(const camera_pos& from, const glm::vec3& dir, float dist): from(from), dir(glm::normalize(dir)), dist(dist) { }

        inline Ray operator*(float scale) const noexcept {
            return Ray(from * scale, dir, dist * scale);
        }

        /// Get path points in integer grid
        /// @note not precise enough
        inline void grid(std::vector<glm::llvec3>& points) const {
            glm::llvec3 current = from.as_voxel();
            const glm::llvec3 d = dir * dist;
            const glm::llvec3 inc = glm::llvec3((d.x < 0) ? -1 : 1, (d.y < 0) ? -1 : 1, (d.z < 0) ? -1 : 1);
            const glm::llvec3 size = glm::abs(d);
            const glm::llvec3 delta = size << 1ll;

            if ((size.x >= size.y) && (size.x >= size.z)) {
                int err_1 = delta.y - size.x;
                int err_2 = delta.z - size.x;
                points.reserve(size.x + 1);
                for (int i = 0; i < size.x; i++) {
                    points.push_back(current);
                    if (err_1 > 0) {
                        current.y += inc.y;
                        err_1 -= delta.x;
                    }
                    if (err_2 > 0) {
                        current.z += inc.z;
                        err_2 -= delta.x;
                    }
                    err_1 += delta.y;
                    err_2 += delta.z;
                    current.x += inc.x;
                }
            } else if ((size.y >= size.x) && (size.y >= size.z)) {
                int err_1 = delta.x - size.y;
                int err_2 = delta.z - size.y;
                points.reserve(size.y + 1);
                for (int i = 0; i < size.y; i++) {
                    points.push_back(current);
                    if (err_1 > 0) {
                        current.x += inc.x;
                        err_1 -= delta.y;
                    }
                    if (err_2 > 0) {
                        current.z += inc.z;
                        err_2 -= delta.y;
                    }
                    err_1 += delta.x;
                    err_2 += delta.z;
                    current.y += inc.y;
                }
            } else {
                int err_1 = delta.y - size.z;
                int err_2 = delta.x - size.z;
                points.reserve(size.z + 1);
                for (int i = 0; i < size.z; i++) {
                    points.push_back(current);
                    if (err_1 > 0) {
                        current.y += inc.y;
                        err_1 -= delta.z;
                    }
                    if (err_2 > 0) {
                        current.x += inc.x;
                        err_2 -= delta.z;
                    }
                    err_1 += delta.y;
                    err_2 += delta.x;
                    current.z += inc.z;
                }
            }
            points.push_back(current);
        }
        /// Iterator implementation of grid
        struct iterator {
            enum class axis { X, Y, Z };

            iterator(const Ray& r): current(r.from.as_voxel()),
                d(r.dir * r.dist), l(glm::abs(d)),
                dir((l.x >= l.y) && (l.x >= l.z) ? axis::X : ((l.y >= l.x) && (l.y >= l.z) ? axis::Y : axis::Z)),
                count(dir == axis::X ? l.x : (dir == axis::Y ? l.y : l.z)),
                inc(swap(glm::llvec3((d.x < 0) ? -1 : 1, (d.y < 0) ? -1 : 1, (d.z < 0) ? -1 : 1), dir)),
                delta(swap(l << 1ll, dir)), err_1(delta.y - count), err_2(delta.z - count) { }

            static constexpr inline glm::llvec3 swap(const glm::llvec3& in, const axis dir) { return dir == axis::X ? in :
                (dir == axis::Y ? glm::llvec3(in.y, in.z, in.x) : glm::llvec3(in.z, in.x, in.y)); }

            static constexpr inline long long& get(glm::llvec3& in, const axis dir, const axis target) {
                const axis v = (axis)(((int)dir + (int)target) % 3);
                return v == axis::X ? in.x : (v == axis::Y ? in.y : in.z);
            }

            bool next(glm::llvec3& out) {
                if (count < 0)
                    return false;

                count--;
                out = current;
                if (err_1 > 0) {
                    get(current, dir, axis::Y) += inc.y;
                    err_1 -= delta.x;
                }
                if (err_2 > 0) {
                    get(current, dir, axis::Z) += inc.z;
                    err_2 -= delta.x;
                }
                err_1 += delta.y;
                err_2 += delta.z;
                get(current, dir, axis::X) += inc.x;
                return true;
            }

            glm::llvec3 current;
            const glm::llvec3 d;
            const glm::llvec3 l;
            const axis dir;
            glm::llvec3::value_type count;
            const glm::llvec3 inc;
            const glm::llvec3 delta;
            int err_1;
            int err_2;
        };

        IBox::ContainmentType intersect(const IBox& box) const {
            const glm::llvec3 start = from.as_voxel();
            if(box.contains(start))
                return IBox::ContainmentType::Contains;

            const auto inv = 1. / glm::dvec3(dir);
            glm::f64 tmin = ((inv.x < 0 ? box.Max : box.Min).x - start.x) * inv.x;
            glm::f64 tmax = ((inv.x < 0 ? box.Min : box.Max).x - start.x) * inv.x;
            glm::f64 tymin = ((inv.y < 0 ? box.Max : box.Min).y - start.y) * inv.y;
            glm::f64 tymax = ((inv.y < 0 ? box.Min : box.Max).y - start.y) * inv.y;

            if ((tmin > tymax) || (tymin > tmax)){
                return IBox::ContainmentType::Disjoint;
            }
            if (tymin > tmin) {
                tmin = tymin;
            }
            if (tymax < tmax){
                tmax = tymax;
            }

            glm::f64 tzmin = ((inv.z < 0 ? box.Max : box.Min).z - start.z) * inv.z;
            glm::f64 tzmax = ((inv.z < 0 ? box.Min : box.Max).z - start.z) * inv.z;

            if ((tmin > tzmax) || (tzmin > tmax)){
                return IBox::ContainmentType::Disjoint;
            }
            if (tzmin > tmin){
                tmin = tzmin;
            }
            if (tzmax < tmax){
                tmax = tzmax;
            }
            // this last check is different from the 'ray' case in below references: 
            // we need to check that the segment is on the span of the line
            // that intersects the box
            return tmax<0.0f || tmin> 1.0f ? IBox::ContainmentType::Intersects : IBox::ContainmentType::Disjoint;
        }
    };
}
