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 java.util.NoSuchElementException;
import javax.measure.Quantity;
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.util.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;
import usda.weru.util.Caster;

/**
 *
 * @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()) {
                        try (FeatureIterator<?> iterator = features.features()) {
                            while (iterator.hasNext()) {
                                Feature feature = iterator.next();
                                if (feature != null) {
                                    temp.add(feature);
                                }
                            }
                        }

                    }

                }
            }

            return temp;
        } catch (NoSuchElementException 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>();

            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()) {
                    try (FeatureIterator<?> iterator = features.features()) {
                        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);
                            }

                        }
                    }

                }

            }

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

    }

    /**
     *
     * @param source
     * @param feature
     * @return
     */
    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);
    }

    /**
     *
     * @param hints
     * @param factory
     * @param source
     * @return
     */
    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);
            }
        };
        Iterator<FeatureSource<?, ?>> s = sources();
        return queryObjects(s, filters);
    }

    /**
     *
     * @param hints
     * @param factory
     * @param source
     * @param latlong
     * @return
     */
    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 Quantity<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);
    }

    /**
     *
     * @param hints
     * @param factory
     * @param source
     * @param latlong
     * @param radius
     * @return
     */
    protected Filter createFilter(Hints hints, FilterFactory2 factory, FeatureSource<?, ?> source,
            DirectPosition latlong, Quantity<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.
     *
     * @param hints
     * @param factory
     * @param source
     * @param object
     * @return
     */
    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());
    }

    /**
     *
     * @return
     */
    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();
        }
    }

    /**
     *
     * @return
     */
    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 final 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;
            }
        }

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

        @Override
        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;
        }

        @Override
        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;
            } else 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;
            } else 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;
        }
    }

    /**
     *
     * @param source
     * @param column
     * @param type
     * @return
     */
    protected boolean hasColumn(FeatureSource<?, ?> source, String column, Class<?> type) {
        FeatureType schema = null;
        try {
            schema = source.getSchema();
        } catch(NoClassDefFoundError e) {
            System.err.println("No class definition in AbstractLookup.hasColumn");
        } catch(java.util.ServiceConfigurationError g) {
            System.err.println("service error in AbstractLookup.hasColumn");
        } catch(Throwable f) {
            System.err.println("Service Config Error");
        }
        if (schema != null) {
            PropertyDescriptor property = schema.getDescriptor(column);
            if (property != null) {
                return type.isAssignableFrom(property.getType().getBinding());
            }
        }
        return false;
    }

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

    /**
     *
     */
    protected abstract class FilterProvider {

        /**
         *
         * @param source
         * @return
         */
        public abstract Filter getFilter(FeatureSource<?, ?> source);
    }
}
