/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2003-2006, Geotools Project Managment Committee (PMC)
 *    (C) 2001, Institut de Recherche pour le Développement
 *
 *    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; either
 *    version 2.1 of the License, or (at your option) any later version.
 *
 *    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.image.io.text;

// J2SE dependencies
import java.util.Arrays;
import javax.imageio.IIOException;

// Geotools dependencies
import org.geotools.resources.Utilities;
import org.geotools.resources.XArray;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;


/**
 * List of data records in an image. One instance of this class is created by
 * {@link TextRecordImageReader} for every image in a file. A {@code RecordList}
 * contains a list of records where each record contains data for one pixel. A record
 * contains usually the following information:
 *
 * <ul>
 *   <li>Pixel's x and y coordinate.</li>
 *   <li>Pixel's values for each band.</li>
 * </li>
 *
 * Those information can appear in arbitrary columns, providing that the column order
 * stay the same for every record in a particular {@code RecordList} instance.
 * Records can appear in arbitrary order.
 * <p>
 * Data can be floating point value ({@code float} type). Current implementation
 * expects pixels distributed on a regular grid. The grid interval will be automatically
 * computed when needed. The interval computation should be accurate even if there is
 * missing and/or duplicated records.
 *
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/coverageio/src/main/java/org/geotools/image/io/text/RecordList.java $
 * @version $Id: RecordList.java 25699 2007-05-31 15:55:07Z desruisseaux $
 * @author Martin Desruisseaux
 */
final class RecordList {
    /**
     * Valeurs minimales des colonnes, ou {@code null} si
     * ces valeurs ne sont pas encore connues.  La longueur de
     * de ce tableau est égale à {@link #dataColumnCount}.
     */
    private double[] min;
    
    /**
     * Valeurs maximales des colonnes, ou {@code null} si
     * ces valeurs ne sont pas encore connues.  La longueur de
     * de ce tableau est égale à {@link #dataColumnCount}.
     */
    private double[] max;
    
    /**
     * Intervals entre les données, ou {@code null} si ces valeurs
     * n'ont pas encore été calculées. La valeur 0 signifie que l'interval
     * pour une colonne en particulier n'a pas encore été calculée.
     */
    private float[] interval;
    
    /**
     * Tableau des valeurs lues,  ou {@code null} si les
     * valeurs n'ont pas encore été lues. Ce tableau contient
     * une suite de lignes qui ont chacun un nombre de colonnes
     * égal à {@link #dataColumnCount}.
     */
    private float[] data;
    
    /**
     * Nombre de colonnes retenues lors de la lecture, ou -1 si ce nombre
     * n'est pas encore connu. Ce nombre de colonnes peut être égal ou
     * inférieur à {@code min.length} et {@code max.length}.
     */
    private int columnCount = -1;
    
    /**
     * Index suivant celui du dernier élément valide de {@link #data}.
     * Ce champ sera augmenté à chaque ajout d'une nouvelle ligne. Sa
     * valeur doit être un multiple entier de {@link #dataColumnCount}.
     */
    private int upper;
    
    /**
     * Nombre de lignes attendues. Cette information n'est qu'à titre
     * indicative, mais accelerera la lecture si elle est exacte.
     */
    private int expectedLineCount = 1024;
    
    /**
     * Construit un {@code ImageData} initiallement vide.
     * La première ligne de données lue déterminera le nombre
     * de colonnes qui seront retenus pour toutes les lignes
     * suivantes.
     */
    public RecordList() {
    }
    
    /**
     * Construit un {@code ImageData} initiallement vide.
     * Pour chaque ligne lue, seule les {@code columnCount}
     * premières colonnes seront retenus.
     *
     * @param columnCount Nombre de colonnes à retenir lors de la lecture.
     * @param expectedLineCount Nombre de lignes attendues. Cette information
     *        n'est qu'à titre indicative, mais accelerera la lecture si elle
     *        est exacte.
     */
    public RecordList(final int columnCount, final int expectedLineCount) {
        this.columnCount       = columnCount;
        this.expectedLineCount = expectedLineCount;
    }
    
    /**
     * Ajoute une ligne de données.  Si la ligne est plus courte que la longueur
     * attendues, les colonnes manquantes seront considérées comme contenant des
     * {@code NaN}.   Si elle est plus longue que la longueur attendue, les
     * colonnes en trop seront ignorées.
     */
    public void add(final double[] line) {
        if (data==null) {
            if (columnCount<0) columnCount=line.length;
            min  = new double[columnCount]; Arrays.fill(min, Double.POSITIVE_INFINITY);
            max  = new double[columnCount]; Arrays.fill(max, Double.NEGATIVE_INFINITY);
            data = new float [columnCount*expectedLineCount];
        }
        final int limit=Math.min(columnCount, line.length);
        final int nextUpper = upper+columnCount;
        if (nextUpper >= data.length) {
            data = XArray.resize(data, Math.max(nextUpper, data.length+Math.min(data.length, 65536)));
        }
        for (int i=0; i<limit; i++) {
            final double value = line[i];
            if (value < min[i]) min[i] = value;
            if (value > max[i]) max[i] = value;
            data[upper+i] = (float)value;
        }
        Arrays.fill(data, upper+limit, nextUpper, Float.NaN);
        upper = nextUpper;
    }
    
    /**
     * Libère la mémoire réservée en trop. Cette méthode peut être appelée
     * lorsqu'on a terminé de lire les données et qu'on veut les conserver
     * en mémoire pendant encore quelque temps.
     */
    public void trimToSize() {
        if (data != null) {
            data = XArray.resize(data, upper);
        }
    }
    
    /**
     * Retourne une référence directe vers les données mémorisées par cet objet.
     * NE PAS MODIFIER CES DONNEES! Les index valides vont de 0 inclusivement
     * jusqu'à {@link #getDataCount} exclusivement.
     */
    final float[] getData() {
        return data;
    }
    
    /**
     * Retourne le nombre de données qui ont été mémorisées.
     */
    final int getDataCount() {
        return upper;
    }
    
    /**
     * Retourne le nombre de lignes qui ont été mémorisées.
     */
    public int getLineCount() {
        if (columnCount <= 0) {
            return 0;
        }
        assert (upper % columnCount) == 0;
        return upper / columnCount;
    }
    
    /**
     * Retourne le nombre de colonnes, ou
     * -1 si ce nombre n'est pas connu.
     */
    public int getColumnCount() {
        return columnCount;
    }
    
    /**
     * Retourne la valeur minimale de la colonne spécifiée,
     * ou {@link Double#NaN} si cette valeur n'est pas connue.
     */
    public double getMinimum(final int column) {
        return (min != null && min[column] <= max[column]) ? min[column] : Double.NaN;
    }
    
    /**
     * Retourne la valeur maximale de la colonne spécifiée,
     * ou {@link Double#NaN} si cette valeur n'est pas connue.
     */
    public double getMaximum(final int column) {
        return (max != null && max[column] >= min[column]) ? max[column] : Double.NaN;
    }
    
    /**
     * Retourne l'interval entre les points de la colonne spécifiée, en supposant que les
     * points se trouvent à un interval régulier. Si ce n'est pas le cas, une exception
     * sera lancée.
     *
     * @param  column Colonne dont on veut l'interval entre les points.
     * @param  eps Petit facteur de tolérance (par exemple 1E-6).
     * @throws IIOException si les points de la colonne spécifiée
     *         ne sont pas distribués à un interval régulier.
     */
    private float getInterval(final int column, final float eps) throws IIOException {
        if (interval == null) {
            if (columnCount <= 0) {
                return Float.NaN;
            }
            interval = new float[columnCount];
        }
        if (interval[column] != 0) {
            return interval[column];
        }
        /*
         * Obtient toutes les valeurs de la colonne
         * spécifiée en ordre croissant.
         */
        int count=0;
        final float[] array = new float[getLineCount()];
        for (int i=column; i<upper; i+=columnCount) {
            array[count++] = data[i];
        }
        assert count == array.length;
        Arrays.sort(array);
        /*
         * Elimine les doublons. Lorsque des doublons seront trouvés, ils iront de
         * {@code lower} à {@code upper} <strong>inclusivement</strong>.
         */
        int upper = count-1;
        int lower = count;
        while (--lower>=1) {
            if (array[upper] != array[lower-1]) {
                if (upper != lower) {
                    System.arraycopy(array, upper, array, lower, count-upper);
                    final int oldCount = count;
                    count -= (upper-lower);
                    Arrays.fill(array, count, oldCount, Float.NaN); // Par prudence.
                }
                upper = lower-1;
            }
        }
        if (upper != lower) {
            System.arraycopy(array, upper, array, lower, count-upper);
            final int oldCount = count;
            count -= (upper-lower);
            Arrays.fill(array, count, oldCount, Float.NaN); // Par prudence.
        }
        /*
         * Recherche le plus petit interval entre deux points. Vérifie ensuite que
         * l'interval entre tous les points est un multiple entier de cet interval
         * minimal (on tient compte ainsi des éventuels données manquantes).
         */
        float delta = Float.POSITIVE_INFINITY;
        for (int i=1; i<count; i++) {
            final float d = array[i] - array[i-1];
            assert d>0;
            if (d < delta) {
                delta = d;
            }
        }
        for (int i=1; i<count; i++) {
            float e = (array[i] - array[i-1]) / delta;
            if (Math.abs(e-Math.rint(e)) > eps) {
                throw new IIOException(Errors.format(ErrorKeys.NOT_A_GRID));
            }
        }
        return interval[column] = Float.isInfinite(delta) ? Float.NaN : delta;
    }
    
    /**
     * Retourne le nombre de points distincts dans la colonne spécifiée. Cette méthode
     * élimine d'abord tous les doublons avant d'effectuer le comptage. Elle vérifie
     * aussi que les points restants sont espacés à un interval régulier, et lancera
     * une exception si ce n'est pas le cas. S'il y a des trous dans les données, il
     * seront pris en compte comme si un point s'y était trouvé.
     *
     * @param  column Colonne dont on veut le nombre de points distincts.
     * @param  eps Petit facteur de tolérance (par exemple 1E-6).
     * @throws IIOException si les points de la colonne spécifiée
     *         ne sont pas distribués à un interval régulier.
     */
    public int getPointCount(final int column, final float eps) throws IIOException {
        return (int)Math.round((getMaximum(column) - getMinimum(column)) / getInterval(column, eps)) +1;
    }
    
    /**
     * Retourne un résumé des informations que contient cet objet. Le résumé contiendra
     * notamment les valeurs minimales et maximales de chaque colonnes.
     *
     * @param  xColumn Colonne des <var>x</var>, ou -1 s'il n'est pas connu.
     * @param  yColumn Colonne des <var>y</var>, ou -1 s'il n'est pas connu.
     * @param  eps Petit facteur de tolérance (par exemple 1E-6).
     * @return Chaîne de caractères résumant l'état des données.
     */
    public String toString(final int xColumn, final int yColumn, final float eps) {
        float xCount = Float.NaN;
        float yCount = Float.NaN;
        if (xColumn >= 0) try {
            xCount = getPointCount(xColumn, eps);
        } catch (IIOException exception) {
            // Ignore.
        }
        if (yColumn >= 0) try {
            yCount = getPointCount(yColumn, eps);
        } catch (IIOException exception) {
            // Ignore.
        }
        return Vocabulary.format(VocabularyKeys.POINT_COUNT_IN_GRID_$3,
                new Integer(upper), new Float(xCount), new Float(yCount));
    }
    
    /**
     * Retourne une chaîne de caractères représentant cet objet.
     * Cette chaîne indiquera le nombre de lignes et de colonnes
     * mémorisées.
     */
    public String toString() {
        return Utilities.getShortClassName(this) +
                '[' + getLineCount() + "\u00A0\u00D7\u00A0" + getColumnCount() + ']';
    }
}
