/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2007, GeoTools Project Managment Committee (PMC)
 *    (C) 2007, Geomatys
 *
 *    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.image.io.metadata;

import java.text.Format;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.logging.LogRecord;
import javax.imageio.ImageReader;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.metadata.IIOInvalidTreeException;
import org.w3c.dom.Node;

import org.geotools.util.logging.LoggedFormat;
import org.geotools.util.logging.Logging;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.OptionalDependencies;
import org.geotools.image.io.GeographicImageReader;
import org.geotools.image.io.GeographicImageWriter;


/**
 * Geographic informations encoded in image as metadata. This class provides various methods for
 * reading and writting attribute values in {@link IIOMetadataNode} according the {@linkplain
 * GeographicMetadataFormat geographic metadata format}. If some inconsistency are found while
 * reading (for example if the coordinate system dimension doesn't match the envelope dimension),
 * then the default implementation {@linkplain #warningOccurred logs a warning}. We do not throw
 * an exception because minor errors are not uncommon in geographic data, and we want to process
 * the data on a "<cite>best effort</cite>" basis. However because every warnings are logged
 * through the {@link #warningOccurred} method, subclasses can override this method if they want
 * treat some warnings as fatal errors.
 *
 * @since 2.4
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/coverageio/src/main/java/org/geotools/image/io/metadata/GeographicMetadata.java $
 * @version $Id: GeographicMetadata.java 27862 2007-11-12 19:51:19Z desruisseaux $
 * @author Martin Desruisseaux
 */
public class GeographicMetadata extends IIOMetadata {
    /**
     * The {@link ImageReader} or {@link ImageWriter} that holds the metadata,
     * or {@code null} if none.
     */
    private final Object owner;

    /**
     * The root node to be returned by {@link #getAsTree}.
     */
    private Node root;

    /**
     * The coordinate reference system node.
     * Will be created only when first needed.
     */
    private ImageReferencing referencing;

    /**
     * The grid geometry node.
     * Will be created only when first needed.
     */
    private ImageGeometry geometry;

    /**
     * The list of {@linkplain Band bands}.
     * Will be created only when first needed.
     */
    private ChildList/*<Bands>*/ bands;

    /**
     * The standard date format. Will be created only when first needed.
     */
    private transient LoggedFormat/*<Date>*/ dateFormat;

    /**
     * Creates a default metadata instance. This constructor defines no standard or native format.
     * The only format defined is the {@linkplain GeographicMetadataFormat geographic} one.
     */
    public GeographicMetadata() {
        this((Object) null);
    }

    /**
     * Creates a default metadata instance for the given reader.
     *
     * @param reader The source image reader, or {@code null} if none.
     */
    public GeographicMetadata(final ImageReader reader) {
        this((Object) reader);
    }

    /**
     * Creates a default metadata instance for the given writer.
     *
     * @param writer The target image writer, or {@code null} if none.
     */
    public GeographicMetadata(final ImageWriter writer) {
        this((Object) writer);
    }

    /**
     * Creates a default metadata instance. This constructor defines no standard or native format.
     * The only format defined is the {@linkplain GeographicMetadataFormat geographic} one.
     */
    private GeographicMetadata(final Object owner) {
        super(false, // Can not return or accept a DOM tree using the standard metadata format.
              null,  // There is no native metadata format.
              null,  // There is no native metadata format.
              new String[] {
                  GeographicMetadataFormat.FORMAT_NAME
              },
              new String[] {
                  "org.geotools.image.io.metadata.GeographicMetadataFormat"
              });
        this.owner = owner;
    }

    /**
     * Constructs a geographic metadata instance with the given format names and format class names.
     * This constructor passes the arguments to the {@linkplain IIOMetadata#IIOMetadata(boolean,
     * String, String, String[], String[]) super-class constructor} unchanged.
     *
     * @param standardMetadataFormatSupported {@code true} if this object can return or accept
     *        a DOM tree using the standard metadata format.
     * @param nativeMetadataFormatName The name of the native metadata, or {@code null} if none.
     * @param nativeMetadataFormatClassName The name of the class of the native metadata format,
     *        or {@code null} if none.
     * @param extraMetadataFormatNames Additional formats supported by this object,
     *        or {@code null} if none.
     * @param extraMetadataFormatClassNames The class names of any additional formats
     *        supported by this object, or {@code null} if none.
     */
    public GeographicMetadata(final boolean  standardMetadataFormatSupported,
                              final String   nativeMetadataFormatName,
                              final String   nativeMetadataFormatClassName,
                              final String[] extraMetadataFormatNames,
                              final String[] extraMetadataFormatClassNames)
    {
        super(standardMetadataFormatSupported,
              nativeMetadataFormatName,
              nativeMetadataFormatClassName,
              extraMetadataFormatNames,
              extraMetadataFormatClassNames);
        owner = null;
    }

    /**
     * Returns {@code false} since this node support some write operations.
     */
    public boolean isReadOnly() {
        return false;
    }

    /**
     * Returns the root of a tree of metadata contained within this object
     * according to the conventions defined by a given metadata format.
     */
    final Node getRootNode() {
        if (root == null) {
            root = new IIOMetadataNode(GeographicMetadataFormat.FORMAT_NAME);
        }
        return root;
    }

    /**
     * Returns the grid referencing.
     */
    public ImageReferencing getReferencing() {
        if (referencing == null) {
            referencing = new ImageReferencing(this);
        }
        return referencing;
    }

    /**
     * Returns the grid geometry.
     */
    public ImageGeometry getGeometry() {
        if (geometry == null) {
            geometry = new ImageGeometry(this);
        }
        return geometry;
    }

    /**
     * Returns the list of all {@linkplain Band bands}.
     */
    final ChildList/*<Bands>*/ getBands() {
        if (bands == null) {
            bands = new ChildList.Bands(this);
        }
        return bands;
    }

    /**
     * Returns the sample type (typically {@value GeographicMetadataFormat#GEOPHYSICS} or
     * {@value GeographicMetadataFormat#PACKED}), or {@code null} if none. This type applies
     * to all {@linkplain Band bands}.
     */
    public String getSampleType() {
        return getBands().getString("type");
    }

    /**
     * Set the sample type for all {@linkplain Band bands}. Valid types include
     * {@value GeographicMetadataFormat#GEOPHYSICS} and {@value GeographicMetadataFormat#PACKED}.
     *
     * @param type The sample type, or {@code null} if none.
     */
    public void setSampleType(final String type) {
        getBands().setEnum("type", type, GeographicMetadataFormat.SAMPLE_TYPES);
    }

    /**
     * Returns the number of {@linkplain Band bands} in the coverage.
     */
    public int getNumBands() {
        return getBands().childCount();
    }

    /**
     * Returns the band at the specified index.
     *
     * @param  bandIndex the band index, ranging from 0 inclusive to {@link #getNumBands} exclusive.
     * @throws IndexOutOfBoundsException if the index is out of bounds.
     */
    public Band getBand(final int bandIndex) throws IndexOutOfBoundsException {
        return (Band) getBands().getChild(bandIndex);
    }

    /**
     * Creates a new band and returns it.
     *
     * @param name The name for the new band.
     */
    public Band addBand(final String name) {
        final Band band = (Band) getBands().addChild();
        band.setName(name);
        return band;
    }

    /**
     * Checks the format name.
     */
    private void checkFormatName(final String formatName) throws IllegalArgumentException {
        if (!GeographicMetadataFormat.FORMAT_NAME.equals(formatName)) {
            throw new IllegalArgumentException(Errors.getResources(getLocale()).getString(
                    ErrorKeys.ILLEGAL_ARGUMENT_$2, "formatName", formatName));
        }
    }

    /**
     * Returns the root of a tree of metadata contained within this object
     * according to the conventions defined by a given metadata format.
     *
     * @param formatName the desired metadata format.
     * @return The node forming the root of metadata tree.
     * @throws IllegalArgumentException if the format name is {@code null} or is not
     *         one of the names returned by {@link #getMetadataFormatNames()
     *         getMetadataFormatNames()}.
     */
    public Node getAsTree(final String formatName) throws IllegalArgumentException {
        checkFormatName(formatName);
        return getRootNode();
    }

    /**
     * Alters the internal state of this metadata from a tree whose syntax is defined by
     * the given metadata format. The default implementation simply replaces all existing
     * state with the contents of the given tree.
     *
     * @param formatName The desired metadata format.
     * @param root An XML DOM Node object forming the root of a tree.
     */
    public void mergeTree(final String formatName, final Node root) throws IIOInvalidTreeException {
        checkFormatName(formatName);
        reset();
        this.root = root;
    }

    /**
     * Alters the internal state of this metadata from a tree defined by the specified metadata.
     * The default implementation expect the {@value GeographicMetadataFormat#FORMAT_NAME} format.
     *
     * @param metadata The metadata to merge to this object.
     */
    public void mergeTree(final IIOMetadata metadata) throws IIOInvalidTreeException {
        mergeTree(GeographicMetadataFormat.FORMAT_NAME,
                metadata.getAsTree(GeographicMetadataFormat.FORMAT_NAME));
    }

    /**
     * Resets all the data stored in this object to default values.
     */
    public void reset() {
        root        = null;
        referencing = null;
        geometry    = null;
        bands       = null;
    }

    /**
     * Returns the language to use when {@linkplain #warningOccurred logging a warning},
     * or {@code null} if none has been set. The default implementation delegates to
     * {@link ImageReader#getLocale} or {@link ImageWriter#getLocale} if possible, or
     * returns {@code null} otherwise.
     */
    public Locale getLocale() {
        if (owner instanceof ImageReader) {
            return ((ImageReader) owner).getLocale();
        }
        if (owner instanceof ImageWriter) {
            return ((ImageWriter) owner).getLocale();
        }
        return null;
    }

    /**
     * Invoked when a warning occured. This method is invoked when some inconsistency has
     * been detected in the geographic metadata. The default implementation delegates to
     * {@link GeographicImageReader#warningOccurred} if possible, or send the record to
     * the {@code "org.geotools.image.io.metadata"} logger otherwise.
     * <p>
     * Subclasses may override this method if more processing is wanted, or for
     * throwing exception if some warnings should be considered as fatal errors.
     */
    protected void warningOccurred(final LogRecord record) {
        if (owner instanceof GeographicImageReader) {
            ((GeographicImageReader) owner).warningOccurred(record);
        } else if (owner instanceof GeographicImageWriter) {
            ((GeographicImageWriter) owner).warningOccurred(record);
        } else {
            Logging.getLogger("org.geotools.image.io.metadata").log(record);
        }
    }

    /**
     * Wraps the specified format in order to either parse fully a string, or log a warning.
     *
     * @param format The format to use for parsing and formatting.
     * @param type   The expected type of parsed values.
     */
    protected /*<T>*/ LoggedFormat createLoggedFormat(final Format format, final Class/*<T>*/ type) {
        return new LoggedFormat/*<T>*/(format, type) {
            //@Override
            protected Locale getWarningLocale() {
                return getLocale();
            }

            //@Override
            protected void logWarning(final LogRecord warning) {
                warningOccurred(warning);
            }
        };
    }

    /**
     * Returns a standard date format to be shared by {@link MetadataAccessor}.
     */
    final LoggedFormat/*<Date>*/ dateFormat() {
        if (dateFormat == null) {
            final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
            format.setTimeZone(TimeZone.getTimeZone("UTC"));
            dateFormat = createLoggedFormat(format, Date.class);
            dateFormat.setLogger("org.geotools.image.io.metadata");
            dateFormat.setCaller(MetadataAccessor.class, "getDate");
        }
        return dateFormat;
    }

    /**
     * Returns a string representation of this metadata, mostly for debugging purpose.
     */
    public String toString() {
        return OptionalDependencies.toString(
                OptionalDependencies.xmlToSwing(getAsTree(GeographicMetadataFormat.FORMAT_NAME)));
    }
}
