package xyz.marsavic.gfxlab.graphics3d.solids;

import xyz.marsavic.functions.interfaces.F1;
import xyz.marsavic.geometry.Vector;
import xyz.marsavic.gfxlab.Vec3;
import xyz.marsavic.gfxlab.graphics3d.Hit;
import xyz.marsavic.gfxlab.graphics3d.Material;
import xyz.marsavic.gfxlab.graphics3d.Ray;
import xyz.marsavic.gfxlab.graphics3d.Solid;

public class Parallelepiped implements Solid {

    private final Vec3 p; // starting vertex
    private final Vec3[][][] vertices;
    HalfSpace [] sides;
    private final F1<Material, Vector> mapMaterial;

    private final Vec3 a, b, c;

    private Parallelepiped(Vec3 point, Vec3 aEdge, Vec3 bEdge, Vec3 cEdge, F1<Material, Vector> mapMaterial) {
        this.mapMaterial = mapMaterial;

        this.p = point;
        this.a = aEdge;
        this.b = bEdge;
        this.c = cEdge;

        vertices = new Vec3[][][]
                {{{p,       p.add(c)},        {p.add(b),        p.add(b).add(c)}},
                        {{p.add(a), p.add(a).add(c)}, {p.add(a).add(b), p.add(a).add(b).add(c)}}};
        sides = new HalfSpace[6];
        sides[0] = HalfSpace.pef(p, b, a, mapMaterial);
        sides[1] = HalfSpace.pef(p.add(c), a, b, mapMaterial);
        sides[2] = HalfSpace.pef(p, a, c, mapMaterial);
        sides[3] = HalfSpace.pef(p.add(b), c, a, mapMaterial);
        sides[4] = HalfSpace.pef(p, c, b, mapMaterial);
        sides[5] = HalfSpace.pef(p.add(a), b, c, mapMaterial);
    }


    public static Parallelepiped pabc(Vec3 p, Vec3 a, Vec3 b, Vec3 c, F1<Material, Vector> mapMaterial) {
        return new Parallelepiped(p, a, b, c, mapMaterial);
    }

    public static Parallelepiped cabc(Vec3 center, Vec3 a, Vec3 b, Vec3 c, F1<Material, Vector> mapMaterial) {
        return new Parallelepiped(center.sub(a.div(2)).sub(b.div(2)).sub(c.div(2)),
                a, b, c, mapMaterial);
    }

    public static Parallelepiped pxyz(Vec3 p, double x, double y, double z, F1<Material, Vector> mapMaterial) {
        return new Parallelepiped(p,
                Vec3.xyz(x, 0, 0), Vec3.xyz(0, y, 0),  Vec3.xyz(0, 0, z),
                mapMaterial);
    }

    public static Parallelepiped cxyz(Vec3 center, double x, double y, double z, F1<Material, Vector> mapMaterial) {
        return new Parallelepiped(center.sub(Vec3.xyz(x/2, y/2, z/2)),
                Vec3.xyz(x, 0, 0), Vec3.xyz(0, y, 0),  Vec3.xyz(0, 0, z),
                mapMaterial);
    }

    public Vec3 p() {
        return p;
    }

    public Vec3 center() { return p.add(a.div(2)).add(b.div(2)).add(c.div(2)); }

    public Vec3 a() {
        return a;
    }

    public Vec3 b() {
        return b;
    }

    public Vec3 c() {
        return c;
    }

    public Vec3[][][] vertices() {
        return vertices.clone();
    }

    private static boolean pointOnParallelogram(Vec3 p, Vec3 e, Vec3 f) {
        // we solve p as a linear combination of a*e+b*f, then check if this combination is in
        // 0<=a<=1 and 0<=b<=1
        double D = e.lengthSquared() * f.lengthSquared() - e.dot(f)*e.dot(f);
        if (D == 0)
            return false;
        // System determinant
        double Da = p.dot(e)*f.lengthSquared() - p.dot(f)*e.dot(f);
        // a's determinant
        double Db = e.lengthSquared()*p.dot(f) - e.dot(f)*p.dot(e);
        double a = Da/D;
        // a's determinant
        double b = Db/D;
        // b's determinant
        return a >= 0 && a <= 1 && b >= 0 && b <= 1;
    }


    @Override
    public Hit firstHit(Ray ray, double afterTime) {
        Hit[] planeHits = new Hit[6];

        for (int i = 0; i < 6; i++) {
            planeHits[i] = sides[i].firstHit(ray, afterTime);
            if (planeHits[i] == null) {
                continue;
            }
            Vec3 rayPlaneIntersect = ray.at(planeHits[i].t());
            if (!pointOnParallelogram(rayPlaneIntersect.sub(sides[i].p()), sides[i].e(), sides[i].f())) {
                planeHits[i] = null;
            }
        }

        double minT = Hit.t(null); // Positive infinity
        int minI = 0;
        for (int i = 0; i < 6; i++) {
            if (Hit.t(planeHits[i]) < minT) {
                minT = Hit.t(planeHits[i]);
                minI = i;
            }
        }

        return planeHits[minI];
    }

}
