package org.geotools.data.collection;

import java.awt.RenderingHints.Key;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.geotools.data.DataAccess;
import org.geotools.data.DataUtilities;
import org.geotools.data.FeatureListener;
import org.geotools.data.Query;
import org.geotools.data.QueryCapabilities;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.data.store.EmptyFeatureCollection;
import org.geotools.data.store.ReTypingFeatureCollection;
import org.geotools.data.store.ReprojectingFeatureCollection;
import org.geotools.feature.collection.DecoratingSimpleFeatureCollection;
import org.geotools.feature.collection.FilteringSimpleFeatureCollection;
import org.geotools.feature.collection.MaxSimpleFeatureCollection;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.Name;
import org.opengis.filter.Filter;
import org.opengis.filter.sort.SortBy;

/**
 * This is a "port" of ContentFeatureSource to work with an iterator.
 * <p>
 * To use this class please "wrap" CollectionFeatureSource around your choice of FeatureCollection.
 * 
 * <pre>
 * SimpleFeatureCollection collection = new ListFeatureCollection(schema);
 * collection.add(feature1);
 * collection.add(feature2);
 * FeatureSource source = new CollectionFeatureSource(collection);
 * </pre>
 * <p>
 * Note to implementors: If you are performing "real I/O" please use ContentFeatureSource as it
 * provides support for IOException.
 * 
 * @author Jody
 *
 * @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/build/maven/javadoc/../../../modules/library/main/src/main/java/org/geotools/data/collection/CollectionFeatureSource.java $
 */
public class CollectionFeatureSource implements SimpleFeatureSource {
    protected SimpleFeatureCollection collection;

    /**
     * observers
     */
    protected List<FeatureListener> listeners = null;

    private QueryCapabilities capabilities;

    private Set<Key> hints;

    public CollectionFeatureSource(SimpleFeatureCollection collection) {
        this.collection = collection;
    }

    public SimpleFeatureType getSchema() {
        return collection.getSchema();
    }

    public synchronized void addFeatureListener(FeatureListener listener) {
        if (listeners == null) {
            listeners = Collections.synchronizedList(new ArrayList<FeatureListener>());
        }
        listeners.add(listener);
    }

    public synchronized void removeFeatureListener(FeatureListener listener) {
        if (listeners == null) {
            return;
        }
        listeners.remove(listener);
    }

    public ReferencedEnvelope getBounds() throws IOException {
        return collection.getBounds();
    }

    public ReferencedEnvelope getBounds(Query query) throws IOException {
        return getFeatures(query).getBounds();
    }

    public int getCount(Query query) throws IOException {
        return getFeatures(query).size();
    }

    public DataAccess<SimpleFeatureType, SimpleFeature> getDataStore() {
        throw new UnsupportedOperationException("CollectionFeatureSource is an inmemory wrapper");
    }

    public Name getName() {
        return collection.getSchema().getName();
    }

    public synchronized QueryCapabilities getQueryCapabilities() {
        if (capabilities == null) {
            capabilities = new QueryCapabilities() {
                public boolean isOffsetSupported() {
                    return true;
                }

                public boolean isReliableFIDSupported() {
                    return true;
                }

                public boolean supportsSorting(org.opengis.filter.sort.SortBy[] sortAttributes) {
                    return true;
                }
            };
        }
        return capabilities;
    }

    public synchronized Set<Key> getSupportedHints() {
        if (hints == null) {
            Set<Key> supports = new HashSet<Key>();
            // supports.add( Hints.FEATURE_DETACHED );
            hints = Collections.unmodifiableSet(supports);
        }
        return hints;
    }

    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder();
        buf.append("CollectionFeatureSource:");
        buf.append(collection);
        return buf.toString();
    }

    //
    // GET FEATURES
    // This forms the heart of the CollectionFeatureSource implementation
    // Use: DataUtilities.mixQueries(this.query, query, "subCollection" ) as needed
    //
    public SimpleFeatureCollection getFeatures() throws IOException {
        return getFeatures( Query.ALL );
    }

    public SimpleFeatureCollection getFeatures(Filter filter) {
        Query query = new Query(getSchema().getTypeName(), filter);
        return getFeatures(query);
    }

    public SimpleFeatureCollection getFeatures(Query query) {
        query = DataUtilities.resolvePropertyNames(query, getSchema());
        final int offset = query.getStartIndex() != null ? query.getStartIndex() : 0;
        if (offset > 0 & query.getSortBy() == null) {
            if (!getQueryCapabilities().supportsSorting(query.getSortBy())){
                throw new IllegalStateException("Feature source does not support this sorting "
                        + "so there is no way a stable paging (offset/limit) can be performed");
            }
            Query copy = new Query(query);
            copy.setSortBy(new SortBy[] { SortBy.NATURAL_ORDER });
            query = copy;
        }
        SimpleFeatureCollection features = collection;
        // step one: filter
        if( query.getFilter() != null && query.getFilter().equals(Filter.EXCLUDE)){
            return new EmptyFeatureCollection( getSchema() );
        }
        if (query.getFilter() != null && query.getFilter() != Filter.INCLUDE) {
            features = new FilteringSimpleFeatureCollection(features, query.getFilter());
        }
        // step two: reproject
        if (query.getCoordinateSystemReproject() != null) {
            features = new ReprojectingFeatureCollection(features, query
                    .getCoordinateSystemReproject());
        }
        // step two sort! (note this makes a sorted copy)
        if (query.getSortBy() != null && query.getSortBy().length != 0) {
            SimpleFeature array[] = features.toArray(new SimpleFeature[features.size()]);
            // Arrays sort is stable (not resorting equal elements)
            for (SortBy sortBy : query.getSortBy()) {
                Comparator<SimpleFeature> comparator = DataUtilities.sortComparator(sortBy);
                Arrays.sort(array, comparator);
            }
            ArrayList<SimpleFeature> list = new ArrayList<SimpleFeature>(Arrays.asList(array));
            features = new ListFeatureCollection(getSchema(), list);
        }

        // step three skip to start and return max number of fetaures
        if (offset > 0 || !query.isMaxFeaturesUnlimited()) {
            long max = Long.MAX_VALUE;
            if (!query.isMaxFeaturesUnlimited()) {
                max = query.getMaxFeatures();
            }
            features = new MaxSimpleFeatureCollection(features, offset, max);
        }

        // step four - retyping
        // (It would be nice to do this earlier so as to not have all the baggage
        // of unneeded attributes)
        if (query.getPropertyNames() != Query.ALL_NAMES) {
            // rebuild the type and wrap the reader
            SimpleFeatureType schema = features.getSchema();
            SimpleFeatureType target = SimpleFeatureTypeBuilder.retype(schema, query
                    .getPropertyNames());

            // do an equals check because we may have needlessly retyped (that is,
            // the subclass might be able to only partially retype)
            if (!target.equals(schema)) {
                features = new ReTypingFeatureCollection(features, target);
            }
        }
        // Wrap up the results in a method that allows subCollection
        return new SubCollection( query, features );
    }

    /**
     * SubCollection for CollectionFeatureSource.
     * <p>
     * Will route any calls refining the feature collection back to CollectionFeatureSource. This is
     * based on the success of ContentFeatureCollection.
     * </p>
     * 
     * @author Jody
     */
    protected class SubCollection extends DecoratingSimpleFeatureCollection {
        private Query query;
        protected SubCollection(Query query, SimpleFeatureCollection features) {
            super(features);
            this.query = query;
        }
        public SimpleFeatureCollection subCollection(Filter filter) {
            Query q = new Query(getSchema().getTypeName(), filter);
            
            Query subQuery = DataUtilities.mixQueries(query, q, q.getHandle() );
            return CollectionFeatureSource.this.getFeatures( subQuery );
        }
    }
}
