/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.sis.internal.netcdf.impl;

import java.util.Map;
import java.util.List;
import java.util.Collection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Spliterator;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import java.util.function.Consumer;
import java.util.OptionalLong;
import java.io.IOException;
import org.apache.sis.math.Vector;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.internal.netcdf.DataType;
import org.apache.sis.internal.netcdf.DiscreteSampling;
import org.apache.sis.internal.netcdf.Resources;
import org.apache.sis.internal.feature.MovingFeature;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.util.collection.BackingStoreException;
import ucar.nc2.constants.CF;

// Branch-dependent imports
import org.apache.sis.feature.AbstractFeature;
import org.apache.sis.feature.DefaultFeatureType;
import org.apache.sis.feature.DefaultAttributeType;
import org.apache.sis.feature.AbstractIdentifiedType;


/**
 * Implementations of the discrete sampling features decoder. This implementation shall be able to decode at least the
 * netCDF files encoded as specified in the OGC 16-114 (OGC Moving Features Encoding Extension: netCDF) specification.
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 1.0
 * @since   0.8
 * @module
 */
final class FeaturesInfo extends DiscreteSampling {
    /**
     * The number of instances for each feature.
     */
    private final Vector counts;

    /**
     * The moving feature identifiers ("mfIdRef").
     * The amount of identifiers shall be the same than the length of the {@link #counts} vector.
     */
    private final VariableInfo identifiers;

    /**
     * The variable that contains time.
     */
    private final VariableInfo time;

    /**
     * The variable that contains <var>x</var> and <var>y</var> coordinate values (typically longitudes and latitudes).
     * All variables in this array shall have the same length, and that length shall be the same than {@link #time}.
     */
    private final VariableInfo[] coordinates;

    /**
     * Any custom properties.
     */
    private final VariableInfo[] properties;

    /**
     * The type of all features to be read by this {@code FeaturesInfo}.
     */
    private final DefaultFeatureType type;

    /**
     * Creates a new discrete sampling parser for features identified by the given variable.
     *
     * @param  decoder      the source of the features to create.
     * @param  counts       the count of instances per feature.
     * @param  identifiers  the feature identifiers.
     * @param  time         the variable that contains time.
     * @param  coordinates  the variable that contains <var>x</var> and <var>y</var> coordinate values.
     * @param  properties   the variables that contain custom properties.
     * @throws IllegalArgumentException if the given library is non-null but not available.
     */
    @SuppressWarnings("rawtypes")                               // Because of generic array creation.
    private FeaturesInfo(final ChannelDecoder decoder,
            final Vector counts, final VariableInfo identifiers, final VariableInfo time,
            final Collection<VariableInfo> coordinates, final Collection<VariableInfo> properties)
    {
        super(decoder.geomlib, decoder.listeners);
        this.counts      = counts;
        this.identifiers = identifiers;
        this.coordinates = coordinates.toArray(new VariableInfo[coordinates.size()]);
        this.properties  = properties .toArray(new VariableInfo[properties .size()]);
        this.time        = time;
        /*
         * Creates a description of the features to be read.
         */
        final Map<String,Object> info = new HashMap<>(4);
        final AbstractIdentifiedType[] pt = new AbstractIdentifiedType[this.properties.length + 2];
        DefaultAttributeType[] characteristics = null;
        for (int i=0; i<pt.length; i++) {
            final VariableInfo variable;
            final Class<?> valueClass;
            int minOccurs = 1;
            int maxOccurs = 1;
            switch (i) {
                case 0: {
                    variable        = identifiers;
                    valueClass      = Integer.class;
                    break;
                }
                case 1: {
                    variable        = null;
                    valueClass      = factory.polylineClass;
                    characteristics = new DefaultAttributeType[] {MovingFeature.TIME};
                    break;
                }
                default: {
                    // TODO: use more accurate Number subtype for value class.
                    variable        = this.properties[i-2];
                    valueClass      = (variable.meaning(0) != null) ? String.class : Number.class;
                    minOccurs       = 0;
                    maxOccurs       = Integer.MAX_VALUE;
                    break;
                }
            }
            info.put(DefaultAttributeType.NAME_KEY, (variable != null) ? variable.getName() : "trajectory");
            // TODO: add description.
            pt[i] = new DefaultAttributeType<>(info, valueClass, minOccurs, maxOccurs, null, characteristics);
        }
        String name = "Features";       // TODO: find a better name.
        info.put(DefaultAttributeType.NAME_KEY, decoder.nameFactory.createLocalName(decoder.namespace, name));
        type = new DefaultFeatureType(info, false, null, pt);
    }

    /**
     * Returns {@code true} if the given attribute value is one of the {@code cf_role} attribute values
     * supported by this implementation.
     */
    private static boolean isSupportedRole(final String role) {
        return CF.TRAJECTORY_ID.equalsIgnoreCase(role);
    }

    /**
     * Creates new discrete sampling parsers from the attribute values found in the given decoder.
     *
     * @throws IllegalArgumentException if the geometric object library is not available.
     * @throws ArithmeticException if the size of a variable exceeds {@link Integer#MAX_VALUE}, or other overflow occurs.
     */
    static FeaturesInfo[] create(final ChannelDecoder decoder) throws IOException, DataStoreException {
        final List<FeaturesInfo> features = new ArrayList<>(3);     // Will usually contain at most one element.
search: for (final VariableInfo counts : decoder.variables) {
            /*
             * Any one-dimensional integer variable having a "sample_dimension" attribute string value
             * will be taken as an indication that we have Discrete Sampling Geometries. That variable
             * shall be counting the number of feature instances, and another variable having the same
             * dimension (optionally plus a character dimension) shall give the feature identifiers.
             * Example:
             *
             *     dimensions:
             *         identifiers = 100;
             *         points = UNLIMITED;
             *     variables:
             *         int identifiers(identifiers);
             *             identifiers:cf_role = "trajectory_id";
             *         int counts(identifiers);
             *             counts:sample_dimension = "points";
             */
            if (counts.dimensions.length == 1 && counts.getDataType().isInteger) {
                final String sampleDimName = counts.getAttributeAsString(CF.SAMPLE_DIMENSION);
                if (sampleDimName != null) {
                    final DimensionInfo featureDimension = counts.dimensions[0];
                    final DimensionInfo sampleDimension = decoder.findDimension(sampleDimName);
                    if (sampleDimension == null) {
                        decoder.listeners.warning(decoder.resources().getString(Resources.Keys.DimensionNotFound_3,
                                decoder.getFilename(), counts.getName(), sampleDimName));
                        continue;
                    }
                    /*
                     * We should have another variable of the same name than the feature dimension name
                     * ("identifiers" in above example). That variable should have a "cf_role" attribute
                     * set to one of the values known to current implementation.  If we do not find such
                     * variable, search among other variables before to give up. That second search is not
                     * part of CF convention and will be accepted only if there is no ambiguity.
                     */
                    VariableInfo identifiers = decoder.findVariable(featureDimension.name);
                    if (identifiers == null || !isSupportedRole(identifiers.getAttributeAsString(CF.CF_ROLE))) {
                        VariableInfo replacement = null;
                        for (final VariableInfo alt : decoder.variables) {
                            if (alt.dimensions.length != 0 && alt.dimensions[0] == featureDimension
                                    && isSupportedRole(alt.getAttributeAsString(CF.CF_ROLE)))
                            {
                                if (replacement != null) {
                                    replacement = null;
                                    break;                  // Ambiguity found: consider that we found no replacement.
                                }
                                replacement = alt;
                            }
                        }
                        if (replacement != null) {
                            identifiers = replacement;
                        }
                        if (identifiers == null) {
                            decoder.listeners.warning(decoder.resources().getString(Resources.Keys.VariableNotFound_2,
                                    decoder.getFilename(), featureDimension.name));
                            continue;
                        }
                    }
                    /*
                     * At this point we found a variable that should be the feature identifiers.
                     * Verify that the variable dimensions are valid.
                     */
                    for (int i=0; i<identifiers.dimensions.length; i++) {
                        final boolean isValid;
                        switch (i) {
                            case 0:  isValid = (identifiers.dimensions[0] == featureDimension); break;
                            case 1:  isValid = (identifiers.getDataType() == DataType.CHAR); break;
                            default: isValid = false; break;                    // Too many dimensions
                        }
                        if (!isValid) {
                            decoder.listeners.warning(decoder.resources().getString(
                                    Resources.Keys.UnexpectedDimensionForVariable_4,
                                    decoder.getFilename(), identifiers.getName(),
                                    featureDimension.getName(), identifiers.dimensions[i].name));
                            continue search;
                        }
                    }
                    /*
                     * At this point, all information have been verified as valid. Now search all variables having
                     * the expected sample dimension. Those variable contains the actual data. For example if the
                     * sample dimension name is "points", then we may have:
                     *
                     *     double longitude(points);
                     *         longitude:axis = "X";
                     *         longitude:standard_name = "longitude";
                     *         longitude:units = "degrees_east";
                     *     double latitude(points);
                     *         latitude:axis = "Y";
                     *         latitude:standard_name = "latitude";
                     *         latitude:units = "degrees_north";
                     *     double time(points);
                     *         time:axis = "T";
                     *         time:standard_name = "time";
                     *         time:units = "minutes since 2014-11-29 00:00:00";
                     *     short myCustomProperty(points);
                     */
                    final Map<String,VariableInfo> coordinates = new LinkedHashMap<>();
                    final List<VariableInfo> properties  = new ArrayList<>();
                    for (final VariableInfo data : decoder.variables) {
                        if (data.dimensions.length == 1 && data.dimensions[0] == sampleDimension) {
                            final String axisType = data.getAttributeAsString(CF.AXIS);
                            if (axisType == null) {
                                properties.add(data);
                            } else if (coordinates.put(axisType, data) != null) {
                                continue search;    // Two axes of the same type: abort.
                            }
                        }
                    }
                    final VariableInfo time = coordinates.remove("T");
                    if (time != null) {
                        features.add(new FeaturesInfo(decoder, counts.read(), identifiers, time, coordinates.values(), properties));
                    }
                }
            }
        }
        return features.toArray(new FeaturesInfo[features.size()]);
    }

    /**
     * Returns the type of all features to be read by this {@code FeaturesInfo}.
     */
    @Override
    public DefaultFeatureType getType() {
        return type;
    }

    /**
     * Returns the number of features in this set.
     *
     * @return the number of features.
     */
    @Override
    protected OptionalLong getFeatureCount() {
        return OptionalLong.of(counts.size());
    }

    /**
     * Returns the stream of features.
     *
     * @param  parallel  ignored, since current version does not support parallelism.
     */
    @Override
    public Stream<AbstractFeature> features(boolean parallel) {
        return StreamSupport.stream(new Iter(), false);
    }

    /**
     * Implementation of the iterator returned by {@link #features(boolean)}.
     */
    private final class Iter implements Spliterator<AbstractFeature> {
        /**
         * Index of the next feature to read.
         */
        private int index;

        /**
         * Position in the data vectors of the next feature to read.
         * This is the sum of the length of data in all previous features.
         */
        private int position;

        /**
         * Creates a new iterator.
         */
        Iter() {
        }

        /**
         * Executes the given action only on the next feature, if any.
         *
         * @throws ArithmeticException if the size of a variable exceeds {@link Integer#MAX_VALUE}, or other overflow occurs.
         * @throws BackingStoreException if an {@link IOException} or {@link DataStoreException} occurred.
         *
         * @todo current reading process implies lot of seeks, which is inefficient.
         */
        @Override
        public boolean tryAdvance(final Consumer<? super AbstractFeature> action) {
            final int length = counts.intValue(index);
            final GridExtent extent = new GridExtent(null, new long[] {position},
                            new long[] {Math.addExact(position, length)}, false);
            final int[] step = {1};
            final Vector   id, t;
            final Vector[] coords = new Vector[coordinates.length];
            final Object[] props  = new Object[properties.length];
            try {
                id = identifiers.read();                    // Efficiency should be okay because of cached value.
                t = time.read(extent, step);
                for (int i=0; i<coordinates.length; i++) {
                    coords[i] = coordinates[i].read(extent, step);
                }
                for (int i=0; i<properties.length; i++) {
                    final VariableInfo p = properties[i];
                    final Vector data = p.read(extent, step);
                    if (p.isEnumeration()) {
                        final String[] meanings = new String[data.size()];
                        for (int j=0; j<meanings.length; j++) {
                            String m = p.meaning(data.intValue(j));
                            meanings[j] = (m != null) ? m : "";
                        }
                        props[i] = Arrays.asList(meanings);
                    } else {
                        props[i] = data;
                    }
                }
            } catch (IOException | DataStoreException e) {
                throw new BackingStoreException(canNotReadFile(), e);
            }
            final AbstractFeature feature = type.newInstance();
            feature.setPropertyValue(identifiers.getName(), id.intValue(index));
            for (int i=0; i<properties.length; i++) {
                feature.setPropertyValue(properties[i].getName(), props[i]);
                // TODO: set time characteristic.
            }
            // TODO: temporary hack - to be replaced by support in Vector.
            final int dimension = coordinates.length;
            final double[] tmp = new double[length * dimension];
            for (int i=0; i<tmp.length; i++) {
                tmp[i] = coords[i % dimension].doubleValue(i / dimension);
            }
            feature.setPropertyValue("trajectory", factory.createPolyline(dimension, Vector.create(tmp)));
            action.accept(feature);
            position = Math.addExact(position, length);
            return ++index < counts.size();
        }

        /**
         * Current implementation can not split this iterator.
         */
        @Override
        public Spliterator<AbstractFeature> trySplit() {
            return null;
        }

        /**
         * Returns the number of features.
         */
        @Override
        public long estimateSize() {
            return counts.size();
        }

        /**
         * Returns the characteristics of the iteration over feature instances.
         * The iteration is assumed {@link #ORDERED} in the declaration order in the netCDF file.
         * The iteration is {@link #NONNULL} (i.e. {@link #tryAdvance(Consumer)} is not allowed
         * to return null value) and {@link #IMMUTABLE} (i.e. we do not support modification of
         * the netCDF file while an iteration is in progress).
         *
         * @return characteristics of iteration over the features in the netCDF file.
         */
        @Override
        public int characteristics() {
            return ORDERED | NONNULL | IMMUTABLE | SIZED;
        }
    }
}
