package eu.jonahbauer.raytracing.scene.hittable3d;

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.render.material.Material;
import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

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

public final class Box implements Hittable {
    private final @NotNull AABB box;
    private final @Nullable Material @NotNull[] materials;

    public Box(@NotNull Vec3 a, @NotNull Vec3 b, @NotNull Material material) {
        this(new AABB(a, b), material);
    }

    public Box(@NotNull AABB box, @NotNull Material material) {
        this(box, Objects.requireNonNull(material, "material"), material, material, material, material, material);
    }

    public Box(
            @NotNull AABB box,
            @Nullable Material top, @Nullable Material bottom,
            @Nullable Material left, @Nullable Material right,
            @Nullable Material front, @Nullable Material back
    ) {
        this.box = Objects.requireNonNull(box, "box");
        this.materials = new Material[] { left, bottom, back, right, top, front };
    }

    @Override
    public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
        // based on AABB#hit with additional detection of the side hit
        var origin = ray.origin();
        var direction = ray.direction();
        var invDirection = direction.inv();

        var tmin = AABB.intersect(box.min(), origin, invDirection);
        var tmax = AABB.intersect(box.max(), origin, invDirection);

        double tlmax = Double.NEGATIVE_INFINITY;
        double tumin = Double.POSITIVE_INFINITY;

        Side entry = null;
        Side exit = null;

        for (int i = 0; i < 3; i++) {
            if (direction.get(i) >= 0) {
                if (tmin[i] > tlmax) {
                    tlmax = tmin[i];
                    entry = Side.NEGATIVE[i];
                }
                if (tmax[i] < tumin) {
                    tumin = tmax[i];
                    exit = Side.POSITIVE[i];
                }
            } else {
                if (tmax[i] > tlmax) {
                    tlmax = tmax[i];
                    entry = Side.POSITIVE[i];
                }
                if (tmin[i] < tumin) {
                    tumin = tmin[i];
                    exit = Side.NEGATIVE[i];
                }
            }
        }

        if (tlmax < tumin && tumin >= range.min() && tlmax <= range.max()) {
            assert entry != null && exit != null;
            return hit0(tlmax, tumin, entry, exit, ray, range);
        } else {
            return Optional.empty();
        }
    }

    private @NotNull Optional<HitResult> hit0(double tmin, double tmax, @NotNull Side entry, @NotNull Side exit, @NotNull Ray ray, @NotNull Range range) {
        double t;
        Side side;
        boolean frontFace;
        Material material;
        Vec3 normal;
        if (range.surrounds(tmin) && materials[entry.ordinal()] != null) {
            t = tmin;
            side = entry;
            frontFace = true;
            material = materials[side.ordinal()];
            normal = side.normal;
        } else if (range.surrounds(tmax) && materials[exit.ordinal()] != null) {
            t = tmax;
            side = exit;
            frontFace = false;
            material = materials[side.ordinal()];
            normal = side.normal.neg();
        } else {
            return Optional.empty();
        }

        var position = ray.at(t);
        var uv = material.texture().isUVRequired();
        var u = uv ? side.getTextureU(box, position) : Double.NaN;
        var v = uv ? side.getTextureV(box, position) : Double.NaN;
        return Optional.of(new HitResult(t, position, normal, material, u, v, frontFace));
    }

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

    private enum Side {
        NEG_X(Vec3.UNIT_X.neg()),
        NEG_Y(Vec3.UNIT_Y.neg()),
        NEG_Z(Vec3.UNIT_Z.neg()),
        POS_X(Vec3.UNIT_X),
        POS_Y(Vec3.UNIT_Y),
        POS_Z(Vec3.UNIT_Z),
        ;

        private static final Side[] NEGATIVE = new Side[] {Side.NEG_X, Side.NEG_Y, Side.NEG_Z};
        private static final Side[] POSITIVE = new Side[] {Side.POS_X, Side.POS_Y, Side.POS_Z};

        private final @NotNull Vec3 normal;

        Side(@NotNull Vec3 normal) {
            this.normal = Objects.requireNonNull(normal, "normal");
        }

        /**
         * {@return the texture u coordinate for a position on this side of the box}
         */
        public double getTextureU(@NotNull AABB box, @NotNull Vec3 pos) {
            return switch (this) {
                case NEG_X -> (pos.z() - box.min().z()) / (box.max().z() - box.min().z());
                case POS_X -> (box.max().z() - pos.z()) / (box.max().z() - box.min().z());
                case NEG_Y, POS_Y, POS_Z -> (pos.x() - box.min().x()) / (box.max().x() - box.min().x());
                case NEG_Z -> (box.max().x() - pos.x()) / (box.max().x() - box.min().x());
            };
        }

        /**
         * {@return the texture v coordinate for a position on this side of the box}
         */
        public double getTextureV(@NotNull AABB box, @NotNull Vec3 pos) {
            return switch (this) {
                case NEG_X, POS_X, NEG_Z, POS_Z -> (pos.y() - box.min().y()) / (box.max().y() - box.min().y());
                case NEG_Y -> (pos.z() - box.min().z()) / (box.max().z() - box.min().z());
                case POS_Y -> (box.max().z() - pos.z()) / (box.max().z() - box.min().z());
            };
        }
    }
}
