package com.limegroup.gnutella.tigertree;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;

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

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.limegroup.gnutella.FileDesc;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.security.Tiger;
import com.limegroup.gnutella.tigertree.dime.TigerDimeReadUtils;

/**
 * Default implementation of {@link HashTreeFactory}.
 */
/* This is public for tests, but should always be referenced by its interface in real code. */
@Singleton
public class HashTreeFactoryImpl implements HashTreeFactory {

    private static final Log LOG = LogFactory.getLog(HashTreeFactoryImpl.class);
    
    private final HashTreeNodeManager hashTreeNodeManager;
    
    @Inject
    public HashTreeFactoryImpl(HashTreeNodeManager hashTreeNodeManager) {
        this.hashTreeNodeManager = hashTreeNodeManager;
    }

    public HashTree createHashTree(FileDesc fd) throws IOException {
        if (LOG.isDebugEnabled())
            LOG.debug("creating hashtree for file " + fd);
        InputStream in = null;
        try {
            in = new BufferedInputStream(new FileInputStream(fd.getFile()));
            return createHashTree(fd.getFileSize(), in, fd.getSHA1Urn());
        } finally {
            IOUtils.close(in);
        }
    }

    /**
     * Reads a new HashTree from the network. It is expected that the data is in
     * DIME format, the first record being an XML description of the tree's
     * structure, and the second record being the breadth-first tree.
     * 
     * @param is the <tt>InputStream</tt> to read from
     * @param sha1 a <tt>String</tt> containing the sha1 URN for the same file
     * @param root32 a <tt>String</tt> containing the Base32 encoded expected
     *        root hash
     * @param fileSize the long specifying the size of the File
     * @return HashTree if we successfully read from the network
     * @throws IOException if there was an error reading from the network or if
     *         the data was corrupted or invalid in any way.
     */
    HashTree createHashTree(InputStream is, String sha1, String root32, long fileSize)
            throws IOException {
        if (LOG.isTraceEnabled())
            LOG.trace("reading " + sha1 + "." + root32 + " dime data.");
        return createHashTree(TigerDimeReadUtils.read(is, fileSize, root32), sha1, fileSize);
    }

    public HashTree createHashTree(List<List<byte[]>> allNodes, String sha1, long fileSize) {
        return createHashTree(allNodes, sha1, fileSize, HashTreeUtils.calculateNodeSize(fileSize,
                allNodes.size() - 1));
    }

    /**
     * Creates a new HashTree for the given file size, input stream and SHA1.
     * 
     * Exists as a hook for tests, to create a HashTree from a File when no
     * FileDesc exists.
     */
    public HashTree createHashTree(long fileSize, InputStream is, URN sha1) throws IOException {
        // do the actual hashing
        int nodeSize = HashTreeUtils.calculateNodeSize(fileSize, HashTreeUtils.calculateDepth(fileSize));
        List<byte[]> nodes = HashTreeUtils.createTreeNodes(nodeSize, fileSize, is, new Tiger());

        // calculate the intermediary nodes to get the root hash & others.
        List<List<byte[]>> allNodes = HashTreeUtils.createAllParentNodes(nodes, new Tiger());
        return createHashTree(allNodes, sha1.toString(), fileSize, nodeSize);
    }

    private HashTree createHashTree(List<List<byte[]>> allNodes, String sha1, long fileSize, int nodeSize) {
        HashTree tree = new HashTreeImpl(allNodes, sha1.toString(), fileSize, nodeSize);
        hashTreeNodeManager.register(tree, allNodes);
        return tree;
    }

}
