/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2003-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.data.shapefile.indexed;

import static org.geotools.data.shapefile.ShpFileType.DBF;
import static org.geotools.data.shapefile.ShpFileType.FIX;
import static org.geotools.data.shapefile.ShpFileType.QIX;
import static org.geotools.data.shapefile.ShpFileType.SHP;
import static org.geotools.data.shapefile.ShpFileType.SHX;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;

import org.geotools.data.DataSourceException;
import org.geotools.data.DataUtilities;
import org.geotools.data.EmptyFeatureReader;
import org.geotools.data.FIDReader;
import org.geotools.data.FeatureReader;
import org.geotools.data.FeatureWriter;
import org.geotools.data.InProcessLockingManager;
import org.geotools.data.Query;
import org.geotools.data.Transaction;
import org.geotools.data.TransactionStateDiff;
import org.geotools.data.shapefile.FileReader;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.geotools.data.shapefile.ShpFileType;
import org.geotools.data.shapefile.dbf.DbaseFileReader;
import org.geotools.data.shapefile.dbf.IndexedDbaseFileReader;
import org.geotools.data.shapefile.shp.IndexFile;
import org.geotools.data.shapefile.shp.ShapefileException;
import org.geotools.data.shapefile.shp.ShapefileReader;
import org.geotools.data.shapefile.shp.ShapefileReader.Record;
import org.geotools.factory.Hints;
import org.geotools.feature.SchemaException;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.feature.visitor.IdCollectorFilterVisitor;
import org.geotools.filter.FilterAttributeExtractor;
import org.geotools.filter.visitor.ExtractBoundsFilterVisitor;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.index.CachedQuadTree;
import org.geotools.index.CloseableIterator;
import org.geotools.index.Data;
import org.geotools.index.DataDefinition;
import org.geotools.index.TreeException;
import org.geotools.index.quadtree.QuadTree;
import org.geotools.index.quadtree.StoreException;
import org.geotools.index.quadtree.fs.FileSystemIndexStore;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.filter.Filter;
import org.opengis.filter.Id;
import org.opengis.filter.identity.Identifier;

import com.vividsolutions.jts.geom.CoordinateSequenceFactory;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.GeometryFactory;

/**
 * A DataStore implementation which allows reading and writing from Shapefiles.
 * 
 * @author Ian Schneider
 * @author Tommaso Nolli
 * @author jesse eichar
 * 
 *
 * @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/modules/plugin/shapefile/src/main/java/org/geotools/data/shapefile/indexed/IndexedShapefileDataStore.java $
 */
public class IndexedShapefileDataStore extends ShapefileDataStore implements
        FileReader {
    private final static class IdentifierComparator implements Comparator<Identifier>
    {
        public int compare(Identifier o1, Identifier o2)
        {
            return o1.toString().compareTo(o2.toString());
        }
    }

    IndexType treeType;

    final boolean useIndex;
    
    final boolean createIndex;
    
    CachedQuadTree cachedTree;

	int maxQixCacheSize = DEFAULT_MAX_QIX_CACHE_SIZE;
	
	static final int DEFAULT_MAX_QIX_CACHE_SIZE;
	
	static {
		int max = -1;
		try {
			String smax = System.getProperty("org.geotools.shapefile.maxQixCacheSize");
			if(smax != null) {
				max = Integer.parseInt(smax);
			}
		} catch(Throwable t) {
			LOGGER.log(Level.SEVERE, "Could not set the max qix cache size", t);
		}
		DEFAULT_MAX_QIX_CACHE_SIZE = max;
	}
    
    public IndexedShapefileDataStore(URL url, URI namespace,
            boolean useMemoryMappedBuffer, boolean cacheMemoryMaps, boolean createIndex,
            IndexType treeType, Charset dbfCharset) {
    	super(url, namespace, useMemoryMappedBuffer, cacheMemoryMaps, dbfCharset);

        this.treeType = treeType;
        this.useIndex = treeType != IndexType.NONE;
        this.createIndex = createIndex;
    }
    
    protected Filter getUnsupportedFilter(String typeName, Filter filter) {

        if (filter instanceof Id && isLocal() && shpFiles.exists(FIX))
            return Filter.INCLUDE;

        return filter;
    }

    /**
     * Creates a new instance of ShapefileDataStore.
     * 
     * @param url
     *                The URL of the shp file to use for this DataSource.
     * @param namespace
     *                DOCUMENT ME!
     * @param useMemoryMappedBuffer
     *                enable/disable memory mapping of files
     * @param cacheMemoryMaps
     *                caches and reuses the read only memory mapped buffers                
     * @param createIndex
     *                enable/disable automatic index creation if needed
     * @param treeType
     *                The type of index used
     * @param dbfCharset
     *                {@link Charset} used to decode strings from the DBF
     * 
     * @throws NullPointerException
     *                 DOCUMENT ME!
     * @throws .
     */
    public FeatureWriter<SimpleFeatureType, SimpleFeature> getFeatureWriterAppend(String typeName,
            Transaction transaction) throws IOException {
        if (transaction == null) {
            throw new NullPointerException(
                    "getFeatureWriter requires Transaction: "
                            + "did you mean to use Transaction.AUTO_COMMIT?");
        }

        FeatureWriter<SimpleFeatureType, SimpleFeature> writer;

        if (transaction == Transaction.AUTO_COMMIT) {
            return super.getFeatureWriterAppend(typeName, transaction);
        } else {
            writer = state(transaction).writer(typeName, Filter.EXCLUDE);
        }

        if (getLockingManager() != null) {
            // subclass has not provided locking so we will
            // fake it with InProcess locks
            writer = ((InProcessLockingManager) getLockingManager())
                    .checkedWriter(writer, transaction);
        }

        while (writer.hasNext())
            writer.next();
        return writer;
    }

    /**
     * This method is identical to the super class WHY?
     */
    protected TransactionStateDiff state(Transaction transaction) {
        synchronized (transaction) {
            TransactionStateDiff state = (TransactionStateDiff) transaction
                    .getState(this);

            if (state == null) {
                state = new TransactionStateDiff(this);
                transaction.putState(this, state);
            }

            return state;
        }
    }

    /**
     * Use the spatial index if available and adds a small optimization: if no
     * attributes are going to be read, don't uselessly open and read the dbf
     * file.
     * 
     * @see org.geotools.data.AbstractDataStore#getFeatureReader(java.lang.String,
     *      org.geotools.data.Query)
     */
    protected  FeatureReader<SimpleFeatureType, SimpleFeature> getFeatureReader(String typeName, Query query)
            throws IOException {
        if (query.getFilter() == Filter.EXCLUDE)
            return new EmptyFeatureReader<SimpleFeatureType, SimpleFeature>(getSchema());

        String[] propertyNames = query.getPropertyNames() == null ? new String[0]
                : query.getPropertyNames();
        String defaultGeomName = schema.getGeometryDescriptor().getLocalName();


        // add the attributes we need to read to keep the filtering going
        if(propertyNames.length  > 0) {
            FilterAttributeExtractor fae = new FilterAttributeExtractor(schema);
            query.getFilter().accept(fae, null);
    
            Set<String> attributes = new LinkedHashSet<String>(Arrays.asList(propertyNames));
            attributes.addAll(fae.getAttributeNameSet());
    
            propertyNames = attributes.toArray(new String[attributes
                    .size()]);
        }

        // check what we actually have to read
        SimpleFeatureType newSchema = schema;
        boolean readDbf = true;
        boolean readGeometry = true;
        try {
            if (((query.getPropertyNames() != Query.NO_NAMES)
                    && (propertyNames.length == 1) && propertyNames[0]
                    .equals(defaultGeomName))) {
                readDbf = false;
                newSchema = createSubType( propertyNames);
            } else if ((query.getPropertyNames() == Query.NO_NAMES)
                    && (propertyNames.length == 0)) {
                readDbf = false;
                readGeometry = false;
                newSchema = createSubType( propertyNames);
            } else if( propertyNames.length > 0 && !propertyNames[0].equals(defaultGeomName) ){
                readGeometry = false;
                newSchema = createSubType(propertyNames);
            } else if(propertyNames.length > 0) {
                newSchema = createSubType(propertyNames);
            }

            return createFeatureReader(typeName, getAttributesReader(readDbf,
                    readGeometry, query, newSchema), newSchema);
        } catch (SchemaException se) {
            throw new DataSourceException("Error creating schema", se);
        }
    }
    
    /**
     * Much like {@link DataUtilities#createSubType(SimpleFeatureType, String[])}, but makes
     * sure to preserve the original attribute order
     * @param properties
     * @return
     * @throws SchemaException
     */
    public SimpleFeatureType createSubType(String[] properties) throws SchemaException {
        if (properties == null || properties.length == 0) {
            return schema;
        }

        boolean same = schema.getAttributeCount() == properties.length;

        for (int i = 0; (i < schema.getAttributeCount()) && same; i++) {
            same = schema.getDescriptor(i).getLocalName().equals(properties[i]);
        }

        if (same) {
            return schema;
        }

        SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder();
        tb.setName( schema.getName() );
        
        Set<String> propIndex = new HashSet<String>(Arrays.asList(properties));
        for(AttributeDescriptor ad : schema.getAttributeDescriptors()) {
            if(propIndex.contains(ad.getLocalName()))
                tb.add(ad);
        }
        return tb.buildFeatureType();
    }

    protected  FeatureReader<SimpleFeatureType, SimpleFeature> createFeatureReader(String typeName,
            IndexedShapefileAttributeReader r, SimpleFeatureType readerSchema)
            throws SchemaException, IOException {
        
        if(r == null) {
            return new EmptyFeatureReader<SimpleFeatureType, SimpleFeature>(readerSchema);
        }

        FIDReader fidReader;
        if (!indexUseable(FIX)) {
            fidReader = new ShapeFIDReader(getCurrentTypeName(), r);
        } else {
            fidReader = new IndexedFidReader(shpFiles, r);
        }
        return new org.geotools.data.FIDFeatureReader(r, fidReader,
                readerSchema);
    }
    
    /**
     * Returns the attribute reader, allowing for a pure shape reader, or a
     * combined dbf/shp reader. Will return null if reading the indexes confirms there is nothing
     * to read
     * 
     * @param readDbf -
     *                if true, the dbf fill will be opened and read
     * @param readGeometry
     *                DOCUMENT ME!
     * @param filter -
     *                a Filter to use
     * 
     * 
     * @throws IOException
     */
    protected IndexedShapefileAttributeReader getAttributesReader(
            boolean readDbf, boolean readGeometry, Query query, SimpleFeatureType targetSchema)
            throws IOException {
        Envelope bbox = new ReferencedEnvelope(); // will be bbox.isNull() to
        // start

        Filter filter = query != null ? query.getFilter() : null;
        CloseableIterator<Data> goodRecs = null;
        if (filter instanceof Id && shpFiles.isLocal() && indexUseable(ShpFileType.FIX)) {
            Id fidFilter = (Id) filter;

            TreeSet idsSet = new TreeSet(new IdentifierComparator());
            idsSet.addAll(fidFilter.getIdentifiers());
            List<Data> records = queryFidIndex(idsSet);
            if(records != null) {
            	goodRecs = new CloseableIteratorWrapper<Data>(records.iterator());
            }
        } else {
            if (filter != null) {
                // Add additional bounds from the filter
                // will be null for Filter.EXCLUDES
                bbox = (Envelope) filter.accept(
                        ExtractBoundsFilterVisitor.BOUNDS_VISITOR, bbox);
                if (bbox == null) {
                    bbox = new ReferencedEnvelope();
                    // we hit Filter.EXCLUDES consider returning an empty
                    // reader?
                    // (however should simplify the filter to detect ff.not(
                    // fitler.EXCLUDE )
                }
            }

            if (!bbox.isNull() && this.useIndex) {
                try {
                    goodRecs = this.queryQuadTree(bbox);
                } catch (TreeException e) {
                    throw new IOException("Error querying index: "
                            + e.getMessage());
                }
            }
        }
        List<AttributeDescriptor> atts = targetSchema.getAttributeDescriptors();

        IndexedDbaseFileReader dbfR = null;
        
        // do we have anything to read at all? If not don't bother opening all the files
        if(goodRecs != null && !goodRecs.hasNext()) {
            // System.out.println("Empty results for " + targetSchema.getName().getLocalPart() + ", skipping read");
            goodRecs.close();
            return null;
        }

        if (!readDbf) {
            LOGGER.fine("The DBF file won't be opened since no attributes "
                    + "will be read from it");
            atts = new ArrayList<AttributeDescriptor>(1);
            atts.add(schema.getGeometryDescriptor());

            if (!readGeometry) {
                atts = new ArrayList<AttributeDescriptor>(1);
            }
        } else {
            dbfR = (IndexedDbaseFileReader) openDbfReader();
        }

        Hints hints = query != null ? query.getHints() : null;
        final ShapefileReader shapeReader = openShapeReader(getGeometryFactory(hints), goodRecs != null);
        IndexedShapefileAttributeReader reader =  new IndexedShapefileAttributeReader(atts, 
                shapeReader, dbfR, goodRecs);
        reader.setTargetBBox(bbox);
        if(hints != null) {
            Number simplificationDistance = (Number) hints.get(Hints.GEOMETRY_DISTANCE);    
            if(simplificationDistance != null) {
                reader.setSimplificationDistance(simplificationDistance.doubleValue());
            }
            
            if(Boolean.TRUE.equals(hints.get(Hints.FEATURE_2D))) {
                shapeReader.setFlatGeometry(true);
            }
        }
        
        return reader;
    }
    
    /**
     * Convenience method for opening a ShapefileReader.
     * 
     * @return A new ShapefileReader.
     * 
     * @throws IOException
     *                 If an error occurs during creation.
     */
    protected ShapefileReader openShapeReader(GeometryFactory gf, boolean onlyRandomAccess) throws IOException {
        try {
            return new ShapefileReader(shpFiles, true, useMemoryMappedBuffer, gf, onlyRandomAccess);
        } catch (ShapefileException se) {
            throw new DataSourceException("Error creating ShapefileReader", se);
        }
    }

    /**
     * Uses the Fid index to quickly lookup the shp offset and the record number
     * for the list of fids
     * 
     * @param fids
     *                the fids of the features to find.  If the set is sorted by alphabet the performance is likely to be better.
     * @return a list of Data objects
     * @throws IOException
     * @throws TreeException
     */
    private List<Data> queryFidIndex(Set<Identifier> idsSet) throws IOException {

        if (!indexUseable(FIX)) {
            return null;
        }

        IndexedFidReader reader = new IndexedFidReader(shpFiles);

        List<Data> records = new ArrayList(idsSet.size());
        try {
            IndexFile shx = openIndexFile();
            try {

                DataDefinition def = new DataDefinition("US-ASCII");
                def.addField(Integer.class);
                def.addField(Long.class);
                for (Identifier identifier : idsSet) {
                    String fid = identifier.toString();
                    long recno = reader.findFid(fid);
                    if (recno == -1){
                        if(LOGGER.isLoggable(Level.FINEST)){
                            LOGGER.finest("fid " + fid+ " not found in index, continuing with next queried fid...");
                        }
                        continue;
                    }
                    try {
                        Data data = new Data(def);
                        data.addValue(new Integer((int) recno + 1));
                        data.addValue(new Long(shx
                                .getOffsetInBytes((int) recno)));
                        if(LOGGER.isLoggable(Level.FINEST)){
                            LOGGER.finest("fid " + fid+ " found for record #"
                                    + data.getValue(0) + " at index file offset "
                                    + data.getValue(1));
                        }
                        records.add(data);
                    } catch (Exception e) {
                        IOException exception = new IOException();
                        exception.initCause(e);
                        throw exception;
                    }
                }
            } finally {
                shx.close();
            }
        } finally {
            reader.close();
        }

        return records;
    }

    /**
     * Returns true if the index for the given type exists and is useable.
     * 
     * @param indexType
     *                the type of index to check
     * 
     * @return true if the index for the given type exists and is useable.
     */
    public boolean indexUseable(ShpFileType indexType) {
        if (isLocal()) {
            if (needsGeneration(indexType) || !shpFiles.exists(indexType)) {
                return false;
            }
        } else {

            ReadableByteChannel read = null;
            try {
                read = shpFiles.getReadChannel(indexType, this);
            } catch (IOException e) {
                return false;
            } finally {
                if (read != null) {
                    try {
                        read.close();
                    } catch (IOException e) {
                        ShapefileDataStoreFactory.LOGGER.log(Level.WARNING,
                                "could not close stream", e);
                    }
                }
            }
        }

        return true;
    }

    boolean needsGeneration(ShpFileType indexType) {
        // happens if the IndexType.NONE.shpFileType is used)
        if(indexType == null)
            return false;
        
        if (!isLocal())
            throw new IllegalStateException(
                    "This method only applies if the files are local and the file can be created");

        URL indexURL = shpFiles.acquireRead(indexType, this);
        URL shpURL = shpFiles.acquireRead(SHP, this);
        try {

            if (indexURL == null) {
                return true;
            }
            // indexes require both the SHP and SHX so if either or missing then
            // you don't need to
            // index
            if (!shpFiles.exists(SHX) || !shpFiles.exists(SHP)) {
                return false;
            }

            File indexFile = DataUtilities.urlToFile(indexURL);
            File shpFile = DataUtilities.urlToFile(shpURL);
            long indexLastModified = indexFile.lastModified();
            long shpLastModified = shpFile.lastModified();
            boolean shpChangedMoreRecently = indexLastModified < shpLastModified;
            return !indexFile.exists() || shpChangedMoreRecently;
        } finally {
            if (shpURL != null) {
                shpFiles.unlockRead(shpURL, this);
            }
            if (indexURL != null) {
                shpFiles.unlockRead(indexURL, this);
            }
        }
    }

    // /**
    // * RTree query
    // *
    // * @param bbox
    // *
    // *
    // * @throws DataSourceException
    // * @throws IOException
    // */
    // private List queryRTree(Envelope bbox) throws DataSourceException,
    // IOException {
    // List goodRecs = null;
    // RTree rtree = this.openRTree();
    //
    // try {
    // if ((rtree != null) && (rtree.getBounds() != null)
    // && !bbox.contains(rtree.getBounds())) {
    // goodRecs = rtree.search(bbox);
    // }
    // } catch (LockTimeoutException le) {
    // throw new DataSourceException("Error querying RTree", le);
    // } catch (TreeException re) {
    // throw new DataSourceException("Error querying RTree", re);
    // }
    //
    // return goodRecs;
    // }

    /**
     * QuadTree Query
     * 
     * @param bbox
     * 
     * 
     * @throws DataSourceException
     * @throws IOException
     * @throws TreeException
     *                 DOCUMENT ME!
     */
    protected CloseableIterator<Data> queryQuadTree(Envelope bbox)
            throws DataSourceException, IOException, TreeException {
        CloseableIterator<Data> tmp = null;
        
        if(cachedTree == null) {
            boolean canCache = false;
            URL treeURL = shpFiles.acquireRead(QIX, this);
            try {
                File treeFile = DataUtilities.urlToFile(treeURL);

                if (treeFile.exists() && treeFile.length() < 1024 * maxQixCacheSize) {
                    canCache = true;
                }
            } finally {
                shpFiles.unlockRead(treeURL, this);
            }

            if(canCache) {
                QuadTree quadTree = openQuadTree();
                if(quadTree != null) {
                    LOGGER.warning("Experimental: loading in memory the quadtree for " + shpFiles.get(SHP));
                    cachedTree = new CachedQuadTree(quadTree);
                    quadTree.close();
                }
            }
        }
        if(cachedTree != null) {
            if(!bbox.contains(cachedTree.getBounds())) {
                return cachedTree.search(bbox);
            } else {
                return null;
            }
        } else {
            try {
                QuadTree quadTree = openQuadTree();
                if ((quadTree != null)
                        && !bbox.contains(quadTree.getRoot().getBounds())) {
                    tmp = quadTree.search(bbox);
                }
                if (tmp == null && quadTree != null) {
                    quadTree.close();
                }
            } catch (Exception e) {
                throw new DataSourceException("Error querying QuadTree", e);
            }
        }

        return tmp;
    }

    /**
     * Convenience method for opening a DbaseFileReader.
     * 
     * @return A new DbaseFileReader
     * 
     * @throws IOException
     *                 If an error occurs during creation.
     */
    protected DbaseFileReader openDbfReader() throws IOException {
        if (shpFiles.get(DBF) == null) {
            return null;
        }

        if (isLocal() && !shpFiles.exists(DBF)) {
            return null;
        }

        return new IndexedDbaseFileReader(shpFiles, useMemoryMappedBuffer, dbfCharset, dbfTimeZone);
    }

    //
    // /**
    // * Convenience method for opening an RTree index.
    // *
    // * @return A new RTree.
    // *
    // * @throws IOException
    // * If an error occurs during creation.
    // * @throws DataSourceException
    // * DOCUMENT ME!
    // */
    // protected RTree openRTree() throws IOException {
    // if (!isLocal()) {
    // return null;
    // }
    // URL treeURL = shpFiles.acquireRead(GRX, this);
    // try {
    // File treeFile = DataUtilities.urlToFile(treeURL);
    //
    // if (!treeFile.exists() || (treeFile.length() == 0)) {
    // treeType = IndexType.NONE;
    // return null;
    // }
    //
    // try {
    // FileSystemPageStore fps = new FileSystemPageStore(treeFile);
    // rtree = new RTree(fps);
    // } catch (TreeException re) {
    // throw new DataSourceException("Error opening RTree", re);
    // }
    //
    // return rtree;
    // } finally {
    // shpFiles.unlockRead(treeURL, this);
    // }
    // }

    /**
     * Convenience method for opening a QuadTree index.
     * 
     * @return A new QuadTree
     * 
     * @throws StoreException
     */
    protected QuadTree openQuadTree() throws StoreException {
        if (!isLocal()) {
            return null;
        }
        URL treeURL = shpFiles.acquireRead(QIX, this);
        try {
            File treeFile = DataUtilities.urlToFile(treeURL);

            if (!treeFile.exists() || (treeFile.length() == 0)) {
                treeType = IndexType.NONE;
                return null;
            }

            try {
                FileSystemIndexStore store = new FileSystemIndexStore(treeFile);
                return store.load(openIndexFile(), useMemoryMappedBuffer);
            } catch (IOException e) {
                throw new StoreException(e);
            }
        } finally {
            shpFiles.unlockRead(treeURL, this);
        }

    }

    /**
     * Create a FeatureWriter for the given type name.
     * 
     * @param typeName
     *                The typeName of the FeatureType to write
     * @param transaction
     *                DOCUMENT ME!
     * 
     * @return A new FeatureWriter.
     * 
     * @throws IOException
     *                 If the typeName is not available or some other error
     *                 occurs.
     */
    protected FeatureWriter<SimpleFeatureType, SimpleFeature> createFeatureWriter(String typeName,
            Transaction transaction) throws IOException {
        return null;
    }

    /**
     * @see org.geotools.data.AbstractDataStore#getBounds(org.geotools.data.Query)
     */
    protected ReferencedEnvelope getBounds(Query query) throws IOException {
        ReferencedEnvelope ret = null;

        Set records = new HashSet();
        Filter filter = query.getFilter();
        if (filter == Filter.INCLUDE || query == Query.ALL) {
            return getBounds();
        }
        // else if (this.useIndex) {
        // if (treeType == IndexType.GRX) {
        // return getBoundsRTree(query);
        // }
        // }

        Comparator<Identifier> identifierComparator = new IdentifierComparator();
        Set<Identifier> fids = (Set<Identifier>) filter.accept(
                IdCollectorFilterVisitor.IDENTIFIER_COLLECTOR, new TreeSet<Identifier>(identifierComparator));

        if (!fids.isEmpty()) {
            List<Data> recordsFound = queryFidIndex(fids);
            if (recordsFound != null) {
                records.addAll(recordsFound);
            }
        }

        if (records.isEmpty())
            return null;
        
        // grab a geometry factory... check for a special hint
        Hints hints = query.getHints(); 
        GeometryFactory geometryFactory = (GeometryFactory) hints.get(Hints.JTS_GEOMETRY_FACTORY);
        if (geometryFactory == null) {
            // look for a coordinate sequence factory
            CoordinateSequenceFactory csFactory = 
                (CoordinateSequenceFactory) hints.get(Hints.JTS_COORDINATE_SEQUENCE_FACTORY);

            if (csFactory != null) {
                geometryFactory = new GeometryFactory(csFactory);
            }
        }

        if (geometryFactory == null) {
            // fall back on the default one
            geometryFactory = new GeometryFactory();
        }

        ShapefileReader reader = new ShapefileReader(shpFiles, false, false, geometryFactory);
        try {
            ret = new ReferencedEnvelope(getSchema().getCoordinateReferenceSystem());
            for (Iterator iter = records.iterator(); iter.hasNext();) {
                Data data = (Data) iter.next();
                reader.goTo(((Long) data.getValue(1)).intValue());
                Record record = reader.nextRecord();
                ret.expandToInclude(new Envelope(record.minX, record.maxX,
                        record.minY, record.maxY));
            }
            return ret;
        } finally {
            reader.close();
        }
    }

    public String id() {
        return getClass().getName() + ": " + getCurrentTypeName();
    }
 
    @Override
    protected Set getSupportedHints() {
        Set<Hints.Key> hints = new HashSet<Hints.Key>();
        hints.add( Hints.FEATURE_DETACHED );
        hints.add( Hints.JTS_GEOMETRY_FACTORY );
        hints.add( Hints.JTS_COORDINATE_SEQUENCE_FACTORY );
        hints.add( Hints.GEOMETRY_DISTANCE);
        return hints;
    }
}
