package cz.fidentis.analyst.face;

import cz.fidentis.analyst.face.events.HumanFaceListener;
import com.google.common.eventbus.EventBus;
import cz.fidentis.analyst.Logger;
import cz.fidentis.analyst.face.events.HumanFaceEvent;
import cz.fidentis.analyst.feature.FeaturePoint;
import cz.fidentis.analyst.feature.services.FeaturePointImportService;
import cz.fidentis.analyst.kdtree.KdTree;
import cz.fidentis.analyst.face.events.KdTreeCreated;
import cz.fidentis.analyst.face.events.KdTreeDestroyed;
import cz.fidentis.analyst.face.events.MeshChangedEvent;
import cz.fidentis.analyst.face.events.SymmetryPlaneChangedEvent;
import cz.fidentis.analyst.mesh.core.MeshModel;
import cz.fidentis.analyst.mesh.core.MeshRectangleFacet;
import cz.fidentis.analyst.mesh.io.MeshObjLoader;
import cz.fidentis.analyst.symmetry.Plane;
import cz.fidentis.analyst.visitors.face.HumanFaceVisitor;
import cz.fidentis.analyst.visitors.mesh.BoundingBox;
import cz.fidentis.analyst.visitors.mesh.BoundingBox.BBox;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.Objects;
//import org.nustaq.serialization.FSTConfiguration;
//import org.nustaq.serialization.FSTObjectInput;
//import org.nustaq.serialization.FSTObjectOutput;

/**
 * This class encapsulates data for a 3D scan of a single human face.
 * <p>
 * Changes in the human face and its data structures (e.g., mesh model, etc.)
 * can be monitored by listeners. Listeners have to implement the 
 * {@link cz.fidentis.analyst.face.events.HumanFaceListener} interface and they have to be
 * registered using the {@link cz.fidentis.analyst.face.HumanFace#registerListener} method.
 * Then they are informed about changes in the human automatically via methods 
 * prescribed by the interface.
 * </p>
 * 
 * @author Radek Oslejsek
 */
public class HumanFace implements Serializable {
    
    private MeshModel meshModel;
    
    private KdTree kdTree;

    private Plane symmetryPlane;
    
    //private MeshFacet symmetryPlaneMesh;
    private BBox bbox;

    private List<FeaturePoint> featurePoints;
    
    private final transient EventBus eventBus;
    
    private final String id;
    
    /**
     * Fast (de)serialization handler
     */
    //private static final FSTConfiguration FST_CONF = FSTConfiguration.createDefaultConfiguration();
    
    /**
     * Reads a 3D human face from the given OBJ file.
     * Use {@link restoreFromFile} to restore the human face from a dump file.
     * 
     * @param file OBJ file
     * @param loadLandmarks If {@code true}, then the constructor aims to load 
     * landmarks along with the mesh. Use {@link #getFeaturePoints()} to check 
     * whether the landmarks (feature points) has been loaded.
     * @throws IOException on I/O failure
     */
    public HumanFace(File file, boolean loadLandmarks) throws IOException {
        meshModel = MeshObjLoader.read(new FileInputStream(file));
        meshModel.simplifyModel();
        this.id = file.getCanonicalPath();
        
        BoundingBox visitor = new BoundingBox();
        meshModel.compute(visitor);
        bbox = visitor.getBoundingBox();
        
        eventBus = new EventBus();
        
        if (loadLandmarks) {
            File landFile = this.findLandmarks();
            if (landFile != null) {
                try {
                    loadFeaturePoints(landFile.getAbsoluteFile().getParent(), landFile.getName());
                } catch(IOException ex) {
                    Logger.print(ex.toString());
                }
            }
        }
    }
    
    /**
     * Reads a 3D human face from the given OBJ file. 
     * Also loads landmarks (feature points) if appropriate file is found.
     * Use {@link restoreFromFile} to restore the human face from a dump file.
     * 
     * @param file OBJ file
     * @throws IOException on I/O failure
     */
    public HumanFace(File file) throws IOException {
        this(file, true);
    }
    
    /**
     * Returns the triangular mesh model of the human face.
     * 
     * @return the triangular mesh model of the human face
     */
    public MeshModel getMeshModel() {
        return meshModel;
    }
    
    /**
     * Sets the mesh model. 
     * Triggers {@link cz.fidentis.analyst.face.events.MeshChangedEvent}.
     * 
     * @param meshModel new mesh model, must not be {@code null}
     * @throws IllegalArgumentException if new model is missing
     */
    public final void setMeshModel(MeshModel meshModel) {
        if (meshModel == null ) {
            throw new IllegalArgumentException("meshModel");
        }
        this.meshModel = meshModel;
        
        BoundingBox visitor = new BoundingBox();
        meshModel.compute(visitor);
        bbox = visitor.getBoundingBox();
        
        announceEvent(new MeshChangedEvent(this, getShortName(), this));
    }

    /**
     * Registers listeners (objects concerned in the human face changes) to receive events.
     * If listener is {@code null}, no exception is thrown and no action is taken.
     * 
     * @param listener Listener concerned in the human face changes.
     */
    public void registerListener(HumanFaceListener listener) {
        if (eventBus != null) { // eventBus is null when the class is deserialized!
            eventBus.register(listener);
        }
    }
    
    /**
     * Unregisters listeners from receiving events.
     * 
     * @param listener Registered listener
     */
    public void unregisterListener(HumanFaceListener listener) {
        if (eventBus != null) { // eventBus is null when the class is deserialized!
            eventBus.unregister(listener);  
        }
    }
    
    /**
     * Broadcast event to registered listeners.
     * 
     * @param evt Event to be triggered.
     */
    public void announceEvent(HumanFaceEvent evt) {
        if (evt != null && eventBus != null) { // eventBus is null when the class is deserialized!
            eventBus.post(evt);
        }
    }

    /**
     * Sets the symmetry plane. If the input argument is {@code null}, then removes the plane.
     * Triggers {@link cz.fidentis.analyst.face.events.SymmetryPlaneChangedEvent}.
     * 
     * @param plane The new symmetry plane; Must not be {@code null}
     */
    public void setSymmetryPlane(Plane plane) {
        this.symmetryPlane = plane;
        this.announceEvent(new SymmetryPlaneChangedEvent(this, getShortName(), this));
    }

    /**
     *
     * @return The face's symmetry plane
     */
    public Plane getSymmetryPlane() {
        return symmetryPlane;
    }
    
    /**
     * Returns rectangular mesh facet of the symmetry plane, if exists.
     * @return a rectangular mesh facet of the symmetry plane or {@code null}
     */
    public MeshRectangleFacet getSymmetryPlaneFacet() {
        return (symmetryPlane == null) ? null : symmetryPlane.getMesh(bbox);
    }
    
    /**
     * Returns bounding box of this face.
     * @return bounding box of this face.
     */
    public BBox getBoundingBox() {
        return bbox;
    }

    /**
     * 
     * @param points List of feature points
     */
    public void setFeaturePoints(List<FeaturePoint> points) {
        featurePoints = points;
    }
    
    /**
     * Reads feature points from a file on the given path.
     * 
     * @param path Directory where the file is located
     * @param fileName Name of the file
     * @throws IOException on I/O failure
     */
    public void loadFeaturePoints(String path, String fileName) throws IOException {
        featurePoints = FeaturePointImportService.importFeaturePoints(path, fileName);
    }
    
    /**
     * 
     * @return The face's feature points.
     */
    public List<FeaturePoint> getFeaturePoints() {
        if (featurePoints == null) {
            return Collections.emptyList();
        }
        return Collections.unmodifiableList(featurePoints);
    }
    
    /**
     * Checks if HumanFace has feature points
     * @return true if yes and false if not
     */
    public boolean hasFeaturePoints() {
        return featurePoints != null;
    }
    
    /**
     * Returns unique ID of the face.
     * 
     * @return unique ID of the face. 
     */
    public String getId() {
        return this.id;
    }
    
    /**
     * Returns canonical path to the face file
     * @return canonical path to the face file
     */
    public String getPath() {
        return getId();
    }
    
    /**
     * Returns short name of the face without its path in the name. May not be unique.
     * @return short name of the face without its path in the name
     */
    public String getShortName() {
        String name = id.substring(0, id.lastIndexOf('.')); // remove extention
        name = name.substring(name.lastIndexOf(File.separatorChar) + 1, name.length());
        return name;
    }
    
    /**
     * Returns already computed k-d tree of the triangular mesh or {@code null}.
     * @return Already computed k-d tree of the triangular mesh or {@code null}
     */
    public KdTree getKdTree() {
        return this.kdTree;
    }
    
    /**
     * Checks if HumanFace has KdTree calculated
     * @return true if yes and false if not
     */
    public boolean hasKdTree() {
        return kdTree != null;
    }
    
    /**
     * Computes new k-d tree or returns the existing one.
     * 
     * @param recompute If {@code true} and an old k-d tree exists, then it is recomputed again. 
     * Otherwise, the tree is computed only if does not exist yet.
     * @return K-d tree with the triangular mesh.
     */
    public KdTree computeKdTree(boolean recompute) {
        if (kdTree == null || recompute) {
            kdTree = new KdTree(new ArrayList<>(meshModel.getFacets()));
            if (eventBus != null) { // eventBus is null when the class is deserialized!
                eventBus.post(new KdTreeCreated(this, this.getShortName(), this));
            }
        }
        return kdTree;
    }
    
    /**
     * Removes k-d tree from the Human face. 
     * @return removed k-d tree or {@code null}
     */
    public KdTree removeKdTree() {
        KdTree ret = this.kdTree;
        this.kdTree = null;
        if (eventBus != null) { // eventBus is null when the class is deserialized!
            eventBus.post(new KdTreeDestroyed(this, this.getShortName(), this));
        }
        return ret;
    }
    
    /**
     * Tries to find a file with landmarks definition based on the name of the face's OBJ file.
     * @return The file with landmarks or {@code null}
     */
    public final File findLandmarks() {
        String filename = getId().split(".obj")[0] + "_landmarks.csv";
        if ((new File(filename)).exists()) {
            return new File(filename);
        }
        
        filename = getId().split("_ECA")[0] + "_landmarks.csv";
        if ((new File(filename)).exists()) {
            return new File(filename);
        }
        
        filename = getId().split("_CA")[0] + "_landmarks.csv";
        if ((new File(filename)).exists()) {
            return new File(filename);
        }
        
        return null;
    }
    
    /**
     * Creates serialized dump of the human face. Event buses are not stored.
     * Therefore, listeners have to re-register again after recovery.
     * 
     * @return Dump file
     * @throws IOException on error in creating the dump file
     */
    public File dumpToFile() throws IOException {
        File tempFile = File.createTempFile(this.getClass().getSimpleName(), ".ser");
        tempFile.deleteOnExit();
        
        try (ObjectOutputStream fos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(tempFile)))) {
            fos.writeObject(this);
            fos.flush();
        }

        /*
        try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(tempFile))) {
            FSTObjectOutput out = FST_CONF.getObjectOutput(bos);
            out.writeObject(this, HumanFace.class);
            out.flush();
        }
        */
        
        return tempFile;
    }
    
    /**
     * Restores human face from a dump file.
     * 
     * @param dumpFile The file
     * @return Human face
     * @throws IOException on error in reading the dump file
     * @throws java.lang.ClassNotFoundException on error when instantiating the human face
     */
    public static HumanFace restoreFromFile(File dumpFile) throws IOException, ClassNotFoundException {
        try (ObjectInputStream fos = new ObjectInputStream(new BufferedInputStream(new FileInputStream(dumpFile)))) {
            return (HumanFace) fos.readObject();
        }
        /*
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(dumpFile))) {
            FSTObjectInput in = FST_CONF.getObjectInput(bis);
            HumanFace face = (HumanFace) in.readObject(HumanFace.class);
            return face;
        }
        */
    }
    
    /**
     * Visits this face.
     * 
     * @param visitor Visitor
     */
    public void accept(HumanFaceVisitor visitor) {
        visitor.visitHumanFace(this);
    }

    @Override
    public int hashCode() {
        int hash = 7;
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final HumanFace other = (HumanFace) obj;
        if (!Objects.equals(this.id, other.id)) {
            return false;
        }
        return true;
    }
    
    
}
