package cz.fidentis.analyst.symmetry;

import cz.fidentis.analyst.mesh.core.MeshRectangleFacet;
import cz.fidentis.analyst.visitors.mesh.BoundingBox.BBox;
import java.io.Serializable;
import java.util.List;
import javax.vecmath.Point3d;
import javax.vecmath.Vector3d;

/**
 * Symmetry plane.
 * 
 * @author Natalia Bebjakova
 * @author Dominik Racek
 */
public class Plane implements Serializable {
   
    private Vector3d normal;
    private double   distance;
    private double   normSquare; // normal.dot(normal)

    /**
     * Constructor.
     * 
     * @param normal Normalized (!) normal vector of the plane
     * @param dist distance
     * @throws IllegalArgumentException if the @code{plane} argument is null
     */
    public Plane(Vector3d normal, double dist) {
        setNormal(new Vector3d(normal));
        setDistance(dist);
    }
    
    /**
     * Copy constructor.
     * @param plane original plane
     * @throws IllegalArgumentException if the @code{plane} argument is null
     */
    public Plane(Plane plane) {
        this(plane.getNormal(), plane.getDistance());
    }
    
    /**
     * Creates average plane from existing planes.
     * 
     * @param planes Source planes
     * @throws IllegalArgumentException if the {@code planes} list is {@code null} or empty
     */
    public Plane(List<Plane> planes) {
        if (planes == null || planes.isEmpty()) {
            throw new IllegalArgumentException("planes");
        }
        
        Vector3d n = new Vector3d();
        double d = 0;
        Vector3d refDir = planes.get(0).getNormal();
        for (int i = 0; i < planes.size(); i++) {
            Vector3d normDir = planes.get(i).getNormal();
            if (normDir.dot(refDir) < 0) {
                n.sub(normDir);
                d -= planes.get(i).getDistance();
            } else {
                n.add(normDir);
                d += planes.get(i).getDistance();
            }
        }
        
        setNormal(n);
        setDistance(d);
        normalize();
    }
    
    protected Plane() {
    }
    
    /**
     * Normalize the plane
     */
    protected final void normalize() {
        double normalLength = normal.length();
        normal.normalize();
        distance /= normalLength; // Do we really want this? --ro
        this.normSquare = normal.dot(normal);
    }
    
    /**
     * Returns string description of the plane
     * 
     * @return description of the plane
     */
    @Override
    public String toString(){
        return "APPROXIMATE PLANE:" + System.lineSeparator() + 
                normal.x + System.lineSeparator() + 
                normal.y + System.lineSeparator() + 
                normal.z + System.lineSeparator() + 
                distance + System.lineSeparator();
    }
    
    public Vector3d getNormal() {
        return new Vector3d(normal);
    }
    
    public double getDistance() {
        return distance;
    }

    /**
     * Translate the plane along its normal
     *
     * @param value
     */
    public void translate(double value) {
        this.distance += value;
    }
    
    /**
     * Returns a point laying on the plane.
     * 
     * @param point A 3D point, which is projected to the plane
     * @return a point laying on the plane.
     * @throws NullPointerException if the {@code point} is {@code null}
     */
    public Point3d projectToPlane(Point3d point) {
        double shiftDist = 
                ((normal.x * point.x) + 
                  (normal.y * point.y) + 
                  (normal.z * point.z) +
                  distance) / normSquare;
        
        Point3d ret = new Point3d(normal);
        ret.scale(-shiftDist);
        ret.add(point);
        return ret;
    }
    
    /**
     * Returns a point laying on the opposite side of the plane ("mirrors" the point).
     * This method implements equation (1) of 
     * <a href="https://link.springer.com/content/pdf/10.1007/s00371-020-02034-w.pdf">Hruda et al: Robust, fast and flexible symmetry plane detection based
on differentiable symmetry measure</a>
     * 
     * @param point A 3D point to be reflected
     * @return a point on the opposite side of the plane.
     * @throws NullPointerException if the {@code point} is {@code null}
     */
    public Point3d reflectOverPlane(Point3d point) {
        double shiftDist = 
                ((normal.x * point.x) + 
                  (normal.y * point.y) + 
                  (normal.z * point.z) +
                  distance) / normSquare;
        
        Point3d ret = new Point3d(normal);
        ret.scale(-2.0 * shiftDist);
        ret.add(point);
        return ret;
    }
    
    /**
     * Returns rectangular mesh facet of this symmetry plane.
     * 
     * @param point A 3D point, which is projected to the plane and used as a center of the rectangular facet
     * @param width Width
     * @param height Height
     * @return a rectangular mesh facet of this symmetry plane
     * @throws NullPointerException if the {@code point} is {@code null}
     * @throws IllegalArgumentException if {@code width} or {@code height} are &lt;= 0
     */
    public MeshRectangleFacet getMesh(Point3d point, double width, double height) {
        return new MeshRectangleFacet(projectToPlane(point), normal, width, height);
    }
    
    /**
     * Returns rectangular mesh facet of this symmetry plane.
     * 
     * @param point A 3D point, which is projected to the plane and used as a center of the rectangular facet
     * @param size Edge length
     * @return a rectangular mesh facet of this symmetry plane
     * @throws NullPointerException if the {@code point} is {@code null}
     * @throws IllegalArgumentException if {@code size} is &lt;= 0
     */
    public MeshRectangleFacet getMesh(Point3d point, double size) {
        return Plane.this.getMesh(point, size, size);
    }

    /**
     * Returns rectangular mesh facet of this symmetry plane. The centroid and size
     * are estimated from bounding box.
     * 
     * @param bbox Bounding box
     * @return a rectangular mesh facet of this symmetry plane
     * @throws NullPointerException if the {@code midPoint} or {@code bbox} are {@code null}
     */
    public MeshRectangleFacet getMesh(BBox bbox) {
        return Plane.this.getMesh(bbox.getMidPoint(), bbox.getDiagonalLength(), bbox.getDiagonalLength());
    }

    protected final void setNormal(Vector3d normal) {
        this.normal = normal;
        this.normSquare = normal.dot(normal);
    }
    
    protected final void setDistance(double dist) {
        this.distance = dist;
    }
    
    /**
     * Returns distance of the point from the plane.
     * @param point Point whose distance is to be computed
     * @return Point's distance. If the point is on the opposite side with
     * the respect to the plane's normal, the negative distance is returned
     */
    public double getPointDistance(Point3d point) {
        return ((normal.x * point.x) + (normal.y * point.y) + (normal.z * point.z) + distance)
                / Math.sqrt(normSquare);
    }

    /**
     * Calculates an intersection of a plane and a line given by two points
     *
     * @param p1 first point of the line
     * @param p2 second point of the line
     * @return The point of intersection of null if no point found
     */
    public Point3d getIntersectionWithLine(Point3d p1, Point3d p2) {
        double distance1 = getPointDistance(p1);
        double distance2 = getPointDistance(p2);
        double t = distance1 / (distance1 - distance2);

        if (distance1 * distance2 > 0) {
            return null;
        }

        Point3d output = new Point3d(p2);
        output.sub(p1);
        output.scale(t);
        output.add(p1);

        return output;
    }
}
