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

import java.util.Arrays;
import java.util.Locale;
import java.util.Objects;
import java.io.Serializable;
import java.io.ObjectStreamException;
import java.text.NumberFormat;
import org.apache.sis.util.Localized;
import org.apache.sis.util.Workaround;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.resources.Errors;
import static org.apache.sis.util.ArgumentChecks.*;


/**
 * The set of symbols to use for <i>Well Known Text</i> (WKT) parsing and formatting.
 * The two constants defined in this class, namely {@link #SQUARE_BRACKETS} and {@link #CURLY_BRACKETS},
 * define the symbols for ISO 19162 compliant WKT formatting. Their properties are:
 *
 * <table class="sis">
 *   <caption>Standard WKT symbols</caption>
 *   <tr>
 *     <th>WKT aspect</th>
 *     <th>Standard value</th>
 *     <th>Comment</th>
 *   </tr>
 *   <tr>
 *     <td>Locale for number format:</td>
 *     <td>{@link Locale#ROOT}</td>
 *     <td></td>
 *   </tr>
 *   <tr>
 *     <td>Bracket symbols:</td>
 *     <td>{@code [}…{@code ]} or {@code (}…{@code )}</td>
 *     <td><span style="font-size: small"><b>Note:</b> the {@code […]} brackets are common in referencing WKT,
 *         while the {@code (…)} brackets are common in geometry WKT.</span></td>
 *   </tr>
 *   <tr>
 *     <td>Quote symbols:</td>
 *     <td>{@code "}…{@code "}</td>
 *     <td><span style="font-size: small"><b>Note:</b> Apache SIS accepts also {@code “…”} quotes
 *         for more readable {@code String} literals in Java code, but this is non-standard.</span></td>
 *   </tr>
 *   <tr>
 *     <td>Sequence symbols:</td>
 *     <td><code>{</code>…<code>}</code></td>
 *     <td></td>
 *   </tr>
 *   <tr>
 *     <td>Separator:</td>
 *     <td>{@code ,}</td>
 *     <td></td>
 *   </tr>
 * </table>
 *
 * Users can create their own {@code Symbols} instance for parsing or formatting a WKT with different symbols.
 *
 * @author  Martin Desruisseaux (IRD, Geomatys)
 * @version 1.3
 *
 * @see WKTFormat#getSymbols()
 * @see WKTFormat#setSymbols(Symbols)
 *
 * @since 0.4
 */
public class Symbols implements Localized, Cloneable, Serializable {
    /**
     * For cross-version compatibility.
     */
    private static final long serialVersionUID = -1730166945430878916L;

    /**
     * Set to {@code true} if parsing and formatting of numbers in scientific notation is allowed.
     * The way to achieve that is currently a hack, because {@link NumberFormat} has no API for
     * managing that as of JDK 1.8.
     *
     * @todo See if a future version of JDK allows us to get ride of this ugly hack.
     */
    @Workaround(library = "JDK", version = "1.8")
    static final boolean SCIENTIFIC_NOTATION = true;

    /**
     * Separator between numbers in a sequence of numbers.
     * This is used for example between the coordinates of a point.
     */
    static final char NUMBER_SEPARATOR = ' ';

    /**
     * The prefix character for the value of a WKT fragment.
     */
    static final char FRAGMENT_VALUE = '$';

    /**
     * A set of symbols with values between square brackets, like {@code DATUM["WGS84"]}.
     * This instance defines:
     *
     * <ul>
     *   <li>{@link Locale#ROOT} for {@linkplain java.text.DecimalFormatSymbols decimal format symbols}.</li>
     *   <li>Square brackets by default, as in {@code DATUM["WGS84"]}, but accepting also curly brackets as in
     *       {@code DATUM("WGS84")}. Both are legal WKT.</li>
     *   <li>English quotation mark ({@code '"'}) by default, but accepting also “…” quotes
     *       for more readable {@link String} constants in Java code.</li>
     *   <li>Coma separator followed by a space ({@code ", "}).</li>
     * </ul>
     *
     * This is the most frequently used WKT format for referencing objects.
     */
    public static final Symbols SQUARE_BRACKETS = new Symbols(
            new int[] {'[', ']', '(', ')'}, new int[] {'"', '"', '“', '”'});

    /**
     * A set of symbols with values between parentheses, like {@code DATUM("WGS84")}.
     * This instance is identical to {@link #SQUARE_BRACKETS} except that the default
     * brackets are the curly ones instead of the square ones (but both are still
     * accepted at parsing time).
     *
     * <p>This format is rare with referencing objects but common with geometry objects.</p>
     */
    public static final Symbols CURLY_BRACKETS = new Symbols(
            new int[] {'(', ')', '[', ']'}, SQUARE_BRACKETS.quotes);

    /**
     * The locale of {@linkplain java.text.DecimalFormatSymbols decimal format symbols} or other symbols.
     *
     * @see #getLocale()
     */
    private Locale locale;

    /**
     * List of characters (as Unicode code points) acceptable as opening or closing brackets.
     * The array shall comply to the following restrictions:
     *
     * <ul>
     *   <li>The characters at index 0 and 1 are the preferred opening and closing brackets respectively.</li>
     *   <li>For each even index <var>i</var>, {@code brackets[i+1]} is the closing bracket matching {@code brackets[i]}.</li>
     * </ul>
     *
     * @see #getOpeningBracket(int)
     * @see #getClosingBracket(int)
     */
    private int[] brackets;

    /**
     * List of characters (as Unicode code point) used for opening or closing a quoted text.
     * The array shall comply to the following restrictions:
     *
     * <ul>
     *   <li>The characters at index 0 and 1 are the preferred opening and closing quotes respectively.</li>
     *   <li>For each even index <var>i</var>, {@code quotes[i+1]} is the closing quote matching {@code quotes[i]}.</li>
     * </ul>
     *
     * Both opening and closing quotes are usually {@code '"'}.
     */
    private int[] quotes;

    /**
     * The preferred closing quote character ({@code quotes[1]}) as a string.
     * We use the closing quote because this is the character that the parser
     * will look for determining the text end.
     *
     * @see #getQuote()
     * @see #readResolve()
     */
    private transient String quote;

    /**
     * The character (as Unicode code point) used for opening ({@code openSequence})
     * or closing ({@code closeSequence}) an array or enumeration.
     */
    private int openSequence, closeSequence;

    /**
     * The string used as a separator in a list of values. This is usually {@code ", "},
     * but may be different if a non-English locale is used for formatting numbers.
     */
    private String separator;

    /**
     * Same value as {@link #separator} but without leading and trailing spaces.
     */
    private transient String trimmedSeparator;

    /**
     * {@code true} if this instance shall be considered as immutable.
     */
    private boolean isImmutable;

    /**
     * Creates a new set of WKT symbols initialized to a copy of the given symbols.
     *
     * @param symbols  the symbols to copy.
     */
    public Symbols(final Symbols symbols) {
        locale           = symbols.locale;
        brackets         = symbols.brackets;
        quotes           = symbols.quotes;
        quote            = symbols.quote;
        openSequence     = symbols.openSequence;
        closeSequence    = symbols.closeSequence;
        separator        = symbols.separator;
        trimmedSeparator = symbols.trimmedSeparator;
    }

    /**
     * Constructor reserved to {@link #SQUARE_BRACKETS} and {@link #CURLY_BRACKETS} constants.
     * The given array is stored by reference - it is not cloned.
     */
    private Symbols(final int[] brackets, final int[] quotes) {
        this.locale           = Locale.ROOT;
        this.brackets         = brackets;
        this.quotes           = quotes;
        this.quote            = "\"";
        this.openSequence     = '{';
        this.closeSequence    = '}';
        this.separator        = ", ";
        this.trimmedSeparator = ",";
        this.isImmutable      = true;
    }

    /**
     * Throws an exception if this set of symbols is immutable.
     */
    private void checkWritePermission() throws UnsupportedOperationException {
        if (isImmutable) {
            throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, "Symbols"));
        }
    }

    /**
     * Returns the default set of symbols.
     * This is currently set to {@link #SQUARE_BRACKETS}.
     *
     * @return the default set of symbols.
     */
    public static Symbols getDefault() {
        return SQUARE_BRACKETS;
    }

    /**
     * Returns the locale for formatting dates and numbers.
     * The default value is {@link Locale#ROOT}.
     *
     * <h4>Relationship between {@code Symbols} locale and {@code WKTFormat} locale</h4>
     * The {@code WKTFormat.getLocale(Locale.DISPLAY)} property specifies the language to use when
     * formatting {@link org.opengis.util.InternationalString} instances and can be set to any value.
     * On the contrary, the {@code Locale} property of this {@code Symbols} class controls
     * the decimal format symbols and is very rarely set to another locale than {@code Locale.ROOT}.
     *
     * @return the locale for dates and numbers.
     *
     * @see WKTFormat#getLocale(Locale.Category)
     */
    @Override
    public final Locale getLocale() {
        return locale;
    }

    /**
     * Sets the locale of decimal format symbols or other symbols.
     * Note that any non-English locale is likely to produce WKT that do not conform to ISO 19162.
     * Such WKT can be used for human reading, but not for data export.
     *
     * @param  locale  the new symbols locale.
     * @throws UnsupportedOperationException if this {@code Symbols} instance is immutable.
     */
    public void setLocale(final Locale locale) {
        checkWritePermission();
        this.locale = Objects.requireNonNull(locale);
    }

    /**
     * Implementation of {@link #matchingBracket(int)} and {@link #matchingQuote(int)}.
     */
    private static int matching(final int[] chars, final int c) {
        for (int i = 0; i < chars.length; i += 2) {
            if (chars[i] == c) {
                return chars[i + 1];
            }
        }
        return -1;
    }

    /**
     * If the given character is an opening bracket, returns the matching closing bracket.
     * Otherwise returns -1.
     */
    final int matchingBracket(final int c) {
        return matching(brackets, c);
    }

    /**
     * Returns the number of paired brackets. For example if the WKT parser accepts both the
     * {@code […]} and {@code (…)} bracket pairs, then this method returns 2.
     *
     * @return the number of bracket pairs.
     *
     * @see #getOpeningBracket(int)
     * @see #getClosingBracket(int)
     */
    public final int getNumPairedBrackets() {
        return brackets.length >>> 1;
    }

    /**
     * Returns the opening bracket character at the given index.
     * Index 0 stands for the default bracket used at formatting time.
     * All other index are for optional brackets accepted at parsing time.
     *
     * @param  index  index of the opening bracket to get, from 0 to {@link #getNumPairedBrackets()} exclusive.
     * @return the opening bracket at the given index, as a Unicode code point.
     * @throws IndexOutOfBoundsException if the given index is out of bounds.
     */
    public final int getOpeningBracket(final int index) {
        return brackets[index << 1];
    }

    /**
     * Returns the closing bracket character at the given index.
     * Index 0 stands for the default bracket used at formatting time.
     * All other index are for optional brackets accepted at parsing time.
     *
     * @param  index  index of the closing bracket to get, from 0 to {@link #getNumPairedBrackets()} exclusive.
     * @return the closing bracket at the given index, as a Unicode code point.
     * @throws IndexOutOfBoundsException if the given index is out of bounds.
     */
    public final int getClosingBracket(final int index) {
        return brackets[(index << 1) | 1];
    }

    /**
     * Sets the opening and closing brackets to the given pairs.
     * Each string shall contain exactly two code points (usually two characters).
     * The first code point is taken as the opening bracket, and the second code point as the closing bracket.
     *
     * <h4>Example</h4>
     * The following code will instruct the WKT formatter to use the (…) pair of brackets at formatting time,
     * but still accept the more common […] pair of brackets at parsing time:
     *
     * {@snippet lang="java" :
     *     symbols.setPairedBrackets("()", "[]");
     *     }
     *
     * @param  preferred     the preferred pair of opening and closing quotes, used at formatting time.
     * @param  alternatives  alternative pairs of opening and closing quotes accepted at parsing time.
     * @throws UnsupportedOperationException if this {@code Symbols} instance is immutable.
     */
    public void setPairedBrackets(final String preferred, final String... alternatives) {
        checkWritePermission();
        brackets = toCodePoints(preferred, alternatives);
    }

    /**
     * If the given character is an opening quote, returns the matching closing quote.
     * Otherwise returns -1.
     */
    final int matchingQuote(final int c) {
        return matching(quotes, c);
    }

    /**
     * Returns the number of paired quotes. For example if the WKT parser accepts both the
     * {@code "…"} and {@code “…”} quote pairs, then this method returns 2.
     *
     * @return the number of quote pairs.
     *
     * @see #getOpeningQuote(int)
     * @see #getClosingQuote(int)
     */
    public final int getNumPairedQuotes() {
        return quotes.length >>> 1;
    }

    /**
     * Returns the opening quote character at the given index.
     * Index 0 stands for the default quote used at formatting time, which is usually {@code '"'}.
     * All other index are for optional quotes accepted at parsing time.
     *
     * @param  index  index of the opening quote to get, from 0 to {@link #getNumPairedQuotes()} exclusive.
     * @return the opening quote at the given index, as a Unicode code point.
     * @throws IndexOutOfBoundsException if the given index is out of bounds.
     */
    public final int getOpeningQuote(final int index) {
        return quotes[index << 1];
    }

    /**
     * Returns the closing quote character at the given index.
     * Index 0 stands for the default quote used at formatting time, which is usually {@code '"'}.
     * All other index are for optional quotes accepted at parsing time.
     *
     * @param  index  index of the closing quote to get, from 0 to {@link #getNumPairedQuotes()} exclusive.
     * @return the closing quote at the given index, as a Unicode code point.
     * @throws IndexOutOfBoundsException if the given index is out of bounds.
     */
    public final int getClosingQuote(final int index) {
        return quotes[(index << 1) | 1];
    }

    /**
     * Returns the preferred closing quote character as a string. This is the quote to double if it
     * appears in a Unicode string to format. We check for the closing quote because this is the one
     * that the parser will look for determining the text end.
     */
    final String getQuote() {
        return quote;
    }

    /**
     * Sets the opening and closing quotes to the given pairs.
     * Each string shall contain exactly two code points (usually two characters).
     * The first code point is taken as the opening quote, and the second code point as the closing quote.
     *
     * <h4>Example</h4>
     * The following code will instruct the WKT formatter to use the prettier “…” quotation marks at formatting time
     * (especially useful for {@code String} constants in Java code), but still accept the standard "…" quotation marks
     * at parsing time:
     *
     * {@snippet lang="java" :
     *     symbols.setPairedQuotes("“”", "\"\"");
     *     }
     *
     * @param  preferred     the preferred pair of opening and closing quotes, used at formatting time.
     * @param  alternatives  alternative pairs of opening and closing quotes accepted at parsing time.
     * @throws UnsupportedOperationException if this {@code Symbols} instance is immutable.
     */
    public void setPairedQuotes(final String preferred, final String... alternatives) {
        checkWritePermission();
        quotes = toCodePoints(preferred, alternatives);
        quote = preferred.substring(Character.charCount(quotes[0])).trim();
    }

    /**
     * Packs the given pairs of bracket or quotes in a single array of code points.
     * This method also verifies arguments validity.
     */
    private static int[] toCodePoints(final String preferred, final String[] alternatives) {
        ensureNonEmpty("preferred", preferred);
        final int n = (alternatives != null) ? alternatives.length : 0;
        final int[] array = new int[(n+1) * 2];
        String name = "preferred";
        String pair = preferred;
        int i=0, j=0;
        while (true) {
            if (pair.codePointCount(0, pair.length()) != 2) {
                throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, name, pair));
            }
            final int c = pair.codePointAt(0);
            ensureValidQuoteOrBracket(name, array[j++] = c);
            ensureValidQuoteOrBracket(name, array[j++] = pair.codePointAt(Character.charCount(c)));
            if (i >= n) {
                break;
            }
            ensureNonNullElement(name = "alternatives", i, pair = alternatives[i++]);
        }
        return array;
    }

    /**
     * Ensures that the given code point is a valid Unicode code point but not a Unicode identifier part.
     */
    private static void ensureValidQuoteOrBracket(final String name, final int code) {
        ensureValidUnicodeCodePoint(name, code);
        if (Character.isUnicodeIdentifierPart(code) || Character.isSpaceChar(code) || code == FRAGMENT_VALUE) {
            throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalCharacter_2,
                    name, String.valueOf(Character.toChars(code))));
        }
    }

    /**
     * Returns the character used for opening a sequence of values.
     * This is usually <code>'{'</code>.
     *
     * @return the character used for opening a sequence of values, as a Unicode code point.
     */
    public final int getOpenSequence() {
        return openSequence;
    }

    /**
     * Returns the character used for closing a sequence of values.
     * This is usually <code>'}'</code>.
     *
     * @return the character used for closing a sequence of values, as a Unicode code point.
     */
    public final int getCloseSequence() {
        return closeSequence;
    }

    /**
     * Sets the characters used for opening and closing a sequence of values.
     *
     * @param  openSequence   the character for opening a sequence of values, as a Unicode code point.
     * @param  closeSequence  the character for closing a sequence of values, as a Unicode code point.
     * @throws UnsupportedOperationException if this {@code Symbols} instance is immutable.
     */
    public void setSequenceBrackets(final int openSequence, final int closeSequence) {
        checkWritePermission();
        ensureValidQuoteOrBracket("openSequence",  openSequence);
        ensureValidQuoteOrBracket("closeSequence", closeSequence);
        this.openSequence  = openSequence;
        this.closeSequence = closeSequence;
    }

    /**
     * Returns the string used as a separator in a list of values. This is usually {@code ", "},
     * but may be different if a non-English locale is used for formatting numbers.
     *
     * @return the string used as a separator in a list of values.
     */
    public final String getSeparator() {
        return separator;
    }

    /**
     * Sets the string to use as a separator in a list of values.
     * The given string will be used "as-is" at formatting time,
     * but leading and trailing spaces will be ignored at parsing time.
     *
     * @param  separator  the new string to use as a separator in a list of values.
     * @throws UnsupportedOperationException if this {@code Symbols} instance is immutable.
     */
    public void setSeparator(final String separator) {
        checkWritePermission();
        ensureNonEmpty("separator", trimmedSeparator = separator.trim().strip());
        this.separator = separator;
    }

    /**
     * Returns the separator without trailing spaces.
     */
    final String trimmedSeparator() {
        return trimmedSeparator;
    }

    /**
     * Returns the value of {@link #getSeparator()} without trailing spaces,
     * followed by the system line separator.
     */
    final String separatorNewLine() {
        final String separator = getSeparator();
        return separator.substring(0, CharSequences.skipTrailingWhitespaces(separator, 0, separator.length()))
                .concat(System.lineSeparator());
    }

    /**
     * Creates a new number format to use for parsing and formatting. Each {@link WKTFormat} will
     * create its own instance, since {@link NumberFormat}s are not guaranteed to be thread-safe.
     *
     * <h4>Scientific notation</h4>
     * The {@link NumberFormat} created here does not use scientific notation. This is okay for many
     * WKT formatting purpose since Earth ellipsoid axis lengths in metres are large enough for triggering
     * scientific notation, while we want to express them as normal numbers with centimetre precision.
     * However, this is problematic for small numbers like 1E-5. Callers may need to adjust the precision
     * depending on the kind of numbers (length or angle) to format.
     */
    final NumberFormat createNumberFormat() {
        final NumberFormat format = NumberFormat.getNumberInstance(locale);
        format.setGroupingUsed(false);
        return format;
    }

    /**
     * Returns {@code true} if the formatter should use scientific notation for the given value.
     * We use scientific notation if the number magnitude is too high or too low. The threshold values used here
     * may be different than the threshold values used in the standard {@link StringBuilder#append(double)} method.
     * In particular, we use a higher threshold for large numbers because ellipsoid axis lengths are above the JDK
     * threshold when the axis length is given in feet (about 2.1E+7) while we still want to format them as usual numbers.
     *
     * Note that we perform this special formatting only if the 'NumberFormat' is not localized (which is the usual case).
     *
     * @param  abs  the absolute value of the number to format.
     */
    final boolean useScientificNotation(final double abs) {
        return SCIENTIFIC_NOTATION && (abs < 1E-3 || abs >= 1E+9) && locale == Locale.ROOT;
    }

    /**
     * Returns {@code true} if the given WKT contains at least one instance of the given element.
     * Invoking this method is equivalent to invoking {@link String#contains(CharSequence)} except
     * for the following:
     *
     * <ul>
     *   <li>The search is case-insensitive.</li>
     *   <li>Characters between {@linkplain #getOpeningQuote(int) opening quotes} and
     *       {@linkplain #getClosingQuote(int) closing quotes} are ignored.</li>
     *   <li>The element found in the given WKT cannot be preceded by other
     *       {@linkplain Character#isUnicodeIdentifierPart(int) Unicode identifier characters}.</li>
     *   <li>The element found in the given WKT must be followed, ignoring space, by an
     *       {@linkplain #getOpeningBracket(int) opening bracket}.</li>
     * </ul>
     *
     * The purpose of this method is to guess some characteristics about the encoded object without
     * the cost of a full WKT parsing.
     *
     * <h4>Example</h4>
     * {@code containsElement(wkt, "AXIS")} returns {@code true} if the given WKT contains at least
     * one instance of the {@code AXIS[…]} element, ignoring case.
     *
     * @param  wkt      the WKT to inspect.
     * @param  element  the element to search for.
     * @return {@code true} if the given WKT contains at least one instance of the given element.
     */
    public boolean containsElement(final CharSequence wkt, final String element) {
        ensureNonNull("wkt", wkt);
        ensureNonEmpty("element", element);
        if (!CharSequences.isUnicodeIdentifier(element)) {
            throw new IllegalArgumentException(Errors.format(Errors.Keys.NotAUnicodeIdentifier_1, element));
        }
        return containsElement(wkt, element, 0);
    }

    /**
     * Implementation of {@link #containsElement(CharSequence, String)} without verification of argument validity.
     *
     * @param  wkt      the WKT to inspect.
     * @param  element  the element to search. Must contains only uppercase letters.
     * @param  offset   the index to start the search from.
     */
    private boolean containsElement(final CharSequence wkt, final String element, int offset) {
        final int[] quotes = this.quotes;
        final int length = wkt.length();
        boolean isQuoting = false;
        int closeQuote = 0;
        while (offset < length) {
            int c = Character.codePointAt(wkt, offset);
            if (closeQuote != 0) {
                if (c == closeQuote) {
                    isQuoting = false;
                }
            } else for (int i=0; i<quotes.length; i+=2) {
                if (c == quotes[i]) {
                    closeQuote = quotes[i | 1];
                    isQuoting = true;
                    break;
                }
            }
            if (!isQuoting && Character.isUnicodeIdentifierStart(c)) {
                /*
                 * Found the beginning of a Unicode identifier.
                 * Check if this is the identifier we were looking for.
                 */
                if (CharSequences.regionMatches(wkt, offset, element, true)) {
                    offset = CharSequences.skipLeadingWhitespaces(wkt, offset + element.length(), length);
                    if (offset >= length) {
                        break;
                    }
                    c = Character.codePointAt(wkt, offset);
                    if (matchingBracket(c) >= 0) {
                        return true;
                    }
                } else {
                    /*
                     * Not the identifier we were looking for. Skip the whole identifier.
                     */
                    do {
                        offset += Character.charCount(c);
                        if (offset >= length) {
                            return false;
                        }
                        c = Character.codePointAt(wkt, offset);
                    } while (Character.isUnicodeIdentifierPart(c));
                }
            }
            offset += Character.charCount(c);
        }
        return false;
    }

    /**
     * Returns an immutable copy of this set of symbols, or {@code this} if this instance is already immutable.
     */
    final Symbols immutable() {
        if (isImmutable) {
            return this;
        }
        final Symbols clone = clone();
        clone.isImmutable = true;
        return clone;
    }

    /**
     * Returns a clone of this {@code Symbols}.
     * The returned instance is modifiable (i.e. setter methods will not throw {@link UnsupportedOperationException}).
     *
     * @return a modifiable clone of this {@code Symbols}.
     */
    @Override
    public Symbols clone() {
        final Symbols clone;
        try {
            clone = (Symbols) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
        /*
         * No needs to copy the arrays, because their content are never modified.
         * Instead, the setter methods create new arrays.
         */
        clone.isImmutable = false;
        return clone;
    }

    /**
     * Compares this {@code Symbols} with the given object for equality.
     *
     * @param  other  the object to compare with this {@code Symbols}.
     * @return {@code true} if both objects are equal.
     */
    @Override
    public boolean equals(final Object other) {
        if (other instanceof Symbols) {
            final Symbols that = (Symbols) other;
            return Arrays.equals(brackets, that.brackets) &&
                   Arrays.equals(quotes, that.quotes) &&
                   // no need to compare 'quote' because it is computed from 'quotes'.
                   openSequence  == that.openSequence &&
                   closeSequence == that.closeSequence &&
                   separator.equals(that.separator) &&
                   locale.equals(that.locale);
        }
        return false;
    }

    /**
     * Returns a hash code value for this object.
     *
     * @return a hash code value.
     */
    @Override
    public int hashCode() {
        return Arrays.deepHashCode(new Object[] {brackets, quotes, openSequence, closeSequence, separator, locale});
    }

    /**
     * Invoked on deserialization for replacing the deserialized instance by the constant instance.
     * This method also opportunistically recompute the {@link #quote} field if no replacement is done.
     *
     * @return the object to use after deserialization.
     * @throws ObjectStreamException required by specification but should never be thrown.
     */
    final Object readResolve() throws ObjectStreamException {
        if (isImmutable) {
            if (equals(SQUARE_BRACKETS)) return SQUARE_BRACKETS;
            if (equals(CURLY_BRACKETS))  return CURLY_BRACKETS;
        }
        quote = String.valueOf(Character.toChars(quotes[1]));
        trimmedSeparator = separator.trim().strip();
        return this;
    }
}
