package cz.fidentis.analyst.visitors.mesh;

import cz.fidentis.analyst.mesh.MeshVisitor;
import cz.fidentis.analyst.mesh.core.MeshFacet;
import cz.fidentis.analyst.mesh.core.MeshTriangle;
import cz.fidentis.analyst.symmetry.CrossSectionCurve;
import cz.fidentis.analyst.symmetry.Plane;

import javax.vecmath.Point3d;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;


/**
 * A visitor that calculates the cross-section of a face and a cutting plane using the edges between triangles.
 * <p>
 * This visitor is thread-safe.
 * </p>
 *
 * @author Dominik Racek
 * @author Radek Oslejsek
 */
public class CrossSectionZigZag extends MeshVisitor {
    private final CrossSectionCurve curve;
    private final Set<Point3d> usedPoints;
    private Set<MeshTriangle> visited;
    private Set<MeshTriangle> toVisit;

    private final Plane plane;

    /**
     * Constructor for CrossSectionZigZag visitor
     *
     * @param plane cutting plane
     */
    public CrossSectionZigZag(Plane plane) {
        this.plane = plane;
        this.curve = new CrossSectionCurve();
        this.usedPoints = new HashSet<>();
    }

    private void addPoint(Point3d p, boolean direction, int segment) {
        if (!usedPoints.contains(p)) {
            usedPoints.add(p);

            if (direction) {
                curve.addPointToSegmentEnd(segment, p);
            } else {
                curve.addPointToSegmentStart(segment, p);
            }
        }
    }

    private void visitMeshFacetRec(MeshFacet facet, int p1, int p2, MeshTriangle previous, boolean direction, int segment) {
        List<MeshTriangle> triangles = facet.getAdjacentTriangles(p1, p2);
        triangles.remove(previous);
        if (triangles.isEmpty()) {
            return;
        }

        //Find the next intersected edge
        MeshTriangle current = triangles.get(0);
        toVisit.remove(current);

        if (visited.contains(current)) {
            return;
        }
        visited.add(current);

        //Find the index of the last vertex of current (first ones being p1 and p2)
        int p3;
        if (current.index1 != p1 && current.index1 != p2) {
            p3 = current.index1;
        } else if (current.index2 != p1 && current.index2 != p2) {
            p3 = current.index2;
        } else {
            p3 = current.index3;
        }

        //Next intersected edge will be between p1-p3 or p2-p3
        Point3d intersection = plane.getIntersectionWithLine(facet.getVertex(p1).getPosition(),
                facet.getVertex(p3).getPosition());

        if (intersection == null) {
            intersection = plane.getIntersectionWithLine(facet.getVertex(p2).getPosition(),
                    facet.getVertex(p3).getPosition());

            addPoint(intersection, direction, segment);
            visitMeshFacetRec(facet, p2, p3, current, direction, segment);
        } else {
            addPoint(intersection, direction, segment);
            visitMeshFacetRec(facet, p1, p3, current, direction, segment);
        }

    }

    @Override
    public void visitMeshFacet(MeshFacet facet) {
        //Logger out = Logger.measureTime();

        // Find all candidates
        visited = new HashSet<>();
        toVisit = facet.getTriangles().parallelStream()
                .filter(t -> t.checkIntersectionWithPlane(plane.getNormal(), plane.getDistance()))
                .collect(Collectors.toSet());
        
        synchronized (this) {
            while (!toVisit.isEmpty()) {
                MeshTriangle tri = toVisit.iterator().next();
                toVisit.remove(tri);
                int segment = curve.addNewSegment();
                
                // Figure out which lines are intersected
                Point3d intersection1 = plane.getIntersectionWithLine(tri.getVertex1(), tri.getVertex2());
                Point3d intersection2 = plane.getIntersectionWithLine(tri.getVertex2(), tri.getVertex3());
                Point3d intersection3 = plane.getIntersectionWithLine(tri.getVertex3(), tri.getVertex1());
                
                if (intersection1 != null) {
                    //Intersection 1
                    addPoint(intersection1, true, segment);
                    visitMeshFacetRec(facet, tri.index1, tri.index2, tri, true, segment);
                    if (intersection2 != null) {
                        //Intersection 1 and 2
                        addPoint(intersection2, false, segment);
                        visitMeshFacetRec(facet, tri.index2, tri.index3, tri, false, segment);
                    } else if (intersection3 != null) {
                        //Intersection 1 and 3
                        addPoint(intersection3, false, segment);
                        visitMeshFacetRec(facet, tri.index3, tri.index1, tri, false, segment);
                    }
                } else if (intersection2 != null) {
                    //Intersection 2
                    addPoint(intersection2, true, segment);
                    visitMeshFacetRec(facet, tri.index2, tri.index3, tri, true, segment);
                    if (intersection3 != null) {
                        //Intersection 2 and 3
                        addPoint(intersection3, false, segment);
                        visitMeshFacetRec(facet, tri.index3, tri.index1, tri, false, segment);
                    }

                } else if (intersection3 != null) {
                    //Intersection 3 only
                    addPoint(intersection3, true, segment);
                    visitMeshFacetRec(facet, tri.index3, tri.index1, tri, true, segment);
                }
                //No intersection
            }
        }

        //out.printDuration("Cross section with zigzag method");
    }

    /**
     * Returns computed cross section curve.
     * @return cross section curve
     */
    public CrossSectionCurve getCrossSectionCurve() {
        return curve;
    }
}
