/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package usda.weru.gis.data;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import javax.measure.Measurable;
import javax.measure.quantity.Length;
import org.geotools.data.DataStore;
import usda.weru.gis.*;
import org.apache.log4j.Logger;
import org.geotools.data.FeatureSource;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.factory.Hints;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.filter.text.cql2.CQL;
import org.geotools.filter.text.cql2.CQLException;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.opengis.feature.Feature;
import org.opengis.feature.Property;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.PropertyDescriptor;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.geometry.DirectPosition;

/**
 *
 * @param <T> Object type returned by this lookup.
 * @author Joseph A. Levin <joelevin@weru.ksu.edu>
 */
public abstract class AbstractLookup<T> implements GISLookup<T> {

    private static final Logger LOGGER = Logger.getLogger(AbstractLookup.class);
    private List<Index> c_sourceIndex;
    private final List<T> EMPTY_OBJECTS = Collections.emptyList();
    private final List<Feature> EMPTY_FEATURES = Collections.emptyList();

    /**
     * 
     * @param sources
     * @param filterProvider
     * @return
     */
    protected List<Feature> queryFeatures(Iterator<FeatureSource> sources, FilterProvider filterProvider) {
        try {
            List<Feature> temp = new LinkedList<Feature>();
            if (sources.hasNext()) {
                while (sources.hasNext()) {
                    FeatureSource source = sources.next();
                    FeatureCollection features = null;
                    try {
                        Filter filter = filterProvider.getFilter(source);
                        if (filter != null) {
                            //grab the filtered features from the source.
                            features = source.getFeatures(filter);
                        } else {
                            //filter is null, continue the loop to the next source
                            continue;
                        }
                    } catch (IOException ioe) {
                        LOGGER.error("Problem executing filter on source.", ioe);
                        //there was an error with the data source
                        return EMPTY_FEATURES;
                    }

                    if (features != null && !features.isEmpty()) {
                        //we got something back from the query, yay!

                        FeatureIterator iterator = features.features();
                        try {
                            while (iterator.hasNext()) {
                                Feature feature = iterator.next();
                                if (feature != null) {
                                    temp.add(feature);
                                }
                            }
                        } finally {
                            features.close(iterator);
                        }

                    }

                }
            }

            return temp;
        } catch (Exception e) {
            LOGGER.error("Problem executing gis query.", e);
            return EMPTY_FEATURES;
        }
    }

    /**
     * 
     * @param sources
     * @param filterProvider
     * @return
     */
    protected List<T> queryObjects(Iterator<FeatureSource> sources, FilterProvider filterProvider) {
        try {

            List<T> temp = new LinkedList<T>();

            if (sources.hasNext()) {
                while (sources.hasNext()) {
                    FeatureSource source = sources.next();
                    FeatureCollection features = null;
                    try {
                        Filter filter = filterProvider.getFilter(source);
                        if (filter != null) {
                            //filter is not null, query the source
                            features = source.getFeatures(filter);
                        } else {
                            //filter is null, continue
                            continue;

                        }
                    } catch (IOException ioe) {
                        //ioe.printStackTrace();
                        //there was an error with the data source
                        return EMPTY_OBJECTS;
                    }

                    if (features != null && !features.isEmpty()) {
                        //we got something back from the query, yay!

                        FeatureIterator iterator = features.features();
                        try {
                            while (iterator.hasNext()) {
                                Feature feature = iterator.next();
                                T t = create(source, feature);
                                //a lookup is allowed to return null if no object should be created for the feature source
                                if (t != null) {
                                    temp.add(t);
                                }

                            }
                        } finally {
                            features.close(iterator);
                        }

                    }

                }
            }

            return temp;
        } catch (Exception e) {
            LOGGER.error("Problem executing gis query.", e);
            return EMPTY_OBJECTS;
        }

    }

    protected abstract T create(FeatureSource source, Feature feature);

    @Override
    public List<T> lookup() {
        final Hints hints = new Hints(Hints.CRS, DefaultGeographicCRS.WGS84);
        final FilterFactory2 factory = CommonFactoryFinder.getFilterFactory2(hints);
        FilterProvider filters = new FilterProvider() {

            @Override
            public Filter getFilter(FeatureSource source) {
                return createFilter(hints, factory, source);
            }
        };
        return queryObjects(sources(), filters);
    }

    protected Filter createFilter(Hints hints, FilterFactory2 factory, FeatureSource source) {
        try {
            //selects everything!
            String cql = "include";
            return CQL.toFilter(cql);
        } catch (CQLException cqe) {
            return null;
        }
    }

    @Override
    public List<T> lookup(final DirectPosition latlong) {
        final Hints hints = new Hints(Hints.CRS, DefaultGeographicCRS.WGS84);
        final FilterFactory2 factory = CommonFactoryFinder.getFilterFactory2(hints);

        FilterProvider filters = new FilterProvider() {

            @Override
            public Filter getFilter(FeatureSource source) {
                return createFilter(hints, factory, source, latlong);
            }
        };
        return queryObjects(sources(), filters);
    }

    protected Filter createFilter(Hints hints, FilterFactory2 factory, FeatureSource source, DirectPosition latlong) {
        if(latlong == null){
            return null;
        }

        double x = latlong.getOrdinate(1);
        double y = latlong.getOrdinate(0);
        try {
            return CQL.toFilter("CONTAINS(the_geom, POINT (" + x + " " + y + "))", factory);

        } catch (CQLException ce) {
            ce.printStackTrace();
            return null;
        }
    }

    @Override
    public List<T> lookup(final DirectPosition latlong, final Measurable<Length> radius) {
        final Hints hints = new Hints(Hints.CRS, DefaultGeographicCRS.WGS84);
        final FilterFactory2 factory = CommonFactoryFinder.getFilterFactory2(hints);

        FilterProvider filters = new FilterProvider() {

            @Override
            public Filter getFilter(FeatureSource source) {
                return createFilter(hints, factory, source, latlong, radius);
            }
        };
        return queryObjects(sources(), filters);
    }

    protected Filter createFilter(Hints hints, FilterFactory2 factory, FeatureSource source, DirectPosition latlong, Measurable<Length> radius) {

        throw new UnsupportedOperationException("Not implemented yet.");
    }

    @Override
    public List<Feature> lookup(final T object) {
        final Hints hints = new Hints(Hints.CRS, DefaultGeographicCRS.WGS84);
        final FilterFactory2 factory = CommonFactoryFinder.getFilterFactory2(hints);

        FilterProvider filters = new FilterProvider() {

            @Override
            public Filter getFilter(FeatureSource source) {
                return createFilter(hints, factory, source, object);
            }
        };
        return queryFeatures(sources(), filters);
    }

    /*
     * Intended as a reverese filter.  Allows a lookup to return all the features
     * that could be used to create the given object.  Must be implemented by subclasses.
     */
    protected Filter createFilter(final Hints hints, final FilterFactory2 factory, final FeatureSource source, final T object) {


        return null;
    }

    @Override
    public boolean supports(Class c) {
        return c.isAssignableFrom(getSupportedClass());
    }

    protected abstract Class<T> getSupportedClass();

    @Override
    public void register(String id, DataStore data) {
        if (id == null || data == null) {
            throw new IllegalStateException("Parameters may not be null.");
        }
        try {
            for (String typeName : data.getTypeNames()) {
                FeatureSource features = data.getFeatureSource(typeName);
                if (supports(features)) {
                    //this featuresource can be used, let's remember that
                    index(id, typeName);
                }
            }
        } catch (IOException ioe) {
            //TODO: handle logging
            ioe.printStackTrace();
        }
    }

    protected Iterator<FeatureSource> sources() {
        return new InternalIndexIterator();
    }

    /**
     *
     * @param source
     * @return true if the lookup will use this featuresource
     */
    protected abstract boolean supports(FeatureSource source);

    //source index methods
    private void index(String storeId, String typeName) {
        if (c_sourceIndex == null) {
            c_sourceIndex = new ArrayList<Index>();
        }

        Index index = new Index(storeId, typeName);
        c_sourceIndex.add(index);

    }

    private class InternalIndexIterator implements Iterator<FeatureSource> {

        private Index[] c_queue;
        int c_index = 0;

        public InternalIndexIterator() {
            if (c_sourceIndex != null) {
                Index[] temp = c_sourceIndex.toArray(new Index[c_sourceIndex.size()]);
                c_queue = temp;
            } else {
                c_queue = null;
            }
        }

        public boolean hasNext() {
            return c_queue != null && c_index < c_queue.length;
        }

        public FeatureSource next() {
            FeatureSource source = null;
            try {
                Index index = c_queue[c_index++];
                source = GISData.getInstance().source(index.getDataId(), index.getTypeName());
            } catch (IOException ioe) {
                source = null;
            }

            //TODO: perhaps handle the exceptions or a null source
            return source;

        }

        public void remove() {
            throw new UnsupportedOperationException("Remove not supported.");
        }
    }

    private static class Index {

        private final String c_data;
        private final String c_type;

        public Index(String data, String type) {
            c_data = data;
            c_type = type;
        }

        public String getDataId() {
            return c_data;
        }

        public String getTypeName() {
            return c_type;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final Index other = (Index) obj;
            if ((this.c_data == null) ? (other.c_data != null) : !this.c_data.equals(other.c_data)) {
                return false;
            }
            if ((this.c_type == null) ? (other.c_type != null) : !this.c_type.equals(other.c_type)) {
                return false;
            }
            return true;
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 17 * hash + (this.c_data != null ? this.c_data.hashCode() : 0);
            hash = 17 * hash + (this.c_type != null ? this.c_type.hashCode() : 0);
            return hash;
        }
    }

    protected boolean hasColumn(FeatureSource source, String column, Class type) {
        FeatureType schema = source.getSchema();
        if (schema != null) {
            PropertyDescriptor property = schema.getDescriptor(column);
            if (property != null) {
                return type.isAssignableFrom(property.getType().getBinding());
            }
        }

        return false;
    }

    protected <T> T getValue(Feature feature, String name, Class<T> type) {
        //TODO: add more robust error handling
        Property property = feature.getProperty(name);
        return (T) property.getValue();
    }

    protected abstract class FilterProvider {

        public abstract Filter getFilter(FeatureSource source);
    }
}
