package org.sunflow.core.camera;

import org.sunflow.SunflowAPI;
import org.sunflow.core.CameraLens;
import org.sunflow.core.ParameterList;
import org.sunflow.core.Ray;

public class ThinLens implements CameraLens {
    private float au, av;
    private float aspect, fov;
    private float shiftX, shiftY;
    private float focusDistance;
    private float lensRadius;
    private int lensSides;
    private float lensRotation;
    private float lensRotationRadians;

    public ThinLens() {
        focusDistance = 1;
        lensRadius = 0;
        fov = 90;
        aspect = 1;
        lensSides = 0; // < 3 means use circular lens
        lensRotation = lensRotationRadians = 0; // this rotates polygonal lenses
    }

    public boolean update(ParameterList pl, SunflowAPI api) {
        // get parameters
        fov = pl.getFloat("fov", fov);
        aspect = pl.getFloat("aspect", aspect);
        shiftX = pl.getFloat("shift.x", shiftX);
        shiftY = pl.getFloat("shift.y", shiftY);
        focusDistance = pl.getFloat("focus.distance", focusDistance);
        lensRadius = pl.getFloat("lens.radius", lensRadius);
        lensSides = pl.getInt("lens.sides", lensSides);
        lensRotation = pl.getFloat("lens.rotation", lensRotation);
        update();
        return true;
    }

    private void update() {
        au = (float) Math.tan(Math.toRadians(fov * 0.5f)) * focusDistance;
        av = au / aspect;
        lensRotationRadians = (float) Math.toRadians(lensRotation);
    }

    public Ray getRay(float x, float y, int imageWidth, int imageHeight, double lensX, double lensY, double time) {
        float du = shiftX * focusDistance - au + ((2.0f * au * x) / (imageWidth - 1.0f));
        float dv = shiftY * focusDistance - av + ((2.0f * av * y) / (imageHeight - 1.0f));

        float eyeX, eyeY;
        if (lensSides < 3) {
            double angle, r;
            // concentric map sampling
            double r1 = 2 * lensX - 1;
            double r2 = 2 * lensY - 1;
            if (r1 > -r2) {
                if (r1 > r2) {
                    r = r1;
                    angle = 0.25 * Math.PI * r2 / r1;
                } else {
                    r = r2;
                    angle = 0.25 * Math.PI * (2 - r1 / r2);
                }
            } else {
                if (r1 < r2) {
                    r = -r1;
                    angle = 0.25 * Math.PI * (4 + r2 / r1);
                } else {
                    r = -r2;
                    if (r2 != 0)
                        angle = 0.25 * Math.PI * (6 - r1 / r2);
                    else
                        angle = 0;
                }
            }
            r *= lensRadius;
            // point on the lens
            eyeX = (float) (Math.cos(angle) * r);
            eyeY = (float) (Math.sin(angle) * r);
        } else {
            // sample N-gon
            // FIXME: this could use concentric sampling
            lensY *= lensSides;
            float side = (int) lensY;
            float offs = (float) lensY - side;
            float dist = (float) Math.sqrt(lensX);
            float a0 = (float) (side * Math.PI * 2.0f / lensSides + lensRotationRadians);
            float a1 = (float) ((side + 1.0f) * Math.PI * 2.0f / lensSides + lensRotationRadians);
            eyeX = (float) ((Math.cos(a0) * (1.0f - offs) + Math.cos(a1) * offs) * dist);
            eyeY = (float) ((Math.sin(a0) * (1.0f - offs) + Math.sin(a1) * offs) * dist);
            eyeX *= lensRadius;
            eyeY *= lensRadius;
        }
        float eyeZ = 0;
        // point on the image plane
        float dirX = du;
        float dirY = dv;
        float dirZ = -focusDistance;
        // ray
        return new Ray(eyeX, eyeY, eyeZ, dirX - eyeX, dirY - eyeY, dirZ - eyeZ);
    }
}