package com.limegroup.gnutella;

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.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.concurrent.ExecutorsHelper;
import org.limewire.io.IOUtils;
import org.limewire.util.CommonUtils;
import org.limewire.util.ConverterObjectInputStream;
import org.limewire.util.GenericsUtils;

import com.google.inject.Singleton;


/**
 * This class contains a systemwide URN cache that persists file URNs (hashes)
 * across sessions.
 *
 * Modified by Gordon Mohr (2002/02/19): Added URN storage, calculation, caching
 * Repackaged by Greg Bildson (2002/02/19): Moved to dedicated class.
 *
 * @see URN
 */
@Singleton
public final class UrnCache {
    
    private static final Log LOG = LogFactory.getLog(UrnCache.class);
    
    /**
     * File where urns (currently SHA1 urns) for files are stored.
     */
    private static final File URN_CACHE_FILE = 
        new File(CommonUtils.getUserSettingsDir(), "fileurns.cache");

    /**
     * Last good version of above.
     */
    private static final File URN_CACHE_BACKUP_FILE = 
        new File(CommonUtils.getUserSettingsDir(), "fileurns.bak");
    
    /**
     * The ProcessingQueue that Files are hashed in.
     */
    private final ExecutorService QUEUE = ExecutorsHelper.newProcessingQueue("Hasher");
    
    /**
     * The set of files that are pending hashing to the callbacks that are listening to them.
     */
    private Map<File, List<UrnCallback>> pendingHashing = new HashMap<File, List<UrnCallback>>();
    
    /**
     * Whether or not data is dirty since the last time we saved.
     */
    private volatile boolean dirty = false;
    
    /** The future that will contain the URN_MAP when it is done. */
    private final Future<Map<UrnSetKey, Set<URN>>> deserializer;

    /**
     * Create and initialize urn cache.
     */
    public UrnCache() {
        deserializer = QUEUE.submit(new Callable<Map<UrnSetKey, Set<URN>>>() {
            @SuppressWarnings("unchecked")
            public Map<UrnSetKey, Set<URN>> call() {
                // This cannot be inside a synchronized block, otherwise other methods
                // can block its construction.
                Map map = createMap();
                dirty = scanAndRemoveOldEntries(map);
                return map;
            }
        });
	}

    /**
     * Calculates the given File's URN and caches it.  The callback will
     * be notified of the URNs.  If they're already calculated, the callback
     * will be notified immediately.  Otherwise, it will be notified when hashing
     * completes, fails, or is interrupted.
     */
    public void calculateAndCacheUrns(File file, UrnCallback callback) {
        Set<URN> urns;
        synchronized (this) {
            urns = getUrns(file);
            // TODO: If we ever create more URN types (other than SHA1)
            // we cannot just check for size == 0, we must check for
            // size == NUM_URNS_WE_WANT, and calculateUrns should only
            // calculate the URN for the specific hash we still need.
            if (urns.isEmpty()) {
                if(LOG.isDebugEnabled())
                    LOG.debug("Adding: " + file + " to be hashed.");
                List<UrnCallback> list = pendingHashing.get(file);
                if(list == null) {
                    list = new ArrayList<UrnCallback>(1);
                    pendingHashing.put(file, list);
                }
                list.add(callback);
                QUEUE.execute(new Processor(file));
            }
        }
        if (!urns.isEmpty())
            callback.urnsCalculated(file, urns);
    }
    
    /**
     * Clears all callbacks that are owned by the given owner.
     */
    public synchronized void clearPendingHashes(Object owner) {
        if(LOG.isDebugEnabled())
            LOG.debug("Clearing all pending hashes owned by: " + owner);
        
        for(Iterator<List<UrnCallback>> i = pendingHashing.values().iterator(); i.hasNext(); ) {
            List<UrnCallback> callbacks = i.next();
            for(int j = callbacks.size() - 1; j >= 0; j--) {
                UrnCallback c = callbacks.get(j);
                if(c.isOwner(owner))
                    callbacks.remove(j);
            }            
            // if there's no more callbacks for this file, remove it.
            if(callbacks.isEmpty())
                i.remove();
        }
    }
    
    /**
     * Clears all callbacks for the given file that are owned by the given owner.
     */
    public synchronized void clearPendingHashesFor(File file, Object owner) {
        if(LOG.isDebugEnabled())
            LOG.debug("Clearing all pending hashes for: " + file + ", owned by: " + owner);
        List<UrnCallback> callbacks = pendingHashing.get(file);
        if(callbacks != null) {
            for(int j = callbacks.size() - 1; j >= 0; j--) {
                UrnCallback c = callbacks.get(j);
                if(c.isOwner(owner))
                    callbacks.remove(j);
            }
            if(callbacks.isEmpty())
                pendingHashing.remove(file);
        }
    }   
    
    /**
     * Adds any URNs that can be locally calculated; may take a while to 
	 * complete on large files.  After calculation, the items are added
	 * for future remembering.
	 *
	 * @param file the <tt>File</tt> instance to calculate URNs for
	 * @return the new <tt>Set</tt> of calculated <tt>URN</tt> instances.  If 
     * the calling thread is interrupted while executing this, returns an empty
     * set.
     */
    public Set<URN> calculateUrns(File file) throws IOException, InterruptedException {
        return URN.createSHA1AndTTRootUrns(file);
	}
    
    /**
     * Find any URNs remembered from a previous session for the specified
	 * <tt>File</tt> instance.  The returned <tt>Set</tt> is
	 * guaranteed to be non-null, but it may be empty.
	 *
	 * @param file the <tt>File</tt> instance to look up URNs for
	 * @return a new <tt>Set</tt> containing any cached URNs for the
	 *  speficied <tt>File</tt> instance, guaranteed to be non-null and 
	 *  unmodifiable, but possibly empty
     */
    public synchronized Set<URN> getUrns(File file) {
        // don't trust failed mod times
        if (file.lastModified() == 0L)
			return Collections.emptySet();

		UrnSetKey key = new UrnSetKey(file);

        // one or more "urn:" names for this file 
		Set<URN> cachedUrns = getUrnMap().get(key);
		if(cachedUrns == null)
			return Collections.emptySet();

		return cachedUrns;
    }
    
    /**
     * Removes any URNs that associated with a specified file.
     */
    public synchronized void removeUrns(File f) {
        UrnSetKey k = new UrnSetKey(f);
        getUrnMap().remove(k);
        dirty = true;
    }

    /**
     * Add URNs for the specified <tt>FileDesc</tt> instance to URN_MAP.
	 *
	 * @param file the <tt>File</tt> instance containing URNs to store
     */
    public synchronized void addUrns(File file, Set<? extends URN> urns) {
		UrnSetKey key = new UrnSetKey(file);
        getUrnMap().put(key, Collections.unmodifiableSet(urns));
        dirty = true;
    }
        
    /**
     * Loads values from cache file, if available.  If the cache file is
     * not readable, tries the backup.
     */
    private static Map createMap() {
        Map result;
        result = readMap(URN_CACHE_FILE);
        if(result == null)
            result = readMap(URN_CACHE_BACKUP_FILE);
        if(result == null)
            result = new HashMap<Object, Object>();
        return result;
    }
    
    /**
     * Loads values from cache file, if available.
     * 
     * @return null if the file does not exist or there was an error
     * reading the map from the file.
     */
    private static Map readMap(File file) {
        if (!file.exists()) {
            return null;
        }
        ObjectInputStream ois = null;
		try {
            ois = new ConverterObjectInputStream(
                    new BufferedInputStream(
                        new FileInputStream(file)));
            return (Map)ois.readObject();
	    } catch(Throwable t) {
	        LOG.error("Unable to read UrnCache", t);
	        return null;
	    } finally {
            IOUtils.close(ois);
        }
	}

	/**
	 * Removes any stale entries from the map so that they will automatically
	 * be replaced.
	 *
	 * @param map the <tt>Map</tt> to check
	 */
    private static boolean scanAndRemoveOldEntries(Map<Object, Object> map) {
        // discard outdated info
        boolean dirty = false;
        for(Iterator<Map.Entry<Object, Object>> i = map.entrySet().iterator(); i.hasNext(); ) {
            Map.Entry<Object, Object> entry = i.next();
            if(!(entry.getKey() instanceof UrnSetKey)) {
                i.remove();
                dirty = true;
                continue;
            }
            
            UrnSetKey key = (UrnSetKey)entry.getKey();
            File f = new File(key._path);
            if (!f.exists() || f.lastModified() != key._modTime) {
                dirty = true;
                i.remove();
                continue;
            }
            
            if(!(entry.getValue() instanceof Set)) {
                i.remove();
                dirty = true;
                continue;
            }
            
            Set<URN> set = GenericsUtils.scanForSet(entry.getValue(),
                                                    URN.class,
                                                    GenericsUtils.ScanMode.NEW_COPY_REMOVED,
                                                    UrnSet.class);
            if(set.isEmpty()) {
                i.remove();
                dirty = true;
                continue;
            }
            
            if(set != entry.getValue()) { // if it changed, replace the value w/ unmodifiable
                dirty = true;
                entry.setValue(Collections.unmodifiableSet(set));
            }
        }
        return dirty;
    }
    
    /**
     * Write cache so that we only have to calculate them once.
     */
    public synchronized void persistCache() {
        getUrnMap(); // make sure it's finished constructing.
        
        if(!dirty)
            return;
        
        //It's not ideal to hold a lock while writing to disk, but I doubt think
        //it's a problem in practice.
        URN_CACHE_FILE.renameTo(URN_CACHE_BACKUP_FILE);
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(
                    new BufferedOutputStream(new FileOutputStream(URN_CACHE_FILE)));
            oos.writeObject(getUrnMap());
            oos.flush();
        } catch (IOException e) {
            LOG.error("Unable to persist cache", e);
        } finally {
            IOUtils.close(oos);
        }
        
        dirty = false;
    }
    
    private Map<UrnSetKey, Set<URN>> getUrnMap() {
        boolean interrupted = Thread.interrupted();
        try {
            while(true) {
                try {
                    return deserializer.get();
                }catch (InterruptedException tryAgain) {
                    interrupted = true;
                }
            }
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        } finally {
            if (interrupted)
                Thread.currentThread().interrupt();
        }
    }
    
    private class Processor implements Runnable {
        private final File file;
        
        Processor(File f) {
            file = f;
        }
        
        public void run() {
            Set<URN> urns;
            List<UrnCallback> callbacks;
            
            synchronized(UrnCache.this) {
                callbacks = pendingHashing.remove(file);
                urns = getUrns(file); // already calculated?
            }
            
            // If there was atleast one callback listening, try and send it out
            // (which may involve calculating it).
            if(callbacks != null && !callbacks.isEmpty()) {
                // If not calculated, calculate OUTSIDE OF LOCK.
                if(urns.isEmpty()) {
                    if(LOG.isDebugEnabled())
                        LOG.debug("Hashing file: " + file);
                    try {
                        urns = calculateUrns(file);
                        addUrns(file, urns);
                    } catch(IOException ignored) {
                        LOG.warn("Unable to calculate URNs", ignored);
                    } catch(InterruptedException ignored) {
                        LOG.warn("Unable to calculate URNs", ignored);
                    }
                }
                
                // Note that because we already removed this list from the Map,
                // we don't need to synchronize while iterating over it, because
                // nothing else can modify it now.
                for(int i = 0; i < callbacks.size(); i++)
                    callbacks.get(i).urnsCalculated(file, urns);
            }
        }
    }

	/**
	 * Private class for the key for the set of URNs for files.
	 */
	private static class UrnSetKey implements Serializable {
		
		static final long serialVersionUID = -7183232365833531645L;

		/**
		 * Constant for the file modification time.
		 * @serial
		 */
		transient long _modTime;

		/**
		 * Constant for the file path.
		 * @serial
		 */
		transient String _path;

		/**
		 * Constant cached hash code, since this class is used exclusively
		 * as a hash key.
		 * @serial
		 */
		transient int _hashCode;

		/**
		 * Constructs a new <tt>UrnSetKey</tt> instance from the specified
		 * <tt>File</tt> instance.
		 *
		 * @param file the <tt>File</tt> instance to use in constructing the
		 *  key
		 */
		UrnSetKey(File file) {
			_modTime = file.lastModified();
			_path = file.getAbsolutePath();
			_hashCode = calculateHashCode();
		}

		/**
		 * Helper method to calculate the hash code.
		 *
		 * @return the hash code for this instance
		 */
		int calculateHashCode() {
			int result = 17;
			result = result*37 + (int)(_modTime ^(_modTime >>> 32));
			result = result*37 + _path.hashCode();
			return result;
		}

		/**
		 * Overrides Object.equals so that keys with equal paths and modification
		 * times will be considered equal.
		 *
		 * @param o the <tt>Object</tt> instance to compare for equality
		 * @return <tt>true</tt> if the specified object is the same instance
		 *  as this object, or if it has the same modification time and the same
		 *  path, otherwise returns <tt>false</tt>
		 */
		@Override
        public boolean equals(Object o) {
			if(this == o) return true;
			if(!(o instanceof UrnSetKey)) return false;
			UrnSetKey key = (UrnSetKey)o;

			// note that the path is guaranteed to be non-null
			return ((_modTime == key._modTime) &&
					(_path.equals(key._path)));
		}

		/**
		 * Overrides Object.hashCode to meet the specification of Object.equals
		 * and to make this class functions properly as a hash key.
		 *
		 * @return the hash code for this instance
		 */
		@Override
        public int hashCode() {
			return _hashCode;
		}

		/**
		 * Serializes this instance.
		 *
		 * @serialData the modification time followed by the file path
		 */
		private void writeObject(ObjectOutputStream s) 
			throws IOException {
			s.defaultWriteObject();
			s.writeLong(_modTime);
			s.writeObject(_path);
		}

		/**
		 * Deserializes this instance, restoring all invariants.
		 */
		private void readObject(ObjectInputStream s) 
			throws IOException, ClassNotFoundException {
			s.defaultReadObject();
			_modTime = s.readLong();
			_path = (String)s.readObject();
			_hashCode = calculateHashCode();
		}
	}
}





