/*
 *    Geotools2 - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2005-2006, GeoTools Project Managment Committee (PMC)
 *
 *    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.complex;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Logger;

import javax.xml.namespace.QName;

import org.apache.commons.collections.map.LinkedMap;
import org.geotools.data.DefaultQuery;
import org.geotools.data.Query;
import org.geotools.data.complex.filter.XPath;
import org.geotools.data.complex.filter.XPath.Step;
import org.geotools.data.complex.filter.XPath.StepList;
import org.geotools.feature.iso.Types;
import org.geotools.filter.FilterFactoryImplNamespaceAware;
import org.opengis.feature.Attribute;
import org.opengis.feature.ComplexAttribute;
import org.opengis.feature.Feature;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.AttributeType;
import org.opengis.feature.type.Name;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.expression.Expression;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.NamespaceSupport;

/**
 * An alternative strategy to fetch grouped multivalued content.
 * 
 * @author Gabriel Roldan
 * @version $Id: GroupingFeatureIterator2.java 27863 2007-11-12 20:34:34Z desruisseaux $
 * @source $URL:
 *         http://svn.geotools.org/geotools/branches/2.4.x/modules/unsupported/community-schemas/community-schema-ds/src/main/java/org/geotools/data/complex/GroupingFeatureIterator.java $
 * @since 2.4
 */
class GroupingFeatureIterator2 extends AbstractMappingFeatureIterator {
    private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger(GroupingFeatureIterator2.class
            .getPackage().getName());

    /**
     * maxFeatures restriction value as provided by query
     */
    private int maxFeatures;

    /**
     * 
     */
    private Feature curSrcFeature;

    /** counter to ensure maxFeatures is not exceeded */
    private int featureCounter;

    /**
     * List of attribute mappings marked as multivalued, and any attribute which
     * maps to a child attribute of any multivalued mapping. The keys in the map
     * are attribute mappings explicitly set as multivalued. The values in the
     * map are the list of mappings whose target xpath expressions correspond to
     * child properties of the target xpath of the mapping used as key.
     */
    private LinkedMap multivaluedMappings;

    /**
     * List of attribute mappings not marked as multivalued and whose target
     * xpath is not a child of any multivalued attribute.
     */
    private List /* AttributeMapping */singleValuedMappings;

    /**
     * flag to avoid fetching multiple source feature in repeated calld to
     * hasNext() without calls to next() in the middle
     */
    private boolean hasNextCalled;

    private XPath xpathAttributeBuilder;

    public GroupingFeatureIterator2(final ComplexDataStore store,
            final FeatureTypeMapping mappings, final Query query) throws IOException {
        super(store, mappings, query);
        xpathAttributeBuilder = new XPath();
        xpathAttributeBuilder.setFeatureFactory(super.attf);
        NamespaceSupport namespaces = mappings.getNamespaces();
        // FilterFactory namespaceAwareFilterFactory =
        // CommonFactoryFinder.getFilterFactory(hints);
        FilterFactory namespaceAwareFilterFactory = new FilterFactoryImplNamespaceAware(namespaces);
        xpathAttributeBuilder.setFilterFactory(namespaceAwareFilterFactory);

        splitMappings();
    }

    protected Query getUnrolledQuery(Query query) {
        maxFeatures = query.getMaxFeatures();
        Query unmappedQuery = store.unrollQuery(query, mapping);
        ((DefaultQuery) unmappedQuery).setMaxFeatures(Integer.MAX_VALUE);

        unmappedQuery = ensureGroupingAttsPresent(unmappedQuery);

        return unmappedQuery;
    }

    public boolean hasNext() {
        if (hasNextCalled) {
            return curSrcFeature != null;
        }

        boolean exists = false;

        if (sourceFeatures != null && featureCounter < maxFeatures) {

            exists = this.sourceFeatures.hasNext();

            if (exists && this.curSrcFeature == null) {
                this.curSrcFeature = (Feature) this.sourceFeatures.next();
            }
        }

        if (!exists) {
            LOGGER.finest("no more features, produced " + featureCounter);
        }

        hasNextCalled = true;
        if(!exists){
            close();
        }
        return exists;
    }

    public Object next() {
        if (!hasNext()) {
            throw new IllegalStateException("there are no more features in this iterator");
        }
        Feature next;
        try {
            next = computeNext();
        } catch (IOException e) {
            close();
            throw new RuntimeException(e);
        }
        hasNextCalled = false;
        ++featureCounter;
        return next;
    }

    // ///////////////////

    /**
     * 
     */
    private Feature computeNext() throws IOException {
        assert this.curSrcFeature != null : "hastNext not called?";

        // get the mapping set of a feature attribute
        final AttributeDescriptor targetNode = mapping.getTargetFeature();

        // create the target feature and iterate in the source ones to set its
        // values.
        final String fid = extractIdForFeature(curSrcFeature);
        final Feature targetFeature = attf.createFeature(null, targetNode, fid);

        setNonMultivaluedAttributes(targetFeature);

        setMultiValuedAttributes(targetFeature);

        if (!sourceFeatures.hasNext()) {
            close();
        }
        return targetFeature;
    }

    private void setNonMultivaluedAttributes(final Feature targetFeature) throws IOException {
        AttributeMapping mapping;
        for (Iterator it = this.singleValuedMappings.iterator(); it.hasNext();) {
            mapping = (AttributeMapping) it.next();
            StepList targetXPath = mapping.getTargetXPath();
            if (targetXPath.size() == 1) {
                Step rootStep = (Step) targetXPath.get(0);
                QName stepName = rootStep.getName();
                if (Types.equals(targetFeature.getDescriptor().getName(), stepName)) {
                    // ignore the top level mapping for the Feature
                    // itself
                    // as it was already set
                    continue;
                }
            }
            setSingleValuedAttribute(targetFeature, curSrcFeature, mapping);
        }
    }

    private void setMultiValuedAttributes(final Feature targetFeature) {
        final List /* Name */groupByAttNames = toTypeNames(mapping.getGroupByAttNames());
        // the grouping values to check for equality
        final List baseGroupingAttributes = extractGroupingAttributes(curSrcFeature,
                groupByAttNames);

        int index = 1;
        setMultiValuedAttributes(targetFeature, curSrcFeature, index);

        List currFeatureGroupingAtts;
        while (sourceFeatures.hasNext()) {
            curSrcFeature = (Feature) sourceFeatures.next();
            currFeatureGroupingAtts = extractGroupingAttributes(curSrcFeature, groupByAttNames);
            if (baseGroupingAttributes.equals(currFeatureGroupingAtts)) {
                index++;
                setMultiValuedAttributes(targetFeature, curSrcFeature, index);

                if (index % 100 == 0) {
                    System.out.print('-');
                }
            } else {
                System.out.print('\n');
                break;
            }
        }
        System.out.print('\n');
    }

    private void setMultiValuedAttributes(final Feature targetFeature, final Feature curSrcFeature,
            final int index) {

        AttributeMapping mapping;
        StepList targetXpathAttr;
        Expression identifierExpression;
        Expression sourceExpression;
        Map clientProperties;
        AttributeType targetNodeInstance;

        for (Iterator it = multivaluedMappings.entrySet().iterator(); it.hasNext();) {
            Map.Entry entry = (Entry) it.next();
            mapping = (AttributeMapping) entry.getKey();
            List children = (List) entry.getValue();
            targetXpathAttr = mapping.getTargetXPath();
            targetXpathAttr = setIndexOfLastStep(targetFeature, targetXpathAttr, index);

            identifierExpression = mapping.getIdentifierExpression();
            sourceExpression = mapping.getSourceExpression();
            clientProperties = mapping.getClientProperties();
            targetNodeInstance = mapping.getTargetNodeInstance();

            setXpathValue(targetFeature, identifierExpression, sourceExpression, targetXpathAttr,
                    targetNodeInstance, clientProperties);

            final int insertPositon = targetXpathAttr.size();
            for (Iterator childIterator = children.iterator(); childIterator.hasNext();) {
                mapping = (AttributeMapping) childIterator.next();
                targetXpathAttr = mapping.getTargetXPath();
                targetXpathAttr = insertIndexInXpath(targetFeature, targetXpathAttr, index,
                        insertPositon);

                identifierExpression = mapping.getIdentifierExpression();
                sourceExpression = mapping.getSourceExpression();
                clientProperties = mapping.getClientProperties();
                targetNodeInstance = mapping.getTargetNodeInstance();

                setXpathValue(targetFeature, identifierExpression, sourceExpression,
                        targetXpathAttr, targetNodeInstance, clientProperties);
            }
        }
    }

    private void setXpathValue(final Attribute targetFeature, final Expression idExpression,
            final Expression sourceExpression, final StepList targetXpathAttr,
            final AttributeType targetNodeInstance, final Map clientProperties) {
        Object value;

        try {
            value = getValue(sourceExpression, curSrcFeature);
        } catch (Exception e) {
            // HACK: what we actually need to resolve is dealing
            // with queries that restricts the attributes returned
            // by the source featurestore
            return;
        }
        String id = extractIdForAttribute(idExpression, curSrcFeature);

        Attribute instance = xpathAttributeBuilder.set(targetFeature, targetXpathAttr, value, id,
                targetNodeInstance);
        Map clientPropsMappings = clientProperties;
        setClientProperties(instance, curSrcFeature, clientPropsMappings);
    }

    private final StepList setIndexOfLastStep(final Attribute root,
            final StepList xpathAttrDefinition, int index) {

        int insertPosition = xpathAttrDefinition.size();

        StepList indexXpath = insertIndexInXpath(root, xpathAttrDefinition, index, insertPosition);

        return indexXpath;
    }

    /**
     * Insert index into step of xpath; Position indicate the step.
     * 
     * @param featureType
     * @param attrXpath
     * @param index
     * @param insertPositon
     * 
     * @return String
     */
    private final StepList insertIndexInXpath(final Attribute root, final StepList attrXpath,
            final int index, final int insertPositon) {

        // Constructs an Xpath adding index in the step corresponding to complex
        // attribute
        StepList stepList = (StepList) attrXpath.clone();

        Step step = (Step) stepList.get(insertPositon - 1);
        Step newStep = new XPath.Step(step.getName(), index);

        stepList.set(insertPositon - 1, newStep);

        return stepList;
    }

    private List toTypeNames(final List groupByAttNames) {
        List typeNames = new ArrayList(groupByAttNames.size());
        String sourceAttName;
        Name attributeName;
        for (Iterator it = groupByAttNames.iterator(); it.hasNext();) {
            sourceAttName = (String) it.next();
            attributeName = Types.typeName(sourceAttName);
            typeNames.add(attributeName);
        }
        return typeNames;
    }

    /**
     * Extract the attributes from grouping attributes.
     * 
     * @param groupByAttNames
     * 
     * @param Feature
     *            a source feature
     * @return List<List<Attribute>> the the contened list has the attributes
     *         required
     */
    private final List/* <List<Attribute>> */extractGroupingAttributes(
            final ComplexAttribute srcFeature, final List /* Name */groupByAttNames) {

        List/* <List<Attribute>> */attrGroup = new ArrayList/* <List<Attribute>> */(
                groupByAttNames.size());

        for (Iterator it = groupByAttNames.iterator(); it.hasNext();) {
            Name name = (Name) it.next();
            List/* <Attribute> */listAttrForName = srcFeature.get(name);
            attrGroup.add(listAttrForName);
        }

        return attrGroup;
    }

    /**
     * Sets the values of grouping attributes.
     * 
     * @param sourceFeature
     * @param groupingMappings
     * @param targetFeature
     * 
     * @return Feature. Target feature sets with simple attributes
     */
    private void setSingleValuedAttribute(final Feature target, final Feature source,
            final AttributeMapping attMapping) throws IOException {

        final Expression sourceExpression = attMapping.getSourceExpression();
        final AttributeType targetNodeType = attMapping.getTargetNodeInstance();
        final StepList xpath = attMapping.getTargetXPath();

        Object value = getValue(sourceExpression, source);

        String id = null;
        if (Expression.NIL != attMapping.getIdentifierExpression()) {
            id = extractIdForAttribute(attMapping.getIdentifierExpression(), this.curSrcFeature);
        }
        Attribute instance = xpathAttributeBuilder.set(target, xpath, value, id, targetNodeType);
        Map clientPropsMappings = attMapping.getClientProperties();
        setClientProperties(instance, source, clientPropsMappings);
    }

    private void setClientProperties(final Attribute target, final Feature source,
            final Map clientProperties) {
        if (clientProperties.size() == 0) {
            return;
        }
        final Map nodeAttributes = new HashMap();
        final AttributeDescriptor node = target.getDescriptor();

        for (Iterator it = clientProperties.entrySet().iterator(); it.hasNext();) {
            Map.Entry entry = (Map.Entry) it.next();
            org.opengis.feature.type.Name propName = (org.opengis.feature.type.Name) entry.getKey();
            Expression propExpr = (Expression) entry.getValue();

            Object propValue = getValue(propExpr, source);

            nodeAttributes.put(propName, propValue);
        }

        node.putUserData(Attributes.class, nodeAttributes);
    }

    /**
     * Split the attribute mappings in two sets, the ones that belong to a
     * multivalued property {@link #multivaluedMappings} and the ones that not
     * {@link #singleValuedMappings}.
     */
    private void splitMappings() {
        this.multivaluedMappings = new LinkedMap();
        this.singleValuedMappings = new ArrayList(mapping.getAttributeMappings());

        for (Iterator it = mapping.getAttributeMappings().iterator(); it.hasNext();) {
            AttributeMapping am = (AttributeMapping) it.next();
            if (am.isMultiValued()) {
                singleValuedMappings.remove(am);
                multivaluedMappings.put(am, new ArrayList(2));
            }
        }

        for (Iterator mvaluedPaths = multivaluedMappings.entrySet().iterator(); mvaluedPaths
                .hasNext();) {
            final Map.Entry entry = (Entry) mvaluedPaths.next();
            final AttributeMapping parentMapping = (AttributeMapping) entry.getKey();
            final List childMappings = (List) entry.getValue();
            final StepList mvaluedPath = parentMapping.getTargetXPath();

            for (Iterator mappings = mapping.getAttributeMappings().iterator(); mappings.hasNext();) {
                final AttributeMapping am = (AttributeMapping) mappings.next();
                if (am == parentMapping) {
                    continue;
                }

                final StepList targetXPath = am.getTargetXPath();
                boolean isChild = true;
                if (targetXPath.size() >= mvaluedPath.size()) {
                    for (int currStepIdx = 0; currStepIdx < mvaluedPath.size(); currStepIdx++) {
                        XPath.Step parentStep = (XPath.Step) mvaluedPath.get(currStepIdx);
                        XPath.Step childStep = (XPath.Step) targetXPath.get(currStepIdx);
                        QName parentStepName = parentStep.getName();
                        QName childStepName = childStep.getName();
                        if (!parentStepName.equals(childStepName)) {
                            isChild = false;
                            break;
                        }
                    }
                } else {
                    isChild = false;
                }
                if (isChild) {
                    singleValuedMappings.remove(am);
                    childMappings.add(am);
                }
            }
        }
    }

    /**
     * Takes a Query and returns another one ensuring that all the grouping
     * attributes are requested, in order to be able of producing the correct
     * number of output features, for example, from a joined set of tables.
     * 
     * @param query
     * @return
     */
    private Query ensureGroupingAttsPresent(Query query) {
        if (query.retrieveAllProperties()) {
            return query;
        }

        List groupByAttributeNames = super.mapping.getGroupByAttNames();
        DefaultQuery neededQuery = new DefaultQuery(query);
        List requestedAtts = Arrays.asList(query.getPropertyNames());
        if (!requestedAtts.containsAll(groupByAttributeNames)) {
            List remaining = new ArrayList(groupByAttributeNames);
            remaining.removeAll(requestedAtts);
            LOGGER.fine("Adding missing grouping atts: " + remaining);

            List queryAtts = new ArrayList(remaining);
            queryAtts.addAll(requestedAtts);

            neededQuery.setPropertyNames(queryAtts);
        }
        return neededQuery;
    }
}
