package eu.jonahbauer.raytracing.render.camera;

import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Objects;
import java.util.random.RandomGenerator;

public final class SimpleCamera implements Camera {
    // image size
    private final int width;
    private final int height;

    // camera position and orientation
    private final @NotNull Vec3 origin;

    // rendering
    private final double blurRadius;

    // internal properties
    private final @NotNull Vec3 u;
    private final @NotNull Vec3 v;

    private final @NotNull Vec3 pixelU;
    private final @NotNull Vec3 pixelV;
    private final @NotNull Vec3 pixel00;

    public static @NotNull Builder builder() {
        return new Builder();
    }

    public static @NotNull Camera withDefaults() {
        return new Builder().build();
    }

    private SimpleCamera(@NotNull Builder builder) {
        this.width = builder.imageWidth;
        this.height = builder.imageHeight;

        var viewportHeight = 2 * Math.tan(0.5 * builder.fov) * builder.focusDistance;
        var viewportWidth = viewportHeight * ((double) width / height);

        this.origin = builder.position;
        var direction = (builder.direction == null ? builder.target.minus(builder.position).unit() : builder.direction);

        this.blurRadius = Math.tan(0.5 * builder.blurAngle) * builder.focusDistance;

        // project direction the horizontal plane
        var dXZ = direction.withY(0).unit();
        this.u = Vec3.rotate(
                new Vec3(- dXZ.z(), 0, dXZ.x()), // perpendicular to d in horizontal plane
                direction, builder.rotation
        );
        this.v = direction.cross(u);  // perpendicular to viewportU and direction

        this.pixelU = u.times(viewportWidth / width);
        this.pixelV = v.times(viewportHeight / height);

        this.pixel00 = origin.plus(direction.times(builder.focusDistance))
                .minus(u.times(0.5 * viewportWidth)).minus(v.times(0.5 * viewportHeight))
                .plus(pixelU.div(2)).plus(pixelV.div(2));
    }

    /**
     * {@inheritDoc}
     */
    public int getWidth() {
        return width;
    }

    /**
     * {@inheritDoc}
     */
    public int getHeight() {
        return height;
    }

    /**
     * {@inheritDoc}
     */
    public @NotNull Ray cast(int x, int y, @NotNull RandomGenerator random) {
        Objects.checkIndex(x, width);
        Objects.checkIndex(y, height);

        var origin = getRayOrigin(random);
        var target = getRayTarget(x, y, random);
        return new Ray(origin, target.minus(origin));
    }

    /**
     * {@return the origin for a ray cast by this camera} The ray origin is randomized within a disk of
     * radius {@link #blurRadius} centered on the camera position and perpendicular to the direction to simulate depth
     * of field.
     */
    private @NotNull Vec3 getRayOrigin(@NotNull RandomGenerator random) {
        if (blurRadius <= 0) return origin;

        while (true) {
            var du = 2 * random.nextDouble() - 1;
            var dv = 2 * random.nextDouble() - 1;
            if (du * du + dv * dv >= 1) continue;

            var ru = blurRadius * du;
            var rv = blurRadius * dv;

            return new Vec3(
                    origin.x() + ru * u.x() + rv * v.x(),
                    origin.y() + ru * u.y() + rv * v.y(),
                    origin.z() + ru * u.z() + rv * v.z()
            );
        }
    }

    /**
     * {@return the target vector for a ray through the given pixel} The position is randomized within the pixel.
     */
    private @NotNull Vec3 getRayTarget(int x, int y, @NotNull RandomGenerator random) {
        double dx = x + random.nextDouble() - 0.5;
        double dy = y + random.nextDouble() - 0.5;
        return pixel00.plus(pixelU.times(dx)).plus(pixelV.times(dy));
    }

    public static class Builder {
        private int imageWidth = 1920;
        private int imageHeight = 1080;

        private @NotNull Vec3 position = Vec3.ZERO;
        private @Nullable Vec3 direction = Vec3.UNIT_Z.neg();
        private @Nullable Vec3 target = null;
        private double rotation = 0.0;

        private double fov = 0.5 * Math.PI;
        private double focusDistance = 10;
        private double blurAngle = 0.0;


        private Builder() {}

        public @NotNull Builder withImage(int width, int height) {
            if (width <= 0 || height <= 0) throw new IllegalArgumentException("width and height must be positive");
            this.imageWidth = width;
            this.imageHeight = height;
            return this;
        }

        public @NotNull Builder withPosition(@NotNull Vec3 position) {
            this.position = Objects.requireNonNull(position);
            return this;
        }

        public @NotNull Builder withDirection(@NotNull Vec3 direction) {
            this.direction = Objects.requireNonNull(direction).unit();
            this.target = null;
            return this;
        }

        public @NotNull Builder withTarget(@NotNull Vec3 target) {
            this.target = Objects.requireNonNull(target);
            this.direction = null;
            return this;
        }

        public @NotNull Builder withRotation(double rotation) {
            if (!Double.isFinite(rotation)) throw new IllegalArgumentException("rotation must be finite");
            this.rotation = rotation;
            return this;
        }

        public @NotNull Builder withFieldOfView(double fov) {
            if (fov <= 0 || fov >= Math.PI || !Double.isFinite(fov)) throw new IllegalArgumentException("fov must be in the range (0, π)");
            this.fov = fov;
            return this;
        }

        public @NotNull Builder withFocusDistance(double focusDistance) {
            if (focusDistance <= 0 || !Double.isFinite(focusDistance)) throw new IllegalArgumentException("focus distance must be positive");
            this.focusDistance = focusDistance;
            return this;
        }

        public @NotNull Builder withBlurAngle(double angle) {
            if (angle < 0 || angle >= Math.PI || !Double.isFinite(angle)) throw new IllegalArgumentException("blur-angle must be in the range [0, π)");
            this.blurAngle = angle;
            return this;
        }

        public @NotNull SimpleCamera build() {
            return new SimpleCamera(this);
        }

    }
}
