/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2005-2008, Open Source Geospatial Foundation (OSGeo)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.index.quadtree;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.NoSuchElementException;

import org.geotools.data.shapefile.shp.IndexFile;
import org.geotools.index.CloseableIterator;
import org.geotools.index.Data;
import org.geotools.index.DataDefinition;

import com.vividsolutions.jts.geom.Envelope;

/**
 * Iterator that search the quad tree depth first. 32000 indices are cached at a
 * time and each time a node is visited the indices are removed from the node so
 * that the memory footprint is kept small. Note that if other iterators operate
 * on the same tree then they can interfere with each other.
 * 
 * @author Jesse
 *
 *
 * @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/modules/plugin/shapefile/src/main/java/org/geotools/index/quadtree/LazySearchIterator.java $
 */
public class LazySearchIterator implements CloseableIterator<Data> {
    
    static final DataDefinition DATA_DEFINITION = new DataDefinition("US-ASCII");

    private static final int MAX_INDICES = 32768;
    static {
        DATA_DEFINITION.addField(Integer.class);
        DATA_DEFINITION.addField(Long.class);
    }

    Data next = null;

    Node current;

    int idIndex = 0;

    private boolean closed;

    private Envelope bounds;

    Iterator data;

    private IndexFile indexfile;
    
    ArrayList<Node> parents = new ArrayList<Node>();
    
    Indices indices = new Indices();

    QuadTree tree;

    public LazySearchIterator(QuadTree tree, Envelope bounds) {
        super();
        this.tree = tree;
        this.indexfile = tree.getIndexfile();
        tree.registerIterator(this);
        this.current = tree.getRoot();
        this.bounds = bounds;
        this.closed = false;
        this.next = null;
    }

    public boolean hasNext() {
        if (closed)
            throw new IllegalStateException("Iterator has been closed!");
        if (next != null)
            return true;
        if (data != null && data.hasNext()) {
            next = (Data) data.next();
        } else {
            data = null;
            fillCache();
            if (data != null && data.hasNext())
                next = (Data) data.next();
        }
        return next != null;
    }

    private void fillCache() {
        indices.clear();
        ArrayList dataList = null;
        try {
            while (indices.size() < MAX_INDICES && current != null) {
                if (idIndex < current.getNumShapeIds() && !current.isVisited()
                        && current.getBounds().intersects(bounds)) {
                    indices.add(current.getShapeId(idIndex));
                    idIndex++;
                } else {
                    // free the shapes id array of the current node and prepare to move to the next
                    current.setShapesId(new int[0]);
                    idIndex = 0;
                    
                    boolean foundUnvisited = false;
                    for (int i = 0; i < current.getNumSubNodes(); i++) {
                        Node node = current.getSubNode(i);
                        if (!node.isVisited()
                                && node.getBounds().intersects(bounds)) {
                            foundUnvisited = true;
                            parents.add(current);
                            current = node;
                            break;
                        }
                    }
                    if (!foundUnvisited) {
                        // mark as visited and free the subnodes
                        current.setVisited(true);
                        current.clean();
                        
                        // move up to parent
                        if(parents.isEmpty())
                            current = null;
                        else
                            current = parents.remove(parents.size() - 1);
                    }
                }
            }
            
            // sort so offset lookup is faster
            indices.sort();
            int size = indices.size();
            dataList = new ArrayList(size);
            for (int i = 0; i < size; i++) {
                int recno = indices.get(i);
                Data data = new Data(DATA_DEFINITION);
                data.addValue(recno + 1);
                data.addValue(new Long(indexfile.getOffsetInBytes(recno)));
                dataList.add(data);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        data = dataList.iterator();
    }

    public Data next() {
        if (!hasNext())
            throw new NoSuchElementException("No more elements available");
        Data temp = next;
        next = null;
        return temp;
    }

    public void remove() {
        throw new UnsupportedOperationException();
    }

    public void close() throws IOException {
        tree.close(this);
        tree.close();
        this.closed = true;
    }

    /**
     * An efficient wrapper around an array of integers
     */
    class Indices {
        /**
         * The current coordinate
         */
        int curr;
        
        /**
         * The ordinates holder
         */
        int[] indices;
        
        public Indices() {
            indices = new int[100];
            curr = -1;
        }
        
        /**
         * The number of coordinates
         * @return
         */
        int size() {
            return curr + 1;
        }
        
        /**
         * Adds a coordinate to this list
         * @param x
         * @param y
         */
        void add(int index) {
            curr++;
            if((curr * 2 + 1) >= indices.length) {
                int newSize = indices.length * 3 / 2;
                if(newSize < 10) {
                    newSize = 10;
                }
                int[] resized = new int[newSize];
                System.arraycopy(indices, 0, resized, 0, indices.length);
                indices = resized;
            }
            indices[curr] = index;
        }
        
        /**
         * Resets the indices
         */
        void clear() {
            curr = -1;
        }
        
        int get(int position) {
            return indices[position];
        }
        
        void sort() {
            Arrays.sort(indices, 0, curr + 1);
        }
    }
}
