package cz.fidentis.analyst.visitors.mesh;

import cz.fidentis.analyst.Logger;
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.Plane;

import javax.vecmath.Point3d;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * A visitor that calculates the cross-section of a face and a cutting plane using the triangles.
 * <p>
 * This visitor is thread-safe.
 * </p>
 *
 * @author Dominik Racek
 */
public class CrossSection extends MeshVisitor {
    private List<Point3d> points;
    private Set<Point3d> usedPoints;
    private Plane plane;

    private Set<MeshTriangle> visited;

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

    private List<MeshTriangle> getValidNeighboringTriangles(MeshFacet facet, MeshTriangle tri) {
        List<MeshTriangle> output = facet.getNeighboringTriangles(tri);
        output.removeIf(n -> ((visited.contains(n))
                || (!n.checkIntersectionWithPlane(plane.getNormal(), plane.getDistance()))));
        return output;
    }

    private List<MeshTriangle> getValidAdjacentTriangles(MeshFacet facet, MeshTriangle tri) {
        List<MeshTriangle> output = facet.getAdjacentTriangles(tri);
        output.removeIf(n -> ((visited.contains(n))
                || (!n.checkIntersectionWithPlane(plane.getNormal(), plane.getDistance()))));
        return output;
    }

    private void addPoint(MeshTriangle tri, MeshTriangle previous, boolean direction) {
        List<Point3d> commonEdge = tri.getCommonPoints(previous);
        if (commonEdge.size() == 2) {
            Point3d p = plane.getIntersectionWithLine(commonEdge.get(0), commonEdge.get(1));
            if (p == null) {
                return;
            }

            if (!usedPoints.contains(p)) {
                usedPoints.add(p);

                if (direction) {
                    points.add(p);
                } else {
                    points.add(0, p);
                }
            }
        }
    }

    private void visitMeshFacetRec(MeshFacet facet, MeshTriangle tri, MeshTriangle previous, boolean direction) {
        visited.add(tri);

        // Depending on the direction, add the point to the start or to the end of points
        addPoint(tri, previous, direction);

        // Recursively call to the end
        // Get all neighbors that have not been visited and are intercepted
        List<MeshTriangle> neighbors = getValidNeighboringTriangles(facet, tri);

        // If no valid neighbours are found, try to look further
        if (neighbors.size() == 0) {
            neighbors = getValidAdjacentTriangles(facet, tri);
        }

        if (neighbors.size() == 1) {
            //Only one neighbor - go there
            visitMeshFacetRec(facet, neighbors.get(0), tri, direction);
        } else if (neighbors.size() == 2) {
            //Two neighbors - they are valid to each other, but one of them has another valid neighbor.
            //                Select the one that only has the other as a neighbor so the route is linear.
            List<MeshTriangle> valid = getValidNeighboringTriangles(facet, neighbors.get(0));

            if (valid.size() == 1) {
                visitMeshFacetRec(facet, neighbors.get(0), tri, direction);
            } else {
                visitMeshFacetRec(facet, neighbors.get(1), tri, direction);
            }
        }
    }

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

        this.visited = new HashSet<>();

        synchronized (this) {

            // Find the first triangle
            MeshTriangle first = null;
            for (MeshTriangle tri : facet) {
                if (tri.checkIntersectionWithPlane(plane.getNormal(), plane.getDistance())) {
                    first = tri;
                    break;
                }
            }

            if (first == null) {
                return;
            }

            // Set found and visited and add to points
            visited.add(first);

            // Find the valid neighboring triangles and act based on the amount
            List<MeshTriangle> neighbors = getValidNeighboringTriangles(facet, first);

            if (neighbors.size() == 1) {
                //Only one neighbor - go there
                visitMeshFacetRec(facet, neighbors.get(0), first, true);
            } else if (neighbors.size() == 2) {
                //Two neighbors - one on each side
                visitMeshFacetRec(facet, neighbors.get(0), first, true);
                visitMeshFacetRec(facet, neighbors.get(1), first, false);
            } else if (neighbors.size() == 3) {
                //Three neighbors - select the two with only one valid neighbor
                List<MeshTriangle> valid = getValidNeighboringTriangles(facet, neighbors.get(0));
                if (valid.size() == 1) {
                    //Index 0 is one of the correct ones, find the other
                    visitMeshFacetRec(facet, neighbors.get(0), first, true);
                    List<MeshTriangle> valid1 = getValidNeighboringTriangles(facet, neighbors.get(1));

                    if (valid1.size() == 1) {
                        visitMeshFacetRec(facet, neighbors.get(1), first, false);
                    } else {
                        visitMeshFacetRec(facet, neighbors.get(2), first, false);
                    }
                } else {
                    //Index 0 is not one of the correct ones, 1 and 2 are
                    visitMeshFacetRec(facet, neighbors.get(1), first, true);
                    visitMeshFacetRec(facet, neighbors.get(2), first, false);
                }
            }
        }

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

    public List<Point3d> getPoints() {
        return points;
    }
}
