package cz.fidentis.analyst.symmetry;

import cz.fidentis.analyst.mesh.core.MeshPoint;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.vecmath.Point3d;

/**
 * A 3D curve produced by a plane cutting a human face.
 * 
 * @author Radek Oslejsek
 */
public class CrossSectionCurve {
    
    private final List<List<Point3d>> segments = new ArrayList<>();
    private final List<Point3d> fpProjections = new ArrayList<>();
    
    /**
     * Add a new point to the beginning of the curve segment.
     * 
     * @param segment Index of the segment
     * @param point A 3D point to be inserted
     * @throws IllegalArgumentException if the segment does not exist or the point is {@code null}
     */
    public void addPointToSegmentStart(int segment, Point3d point) {
        if (segment < 0 || segment > segments.size() - 1) {
            throw new IllegalArgumentException("segment");
        }
        
        if (point == null) {
            throw new IllegalArgumentException("point");
        }
        
        segments.get(segment).add(0, point);
    }
    
    /**
     * Add a new point to the end of the curve segment.
     * 
     * @param segment Index of the segment
     * @param point A 3D point to be inserted
     * @throws IllegalArgumentException if the segment does not exist or the point is {@code null}
     */
    public void addPointToSegmentEnd(int segment, Point3d point) {
        if (segment < 0 || segment > segments.size() - 1) {
            throw new IllegalArgumentException("segment");
        }
        
        if (point == null) {
            throw new IllegalArgumentException("point");
        }
        
        segments.get(segment).add(point);
    }
    
    /**
     * Adds a new segment to the curve.
     * 
     * @return Index of the new segment
     */
    public int addNewSegment() {
        segments.add(new ArrayList<>());
        return segments.size() - 1;
    }
    
    /**
     * Returns number of segments.
     * @return number of segments.
     */
    public int getNumSegments() {
        return segments.size();
    }
    
    /**
     * Returns number of points in given segment.
     * 
     * @param segment Index of the segment
     * @return number of points in given segment.
     * @throws IllegalArgumentException if the segment does not exist
     */
    public int getSegmentSize(int segment) {
        if (segment < 0 || segment > segments.size() - 1) {
            throw new IllegalArgumentException("segment");
        }
        return segments.get(segment).size();
    }
    
    /**
     * Returns given segment.
     * 
     * @param segment Index of the segment
     * @return Points of the segment
     * @throws IllegalArgumentException if the segment does not exist
     */
    public List<Point3d> getSegment(int segment) {
        if (segment < 0 || segment > segments.size() - 1) {
            throw new IllegalArgumentException("segment");
        }
        return Collections.unmodifiableList(segments.get(segment));
    }
    
    /**
     * Returns all segments.
     * @return Curve segments
     */
    public List<List<Point3d>> getSegments() {
        return Collections.unmodifiableList(segments);
    }
    
    /**
     * Returns given point of in given segment.
     * 
     * @param segment Index of the segment
     * @param point Index of the point in the segment
     * @return Points of the curve
     * @throws IndexOutOfBoundsException if the input arguments are out of range
     */
    public Point3d getPoint(int segment, int point) {
        return segments.get(segment).get(point);
    }
    
    /**
     * Find the closest point of the curve to the given point.
     * Returned list contains two indexes. The first one is the index of 
     * curve segment, the second one is the index of the closest point in the segment.
     * 
     * @param point A 3D point
     * @return The closest point of the curve or {@code null}
     * @throws NullPointerException if the input argument is {@code null}
     */
    public Point3d getClosestPoint(Point3d point) {
        Point3d ret = null;
        double minDist = Double.POSITIVE_INFINITY;
        
        for (int i = 0; i < getNumSegments(); i++) {
            for (int j = 0; j < getSegmentSize(i); j++) {
                double dist = MeshPoint.distance(point, getPoint(i, j));
                if (dist < minDist) {
                    minDist = dist;
                    ret = getPoint(i, j);
                }
            }
        }
        
        return ret;
    }
    
    /**
     * Finds points of the curve that are the closest the the given list of 3D points
     * (locations of feature points) and returns them.
     * 
     * @param candidatePoints (Feature) points to be projected to the curve
     * @return projections, i.e., points of the curve that are the closest to the candidate points.
     */
    public List<Point3d> projectFeaturePointsToCurve(List<Point3d> candidatePoints) {
        this.fpProjections.clear();
        for (Point3d p: candidatePoints) {
            this.fpProjections.add(getClosestPoint(p));
        }
        return Collections.unmodifiableList(fpProjections);
    }
    
    /**
     * Returns projected feature points.
     * @return projected feature points.
     */
    public List<Point3d> getProjectedFeaturePoints() {
        return Collections.unmodifiableList(fpProjections);
    }
    
    /**
     * Finds the closest point (see {@link #getClosestPoint(javax.vecmath.Point3d)}.
     * If the point is inside of some segment, the segment is split. 
     * 
     * @param point A 3D point whose closeness is calculated
     * @return The index of the original segment that has been split. -1 if no slitting has been made.
     * @throws NullPointerException if the input argument is {@code null}
     */
    /*
    public int splitByClosestPoint(Point3d point) {
        List<Integer> closest = getClosestPoint(point);
        int clSegment = closest.get(0);
        int clPoint = closest.get(1);
        
        if (closest == null) {
            return -1;
        }
        
        if (clPoint == 0 || clPoint == getSegmentSize(clSegment)-1) { // bordery points
            return -1;
        }
        
        List<Point3d> newSegment = new ArrayList<>();
        newSegment.add(new Point3d(getPoint(clSegment, clPoint)));
        int i = clPoint + 1;
        while (i < getSegmentSize(closest.get(0))) {
            newSegment.add(segments.get(clSegment).remove(i));
        }
        segments.add(clSegment+1, newSegment);
        
        return clSegment;
    }
    */
    
    /**
     * Joints neighboring segments. Independent segments (separated by a whole)
     * remain separated.
     */
    public void joinSegments() {
        for (int i = 0; i < getNumSegments() -1 ; i++) {
            if (getPoint(i, getSegmentSize(i)-1).equals(getPoint(i+1, 0))) {
                segments.get(i+1).remove(0);
                segments.get(i).addAll(segments.remove(i+1));
                i--;
            }
        }
    }
}
