package cz.fidentis.analyst.symmetry;

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;

    /**
     * Creates new plane.
     * 
     * @param a a coordinate 
     * @param b b coordinate
     * @param c c coordinate
     * @param d d coordinate
     */
    public Plane(double a, double b, double c, double d) {
        normal = new Vector3d(a,b,c);
        distance = d;
    }
    
    /**
     * Constructor.
     * @param normal Normal vector of the plane
     * @param dist distance
     * @throws IllegalArgumentExpcption if the @code{plane} argument is null
     */
    public Plane(Vector3d normal, double dist) {
        this(normal.x, normal.y, normal.z, dist);
    }
    
    /**
     * Copy constructor.
     * @param plane original plane
     * @throws IllegalArgumentExpcption 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 {@code IllegalArgumentException} 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
     */
    public void normalize() {
        double normalLength = normal.length();
        normal.normalize();
        distance /= normalLength; // Do we really want this? --ro
    }
    
    /**
     * 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;
    }
    
    protected void setNormal(Vector3d normal) {
        this.normal = normal;
    }
    
    protected void setDistance(double dist) {
        this.distance = dist;
    }

    /**
     * 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 = ((normal.x * p1.x) + (normal.y * p1.y) + (normal.z * p1.z) + distance)
                / Math.sqrt(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z);

        double distance2 = ((normal.x * p2.x) + (normal.y * p2.y) + (normal.z * p2.z) + distance)
                / Math.sqrt(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z);

        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;
    }
}
