package eu.jonahbauer.raytracing.math;

import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull;

import java.util.Comparator;
import java.util.List;
import java.util.Optional;

/**
 * An axis-aligned bounding box.
 */
public record AABB(@NotNull Vec3 min, @NotNull Vec3 max) {
    public static final AABB UNIVERSE = new AABB(Vec3.MIN, Vec3.MAX);
    public static final AABB EMPTY = new AABB(Vec3.ZERO, Vec3.ZERO);
    public static final Comparator<AABB> X_AXIS = Comparator.comparing(AABB::min, Comparator.comparingDouble(Vec3::x));
    public static final Comparator<AABB> Y_AXIS = Comparator.comparing(AABB::min, Comparator.comparingDouble(Vec3::y));
    public static final Comparator<AABB> Z_AXIS = Comparator.comparing(AABB::min, Comparator.comparingDouble(Vec3::z));

    public AABB {
        var a = min;
        var b = max;
        min = Vec3.min(a, b);
        max = Vec3.max(a, b);
    }

    public AABB(@NotNull Range x, @NotNull Range y, @NotNull Range z) {
        this(new Vec3(x.min(), y.min(), z.min()), new Vec3(x.max(), y.max(), z.max()));
    }

    public static @NotNull Optional<AABB> getBoundingBox(@NotNull List<? extends @NotNull Hittable> objects) {
        var bbox = (AABB) null;
        for (var object : objects) {
            bbox = bbox == null ? object.getBoundingBox() : bbox.expand(object.getBoundingBox());
        }
        return Optional.ofNullable(bbox);
    }

    public @NotNull Range x() {
        return new Range(min.x(), max.x());
    }

    public @NotNull Range y() {
        return new Range(min.y(), max.y());
    }

    public @NotNull Range z() {
        return new Range(min.z(), max.z());
    }

    public @NotNull Vec3 center() {
        return Vec3.average(min, max, 2);
    }

    public @NotNull AABB expand(@NotNull AABB box) {
        return new AABB(Vec3.min(this.min, box.min), Vec3.max(this.max, box.max));
    }

    public boolean hit(@NotNull Ray ray) {
        return intersect(ray).isPresent();
    }

    public @NotNull Optional<Range> intersect(@NotNull Ray ray) {
        if (this == UNIVERSE) return Optional.of(Range.UNIVERSE);
        if (this == EMPTY) return Optional.empty();

        int vmask = ray.vmask();

        var origin = ray.origin();
        var invDirection = ray.direction().inv();

        // calculate t values for intersection points of ray with planes through min
        var tmin = intersect(min(), origin, invDirection);
        // calculate t values for intersection points of ray with planes through max
        var tmax = intersect(max(), origin, invDirection);

        // determine range of t for which the ray is inside this voxel
        double tlmax = Double.NEGATIVE_INFINITY; // lower limit maximum
        double tumin = Double.POSITIVE_INFINITY; // upper limit minimum
        for (int i = 0; i < 3; i++) {
            // classify t values as lower or upper limit based on vmask
            if ((vmask & (1 << i)) == 0) {
                // min is lower limit and max is upper limit
                tlmax = Math.max(tlmax, tmin[i]);
                tumin = Math.min(tumin, tmax[i]);
            } else {
                // max is lower limit and min is upper limit
                tlmax = Math.max(tlmax, tmax[i]);
                tumin = Math.min(tumin, tmin[i]);
            }
        }

        return tlmax < tumin ? Optional.of(new Range(tlmax, tumin)) : Optional.empty();
    }

    public static double @NotNull[] intersect(@NotNull Vec3 corner, @NotNull Ray ray) {
        return intersect(corner, ray.origin(), ray.direction().inv());
    }

    private static double @NotNull[] intersect(@NotNull Vec3 corner, @NotNull Vec3 origin, @NotNull Vec3 invDirection) {
        return new double[] {
                (corner.x() - origin.x()) * invDirection.x(),
                (corner.y() - origin.y()) * invDirection.y(),
                (corner.z() - origin.z()) * invDirection.z(),
        };
    }
}
