package cz.fidentis.analyst.symmetry;

import cz.fidentis.analyst.mesh.MeshVisitor;
import cz.fidentis.analyst.mesh.core.MeshFacet;
import cz.fidentis.analyst.visitors.mesh.BoundingBox;
import cz.fidentis.analyst.visitors.mesh.BoundingBox.BBox;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Level;

/**
 * Main class for computing approximate plane of symmetry of the 3D model.
 * Default values of the configuration are given due to the best results on tested objects.
 * On many different 3D models, it exists other values of config that will have better impact on result. 
 * <p>
 * This visitor <b>is not thread-safe</b>, i.e., a single instance of the visitor 
 * cannot be used to inspect multiple meshes simultaneously  (sequential inspection is okay). 
 * It because the underlying {@link SignificantPoints} visitor is not thread-safe.
 * </p>
 * <p>
 * The main symmetry plane computation is performed by the {@link #getSymmetryPlane()} method,
 * not the {@link #visitMeshFacet(cz.fidentis.analyst.mesh.core.MeshFacet)}.
 * </p>
 * 
 * @author Natalia Bebjakova
 * @author Radek Oslejsek
 */
public class SymmetryEstimator extends MeshVisitor {
    
    private final SymmetryConfig config;
    private final SignificantPoints sigPointsVisitor;
    private final BoundingBox bbVisitor = new BoundingBox();
    
    private Plane symmetryPlane;
    
    /**
     * Constructor.
     * 
     * @param config Algorithm options
     * @throws IllegalArgumentException if some input parameter is missing
     */
    public SymmetryEstimator(SymmetryConfig config) {
        if (config == null) {
            throw new IllegalArgumentException("config");
        }
        this.config = config;
        this.sigPointsVisitor = new SignificantPoints(config.getCurvatureAlg(), config.getSignificantPointCount());
    }
    
    @Override
    public boolean isThreadSafe() {
        return false;
    }
    
    @Override
    public void visitMeshFacet(MeshFacet facet) {
        // We need vertex normals for the plane computation
        synchronized (this) {
            if (!facet.hasVertexNormals()) {
                facet.calculateVertexNormals();
            }
        }
        sigPointsVisitor.visitMeshFacet(facet);
        bbVisitor.visitMeshFacet(facet);
    }
    
    /**
     * Computes the symmetry plane concurrently.The plane is computed only once, then the same instance is returned.
     * 
     * @return Symmetry plane
     */
    public Plane getSymmetryPlane() {
        return getSymmetryPlane(true);
    }
    
    /**
     * Computes and returns the symmetry plane. The plane is computed only once, 
     * then the same instance is returned.
     * 
     * @param concurrently If {@code true}, then parallel computation is used utilizing all CPU cores.
     * @return the symmetry plane or {@code null}
     */
    public Plane getSymmetryPlane(boolean concurrently) {
        if (symmetryPlane == null) {
            this.calculateSymmetryPlane(concurrently);
        }
        return symmetryPlane;
    }
    
    /**
     * Returns the bounding box computed during the {@link SymmetryEstimator#calculateSymmetryPlane}.
     * @return the bounding box or {@code null}
     */
    public BBox getBoundingBox() {
        return bbVisitor.getBoundingBox();
    }
    
    /**
     * Calculates the symmetry plane.
     * @param concurrently If {@code true}, then parallel computation is used utilizing all CPU cores.
     * Otherwise, the computation is sequential.
     */
    protected void calculateSymmetryPlane(boolean concurrently) {
        final List<Plane> planes = new ArrayList<>();
        final double maxDistance = bbVisitor.getBoundingBox().getDiagonalLength() * config.getMaxRelDistance();
        
        /*
         * Sequential computation
         */
        if (!concurrently) {
            int maxVotes = 0;
            for (int i = 0; i < sigPointsVisitor.size(); i++) {
                for (int j = 0; j < sigPointsVisitor.size(); j++) {
                    ApproxSymmetryPlane newPlane;
                    try {
                        newPlane = new ApproxSymmetryPlane(sigPointsVisitor, config, i, j, maxDistance);
                    } catch(UnsupportedOperationException ex) {
                        continue;
                    }
                    maxVotes = checkAndUpdatePlanes(planes, newPlane, maxVotes);
                }
            }
            setSymmetryPlane(planes);
            return;
        }
        
        /*
         * Concurrent computation
         */
        
        // Initiate structure for concurrent computation:
        ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        final List<Future<ApproxSymmetryPlane>> results = new ArrayList<>(sigPointsVisitor.size()*sigPointsVisitor.size());
        
        // Compute candidate planes concurrently:
        for (int i = 0; i < sigPointsVisitor.size(); i++) {
            for (int j = 0; j < sigPointsVisitor.size(); j++) {
                int finalI = i;
                int finalJ = j;
                
                Callable<ApproxSymmetryPlane> worker = new  Callable<ApproxSymmetryPlane>() {
                    @Override
                    public ApproxSymmetryPlane call() throws Exception {
                        try {
                            return new ApproxSymmetryPlane(sigPointsVisitor, config, finalI, finalJ, maxDistance);
                        } catch(UnsupportedOperationException ex) {
                            return null;
                        }
                    }
                };
                
                results.add(executor.submit(worker));
            }
        }
        
        // Wait until all symmetry planes are computed:
        executor.shutdown();
        while (!executor.isTerminated()){}
        
        // Get the best-fitting planes:
        try {
            int maxVotes = 0;
            for (Future<ApproxSymmetryPlane> res: results) {
                ApproxSymmetryPlane newPlane = res.get();
                if (newPlane != null) {
                    maxVotes = checkAndUpdatePlanes(planes, newPlane, maxVotes);
                }
            }
        } catch (final InterruptedException | ExecutionException ex) {
            java.util.logging.Logger.getLogger(SymmetryEstimator.class.getName()).log(Level.SEVERE, null, ex);
        }
        
        setSymmetryPlane(planes);
    }
    
    protected int checkAndUpdatePlanes(List<Plane> planes, ApproxSymmetryPlane newPlane, int maxVotes) {
        if (newPlane.getVotes() > maxVotes) {
            planes.clear();
            planes.add(newPlane);
            return newPlane.getVotes();
        } else if (newPlane.getVotes() == maxVotes) {
            planes.add(newPlane);
        }
        return maxVotes;
    }

    protected void setSymmetryPlane(List<Plane> planes) {
        if (config.isAveraging() && !planes.isEmpty()) {
            symmetryPlane = new Plane(planes);
        } else {
            symmetryPlane = planes.isEmpty() ? null : planes.get(0);
        }
    }
}
