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

import java.io.*; // Many imports, including some for javadoc only.
import java.nio.charset.Charset;
import java.net.URL;
import java.net.URLConnection;
import java.util.Locale;
import java.text.NumberFormat;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.awt.image.DataBuffer;
import javax.imageio.IIOImage;
import javax.imageio.ImageWriteParam;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
import javax.media.jai.iterator.RectIter;

import org.geotools.resources.XMath;
import org.geotools.image.io.StreamImageWriter;


/**
 * Base class for text image encoders. "Text images" are usually ASCII files
 * containing pixel values (often geophysical values, like sea level anomalies).
 *
 * @since 2.4
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/coverageio/src/main/java/org/geotools/image/io/text/TextImageWriter.java $
 * @version $Id: TextImageWriter.java 26833 2007-09-04 18:01:08Z desruisseaux $
 * @author Martin Desruisseaux
 */
public abstract class TextImageWriter extends StreamImageWriter {
    /**
     * Maximum number of digits to be allowed by {@link #createNumberFormat}.
     */
    private static final int MAXIMUM_DIGITS = 12;

    /**
     * Do not force the formatting of fraction digits if the sample values are equal or
     * greater than 1E+6.
     */
    private static final double NODIGITS_THRESHOLD = 1E+6;

    /**
     * Number of digits to check after the last one, as 10<sup>-n</sup>. The default value is
     * 1E-1. This means that if the digit immediately after the last one is 0, we will consider
     * that we have reached the intented precision.
     *
     * For adjusting the number of digits to check after the last one, just put this number
     * as a negative power in place of the "-1" above.
     */
    private static final double DELTA_THRESHOLD = 1E-1;

    /**
     * The maximum value found during the last call to {@link #createNumberFormat}.
     */
    private double maximum;

    /**
     * {@link #output} as a writer, or {@code null} if none.
     *
     * @see #getWriter
     */
    private BufferedWriter writer;

    /**
     * Constructs a {@code TextImageWriter}.
     *
     * @param originatingProvider The {@code ImageWriterSpi} that
     *        is constructing this object, or {@code null}.
     */
    protected TextImageWriter(final ImageWriterSpi provider) {
        super(provider);
    }

    /**
     * Returns the locale to use for encoding values, or {@code null} for the
     * {@linkplain Locale#getDefault default}. The default implementation returns the
     * {@linkplain Spi#locale locale} specified to the {@link Spi} object given to this
     * {@code TextImageWriter} constructor. Subclasses can override this method if they
     * want to specify the data locale in some other way.
     * <p>
     * <b>Note:</b> This locale should not be confused with {@link #getLocale}.
     *
     * @param  parameters The write parameters, or {@code null} for the defaults.
     * @return The locale to use for parsing numbers in the image file.
     *
     * @see Spi#locale
     */
    protected Locale getDataLocale(final ImageWriteParam parameters) {
        return (originatingProvider instanceof Spi) ? ((Spi)originatingProvider).locale : null;        
    }

    /**
     * Returns the character set to use for encoding the string to the output stream.
     * The default implementation returns the {@linkplain Spi#charset character set}
     * specified to the {@link Spi} object given to this {@code TextImageWriter} constructor.
     * Subclasses can override this method if they want to specify the character encoding in
     * some other way.
     *
     * @param  parameters The write parameters, or {@code null} for the defaults.
     * @return The character encoding, or {@code null} for the platform default encoding.
     * @throws IOException If reading from the output stream failed.
     *
     * @see Spi#charset
     */
    protected Charset getCharset(final ImageWriteParam parameters) throws IOException {
        return (originatingProvider instanceof Spi) ? ((Spi)originatingProvider).charset : null;
    }

    /**
     * Returns the line separator to use when writing to the output stream. The default
     * implementation returns the {@linkplain Spi#lineSeparator line separator} specified
     * to the {@link Spi} object given to this {@code TextImageWriter} constructor. Subclasses
     * can override this method if they want to specify the line separator in some other way.
     *
     * @param  parameters The write parameters, or {@code null} for the defaults.
     * @return The line separator to use for writting the image.
     *
     * @see Spi#lineSeparator
     */
    protected String getLineSeparator(final ImageWriteParam parameters) {
        if (originatingProvider instanceof Spi) {
            final String lineSeparator = ((Spi)originatingProvider).lineSeparator;
            if (lineSeparator != null) {
                return lineSeparator;
            }
        }
        return System.getProperty("line.separator", "\n");
    }

    /**
     * Returns the {@linkplain #output output} as an {@linkplain BufferedWriter buffered writer}.
     * If the output is already a buffered writer, it is returned unchanged. Otherwise this method
     * creates a new {@linkplain BufferedWriter buffered writer} from various output types
     * including {@link File}, {@link URL}, {@link URLConnection}, {@link Writer},
     * {@link OutputStream} and {@link ImageOutputStream}.
     * <p>
     * This method creates a new {@linkplain BufferedWriter writer} only when first invoked.
     * All subsequent calls will returns the same instance. Consequently, the returned writer
     * should never be closed by the caller. It may be {@linkplain #close closed} automatically
     * when {@link #setOutput setOutput(Object)}, {@link #reset() reset()} or {@link #dispose()
     * dispose()} methods are invoked.
     *
     * @param  parameters The write parameters, or {@code null} for the defaults.
     * @return {@link #getOutput} as a {@link BufferedWriter}.
     * @throws IllegalStateException if the {@linkplain #output output} is not set.
     * @throws IOException If the output stream can't be created for an other reason.
     *
     * @see #getOutput
     * @see #getOutputStream
     */
    protected BufferedWriter getWriter(final ImageWriteParam parameters)
            throws IllegalStateException, IOException
    {
        if (writer == null) {
            final Object output = getOutput();
            if (output instanceof BufferedWriter) {
                writer = (BufferedWriter) output;
                closeOnReset = null; // We don't own the underlying writer, so don't close it.
            } else if (output instanceof Writer) {
                writer = new BufferedWriter((Writer) output);
                closeOnReset = null; // We don't own the underlying writer, so don't close it.
            } else {
                final OutputStream stream = getOutputStream();
                final Charset charset = getCharset(parameters);
                writer = new BufferedWriter((charset != null) ?
                        new OutputStreamWriter(stream, charset) : new OutputStreamWriter(stream));
                if (closeOnReset == stream) {
                    closeOnReset = writer;
                }
            }
        }
        return writer;
    }

    /**
     * Returns a number format to be used for formatting the sample values in the given image.
     *
     * @param image The image or raster to be written.
     * @param parameters The write parameters, or {@code null} if the whole image will be written.
     * @return A number format appropriate for the given image.
     */
    protected strictfp NumberFormat createNumberFormat(final IIOImage        image,
                                                       final ImageWriteParam parameters)
    {
        final Locale locale = getDataLocale(parameters);
        final int type = image.hasRaster() ? image.getRaster().getTransferType() :
                         image.getRenderedImage().getSampleModel().getDataType();
        if (type != DataBuffer.TYPE_FLOAT && type != DataBuffer.TYPE_DOUBLE) {
            maximum = Short.MAX_VALUE; // TODO: This is not really accurate...
            return (locale != null) ? NumberFormat.getIntegerInstance(locale)
                                    : NumberFormat.getIntegerInstance();
        }
        int digits = 0;
        double multiple = 1;
        maximum = Double.NEGATIVE_INFINITY;
        final RectIter iterator = createRectIter(image, parameters);
        if (!iterator.finishedBands()) do {
            if (!iterator.finishedLines()) do {
                if (!iterator.finishedPixels()) do {
                    final double value = Math.abs(iterator.getSampleDouble());
                    if (Double.isInfinite(value)) {
                        continue;
                    }
                    // Following code is NaN tolerant - no need for explicit check.
                    if (value > maximum) {
                        maximum = value;
                    }
                    while (true) {
                        double scaled = value * multiple;
                        if (type == DataBuffer.TYPE_FLOAT) {
                            scaled = (float) scaled; // Drops the extra digits.
                        }
                        // Condition below uses '!' in order to cath NaN values.
                        if (!(StrictMath.abs(scaled - StrictMath.rint(scaled)) >= DELTA_THRESHOLD)) {
                            break;
                        }
                        if (++digits > MAXIMUM_DIGITS) {
                            return NumberFormat.getNumberInstance(locale);
                        }
                        multiple *= 10;
                    }
                } while (!iterator.nextPixelDone());
                iterator.startPixels();
            } while (!iterator.nextLineDone());
            iterator.startLines();
        } while (!iterator.nextBandDone());
        /*
         * 'digits' should now be the exact number of fraction digits to format. However the above
         * algorithm do not work if all values are smaller (in absolute value) to DELTA_THRESHOLD,
         * in which case 'digits' is still set to 0. In such case it is better to keep the default
         * format unchanged, since it should be generic enough.
         */
        final NumberFormat format = (locale != null) ?
                NumberFormat.getNumberInstance(locale) : NumberFormat.getNumberInstance();
        if (digits != 0 || maximum >= DELTA_THRESHOLD) {
            format.setMaximumFractionDigits(digits);
            if (maximum < NODIGITS_THRESHOLD) {
                format.setMinimumFractionDigits(digits);
            }
        }
        return format;
    }

    /**
     * Returns the expected position of the fraction part for numbers to be formatted using the
     * given format. This method should be invoked after {@link #createNumberFormat}, but the
     * given format doesn't need to be the instance returned by the later.
     *
     * @param  format The format to be used for formatting numbers.
     * @return The expected position of the fraction part.
     *
     * @todo I don't really kwon what to do if the format is not an instance of
     *       {@link DecimalFormat}...
     */
    protected FieldPosition getExpectedFractionPosition(final NumberFormat format) {
        int width  = Math.max((int) Math.floor(XMath.log10(maximum)) + 1,
                              format.getMinimumIntegerDigits());
        int digits = Math.min(format.getMaximumFractionDigits(), MAXIMUM_DIGITS);
        if (format instanceof DecimalFormat) {
            final DecimalFormat decimal = (DecimalFormat) format;
            if (digits>0 || decimal.isDecimalSeparatorAlwaysShown()) {
                width++;
            }
            width  += Math.max(decimal.getNegativePrefix().length(),
                               decimal.getPositivePrefix().length());
            digits += Math.max(decimal.getNegativeSuffix().length(),
                               decimal.getPositiveSuffix().length());
        }
        final FieldPosition position = new FieldPosition(NumberFormat.FRACTION_FIELD);
        position.setBeginIndex(width);
        position.setEndIndex(width += digits);
        // 'width' is now the full width. We don't do anything with it at this time,
        // but maybe in some future version...
        return position;
    }

    /**
     * Closes the writer created by {@link #getWriter()}. This method does nothing if
     * the writer is the {@linkplain #output output} instance given by the user rather
     * than a writer created by this class from a {@link File} or {@link URL} output.
     *
     * @see #closeOnReset
     */
    //@Override
    protected void close() throws IOException {
        writer = null;
        super.close();
    }




    /**
     * Service provider interface (SPI) for {@link TextImageWriter}s. This SPI provides a
     * convenient way to control the {@link TextImageWriter} character encoding: the
     * {@link #charset} field. For example, many {@code Spi} subclasses will put the
     * following line in their constructor:
     *
     * <blockquote><pre>
     * {@link #charset} = Charset.forName("ISO-LATIN-1"); // ISO Latin Alphabet No. 1 (ISO-8859-1)
     * </pre></blockquote>
     *
     * @since 2.4
     * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/coverageio/src/main/java/org/geotools/image/io/text/TextImageWriter.java $
     * @version $Id: TextImageWriter.java 26833 2007-09-04 18:01:08Z desruisseaux $
     * @author Martin Desruisseaux
     */
    public static abstract class Spi extends StreamImageWriter.Spi {
        /**
         * List of legal output types for {@link TextImageWriter}.
         */
        private static final Class[] INPUT_TYPES = new Class[] {
            File.class,
            URL.class,
            URLConnection.class,
            Writer.class,
            OutputStream.class,
            ImageOutputStream.class,
            String.class  // To be interpreted as file path.
        };

        /**
         * Default list of file extensions.
         */
        private static final String[] EXTENSIONS = new String[] {
            "txt", "TXT", "asc", "ASC", "dat", "DAT"
        };

        /**
         * Character encoding, or {@code null} for the default. This field is initially
         * {@code null}. A value shall be set by subclasses if the files to be encoded
         * use some specific character encoding.
         *
         * @see TextImageWriter#getCharset
         */
        protected Charset charset;

        /**
         * The locale for numbers formatting. For example {@link Locale#US} means that
         * numbers are expected to use dot as decimal separator. This field is initially
         * {@code null}, which means that default locale should be used.
         *
         * @see TextImageWriter#getDataLocale
         */
        protected Locale locale;

        /**
         * The line separator to use, or {@code null} for the system default.
         *
         * @see TextImageWriter#getLineSeparator
         */
        protected String lineSeparator;

        /**
         * Constructs a quasi-blank {@code TextImageWriter.Spi}. It is up to the subclass to
         * initialize instance variables in order to provide working versions of all methods.
         * This constructor provides the following defaults:
         *
         * <ul>
         *   <li>{@link #outputTypes} = {{@link File}, {@link URL}, {@link URLConnection},
         *       {@link Writer}, {@link OutputStream}, {@link ImageOutputStream}, {@link String}}</li>
         *
         *   <li>{@link #suffixes} = {{@code "txt"}, {@code "asc"}, {@code "dat"}}
         *       (lowercases and uppercases)</li>
         * </ul>
         *
         * For efficienty reasons, the above fields are initialized to shared arrays. Subclasses
         * can assign new arrays, but should not modify the default array content.
         */
        public Spi() {
            outputTypes = INPUT_TYPES;
            suffixes    = EXTENSIONS;
        }
    }
}
