package com.limegroup.gnutella.gui.search;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.SortedMap;
import java.util.TreeMap;

import com.limegroup.gnutella.Assert;
import com.limegroup.gnutella.util.ApproximateMatcher;
import com.limegroup.gnutella.util.Comparators;

/** 
 * Used by TableLineModel to quickly find similar SearchResults.  This takes
 * advantage of the fact that two SearchResults' are similar only if their file
 * sizes are similar.  A typical TableLineGrouper is a set of SearchResults', 
 * {a1,..., an}.
 */
final public class TableLineGrouper {        
    /** Maps sizes to lists of index/line pairs.  This list is needed in case
     *  there are multiple files with same size.  (Performance is severely degraded
     *  in this case.) */
    private /* Integer -> List<SearchResult> */ SortedMap map=
        new TreeMap(Comparators.integerComparator());

    /** Used to compare all filenames in this.  Ignores case, whitespace.  */
    final private ApproximateMatcher matcher;
    
    public TableLineGrouper() {
        this.matcher=new ApproximateMatcher(120);
        this.matcher.setIgnoreCase(true);
        this.matcher.setIgnoreWhitespace(true);
        this.matcher.setCompareBackwards(true);
    }

    /** Returns true if empty, i.e., cleared. */
    public boolean isEmpty() {
        return map.isEmpty();
    }

    public void clear() {
        map.clear();
    }

    /** Returns an j s.t. list[j].match(line), 
     *  or -1 if no such i but the caller should keep searching
     *      (because no line in list is close in size)
     *  or -2 if no such i but the caller shouldn't keep searching
     *      (because no line in list is close in size)
     *  @requires elements of list are GrouperPair */
    private int matchHelper(List /* of SearchResult */ list,
                                   SearchResult line) {
        Assert.that(list!=null, "Trying to match null list");
        Assert.that(line!=null, "Trying to match null line");
        for (int j=0; j<list.size(); j++) {
            SearchResult line2=(SearchResult)list.get(j);
            int match=line2.match(line, matcher);
            if (match==0)          //matches
                return j;
            else if (match==2)     //non-similar sizes
                return -2;
        }
        return -1;
    }

    /** Returns a line G in this s.t. G.similar(line), or null of no such line
     *  exists. */
    public SearchResult match(SearchResult line) {
        //OK. This line has no hash...use old algorithm...
        Integer key=new Integer(line.getSize());
        //1. Search forward for results with similar (but possibly larger) sizes
        Iterator iter=map.tailMap(key).values().iterator();
        while (iter.hasNext()) {
            List lines=(List)iter.next();            
            int ret=matchHelper(lines, line);
            if (ret>=0)
                return (SearchResult)lines.get(ret);
            else if (ret==-2)
                break;            
        }
        //2. Search backwards for results with similar (but possibly smaller)
        //   sizes.  Note that we do this in a different way from (1) because 
        //   sorted maps provide no way of iterating through values backwards.
        SortedMap map=this.map.headMap(key);
        while (true) {
            Integer key2;
            try {
                key2=(Integer)map.lastKey();
            } catch (NoSuchElementException e) {
                break;
            }

            List lines=(List)map.get(key2);
            int ret=matchHelper(lines, line);
            if (ret>=0)
                return (SearchResult)lines.get(ret);
            else if (ret==-2)
                break;              

            map=map.headMap(key2);
        }
        //3. No luck?  
        return null;
    }

    /**
     * Adds line to this.  Generally there should be no lines similar to line
     * in this, i.e., this.match(line)==null, but this isn't strictly required.
     *     @requires line not in this
     *     @modifies this 
     */
    public void add(SearchResult line) {
        Assert.that(line!=null,"Attempting to add null line");
        Integer key=new Integer(line.getSize());
        List lines=(List)map.get(key);
        if (lines==null) {
            lines=new LinkedList();
            map.put(key, lines);
        }
        
        lines.add(line);        
    }

    /*
    public static void main(String[] args) {
        System.out.println("Starting.");
        //1. Basic tests
        TableLineGrouper grouper=new TableLineGrouper();
        TableLine line1=new TableLine("file.mp3", new Integer(100),
            new Integer(0), new Integer(0), "host", new Integer(6346),
            new byte[16], new Integer(0));
        TableLine line2=new TableLine("file.mp3", new Integer(99),
            new Integer(0), new Integer(0), "host", new Integer(6346),
            new byte[16], new Integer(0));
        TableLine line3=new TableLine("file.mp3", new Integer(101),
            new Integer(0), new Integer(0), "host", new Integer(6346),
            new byte[16], new Integer(0));
        TableLine line4=new TableLine("different.mp3", new Integer(100),
            new Integer(0), new Integer(0), "host", new Integer(6346),
            new byte[16], new Integer(0));
        Assert.that(grouper.match(line1)==null);
        grouper.add(line1);
        Assert.that(grouper.match(line1)==line1);
        Assert.that(grouper.match(line2)==line1);
        Assert.that(grouper.match(line3)==line1);
        Assert.that(grouper.match(line4)==null);        
        TableLine line5=new TableLine("file.mp3", new Integer(1000000),
            new Integer(0), new Integer(0), "host", new Integer(6346),
            new byte[16], new Integer(0));
        TableLine line6=new TableLine("file.mp3", new Integer(1001000),
            new Integer(0), new Integer(0), "host", new Integer(6346),
            new byte[16], new Integer(0));
        TableLine line7=new TableLine("file.mp3", new Integer(999000),
            new Integer(0), new Integer(0), "host", new Integer(6346),
            new byte[16], new Integer(0));
        TableLine line8=new TableLine("file.mp3", new Integer(10000000),
            new Integer(0), new Integer(0), "host", new Integer(6346),
            new byte[16], new Integer(0));
        Assert.that(grouper.match(line5)==null);
        grouper.add(line5);
        grouper.add(line8);
        Assert.that(grouper.match(line6)==line5);
        Assert.that(grouper.match(line7)==line5);     
        System.out.println("Done.");   
    }
    */
    
    /*  
        //Old unit test not adapted to new interface.
      
        //2. Tests whether match searches forward and backward correctly
        grouper=new TableLineGrouper();
        TableLine line0=new TableLine("zero.mp3", new Integer(98),
            new Integer(0), new Integer(0), "host", new Integer(6346),
            new byte[16], new Integer(0));
        line1=new TableLine("first.mp3", new Integer(99),
            new Integer(0), new Integer(0), "host", new Integer(6346),
            new byte[16], new Integer(0));
        line2=new TableLine("second.mp3", new Integer(100),
            new Integer(0), new Integer(0), "host", new Integer(6346),
            new byte[16], new Integer(0));
        line3=new TableLine("third.mp3", new Integer(101),
            new Integer(0), new Integer(0), "host", new Integer(6346),
            new byte[16], new Integer(0));
        line4=new TableLine("fourth.mp3", new Integer(102),
            new Integer(0), new Integer(0), "host", new Integer(6346),
            new byte[16], new Integer(0));
        Assert.that(grouper.add(line0)==0);
        Assert.that(grouper.add(line1)==1);
        Assert.that(grouper.add(line2)==2);
        Assert.that(grouper.add(line3)==3);
        Assert.that(grouper.add(line4)==4);
        TableLine line0c=new TableLine("zero.mp3", new Integer(100),
            new Integer(0), new Integer(0), "host", new Integer(6346),
            new byte[16], new Integer(0));
        TableLine line1c=new TableLine("first.mp3", new Integer(100),
            new Integer(0), new Integer(0), "host", new Integer(6346),
            new byte[16], new Integer(0));        
        TableLine line3c=new TableLine("third.mp3", new Integer(100),
            new Integer(0), new Integer(0), "host", new Integer(6346),
            new byte[16], new Integer(0));
        TableLine line4c=new TableLine("fourth.mp3", new Integer(100),
            new Integer(0), new Integer(0), "host", new Integer(6346),
            new byte[16], new Integer(0));
        Assert.that(grouper.match(line1c)==1);
        Assert.that(grouper.match(line3c)==3);
        Assert.that(grouper.match(line0c)==0);
        Assert.that(grouper.match(line4c)==4);
    }
    */
}
