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

import java.util.Map;
import java.util.HashMap;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.Objects;
import java.text.Format;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.text.NumberFormat;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.AttributedCharacterIterator;
import java.text.FieldPosition;
import java.text.ParseException;
import java.text.ParsePosition;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZonedDateTime;
import java.time.format.FormatStyle;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.Temporal;
import java.lang.reflect.InaccessibleObjectException;
import javax.measure.Unit;
import org.apache.sis.util.Numbers;
import org.apache.sis.util.Localized;
import org.apache.sis.util.UnconvertibleObjectException;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.internal.shared.LocalizedParseException;
import org.apache.sis.util.internal.shared.Numerics;


/**
 * Parses and formats {@link Range} instances according the given locale.
 * This class complies to the format described in the <a href="https://en.wikipedia.org/wiki/ISO_31-11">ISO 31-11</a>
 * standard, except that the minimal and maximal values are separated by the "{@code …}" character
 * instead of coma. More specifically, the format is defined as below:
 *
 * <ul>
 *   <li>If the range {@linkplain Range#isEmpty() is empty}, then the range is represented by "{@code {}}".</li>
 *   <li>Otherwise if the {@linkplain Range#getMinValue() minimal value} is equal to the
 *       {@linkplain Range#getMaxValue() maximal value}, then that single value is formatted
 *       inside braces as in "{@code {value}}".</li>
 *   <li>Otherwise the minimal and maximal values are formatted inside bracket or parenthesis,
 *       depending on whether each endpoint is inclusive or exclusive:
 *     <ul>
 *       <li>"{@code [min … max]}" if both endpoints are inclusive (<dfn>closed interval</dfn>);</li>
 *       <li>"{@code (min … max)}" if both endpoints are exclusive (<dfn>open interval</dfn>);</li>
 *       <li>or a mix of both styles if an endpoint is inclusive while the other is exclusive.</li>
 *     </ul>
 *     The "{@code ∞}" symbol is used in place of {@code min} or {@code max} for unbounded ranges.</li>
 * </ul>
 *
 * If the range to format is an instance of {@link MeasurementRange}, then the
 * {@linkplain Unit unit of measurement} is appended except for empty ranges.
 *
 * <h2>Lenient parsing</h2>
 * At parsing time, the above formatting rules are relaxed as below:
 *
 * <ul>
 *   <li>Empty ranges can be represented by "{@code []}"or "{@code ()}" in addition to the
 *       standard "{@code {}}".</li>
 *   <li>The braces are optional for singleton values, i.e. "{@code value}" is accepted
 *       as well as "{@code {value}}".</li>
 * </ul>
 *
 * <h2>Range type and type of range elements</h2>
 * The kind of ranges created by the {@link #parse(String) parse(…)} methods is determined
 * by the type of range elements:
 *
 * <ul>
 *   <li>If the elements type is assignable to {@link Date}, then the {@code parse(…)} methods
 *       will create {@code Range<Date>} objects.</li>
 *   <li>If the elements type is assignable to {@link Number}, then:
 *     <ul>
 *       <li>If the text to parse contains a {@linkplain Unit unit of measurement}, then
 *           the {@code parse(…)} methods will create {@link MeasurementRange} objects.</li>
 *       <li>Otherwise the {@code parse(…)} methods will create {@link NumberRange} objects.</li>
 *     </ul>
 *   </li>
 * </ul>
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 1.4
 *
 * @see Range#toString()
 * @see <a href="https://en.wikipedia.org/wiki/ISO_31-11">Wikipedia: ISO 31-11</a>
 *
 * @since 0.3
 */
public class RangeFormat extends Format implements Localized {
    /**
     * For cross-version compatibility.
     */
    private static final long serialVersionUID = 2459948572315667868L;

    /**
     * The constant value for {@link FieldPosition} which designate the minimal value.
     *
     * @see Field#MIN_VALUE
     */
    private static final int MIN_VALUE_FIELD = 0;

    /**
     * The constant value for {@link FieldPosition} which designate the maximal value.
     *
     * @see Field#MAX_VALUE
     */
    private static final int MAX_VALUE_FIELD = 1;

    /**
     * The constant value for {@link FieldPosition} which designate the unit of measurement.
     *
     * @see Field#UNIT
     */
    private static final int UNIT_FIELD = 2;

    /**
     * Constants that are used as attribute keys in the iterator returned from
     * {@link RangeFormat#formatToCharacterIterator(Object)}.
     *
     * @author  Martin Desruisseaux (Geomatys)
     * @version 0.3
     * @since   0.3
     */
    public static final class Field extends FormatField {
        /**
         * For cross-version compatibility.
         */
        private static final long serialVersionUID = 2000378602311146796L;

        /**
         * Creates a new field of the given name. The given name shall
         * be identical to the name of the public static constant.
         */
        private Field(final String name, final int fieldID) {
            super(name, fieldID);
        }

        /**
         * Identifies the minimal value field in a range.
         * When formatting a string, this value may be specified to the {@link FieldPosition}
         * constructor in order to get the bounding index where the minimal value has been written.
         */
        public static final Field MIN_VALUE = new Field("MIN_VALUE", MIN_VALUE_FIELD);

        /**
         * Identifies the maximal value field in a range.
         * When formatting a string, this value may be specified to the {@link FieldPosition}
         * constructor in order to get the bounding index where the maximal value has been written.
         */
        public static final Field MAX_VALUE = new Field("MAX_VALUE", MAX_VALUE_FIELD);

        /**
         * Identifies the unit field in a range, if any.
         * When formatting a string, this value may be specified to the {@link FieldPosition}
         * constructor in order to get the bounding index where the unit has been written.
         */
        public static final Field UNIT = new Field("UNIT", UNIT_FIELD);

        /**
         * Returns the field constant for the given numeric identifier.
         */
        static Field forCode(final int field) {
            switch (field) {
                case MIN_VALUE_FIELD: return MIN_VALUE;
                case MAX_VALUE_FIELD: return MAX_VALUE;
                case UNIT_FIELD:      return UNIT;
                default: throw new AssertionError(field);
            }
        }
    }

    // All "character" fields below are code point values.
    /**
     * The character opening an empty range or a range containing only one element.
     * The default value is <code>'&#123;'</code>.
     */
    private final int openSet;

    /**
     * The character opening a range in which the minimal value is inclusive.
     * The default value is {@code '['}.
     */
    private final int openInclusive;

    /**
     * The character opening a range in which the minimal value is exclusive.
     * The default value is {@code '('}. Note that the {@code ']'} character
     * is also sometimes used.
     */
    private final int openExclusive;

    /**
     * An alternative character opening a range in which the minimal value is exclusive.
     * This character is not used for formatting (only {@link #openExclusive} is used),
     * but is accepted during parsing. The default value is {@code ']'}.
     */
    private final int openExclusiveAlt;

    /**
     * The character closing an empty range or a range containing only one element.
     * The default value is <code>'&#125;'</code>.
     */
    private final int closeSet;

    /**
     * The character closing a range in which the maximal value is inclusive.
     * The default value is {@code ']'}.
     */
    private final int closeInclusive;

    /**
     * The character closing a range in which the maximal value is exclusive.
     * The default value is {@code ')'}. Note that the {@code '['} character
     * is also sometimes used.
     */
    private final int closeExclusive;

    /**
     * An alternative character closing a range in which the maximal value is exclusive.
     * This character is not used for formatting (only {@link #closeExclusive} is used),
     * but is accepted during parsing. The default value is {@code '['}.
     */
    private final int closeExclusiveAlt;

    /**
     * The string to use as a separator between minimal and maximal value, not including
     * whitespaces. The default value is {@code "…"} (Unicode 2026).
     */
    private final String separator;

    /**
     * Symbols used by this format, inferred from {@link DecimalFormatSymbols}.
     */
    private final char minusSign;

    /**
     * Symbols used by this format, inferred from {@link DecimalFormatSymbols}.
     */
    private final String infinity;

    /**
     * {@code true} if {@code RangeFormat} shall use the alternate form at formatting time.
     * This flag as no effect on parsing, since both forms are accepted.
     *
     * @see #isAlternateForm()
     */
    private boolean alternateForm;

    /**
     * The type of the range components. Valid types are {@link Number}, {@link Angle},
     * {@link Date} or a subclass of those types. This value determines the kind of range
     * to be created by the parse method:
     *
     * <ul>
     *   <li>{@code NumberRange<?>} if the element type is assignable to {@link Number} or {@link Angle}.</li>
     *   <li>{@code Range<Date>} if the element type is assignable to {@link Date}.</li>
     * </ul>
     *
     * @see Range#getElementType()
     */
    protected final Class<?> elementType;

    /**
     * The format to use for parsing and formatting the range components.
     * The format is determined from the {@linkplain #elementType element type}:
     *
     * <ul>
     *   <li>{@link AngleFormat}  if the element type is assignable to {@link Angle}.</li>
     *   <li>{@link NumberFormat} if the element type is assignable to {@link Number}.</li>
     *   <li>{@link DateFormat}   if the element type is assignable to {@link Date}.</li>
     * </ul>
     */
    protected final Format elementFormat;

    /**
     * The format for unit of measurement, or {@code null} if none. This is non-null if and
     * only if {@link #elementType} is assignable to {@link Number} but not to {@link Angle}.
     */
    protected final UnitFormat unitFormat;

    /**
     * Whether we should insert a space between the bracket and the unit symbol.
     *
     * @see #insertSpaceBeforeUnit(Unit)
     */
    private transient Map<Unit<?>,Boolean> insertSpaceBeforeUnit;

    /**
     * The locale for error message, or {@code null} for the default.
     */
    private Locale locale;

    /**
     * Creates a new format for parsing and formatting {@linkplain NumberRange number ranges}
     * using the {@linkplain Locale#getDefault() default locale}.
     */
    public RangeFormat() {
        this(Locale.getDefault(Locale.Category.FORMAT));
        locale = Locale.getDefault(Locale.Category.DISPLAY);
    }

    /**
     * Creates a new format for parsing and formatting {@linkplain NumberRange number ranges}
     * using the given locale.
     *
     * @param  locale  the locale for parsing and formatting range components.
     */
    public RangeFormat(final Locale locale) {
        this(locale, Number.class);
    }

    /**
     * Creates a new format for parsing and formatting {@code Range<Date>}
     * using the given locale and timezone.
     *
     * @param locale    the locale for parsing and formatting range components.
     * @param timezone  the timezone for the dates to be formatted.
     */
    public RangeFormat(final Locale locale, final TimeZone timezone) {
        this(locale, Date.class);
        ((DateFormat) elementFormat).setTimeZone(timezone);
    }

    /**
     * Creates a new format for parsing and formatting {@linkplain Range ranges} of
     * the given element type using the given locale. The element type is typically
     * {@code Date.class} or some subclass of {@code Number.class}.
     *
     * @param  locale       the locale for parsing and formatting range components.
     * @param  elementType  the type of range components.
     * @throws IllegalArgumentException if the given type is not recognized by this constructor.
     */
    public RangeFormat(final Locale locale, final Class<?> elementType) throws IllegalArgumentException {
        this.locale      = Objects.requireNonNull(locale);
        this.elementType = Objects.requireNonNull(elementType);
        if (Angle.class.isAssignableFrom(elementType)) {
            elementFormat = AngleFormat.getInstance(locale);
            unitFormat    = null;
        } else if (Number.class.isAssignableFrom(elementType)) {
            elementFormat = NumberFormat.getNumberInstance(locale);
            unitFormat    = new UnitFormat(locale);
        } else if (Date.class.isAssignableFrom(elementType) || elementType == Instant.class) {
            elementFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale);
            unitFormat    = null;
        } else if (Temporal.class.isAssignableFrom(elementType)) {
            final FormatStyle dateStyle = hasDateFields(elementType) ? FormatStyle.SHORT : null;
            final FormatStyle timeStyle = hasTimeFields(elementType) ? FormatStyle.SHORT : null;
            elementFormat = new DateTimeFormatterBuilder().appendLocalized(dateStyle, timeStyle).toFormatter(locale).toFormat();
            unitFormat    = null;
        } else {
            throw new IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedType_1, elementType));
        }
        final DecimalFormatSymbols ds;
        if (elementFormat instanceof DecimalFormat) {
            ds = ((DecimalFormat) elementFormat).getDecimalFormatSymbols();
        } else {
            ds = DecimalFormatSymbols.getInstance(locale);
        }
        minusSign         = ds.getMinusSign();
        infinity          = ds.getInfinity();
        openSet           = '{';
        openInclusive     = '[';        // Future SIS version may determine those characters from the locale.
        openExclusive     = '(';        // We may also provide an 'applyPattern(String)' method for setting those char.
        openExclusiveAlt  = ']';
        closeSet          = '}';
        closeInclusive    = ']';
        closeExclusive    = ')';
        closeExclusiveAlt = '[';
        separator         = "…";
    }

    /**
     * Returns {@code true} if the given character is any of the opening bracket characters.
     */
    private boolean isOpen(final int c) {
        return (c == openInclusive) || (c == openExclusive) || (c == openExclusiveAlt);
    }

    /**
     * Returns {@code true} if the given character is any of the closing bracket characters.
     */
    private boolean isClose(final int c) {
        return (c == closeInclusive) || (c == closeExclusive) || (c == closeExclusiveAlt);
    }

    /**
     * Returns {@code true} if objects of the given class have day, month and hour fields.
     * This method is defined here for having a single class where to concentrate such heuristic rules.
     * Note that {@link Instant} does not have date fields.
     *
     * @param  date  class of object to test (may be {@code null}).
     * @return whether the given class is {@link LocalDate} or one of the classes with date + time.
     *         This list may be expanded in future versions.
     */
    public static boolean hasDateFields(final Class<?> date) {
        return date == LocalDate.class
            || date == LocalDateTime.class
            || date == OffsetDateTime.class
            || date == ZonedDateTime.class;
    }

    /**
     * Returns {@code true} if objects of the given class have time fields.
     * This method is defined here for having a single class where to concentrate such heuristic rules.
     * Note that {@link Instant} does not have hour fields.
     *
     * @param  date  class of object to test (may be {@code null}).
     * @return whether the given class is {@link LocalTime}, {@link OffsetTime} or one of the classes with date + time.
     *         This list may be expanded in future versions.
     */
    public static boolean hasTimeFields(final Class<?> date) {
        return date == LocalTime.class
            || date == OffsetTime.class
            || date == LocalDateTime.class
            || date == OffsetDateTime.class
            || date == ZonedDateTime.class;
    }

    /**
     * Returns this formatter locale. This is the locale specified at construction time if any,
     * or the {@linkplain Locale#getDefault() default locale} at construction time otherwise.
     *
     * @return this formatter locale (never {@code null}).
     *
     * @since 1.0
     */
    @Override
    public Locale getLocale() {
        return locale;
    }

    /**
     * Returns the pattern used by {@link #elementFormat} for formatting the minimum and
     * maximum values. If the element format does not use pattern, returns {@code null}.
     *
     * @param  localized {@code true} for returning the localized pattern, or {@code false} for the unlocalized one.
     * @return the pattern, or {@code null} if the {@link #elementFormat} doesn't use pattern.
     *
     * @see DecimalFormat#toPattern()
     * @see SimpleDateFormat#toPattern()
     * @see AngleFormat#toPattern()
     */
    public String getElementPattern(final boolean localized) {
        final Format format = elementFormat;
        if (format instanceof DecimalFormat) {
            final DecimalFormat df = (DecimalFormat) format;
            return localized ? df.toLocalizedPattern() : df.toPattern();
        }
        if (format instanceof SimpleDateFormat) {
            final SimpleDateFormat df = (SimpleDateFormat) format;
            return localized ? df.toLocalizedPattern() : df.toPattern();
        }
        if (format instanceof AngleFormat) {
            return ((AngleFormat) format).toPattern();
        }
        return null;
    }

    /**
     * Sets the pattern to be used by {@link #elementFormat} for formatting the minimum and
     * maximum values.
     *
     * @param  pattern    the new pattern.
     * @param  localized  {@code true} if the given pattern is localized.
     * @throws IllegalStateException if the {@link #elementFormat} does not use pattern.
     *
     * @see DecimalFormat#applyPattern(String)
     * @see SimpleDateFormat#applyPattern(String)
     * @see AngleFormat#applyPattern(String)
     */
    public void setElementPattern(final String pattern, final boolean localized) {
        final Format format = elementFormat;
        if (format instanceof DecimalFormat) {
            final DecimalFormat df = (DecimalFormat) format;
            if (localized) {
                df.applyLocalizedPattern(pattern);
            } else {
                df.applyPattern(pattern);
            }
        } else if (format instanceof SimpleDateFormat) {
            final SimpleDateFormat df = (SimpleDateFormat) format;
            if (localized) {
                df.applyLocalizedPattern(pattern);
            } else {
                df.applyPattern(pattern);
            }
        } else if (format instanceof AngleFormat) {
            ((AngleFormat) format).applyPattern(pattern);
        } else {
            throw new IllegalStateException();
        }
    }

    /**
     * Returns {@code true} if this {@code RangeFormat} shall use the alternate form at
     * formatting time. The alternate form expresses open intervals like {@code ]a…b[}
     * instead of {@code (a…b)}.
     *
     * <p>This flag has no effect on parsing, since the parser accepts both forms.</p>
     *
     * @return {@code true} for using the alternate format instead of the default format.
     */
    public boolean isAlternateForm() {
        return alternateForm;
    }

    /**
     * Sets whether this {@code RangeFormat} shall use the alternate form at formatting time.
     * The alternate form expresses open intervals like {@code ]a…b[} instead of {@code (a…b)}.
     *
     * @param alternateForm {@code true} for using the alternate format, or {@code false} for using the default format.
     */
    public void setAlternateForm(final boolean alternateForm) {
        this.alternateForm = alternateForm;
    }

    /**
     * Returns whether we should insert a space between the bracket and the unit symbol.
     * We cache the result because checking for this condition forces us to format the unit symbol twice.
     */
    private boolean insertSpaceBeforeUnit(final Unit<?> unit) {
        if (insertSpaceBeforeUnit == null) {
            insertSpaceBeforeUnit = new HashMap<>();
        }
        Boolean value = insertSpaceBeforeUnit.get(unit);
        if (value == null) {
            final String symbol = unitFormat.format(unit);
            value = !symbol.isEmpty() && Character.isLetterOrDigit(symbol.codePointAt(0));
            insertSpaceBeforeUnit.put(unit, value);
        }
        return value;
    }

    /**
     * Returns the {@code *_FIELD} constant for the given field position, or -1 if none.
     */
    private static int getField(final FieldPosition position) {
        if (position != null) {
            final Format.Field field = position.getFieldAttribute();
            if (field instanceof Field) {
                return ((Field) field).field;
            }
            return position.getField();
        }
        return -1;
    }

    /**
     * Casts the given object to a {@code Range}, or throws an {@code IllegalArgumentException}
     * if the given object is not a {@code Range} instance. This is used for validating argument
     * of {@link Object} type in formatting methods.
     */
    private static Range<?> cast(final Object range) throws IllegalArgumentException {
        if (range instanceof Range<?>) {
            return (Range<?>) range;
        }
        final String message;
        if (range == null) {
            message = Errors.format(Errors.Keys.NullArgument_1, "range");
        } else {
            message = Errors.format(Errors.Keys.IllegalArgumentClass_3, "range", Range.class, range.getClass());
        }
        throw new IllegalArgumentException(message);
    }

    /**
     * Formats a {@link Range} and appends the resulting text to a given string buffer.
     * See the <a href="#skip-navbar_top">class javadoc</a> for a description of the format.
     *
     * @param  range       the {@link Range} object to format.
     * @param  toAppendTo  where the text is to be appended.
     * @param  pos         identifies a field in the formatted text, or {@code null} if none.
     * @return the string buffer passed in as {@code toAppendTo}, with formatted text appended.
     * @throws IllegalArgumentException if this formatter cannot format the given object.
     */
    @Override
    public StringBuffer format(final Object range, final StringBuffer toAppendTo, final FieldPosition pos) {
        format(cast(range), toAppendTo, pos, null);
        return toAppendTo;
    }

    /**
     * Implementation of the format methods.
     *
     * @param  range              the range to format.
     * @param  toAppendTo         where the text is to be appended.
     * @param  pos                identifies a field in the formatted text, or {@code null} if none.
     * @param  characterIterator  the character iterator for which the attributes need to be set, or null if none.
     */
    @SuppressWarnings("fallthrough")
    private void format(final Range<?> range, final StringBuffer toAppendTo, final FieldPosition pos,
            final FormattedCharacterIterator characterIterator)
    {
        /*
         * Special case for an empty range. This is typically formatted as "{}". The field
         * position is unconditionally set to the empty substring inside the brackets.
         */
        int fieldPos = getField(pos);
        if (range.isEmpty()) {
            toAppendTo.appendCodePoint(openSet);
            if (fieldPos >= MIN_VALUE_FIELD && fieldPos <= UNIT_FIELD) {
                final int p = toAppendTo.length();
                pos.setBeginIndex(p);                                   // First index, inclusive.
                pos.setEndIndex  (p);                                   // Last index, exclusive
            }
            toAppendTo.appendCodePoint(closeSet);
            return;
        }
        /*
         * Format a non-empty range by looping over all possible fields.
         *
         * Secial case: if minimal and maximal values are the same,
         * formats only the maximal value.
         */
        final Comparable<?> minValue = range.getMinValue();
        final Comparable<?> maxValue = range.getMaxValue();
        final boolean isSingleton = (minValue != null) && minValue.equals(maxValue);
        int field = MIN_VALUE_FIELD;
        if (isSingleton) {
            if (fieldPos == MIN_VALUE_FIELD) {
                fieldPos = MAX_VALUE_FIELD;
            }
            field = MAX_VALUE_FIELD;
        }
        toAppendTo.appendCodePoint(                     // Select the char for the first condition to be true below:
                isSingleton           ? openSet :
                range.isMinIncluded() ? openInclusive :
                alternateForm         ? openExclusiveAlt :
                /* otherwise */         openExclusive);
        for (; field <= UNIT_FIELD; field++) {
            final Object value;
            switch (field) {
                case MIN_VALUE_FIELD: value = toFormattable(minValue); break;
                case MAX_VALUE_FIELD: value = toFormattable(maxValue); break;
                case UNIT_FIELD:      value = range.unit(); break;
                default: throw new AssertionError(field);
            }
            int startPosition = toAppendTo.length();
            if (value == null) {
                switch (field) {
                    case MIN_VALUE_FIELD: toAppendTo.append(minusSign != '-' ? minusSign : '−');    // Fall through
                    case MAX_VALUE_FIELD: toAppendTo.append(infinity); break;
                }
            } else {
                final Format format;
                if (field == UNIT_FIELD) {
                    if (insertSpaceBeforeUnit((Unit) value)) {
                        startPosition = toAppendTo.append(' ').length();
                    }
                    format = unitFormat;
                } else {
                    format = elementFormat;
                }
                Numerics.useScientificNotationIfNeeded(format, value, (f,v) -> {
                    if (characterIterator != null) {
                        characterIterator.append(f.formatToCharacterIterator(v), toAppendTo);
                    } else {
                        f.format(v, toAppendTo, new FieldPosition(-1));
                    }
                    return null;
                });
            }
            /*
             * At this point, the field has been formatted. Now store the field index,
             * then append the separator between this field and the next one.
             */
            if (characterIterator != null) {
                characterIterator.addFieldLimit(Field.forCode(field), value, startPosition);
            }
            if (field == fieldPos) {
                pos.setBeginIndex(startPosition);
                pos.setEndIndex(toAppendTo.length());
            }
            switch (field) {
                case MIN_VALUE_FIELD: {
                    toAppendTo.append(' ').append(separator).append(' ');
                    break;
                }
                case MAX_VALUE_FIELD: {                 // Select the char for the first condition to be true below:
                    toAppendTo.appendCodePoint(
                            isSingleton           ? closeSet :
                            range.isMaxIncluded() ? closeInclusive :
                            alternateForm         ? closeExclusiveAlt :
                            /* otherwise */         closeExclusive);
                    break;
                }
            }
        }
    }

    /**
     * Formats a range as an attributed character iterator.
     * Callers can iterate and queries the attribute values as in the following example:
     *
     * {@snippet lang="java" :
     *     AttributedCharacterIterator it = rangeFormat.formatToCharacterIterator(myRange);
     *     for (char c=it.first(); c!=AttributedCharacterIterator.DONE; c=c.next()) {
     *         // 'c' is a character from the formatted string.
     *         if (it.getAttribute(RangeFormat.Field.MIN_VALUE) != null) {
     *             // If we enter this block, then the character 'c' is part of the minimal value,
     *             // This field extends from it.getRunStart(MIN_VALUE) to it.getRunLimit(MIN_VALUE).
     *         }
     *     }
     *     }
     *
     * Alternatively, if the current {@linkplain AttributedCharacterIterator#getIndex() iterator
     * index} is before the start of the minimum value field, then the starting position of that
     * field can be obtained directly by {@code it.getRunLimit(MIN_VALUE)}. If the current iterator
     * index is inside the minimum value field, then the above method call will rather returns the
     * end of that field. The same strategy works for other all fields too.
     *
     * <p>The returned character iterator contains all {@link java.text.NumberFormat.Field},
     * {@link java.text.DateFormat.Field} or {@link org.apache.sis.measure.AngleFormat.Field}
     * attributes in addition to the {@link Field} ones. Consequently, the same character may
     * have more than one attribute.</p>
     *
     * <p>In Apache SIS implementation, the returned character iterator also implements the
     * {@link CharSequence} interface for convenience.</p>
     *
     * @param  range  the {@link Range} object to format.
     * @return a character iterator together with the attributes describing the formatted value.
     * @throws IllegalArgumentException if {@code value} if not an instance of {@link Range}.
     */
    @Override
    public AttributedCharacterIterator formatToCharacterIterator(final Object range) {
        final StringBuffer buffer = new StringBuffer();
        final FormattedCharacterIterator it = new FormattedCharacterIterator(buffer);
        format(cast(range), buffer, null, it);
        return it;
    }

    /**
     * Parses text from a string to produce a range. The default implementation delegates to
     * {@link #parse(String)} with no additional work.
     *
     * @param  source  the text, part of which should be parsed.
     * @return a range parsed from the string.
     * @throws ParseException if the given string cannot be fully parsed.
     */
    @Override
    public Object parseObject(final String source) throws ParseException {
        return parse(source);
    }

    /**
     * Parses text from a string to produce a range. The default implementation delegates to
     * {@link #parse(String, ParsePosition)} with no additional work.
     *
     * @param  source  the text, part of which should be parsed.
     * @param  pos     index and error index information as described above.
     * @return a range parsed from the string, or {@code null} in case of error.
     */
    @Override
    public Object parseObject(final String source, final ParsePosition pos) {
        return parse(source, pos);
    }

    /**
     * Parses text from the given string to produce a range. This method use the full string.
     * If there is some unparsed characters after the parsed range, then this method thrown an
     * exception.
     *
     * @param  source  the text to parse.
     * @return the parsed range (never {@code null}).
     * @throws ParseException if the given string cannot be fully parsed.
     */
    public Range<?> parse(final String source) throws ParseException {
        final ParsePosition pos = new ParsePosition(0);
        UnconvertibleObjectException failure = null;
        try {
            final Range<?> range = tryParse(source, pos);
            if (range != null) {
                return range;
            }
        } catch (UnconvertibleObjectException e) {
            failure = e;
        }
        throw new LocalizedParseException(locale, elementType, source, pos).initCause(failure);
    }

    /**
     * Parses text from a string to produce a range. The method attempts to parse text starting
     * at the index given by {@code pos}. If parsing succeeds, then the index of {@code pos} is
     * updated to the index after the last character used, and the parsed range is returned. If
     * an error occurs, then the index of {@code pos} is not changed, the error index of {@code pos}
     * is set to the index of the character where the error occurred, and {@code null} is returned.
     *
     * @param  source  the text, part of which should be parsed.
     * @param  pos     index and error index information as described above.
     * @return a range parsed from the string, or {@code null} in case of error.
     */
    public Range<?> parse(final String source, final ParsePosition pos) {
        final int origin = pos.getIndex();
        Range<?> range;
        try {
            // Remainder: tryParse may return null.
            range = tryParse(source, pos);
        } catch (UnconvertibleObjectException e) {
            // Ignore - the error will be reported through the error index.
            range = null;
        }
        if (range != null) {
            pos.setErrorIndex(-1);
        } else {
            pos.setIndex(origin);
        }
        return range;
    }

    /**
     * Tries to parse the given text. In case of success, the error index is undetermined and
     * need to be reset to -1.  In case of failure (including an exception being thrown), the
     * parse index is undetermined and need to be reset to its initial value.
     */
    @SuppressWarnings({"unchecked","rawtypes"})
    private Range<?> tryParse(final String source, final ParsePosition pos)
            throws UnconvertibleObjectException
    {
        final int length = source.length();
        /*
         * Skip leading whitespace and find the first non-blank character. It is usually an opening bracket,
         * except if minimal and maximal values are the same in which case the brackets may be omitted.
         */
        int index, c;
        for (index = pos.getIndex(); ; index += Character.charCount(c)) {
            if (index >= length) {
                pos.setErrorIndex(length);
                return null;
            }
            c = source.codePointAt(index);
            if (!Character.isWhitespace(c)) break;
        }
        final Object minValue, maxValue;
        final boolean isMinIncluded, isMaxIncluded;
        if (!isOpen(c)) {
            /*
             * No bracket, or curly bracket. We have eigher an empty range (as in "{}") or a single value for the range.
             * The braces are optional for single value. In other words, this block parses all of the following cases:
             *
             *  - {}
             *  - {value}
             *  - value         (not standard, but accepted by this parser)
             */
            final boolean hasBraces = (c == openSet);
            if (hasBraces) {
                // Skip the opening brace and following whitespaces.
                while ((index += Character.charCount(c)) < length) {
                    c = source.codePointAt(index);
                    if (!Character.isWhitespace(c)) break;
                }
            }
            if (hasBraces && c == closeSet) {
                // Empty range represented by {}
                minValue = maxValue = valueOfNil();
                isMinIncluded = isMaxIncluded = false;
            } else {
                // Singleton value, with or without braces.
                pos.setIndex(index);
                final Object value = elementFormat.parseObject(source, pos);
                if (value == null) {
                    return null;
                }
                pos.setErrorIndex(index);                   // In case of failure during the conversion.
                minValue = maxValue = convert(value);
                index = pos.getIndex();
                isMinIncluded = isMaxIncluded = true;
            }
            if (hasBraces) {
                // Skip whitespaces, then skip the closing brace.
                // Absence of closing brace is considered an error.
                do {
                    if (index >= length) {
                        pos.setErrorIndex(length);
                        return null;
                    }
                    c = source.codePointAt(index);
                    index += Character.charCount(c);
                } while (Character.isWhitespace(c));
                if (c != closeSet) {
                    pos.setErrorIndex(index - Character.charCount(c));
                    return null;
                }
                pos.setIndex(index);
            }
        } else {
            /*
             * We found an opening bracket. Skip the whitespaces. If the next character is a closing bracket,
             * then we have an empty range. The latter case is an extension to the standard format since empty
             * ranges are usually represented by {} instead of [].
             */
            isMinIncluded = (c == openInclusive);
            do {                                            // Skip whitespaces.
                index += Character.charCount(c);
                if (index >= length) {
                    pos.setErrorIndex(length);
                    return null;
                }
                c = source.codePointAt(index);
            } while (Character.isWhitespace(c));
            if (isClose(c)) {
                pos.setErrorIndex(index);                   // In case of failure during the conversion.
                minValue = maxValue = valueOfNil();
                isMaxIncluded = false;
                index += Character.charCount(c);
            } else {
                /*
                 * At this point, we have determined that the range is non-empty and there is at least one value to parse.
                 * First, parse the minimal value. If we fail to parse, check if it was the infinity value. Note that "-∞"
                 * and "∞" should have been parsed successfully if the format is DecimalFormat, but not necessarily "−∞".
                 * The difference is in the character used for the minus sign (ASCII hyphen versus Unicode minus sign).
                 */
                pos.setIndex(index);
                int savedIndex = index;
                Object value = elementFormat.parseObject(source, pos);
                if (value == null) {
                    if (c == minusSign || c == '−') {
                        index += Character.charCount(c);
                    }
                    if (!source.startsWith(infinity, index)) {
                        return null;
                    }
                    pos.setIndex(index += infinity.length());
                }
                pos.setErrorIndex(savedIndex);              // In case of failure during the conversion.
                minValue = convert(value);
                /*
                 * Parsing of 'minValue' succeed and its type is valid. Now look for the separator. If it is not present,
                 * then assume that we have a single value for the range. The default RangeFormat implementation does not
                 * format brackets in such case (see the "No bracket" case above), but we make the parser tolerant to the
                 * case where the brackets are present.
                 */
                for (index = pos.getIndex(); ; index += Character.charCount(c)) {
                    if (index >= length) {
                        pos.setErrorIndex(length);
                        return null;
                    }
                    c = source.codePointAt(index);
                    if (!Character.isWhitespace(c)) break;
                }
                final String separator = this.separator;
                if (source.startsWith(separator, index)) {
                    index += separator.length();
                    for (;; index += Character.charCount(c)) {
                        if (index >= length) {
                            pos.setErrorIndex(length);
                            return null;
                        }
                        c = source.codePointAt(index);
                        if (!Character.isWhitespace(c)) break;
                    }
                    /*
                     * Now parse the maximum value. A special case is applied for infinity value
                     * in a similar way than we did for the minimal value.
                     */
                    pos.setIndex(index);
                    value = elementFormat.parseObject(source, pos);
                    if (value == null) {
                        if (!source.startsWith(infinity, index)) {
                            return null;
                        }
                        pos.setIndex(index += infinity.length());
                    }
                    pos.setErrorIndex(index);               // In case of failure during the conversion.
                    maxValue = convert(value);
                    /*
                     * Skip one last time the whitespaces. The check for the closing bracket (which is mandatory)
                     * is performed outside the "if" block since it is common to the two "if ... else" cases.
                     */
                    for (index = pos.getIndex(); ; index += Character.charCount(c)) {
                        if (index >= length) {
                            pos.setErrorIndex(length);
                            return null;
                        }
                        c = source.charAt(index);
                        if (!Character.isWhitespace(c)) break;
                    }
                } else {
                    maxValue = minValue;
                }
                if (!isClose(c)) {
                    pos.setErrorIndex(index);
                    return null;
                }
                index += Character.charCount(c);
                isMaxIncluded = (c == closeInclusive);
            }
            pos.setIndex(index);
        }
        /*
         * Parses the unit, if any. The units are always optional: if we cannot parse
         * them, then we will consider that the parsing stopped before the unit.
         */
        Unit<?> unit = null;
        if (unitFormat != null) {
            while (index < length) {
                c = source.codePointAt(index);
                if (Character.isWhitespace(c)) {
                    index += Character.charCount(c);
                    continue;
                }
                // At this point we found a character that could be
                // the beginning of a unit symbol. Try to parse that.
                pos.setIndex(index);
                unit = unitFormat.parse(source, pos);
                break;
            }
        }
        /*
         * At this point, all required information are available. Now build the range.
         * In the special case were the target type is the generic Number type instead
         * than a more specialized type, the finest suitable type will be determined.
         */
        if (Number.class.isAssignableFrom(elementType)) {
            Class<? extends Number> type = (Class) elementType;
            Number min = (Number) minValue;
            Number max = (Number) maxValue;
            if (type == Number.class) {
                type = Numbers.widestClass(Numbers.narrowestClass(min), Numbers.narrowestClass(max));
                min  = Numbers.cast(min, type);
                max  = Numbers.cast(max, type);
            }
            if (min != null && min.doubleValue() == Double.NEGATIVE_INFINITY) min = null;
            if (max != null && max.doubleValue() == Double.POSITIVE_INFINITY) max = null;
            if (unit != null) {
                final MeasurementRange<?> range = new MeasurementRange(type, min, isMinIncluded, max, isMaxIncluded, unit);
                return range;
            }
            return new NumberRange(type, min, isMinIncluded, max, isMaxIncluded);
        } else if (Date.class.isAssignableFrom(elementType)) {
            return new Range(Date.class, (Date) minValue, isMinIncluded, (Date) maxValue, isMaxIncluded);
        } else {
            return new Range(elementType,
                    (Comparable<?>) minValue, isMinIncluded,
                    (Comparable<?>) maxValue, isMaxIncluded);
        }
    }

    /**
     * Converts the given value to an instance of the {@link #elementType} type.
     * This method is partially the converse of {@link #toFormattable(Object)}.
     */
    @SuppressWarnings("unchecked")
    private Object convert(final Object value) throws UnconvertibleObjectException {
        if (value == null || elementType.isInstance(value)) {
            return value;
        }
        if (value instanceof Number && Number.class.isAssignableFrom(elementType)) {
            return Numbers.cast((Number) value, (Class<? extends Number>) elementType);
        }
        if (value instanceof Date && elementType == Instant.class) {
            return ((Date) value).toInstant();
        }
        throw new UnconvertibleObjectException(Errors.format(
                Errors.Keys.IllegalClass_2, elementType, value.getClass()));
    }

    /**
     * Converts the given object to a type that {@code format(…)} method can process.
     * This method is partially the converse of {@link #convert(Object)}.
     */
    private Object toFormattable(final Object value) {
        if (value instanceof Instant && elementType == Instant.class) {
            return Date.from((Instant) value);
        }
        return value;
    }

    /**
     * Returns a "nil" value. This is used for creating empty ranges.
     */
    private Object valueOfNil() {
        Object value = Numbers.valueOfNil(elementType);
        if (value == null) {
            if (Date.class.isAssignableFrom(elementType)) {
                value = new Date();
            } else {
                value = 0;
            }
        }
        return convert(value);
    }

    /**
     * Returns a clone of this range format.
     *
     * @return a clone of this range format.
     */
    @Override
    public RangeFormat clone() {
        final RangeFormat f = (RangeFormat) super.clone();
        try {
            f.clone("elementFormat");
            f.clone("unitFormat");
        } catch (ReflectiveOperationException e) {
            throw (InaccessibleObjectException) new InaccessibleObjectException().initCause(e);
        }
        return f;
    }

    /**
     * Clones the value in the specified field.
     */
    private void clone(final String field) throws ReflectiveOperationException {
        final var f = RangeFormat.class.getDeclaredField(field);
        f.setAccessible(true);
        f.set(this, ((Format) f.get(this)).clone());
    }
}
