/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2007-2008, Open Source Geospatial Foundation (OSGeo)
 *
 *    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.metadata;

import java.text.DateFormat;
import java.text.NumberFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.Iterator;
import java.util.Locale;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeNode;

import org.opengis.util.CodeList;
import org.opengis.util.InternationalString;
import org.geotools.util.Utilities;
import org.geotools.resources.Classes;
import org.geotools.resources.OptionalDependencies;


/**
 * Represents the metadata property as a tree made from {@linkplain TreeNode tree nodes}.
 * Note that while {@link TreeNode} is defined in the {@link javax.swing.tree} package,
 * it can be seen as a data structure independent of Swing.
 * <p>
 * Note: this method is called {@code PropertyTree} because it may implements
 * {@link javax.swing.tree.TreeModel} in some future Geotools implementation.
 *
 * @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/modules/library/metadata/src/main/java/org/geotools/metadata/PropertyTree.java $
 * @version $Id: PropertyTree.java 30640 2008-06-12 17:34:32Z acuster $
 * @author Martin Desruisseaux (Geomatys)
 */
final class PropertyTree {
    /**
     * The default number of significant digits (may or may not be fraction digits).
     */
    private static final int PRECISION = 12;

    /**
     * The expected standard implemented by the metadata.
     */
    private final MetadataStandard standard;

    /**
     * The locale to use for {@linkplain Date date}, {@linkplain Number number}
     * and {@linkplain InternationalString international string} formatting.
     */
    private final Locale locale;

    /**
     * The object to use for formatting numbers.
     * Will be created only when first needed.
     */
    private transient NumberFormat numberFormat;

    /**
     * The object to use for formatting dates.
     * Will be created only when first needed.
     */
    private transient DateFormat dateFormat;

    /**
     * Creates a new tree builder using the default locale.
     *
     * @param standard The expected standard implemented by the metadata.
     */
    public PropertyTree(final MetadataStandard standard) {
        this(standard, Locale.getDefault());
    }

    /**
     * Creates a new tree builder.
     *
     * @param standard The expected standard implemented by the metadata.
     * @param locale   The locale to use for {@linkplain Date date}, {@linkplain Number number}
     *                 and {@linkplain InternationalString international string} formatting.
     */
    public PropertyTree(final MetadataStandard standard, final Locale locale) {
        this.standard = standard;
        this.locale   = locale;
    }

    /**
     * Creates a tree for the specified metadata.
     */
    public MutableTreeNode asTree(final Object metadata) {
        final String name = Classes.getShortName(standard.getInterface(metadata.getClass()));
        final DefaultMutableTreeNode root =
                OptionalDependencies.createTreeNode(localize(name), metadata, true);
        append(root, metadata);
        return root;
    }

    /**
     * Appends the specified value to a branch. The value may be a metadata
     * (treated {@linkplain AbstractMetadata#asMap as a Map} - see below),
     * a collection or a singleton.
     * <p>
     * Map or metadata are constructed as a sub tree where every nodes is a
     * property name, and the childs are the value(s) for that property.
     */
    private void append(final DefaultMutableTreeNode branch, final Object value) {
        if (value instanceof Map) {
            appendMap(branch, (Map) value);
            return;
        }
        if (value instanceof AbstractMetadata) {
            appendMap(branch, ((AbstractMetadata) value).asMap());
            return;
        }
        if (value != null) {
            final PropertyAccessor accessor = standard.getAccessorOptional(value.getClass());
            if (accessor != null) {
                appendMap(branch, new PropertyMap(value, accessor));
                return;
            }
        }
        if (value instanceof Collection) {
            for (final Iterator it=((Collection) value).iterator(); it.hasNext();) {
                final Object element = it.next();
                if (!PropertyAccessor.isEmpty(element)) {
                    append(branch, element);
                }
            }
            return;
        }
        final String asText;
        if (value instanceof CodeList) {
            asText = localize((CodeList) value);
        } else if (value instanceof Date) {
            asText = format((Date) value);
        } else if (value instanceof Number) {
            asText = format((Number) value);
        } else if (value instanceof InternationalString) {
            asText = ((InternationalString) value).toString(locale);
        } else {
            asText = String.valueOf(value);
        }
        branch.add(OptionalDependencies.createTreeNode(asText, value, false));
    }

    /**
     * Appends the specified map (usually a metadata) to a branch. Each map keys
     * is a child in the specified {@code branch}, and each value is a child of
     * the map key. There is often only one value for a map key, but not always;
     * some are collections, which are formatted as many childs for the same key.
     */
    private void appendMap(final DefaultMutableTreeNode branch, final Map asMap) {
        for (final Iterator it=asMap.entrySet().iterator(); it.hasNext();) {
            final Map.Entry entry = (Map.Entry) it.next();
            final Object value = entry.getValue();
            if (!PropertyAccessor.isEmpty(value)) {
                final String name = localize((String) entry.getKey());
                final DefaultMutableTreeNode child =
                        OptionalDependencies.createTreeNode(name, value, true);
                append(child, value);
                branch.add(child);
            }
        }
    }

    /**
     * Formats the specified number.
     */
    private String format(final Number value) {
        if (numberFormat == null) {
            numberFormat = NumberFormat.getNumberInstance(locale);
            numberFormat.setMinimumFractionDigits(0);
        }
        int precision = 0;
        if (!Classes.isInteger(value.getClass())) {
            precision = PRECISION;
            final double v = Math.abs(value.doubleValue());
            if (v > 0) {
                final int digits = (int) Math.log10(v);
                if (Math.abs(digits) >= PRECISION) {
                    // TODO: Switch to exponential notation when a convenient API will be available in J2SE.
                    return value.toString();
                }
                if (digits >= 0) {
                    precision -= digits;
                }
                precision = Math.max(0, PRECISION - precision);
            }
        }
        numberFormat.setMaximumFractionDigits(precision);
        return numberFormat.format(value);
    }

    /**
     * Formats the specified date.
     */
    private String format(final Date value) {
        if (dateFormat == null) {
            dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);
        }
        return dateFormat.format(value);
    }

    /**
     * Localize the specified property name. In current version, this is merely
     * a hook for future development. For now we reformat the programatic name.
     */
    private String localize(String name) {
        name = name.trim();
        final int length = name.length();
        if (length != 0) {
            final StringBuilder buffer = new StringBuilder();
            buffer.append(Character.toUpperCase(name.charAt(0)));
            boolean previousIsUpper = true;
            int base = 1;
            for (int i=1; i<length; i++) {
                final boolean currentIsUpper = Character.isUpperCase(name.charAt(i));
                if (currentIsUpper != previousIsUpper) {
                    /*
                     * When a case change is detected (lower case to upper case as in "someName",
                     * or "someURL", or upper case to lower case as in "HTTPProxy"), then insert
                     * a space just before the upper case letter.
                     */
                    int split = i;
                    if (previousIsUpper) {
                        split--;
                    }
                    if (split > base) {
                        buffer.append(name.substring(base, split)).append(' ');
                        base = split;
                    }
                }
                previousIsUpper = currentIsUpper;
            }
            final String candidate = buffer.append(name.substring(base)).toString();
            if (!candidate.equals(name)) {
                // Holds a reference to this new String object only if it worth it.
                name = candidate;
            }
        }
        return name;
    }

    /**
     * Localize the specified property name. In current version, this is merely
     * a hook for future development.  For now we reformat the programatic name.
     */
    private String localize(final CodeList code) {
        return code.name().trim().replace('_', ' ').toLowerCase(locale);
    }

    /**
     * Returns a string representation of the specified tree node.
     */
    public static String toString(final TreeNode node) {
        final StringBuilder buffer = new StringBuilder();
        toString(node, buffer, 0, System.getProperty("line.separator", "\n"));
        return buffer.toString();
    }

    /**
     * Append a string representation of the specified node to the specified buffer.
     */
    private static void toString(final TreeNode      node,
                                 final StringBuilder buffer,
                                 final int           indent,
                                 final String        lineSeparator)
    {
        final int count = node.getChildCount();
        if (count == 0) {
            if (node.isLeaf()) {
                /*
                 * If the node has no child and is a leaf, then it is some value like a number,
                 * a date or a string.  We just display this value, which is usually part of a
                 * collection. If the node has no child and is NOT a leaf, then it is an empty
                 * metadata and we just ommit it.
                 */
                buffer.append(Utilities.spaces(indent)).append(node).append(lineSeparator);
            }
            return;
        }
        buffer.append(Utilities.spaces(indent)).append(node).append(':');
        if (count == 1) {
            final TreeNode child = node.getChildAt(0);
            if (child.isLeaf()) {
                buffer.append(' ').append(child).append(lineSeparator);
                return;
            }
        }
        for (int i=0; i<count; i++) {
            final TreeNode child = node.getChildAt(i);
            if (i == 0) {
                buffer.append(lineSeparator);
            }
            toString(child, buffer, indent+2, lineSeparator);
        }
    }
}
