/*
 * 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.feature;

import java.util.Map;
import java.util.HashMap;
import java.util.Date;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.function.Consumer;
import java.lang.reflect.Array;
import java.time.Instant;
import org.opengis.util.LocalName;
import org.apache.sis.math.Vector;
import org.apache.sis.util.iso.Names;
import org.apache.sis.util.CorruptedObjectException;
import org.apache.sis.internal.util.UnmodifiableArrayList;

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


/**
 * A feature implementation where the geometry is a trajectory and some property values may change with time.
 * In current implementation this is a helper method for updating a {@code Feature} attribute.
 * However in future versions, it could extend {@code DenseFeature} directly.
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 0.8
 * @since   0.8
 * @module
 */
public final class MovingFeature {
    /**
     * Definition of characteristics containing a list of time instants in chronological order, without duplicates.
     */
    public static final DefaultAttributeType<Instant> TIME;
    static {
        final LocalName scope = Names.createLocalName("OGC", null, "MF");
        final Map<String,Object> properties = new HashMap<>(4);
        properties.put(DefaultAttributeType.NAME_KEY, Names.createScopedName(scope, null, "datetimes"));
        TIME = new DefaultAttributeType<>(properties, Instant.class, 0, Integer.MAX_VALUE, null);
    }

    /**
     * The properties having values that may change in time.
     * May contain the values of arbitrary properties (e.g. as {@link String} instances),
     * or may contain the coordinates of part of a trajectory as array of primitive type like {@code float[]}.
     * Trajectories may be specified in many parts if for example, the different parts are given on different
     * lines of a CSV file.
     */
    private final Period[] properties;

    /**
     * Number of {@code Property} instances added for each index of the {@link #properties} table.
     */
    private final int[] count;

    /**
     * A dynamic property value together with the period of time in which this property is valid.
     */
    private static final class Period {
        /**
         * Beginning in milliseconds since Java epoch of the period when the property value is valid.
         */
        final long startTime;

        /**
         * End in milliseconds since Java epoch of the period when the the property value is valid.
         * This end time will be adjusted if the property has the same valid in the next time step.
         */
        long endTime;

        /**
         * The property value.
         */
        final Object value;

        /**
         * Previous property in a chained list of properties.
         */
        final Period previous;

        /**
         * Creates a new property value valid on the given period of time.
         */
        Period(final Period previous, final long startTime, final long endTime, final Object value) {
            this.previous  = previous;
            this.startTime = startTime;
            this.endTime   = endTime;
            this.value     = value;
        }
    }

    /**
     * Overall start and end time over all properties.
     */
    private long tmin = Long.MAX_VALUE,
                 tmax = Long.MIN_VALUE;

    /**
     * Creates a new moving feature.
     *
     * @param numProperties  maximal number of dynamic properties.
     */
    public MovingFeature(final int numProperties) {
        properties = new Period[numProperties];
        count      = new int   [numProperties];
    }

    /**
     * Adds a time range.
     * The minimal and maximal values will be used by {@code storeTimeRange(String, String, Feature)}.
     *
     * @param  startTime  beginning in milliseconds since Java epoch of the period when the property value is valid.
     * @param  endTime    end in milliseconds since Java epoch of the period when the the property value is valid.
     */
    public final void addTimeRange(final long startTime, final long endTime) {
        if (startTime < tmin) tmin = startTime;
        if (  endTime > tmax) tmax =   endTime;
    }

    /**
     * Adds a dynamic property value. This method shall be invoked with time periods in chronological order.
     *
     * @param  index      the property index.
     * @param  startTime  beginning in milliseconds since Java epoch of the period when the property value is valid.
     * @param  endTime    end in milliseconds since Java epoch of the period when the the property value is valid.
     * @param  value      the property value which is valid during the given period.
     */
    public final void addValue(final int index, final long startTime, final long endTime, final Object value) {
        final Period p = properties[index];
        if (p != null && p.endTime == startTime && Objects.equals(p.value, value)) {
            p.endTime = endTime;
        } else {
            properties[index] = new Period(p, startTime, endTime, value);
            count[index]++;
        }
    }

    /**
     * Stores the start time and end time in the given feature.
     *
     * @param  startTime  name of the property where to store the start time.
     * @param  endTime    name of the property where to store the end time.
     * @param  dest       feature where to store the start time and end time.
     */
    public final void storeTimeRange(final String startTime, final String endTime, final AbstractFeature dest) {
        if (tmin < tmax) {
            final Instant t = Instant.ofEpochMilli(tmin);
            dest.setPropertyValue(startTime, t);
            dest.setPropertyValue(endTime, (tmin == tmax) ? t : Instant.ofEpochMilli(tmax));
        }
    }

    /**
     * Sets the values of the given attribute to the values collected by this {@code MovingFeatures}.
     * This method sets also the {@code "datetimes"} characteristic.
     *
     * @param  <V>    the type of values in the given attribute.
     * @param  index  index of the property for which values are desired.
     * @param  dest   attribute where to store the value.
     */
    @SuppressWarnings("unchecked")
    public final <V> void storeAttribute(final int index, final AbstractAttribute<V> dest) {
        int n = count[index];
        final long[] times  = new long[n];
        final V[]    values = (V[]) Array.newInstance(dest.getType().getValueClass(), n);
        for (Period p = properties[index]; p != null; p = p.previous) {
            times [--n] = p.startTime;
            values[  n] = (V) p.value;
        }
        if (n != 0) {
            // Should never happen unless this object has been modified concurrently in another thread.
            throw new CorruptedObjectException();
        }
        dest.setValues(UnmodifiableArrayList.wrap(values));
        final AbstractAttribute<Instant> c = TIME.newInstance();
        c.setValues(new DateList(times));
        dest.characteristics().values().add(c);
    }

    /**
     * Sets the geometry of the given attribute to the values collected by this {@code MovingFeatures}.
     * This method sets also the {@code "datetimes"} characteristic.
     *
     * @param  <G>              the type of the geometry value.
     * @param  featureName      the name of the feature containing the attribute to update, for logging purpose.
     * @param  index            index of the property for which geometry value is desired.
     * @param  dimension        number of dimensions for all coordinates.
     * @param  factory          the factory to use for creating the geometry object.
     * @param  dest             attribute where to store the geometry value.
     * @param  warningListener  where to report warnings. Implementation should set the source class name,
     *                          source method name and logger name, then forward to a {@code WarningListener}.
     */
    public final <G> void storeGeometry(final String featureName, final int index, final int dimension,
            final Geometries<G> factory, final AbstractAttribute<G> dest, final Consumer<LogRecord> warningListener)
    {
        int n = count[index];
        final Vector[] vectors = new Vector[n];
        for (Period p = properties[index]; p != null; p = p.previous) {
            vectors[--n] = Vector.create(p.value, false);
        }
        if (n != 0) {
            // Should never happen unless this object has been modified concurrently in another thread.
            throw new CorruptedObjectException();
        }
        int    warnings = 10;                   // Maximal number of warnings, for avoiding to flood the logger.
        int    numPts   = 0;                    // Total number of points in all vectors, ignoring null vectors.
        Vector previous = null;                 // If non-null, shall be non-empty.
        for (int i=0; i<vectors.length; i++) {
            Vector v = vectors[i];
            int length;
            if (v == null || (length = v.size()) == 0) {
                continue;
            }
            if ((length % dimension) != 0) {
                if (--warnings >= 0) {
                    Period p = properties[index];
                    for (int j=i; --j >= 0;) {          // This is inefficient but used only in case of warnings.
                        p = p.previous;
                    }
                    warningListener.accept(Resources.forLocale(null).getLogRecord(Level.WARNING,
                            Resources.Keys.UnexpectedNumberOfCoordinates_4, featureName, new Date(p.startTime), dimension, length));
                }
                continue;
            }
            /*
             * At this point we have a non-empty valid sequence of coordinate values. If the first point of current
             * vector is equals to the last point of previous vector, assume that they form a continuous polyline.
             */
            if (previous != null) {
                if (equals(previous, v, dimension)) {
                    v = v.subList(dimension, length);                               // Skip the first coordinate.
                    length -= dimension;
                    if (length == 0) {
                        vectors[i] = null;
                        continue;
                    }
                    vectors[i] = v;
                }
            }
            numPts += length;
            previous = v;
        }
        /*
         * At this point we got the list of all coordinates to join together in a polyline.
         * We will create the geometry at the end of this method. Before that, interpolate
         * the dates and times.
         */
        int i = vectors.length;
        numPts /= dimension;
        final long[] times = new long[numPts];
        for (Period p = properties[index]; p != null; p = p.previous) {
            final Vector v = vectors[--i];
            if (v != null) {
                int c = v.size() / dimension;
                if (c == 1) {
                    times[--numPts] = p.endTime;
                } else {
                    final long startTime = p.startTime;
                    final double scale = (p.endTime - startTime) / (double) (c-1);
                    while (--c >= 0) {
                        times[--numPts] = startTime + Math.round(scale * c);
                    }
                }
            }
        }
        if (numPts != 0) {
            // Should never happen unless this object has been modified concurrently in another thread.
            throw new CorruptedObjectException();
        }
        /*
         * Store the geometry and characteristics in the attribute.
         */
        dest.setValue(factory.createPolyline(dimension, vectors));
        final AbstractAttribute<Instant> c = TIME.newInstance();
        c.setValues(new DateList(times));
        dest.characteristics().values().add(c);
    }

    /**
     * Returns {@code true} if the last coordinate of the {@code previous} vector is equals to the first
     * coordinate of the {@code next} vector.
     *
     * @param previous   the previous vector.
     * @param next       the next vector.
     * @param dimension  number of dimension in each coordinate.
     */
    private static boolean equals(final Vector previous, final Vector next, int dimension) {
        int p = previous.size();
        while (--dimension >= 0) {
            if (next.doubleValue(dimension) != previous.doubleValue(--p)) {
                return false;
            }
        }
        return true;
    }
}
