package eu.jonahbauer.raytracing.scene.hittable3d;

import eu.jonahbauer.raytracing.render.material.Material;
import eu.jonahbauer.raytracing.math.AABB;
import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull;

import java.util.Objects;
import java.util.Optional;

public final class Sphere implements Hittable {
    private final @NotNull Vec3 center;
    private final double radius;
    private final @NotNull Material material;

    private final @NotNull AABB bbox;

    public Sphere(@NotNull Vec3 center, double radius, @NotNull Material material) {
        this.center = Objects.requireNonNull(center, "center");
        this.material = Objects.requireNonNull(material, "material");
        if (radius <= 0 || !Double.isFinite(radius)) throw new IllegalArgumentException("radius must be positive");
        this.radius = radius;

        this.bbox = new AABB(
                center.minus(radius, radius, radius),
                center.plus(radius, radius, radius)
        );
    }

    @Override
    public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
        var oc = ray.origin().minus(center);

        var a = ray.direction().squared();
        var h = ray.direction().times(oc);
        var c = oc.squared() - radius * radius;

        var discriminant = h * h - a * c;
        if (discriminant < 0) return Optional.empty();

        var sd = Math.sqrt(discriminant);

        double t = (- h - sd) / a;
        if (!range.surrounds(t)) t = (- h + sd) / a;
        if (!range.surrounds(t)) return Optional.empty();

        var position = ray.at(t);
        var normal = position.minus(center).div(radius);
        var frontFace = normal.times(ray.direction()) < 0;

        double u;
        double v;
        if (material.texture().isUVRequired()) {
            var theta = Math.acos(-normal.y());
            var phi = Math.atan2(-normal.z(), normal.x()) + Math.PI;
            u = phi / (2 * Math.PI);
            v = theta / Math.PI;
        } else {
            u = Double.NaN;
            v = Double.NaN;
        }

        return Optional.of(new HitResult(
                t, position, frontFace ? normal : normal.neg(),
                material, u, v, frontFace
        ));
    }

    @Override
    public @NotNull AABB getBoundingBox() {
        return bbox;
    }
}
