
import java.util.*;
import java.util.zip.*;
import java.io.*;


class MaybeDups
{
    // Each hashtable element is a vector of Files
    private Hashtable byKey = new Hashtable ();
    private int nfiles = 0;
    
    public void add (Object key, File f)
    {
	Vector files = (Vector)byKey.get (key);
	if (files == null) {
	    files = new Vector ();
	    byKey.put (key, files); 
	}
	files.addElement (f);
	nfiles++;
    }
    
    public Enumeration keys ()
    {
	return byKey.keys ();
    }
    
    public Vector files (Object key)
    {
	return (Vector)byKey.get (key);
    }
    
    public int keyCount ()
    {
	return byKey.size ();
    }
    
    public int fileCount ()
    {
	return nfiles;
    }
    
    public void deleteUnique ()
    {
	Enumeration enum = keys ();
	while (enum.hasMoreElements ()) {
	    Object k = enum.nextElement ();
	    Vector files = files (k);
	    if (files.size () < 2) {
		nfiles -= files.size ();
		byKey.remove (k);
	    }
	}
    }
}

public class DupFinder
{
    static private MaybeDups groupBySize (String[] files)
    {
	MaybeDups dups = new MaybeDups ();
	for (int i = 0; i < files.length; i++) {
	    File f = new File (files[i]);
	    dups.add (new Long (f.length ()), f);
	}
	return dups;
    }
    
    static private MaybeDups groupByChecksum (MaybeDups dups)
    {
	MaybeDups newDups = new MaybeDups ();
	Enumeration enum1 = dups.keys ();
	while (enum1.hasMoreElements ()) {
	    Object k = enum1.nextElement ();
	    MaybeDups sameSize = new MaybeDups ();
	    Enumeration enum2 = dups.files (k).elements ();
	    while (enum2.hasMoreElements ()) {
		File f = (File)enum2.nextElement ();
		try {
		    sameSize.add (new Long (getChecksum (f)), f);
		}
		catch (Exception e) {
		    // ignore I/O errors
		    //System.err.println ("Error: " + arq.getAbsolutePath ());
		    //System.err.println (e);
		    //System.err.println (e.getMessage ());
		}
	    }
	    sameSize.deleteUnique ();
	    Enumeration enum3 = sameSize.keys ();
	    while (enum3.hasMoreElements ()) {
		Object newK = enum3.nextElement ();
		String compositeK = k.toString () + "/" + newK.toString ();
		Enumeration enum4 = sameSize.files (newK).elements ();
		while (enum4.hasMoreElements ())
		    newDups.add (compositeK, (File)enum4.nextElement ());
	    }
	}
	return newDups;
    }

    static private long getChecksum (File f) throws IOException
    {
	//java.util.zip.CRC32
	//java.util.zip.Adler32
	Adler32 checksum = new Adler32 ();
	CheckedInputStream in = new CheckedInputStream (
	    new BufferedInputStream (new FileInputStream (f)),
	    checksum);
	while (in.read () != -1) ;
	return checksum.getValue ();
    }
    
    static private void listDupFiles (MaybeDups dups, String label)
    {
	Enumeration it1 = dups.keys ();
	while (it1.hasMoreElements ()) {
	    Object k = it1.nextElement ();
	    System.out.println (label + ": " + k);
	    Enumeration enum2 = dups.files (k).elements ();
	    while (enum2.hasMoreElements ()) {
		File f = (File)enum2.nextElement ();
		System.out.println (f.getAbsolutePath ());
	    }
	}
    }

    private static void showStats (int total, MaybeDups dups)
    {
	System.out.println ("Scanned " + total + " files.");
	System.out.println ("Found " + dups.fileCount () + " duplicate files.");
	System.out.println ("grouped in " + dups.keyCount () + " clusters.");
    }

    public static void main (String[] args)
    {
	if (args.length == 0) {
	    System.err.println ("Must gimme some file names");
	    System.exit (1);
	}
	System.out.println ();
	System.out.println ("Pass #1: Checking file sizes");
	System.out.print ("Scanning files... ");
	MaybeDups dups = groupBySize (args);
	int total = dups.fileCount ();
	dups.deleteUnique ();
	showStats (total, dups);
	//listDupFiles (dups, "Size (Bytes)");
	System.out.println ();
	System.out.println ("Pass #2: Getting checksums");
	System.out.print ("Scanning files... ");
	total = dups.fileCount ();
	MaybeDups dups2 = groupByChecksum (dups);
	showStats (total, dups2);
	listDupFiles (dups2, "Size (Bytes)/Checksum");
    }
}
