package com.limegroup.gnutella.library;

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.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.RandomAccess;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.io.IOUtils;
import org.limewire.util.GenericsUtils;

import com.limegroup.gnutella.util.LimeWireUtils;

/**
 * A container for storing serialized objects to disk.
 * This supports only storing objects that fit in the Collections framework.
 * Either Collections or Maps.
 * All collections are returned as synchronized on this container.
 */
class Container {
    
    private static final Log LOG = LogFactory.getLog(Container.class);
    
    private final Map<String, Collection<File>> STORED = new HashMap<String, Collection<File>>();
    private final String filename;
    
    /**
     * Constructs a new container with the given filename.
     * It will always save to this name in the user's
     * setting's directory, also loading the data from disk.
     */
    Container(String name) {
        filename = name;
        load();
    }
    
    /**
     * Loads data from disk.
     */
    void load() {
        // Read without grabbing the lock.
        Map<String, Collection<File>> read = readFromDisk();
        
        synchronized(this) {
            // Simple case -- no stored data yet.
            if(STORED.isEmpty()) {
                STORED.putAll(read);
            } else {
                // If data was stored, we can't replace, we have to refresh.
                for(Map.Entry<String, Collection<File>> entry : read.entrySet()) {
                    String k = entry.getKey();
                    Collection<File> v = entry.getValue();
                    Collection<File> storedV = STORED.get(k);
                    if(storedV == null) {
                        // Another simple case -- key wasn't stored yet.
                        STORED.put(k, v);
                    } else {
                        synchronized(storedV) {
                            storedV.clear();
                            storedV.addAll(v);
                        }
                    }
                }
            }
        }
    }
    
    /**
     * Retrieves a set from the Container.  If the object
     * stored is not null or is not a set, a Set is inserted instead.
     *
     * The returned sets are synchronized, but the serialized sets are NOT SYNCHRONIZED.
     * This means that the future can change what they synchronize on easily.
     */
    synchronized Set<File> getSet(String name) {
        Collection<File> data = STORED.get(name);
        if (data != null) {
            return (Set<File>)data;
        } else { 
            Set<File> set = Collections.synchronizedSet(new HashSet<File>());
            STORED.put(name, set);
            return set;
        }
    }
    
    /**
     * Clears all entries.
     */
    synchronized void clear() {
        for(Collection<File> data : STORED.values()) {
            data.clear();
        }
    }
        
    
    /**
     * Saves the data to disk.
     */
    void save() {
        Map<String, Collection<File>> toSave;
        
        synchronized(this) {
            toSave = new HashMap<String, Collection<File>>(STORED.size());
            for(Map.Entry<String, Collection<File>> entry : STORED.entrySet()) {
                String k = entry.getKey();
                Collection<File> v = entry.getValue();
                synchronized(v) {
                	if(v instanceof SortedSet)
            			toSave.put(k, new TreeSet<File>((SortedSet<File>)v));
            		else if(v instanceof Set)
            			toSave.put(k, new HashSet<File>(v));
            		else if(v instanceof List) {
            			if (v instanceof RandomAccess)
            				toSave.put(k, new ArrayList<File>(v));
            			else 
            				toSave.put(k, new LinkedList<File>(v));
            		} else {
                        if(LOG.isWarnEnabled())
                            LOG.warn("Update to clone! key: " + k);
                        toSave.put(k, v);
                    }
                }
            }
        }
        
        writeToDisk(toSave);
    }
    
    /**
     * Saves the given object to disk.
     */
    private void writeToDisk(Object o) {
        File f = new File(LimeWireUtils.getUserSettingsDir(), filename);
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(f)));
            oos.writeObject(o);
            oos.flush();
        } catch(IOException iox) {
            LOG.warn("Can't write to disk!", iox);
        } finally {
            IOUtils.close(oos);
        }
    }
    
    /**
     * Reads a Map from disk.
     */
    private Map<String, Collection<File>> readFromDisk() {
        File f = new File(LimeWireUtils.getUserSettingsDir(), filename);
        ObjectInputStream ois = null;
        Map map = null;
        try {
            ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(f)));
            map = (Map)ois.readObject();
        } catch(ClassCastException cce) {
            LOG.warn("Not a map!", cce);
        } catch(IOException iox) {
            LOG.warn("Can't write to disk!", iox);
        } catch(Throwable x) {
            LOG.warn("Error reading!", x);
        } finally {
            IOUtils.close(ois);
        }
        
        if (map != null) {
        	HashMap<String, Collection<File>> toReturn = new HashMap<String, Collection<File>>(map.size());
        	for (Iterator i = map.entrySet().iterator(); i.hasNext();) {
        		Map.Entry entry = (Map.Entry)i.next();
                if(!(entry.getKey() instanceof String)) {
                    if(LOG.isWarnEnabled())
                        LOG.warn("Ignoring key: " + entry.getKey());
                    continue;
                }
                String k = (String)entry.getKey();
                if(!(entry.getValue() instanceof Collection)) {
                    if(LOG.isWarnEnabled())
                        LOG.warn("Ignoring value: " + entry.getValue());
                    continue;
                }
                Collection<File> v = GenericsUtils.scanForCollection(entry.getValue(), File.class,
                                                                     GenericsUtils.ScanMode.REMOVE);
        		if(v instanceof SortedSet)
        			toReturn.put(k, Collections.synchronizedSortedSet((SortedSet<File>)v));
        		else if(v instanceof Set)
        			toReturn.put(k, Collections.synchronizedSet((Set<File>)v));
        		else if(v instanceof List)
        			toReturn.put(k, Collections.synchronizedList((List<File>)v));
        		else {
        			if(LOG.isWarnEnabled())
        				LOG.warn("Update to clone! key: " + k);
        			toReturn.put(k, v);
        		}
        	}
        	return toReturn;
        }
        else {
        	return new HashMap<String, Collection<File>>();
        }
    }
}