package raytracing;

import basics.math.algebra.Ray;
import basics.math.algebra.Vector;
import geometry.scene.Hit;
import geometry.scene.Scene;
import optics.light.Color;
import optics.light.LightSource;
import optics.view.Camera;
import optics.view.PinholeCamera;
import renderer.Renderer;

import java.util.Optional;

public class Raytracer implements Renderer {

    private final Camera camera = new PinholeCamera(new Vector(0,0,-4), Vector.origin(), 90, 1e-3, 1e3);
    private final Scene scene = Scene.generateRaytracingExampleScene();

    private double traceShadow(Vector point, Vector direction, double distance) {
        Ray shadowRay = new Ray(point, direction, 1e-3, distance);

        Optional<Hit> result = scene.intersect(shadowRay);

        if (result.isEmpty()) {
            return 1.0;
        }

        return 0.0;
    }

    private Color traceDirect(Ray directRay, int depth) {
        if (depth == 8) {
            return Color.BLACK;
        }

        Optional<Hit> result = scene.intersect(directRay);

        if (result.isEmpty()) {
            // Background color
            return Color.BLACK;
        }

        Vector intersection = directRay.travel(result.get().getDistance());
        Vector normal = result.get().getMesh().normalAt(intersection, directRay.getDirection());

        BasicMaterial material = (BasicMaterial) result.get().getMesh().getMaterial();

        // indirect light sum
        Color incoming = new Color(0,0,0);

        if (material.refracts()) {
            var reflection = reflectedLight(directRay, material, normal, intersection, depth);
            var refraction = refractedLight(directRay, material, normal, intersection, depth);

            var fac = -directRay.getDirection().dot(normal);

            return refraction.lerp(reflection, Math.pow(fac, 0.5));
        }

        Color reflectedLight = reflectedLight(directRay, material, normal, intersection, depth);

        for (LightSource light : scene.getLights()) {
            double directInfluence = castShadow(light, intersection, normal);

            Color directLight = material.getDiffuse().scale(directInfluence).mul(light.getColor());

            incoming = incoming.add(directLight);
        }

        return reflectedLight.lerp(incoming, material.getGlossieness());
    }

    private Color reflectedLight(Ray incomingRay, BasicMaterial material, Vector normal, Vector point, int depth) {
        Vector reflected = incomingRay.getDirection().reflect(normal);

        Color reflectedLight = traceDirect(new Ray(point, reflected, 1e-3, incomingRay.getFar()), depth + 1);

        return reflectedLight.mul(material.getReflectionTint());
    }

    private Color refractedLight(Ray incomingRay, BasicMaterial material, Vector normal, Vector point, int depth) {
        Vector refracted = incomingRay.getDirection().refract(normal, incomingRay.getDirection(), 1.45);

        return traceDirect(new Ray(point, refracted, 1e-3, incomingRay.getFar()), depth + 1);
    }

    private double castShadow(LightSource target, Vector point, Vector surfaceNormal) {
        Vector lightDirection = target.getDirection(point);
        double distance = target.getDistance(point);
        double shadow = traceShadow(point.add(surfaceNormal.scale(1e-3)), lightDirection, distance);
        return Math.max(surfaceNormal.dot(lightDirection), 0.0) * shadow;
    }

    @Override
    public Color traceCameraRay(double u, double v) {
        Ray viewRay = camera.generateViewRay(u, v);

        return traceDirect(viewRay, 0);
    }

    @Override
    public Camera getCamera() {
        return camera;
    }
}
