package cz.fidentis.analyst.face;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.logging.Level;

/**
 * A flyweight factory that creates and caches human faces. Faces are
 * stored in the memory until there is enough space in the Java heap. Then they 
 * are cached on disk automatically. The dumping strategy can be switched at run-time.
 * <p>
 * Currently, listeners registered to {@code HumanFace} are neither dumped nor recovered!
 * </p>
 * 
 * @author Radek Oslejsek
 */
public class HumanFaceFactory {

    /**
     * Memory clean  up, i.e. dumping the file into disk and de-referencing it, is
     * very fast. Faster real objects removal by JVM. 
     * Therefore, the {@link #checkMemAndDump()} method removes almost
     * {@code MAX_DUMP_FACES} at once.
     */
    private static final int MAX_DUMP_FACES = 3;
    
    /**
     * Dumping strategies.
     * @author Radek Oslejsek
     */
    public enum Strategy {
        /**
         * least recently used faces are dumped first
         */
        LRU,
        
        /**
         * most recently used faces are dumped first
         */
        MRU
    }
    
    /**
     * Keep at least this portion of the Java heap memory free
     */
    public static final double MIN_FREE_MEMORY = 0.1; // 10%
    
    /**
     * Human faces currently being stored on disk.
     * <ul>
     * <li>Key = Face ID<li>
     * <li>Value = Dump file<li>
     * </ul>
     */
    private final Map<String, File> dumpedFaces = new HashMap<>();
    
    /**
     * Human faces currently being allocated in the memory.
     * <ul>
     * <li>Key = Last access time (milliseconds)<li>
     * <li>Value = Human face<li>
     * </ul>
     */
    private final SortedMap<Long, HumanFace> inMemoryFaces = new TreeMap<>();
    
    /**
     * The usage "table"
     * <ul>
     * <li>Key = Face ID<li>
     * <li>Value = Last access time (milliseconds)<li>
     * </ul>
     */
    private final Map<String, Long> usage = new HashMap<>();
    
    /**
     * Dumping strategy.
     */
    private Strategy strategy = Strategy.LRU;
    
    private boolean reuseDumpFile = false;
    
    
    /**
     * Changes the dumping strategy
     * @param strategy Dumping strategy. Must not be {@code null}
     * @throws IllegalArgumentException if the strategy is missing
     */
    public void setStrategy(Strategy strategy) {
        if (strategy == null) {
            throw new IllegalArgumentException("strategy");
        }
        this.strategy = strategy;
    }
    
    /**
     * Returns current dumping strategy.
     * @return current dumping strategy
     */
    public Strategy getStrategy() {
        return this.strategy;
    }
    
    /**
     * I set to {@code true}, then the dump file of a face is created only once
     * and then reused for every recovery (it never overwritten).
     * It accelerates the dumping face, but can be used only if the state of
     * faces never changes between the first dump a consequent recoveries. 
     * 
     * @param use If {@code true}, then this optimization is turned on.
     */
    public void setReuseDumpFile(boolean use) {
        this.reuseDumpFile = use;
    }
    
    /**
     * Loads new face. If the face is already loaded, then the ID of existing instance
     * is returned. To access the human face instance, use {@link getFace}.
     * 
     * @param file OBJ file with human face.
     * @return ID of the human face or {@code null}
     */
    public String loadFace(File file) {
        // Create face ID:
        String faceId;
        try {
            faceId = file.getCanonicalPath();
            //faceId = Long.toHexString(Double.doubleToLongBits(Math.random()));
        } catch (IOException ex) {
            java.util.logging.Logger.getLogger(HumanFaceFactory.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
        
        // Existing face, possibly recovered from a dump file
        HumanFace face = getFace(faceId);
        if (face != null) {
            return faceId;
        }
        
        try {
            face = new HumanFace(file); // read from file
            checkMemAndDump(); // free the memory, if necessary
        } catch (IOException ex) {
            java.util.logging.Logger.getLogger(HumanFaceFactory.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }        
        
        // Update data structures:
        long time = System.currentTimeMillis();
        inMemoryFaces.put(time, face);
        usage.put(faceId, time);
        return faceId;
    }
    
    /**
     * Returns a human face. Recovers the face from the disk if necessary.
     * 
     * @param faceId ID of the face
     * @return Human face or {@code null}
     */
    public HumanFace getFace(String faceId) {
        if (updateAccessTime(faceId)) { // in memory face
            return inMemoryFaces.get(usage.get(faceId));
        }
        
        File dumpFile = dumpedFaces.get(faceId);
        if (dumpFile == null) { // unknown face
            return null;
        }
         
        // Free memory and recover human face from dump file:
        HumanFace face;
        try {
            face = HumanFace.restoreFromFile(dumpFile); // recover face from disk
            checkMemAndDump(); // free the memory, if necessary
            //System.out.println(face.getShortName() + " recovered");
        } catch (ClassNotFoundException|IOException ex) {
            java.util.logging.Logger.getLogger(HumanFaceFactory.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
        
        // Update data structures:
        long time = System.currentTimeMillis();
        while (inMemoryFaces.containsKey(time)) { // wait until we get unique ms
            time = System.currentTimeMillis();
        }
        //dumpedFaces.remove(faceId);
        inMemoryFaces.put(time, face);
        usage.put(faceId, time);
        return face;
    }
    
    /**
     * Removes the face from either memory or swap space. To re-initiate the face,
     * use {@link #loadFace(java.io.File)} again.
     * 
     * @param faceId Face ID
     * @return true if the face existed and was removed.
     */
    public boolean removeFace(String faceId) {
        if (usage.containsKey(faceId)) {
            inMemoryFaces.remove(usage.remove(faceId));
            dumpedFaces.remove(faceId);
            return true; // remove from memory;
        }
        
        return dumpedFaces.remove(faceId) != null;
    }
    
    @Override
    public String toString() {
        StringBuilder ret = new StringBuilder();
        ret.append(formatSize(presumableFreeMemory())).
                append(" Java heap memory available (out of ").
                append(formatSize(Runtime.getRuntime().maxMemory()));
        ret.append("). In memory: ").
                append(this.inMemoryFaces.size()).
                append(", dump files: ").
                append(this.dumpedFaces.size());
        return ret.toString();
    }
    
    /**
     * Updates last access time of the face allocated in the memory. 
     * Returns {@code false} if the face is not in the memory.
     * 
     * @param faceId Face to be updated
     * @return {@code false} if the face is not in the memory.
     */
    protected boolean updateAccessTime(String faceId) {
        Long oldTime = usage.get(faceId);
        if (oldTime == null) {
            return false;
        }
        
        HumanFace face = inMemoryFaces.get(oldTime);
        
        long newTime = System.currentTimeMillis();
        while (inMemoryFaces.containsKey(newTime)) { // wait until we get unique ms
            newTime = System.currentTimeMillis();
        }
        inMemoryFaces.remove(oldTime);
        inMemoryFaces.put(newTime, face);
        usage.put(faceId, newTime);
        return true;
    }
    
    /**
     * Checks and releases the heap, if necessary.
     * 
     * @return true if some existing face has been dumped to free the memory.
     * @throws IOException on I/O error
     */
    protected int checkMemAndDump() throws IOException {
        int ret =  0;
        int counter = 0;
        while (counter++ < this.MAX_DUMP_FACES &&
                (double) presumableFreeMemory() / Runtime.getRuntime().maxMemory() <= MIN_FREE_MEMORY) {
            
            Long time = null;
            switch (strategy) {
                case MRU:
                    time = inMemoryFaces.lastKey();
                    break;
                default:
                case LRU:
                    time = inMemoryFaces.firstKey();
                    break;
            }
            
            if (time == null) { // no face in the memory
                break; 
            }
        
            HumanFace faceToDump = this.inMemoryFaces.remove(time);
            this.usage.remove(faceToDump.getId());
            if (!reuseDumpFile || !dumpedFaces.containsKey(faceToDump.getId())) { // dump only if it's required
                dumpedFaces.put(faceToDump.getId(), faceToDump.dumpToFile());
            }
            //System.out.println(faceToDump.getShortName() + " dumped");
        }
        
        return ret;        
    }
        
    protected static String formatSize(long v) {
        if (v < 1024) {
            return v + " B";
        }
        int z = (63 - Long.numberOfLeadingZeros(v)) / 10;
        return String.format("%.1f %sB", (double)v / (1L << (z*10)), " KMGTPE".charAt(z));
    }
    
    /**
     * https://stackoverflow.com/questions/12807797/java-get-available-memory
     * @return 
     */
    protected long presumableFreeMemory() {
        long allocatedMemory = (Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory());
        return Runtime.getRuntime().maxMemory() - allocatedMemory;
    }
}
