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

import java.net.URI;
import java.net.URL;
import java.net.URISyntaxException;
import java.net.MalformedURLException;
import java.util.MissingResourceException;
import java.util.IllformedLocaleException;
import java.util.Locale;
import java.util.UUID;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import javax.measure.Unit;
import javax.measure.format.ParserException;
import org.apache.sis.measure.Units;
import org.apache.sis.util.Locales;

import static org.apache.sis.util.CharSequences.trimWhitespaces;


/**
 * Performs conversions of XML element or attribute values encountered during XML (un)marshalling.
 * Each method in this class is a converter and can be invoked at (un)marshalling time.
 * The default implementation is straightforward and documented in the javadoc of each method.
 *
 * <p>This class provides a way to handle the errors which may exist in some XML documents.
 * For example a URL in the document may be malformed, causing a {@link MalformedURLException}
 * to be thrown. If this error is not handled, it will cause the (un)marshalling of the entire
 * document to fail. An application may want to change this behavior by replacing URLs that
 * are known to be erroneous by fixed versions of those URLs. Example:</p>
 *
 * {@preformat java
 *     class URLFixer extends ValueConverter {
 *         &#64;Override
 *         public URL toURL(MarshalContext context, URI uri) throws MalformedURLException {
 *             try {
 *                 return super.toURL(context, uri);
 *             } catch (MalformedURLException e) {
 *                 if (uri.equals(KNOWN_ERRONEOUS_URI) {
 *                     return FIXED_URL;
 *                 } else {
 *                     throw e;
 *                 }
 *             }
 *         }
 *     }
 * }
 *
 * See the {@link XML#CONVERTER} javadoc for an example of registering a custom
 * {@code ValueConverter} to a (un)marshaller.
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 0.5
 * @since   0.3
 * @module
 */
public class ValueConverter {
    /**
     * The default, thread-safe and immutable instance. This instance defines the
     * converters used during every (un)marshalling if no {@code ValueConverter}
     * was explicitly set.
     */
    public static final ValueConverter DEFAULT = new ValueConverter();

    /**
     * Creates a default {@code ValueConverter}. This is for subclasses only,
     * since new instances are useful only if at least one method is overridden.
     */
    protected ValueConverter() {
    }

    /**
     * Invoked when an exception occurred in any {@code toXXX(…)} method. The default implementation
     * does nothing and return {@code false}, which will cause the (un)marshalling process of the
     * whole XML document to fail.
     *
     * <p>This method provides a single hook that subclasses can override in order to provide their
     * own error handling for every methods defined in this class, like the example documented in
     * the {@link XML#CONVERTER} javadoc. Subclasses also have the possibility to override individual
     * {@code toXXX(…)} methods, like the example provided in this <a href="#skip-navbar_top">class
     * javadoc</a>.</p>
     *
     * @param  <T>         the compile-time type of the {@code sourceType} argument.
     * @param  context     context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
     * @param  value       the value that can't be converted.
     * @param  sourceType  the base type of the value to convert. This is determined by the argument type of the method
     *                     that caught the exception. For example the source type is always {@code URI.class}
     *                     if the exception has been caught by the {@link #toURL(MarshalContext, URI)} method.
     * @param  targetType  the expected type of the converted object.
     * @param  exception   the exception that occurred during the conversion attempt.
     * @return {@code true} if the (un)marshalling process should continue despite this error,
     *         or {@code false} (the default) if the exception should be propagated, thus causing
     *         the (un)marshalling to fail.
     */
    protected <T> boolean exceptionOccured(MarshalContext context, T value,
            Class<T> sourceType, Class<?> targetType, Exception exception)
    {
        return false;
    }

    /**
     * Converts the given locale to a language code. For better compliance with ISO standards, the language code
     * should be a 3-letters ISO 639-2 code (e.g. {@code "jpn"} for {@linkplain Locale#JAPANESE Japanese}).
     * However those codes may not be available for every locales.
     *
     * <p>The default implementation performs the following steps:</p>
     * <ul>
     *   <li>Try {@link Locale#getISO3Language()}:<ul>
     *     <li>On success, return that value if non-empty, or {@code null} otherwise.</li>
     *     <li>If an exception has been thrown, then:<ul>
     *       <li>If {@link #exceptionOccured exceptionOccured(…)} return {@code true}, then
     *           returns {@code value.getLanguage()} if non-empty or {@code null} otherwise.</li>
     *       <li>If {@code exceptionOccured(…)} returned {@code false} (which is the default
     *           behavior), then let the exception propagate.</li>
     *     </ul></li>
     *   </ul></li>
     * </ul>
     *
     * @param  context  context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
     * @param  value    the locale to convert to a language code, or {@code null}.
     * @return the language code, or {@code null} if the given value was null or does not contains a language code.
     * @throws MissingResourceException if no language code can be found for the given locale.
     *
     * @see Locale#getISO3Language()
     * @see Locale#getLanguage()
     */
    public String toLanguageCode(final MarshalContext context, final Locale value) throws MissingResourceException {
        if (value != null) {
            String code;
            try {
                code = value.getISO3Language();
            } catch (MissingResourceException e) {
                if (!exceptionOccured(context, value, Locale.class, String.class, e)) {
                    throw e;
                }
                code = value.getLanguage();
            }
            if (!code.isEmpty()) {
                return code;
            }
        }
        return null;
    }

    /**
     * Converts the given locale to a country code. For better compliance with ISO standards, the country code
     * should be a 2-letters ISO 3166 code (e.g. {@code "JP"} for {@linkplain Locale#JAPAN Japan}).
     *
     * <p>The default implementation returns {@link Locale#getCountry()} if non-empty, or {@code null} otherwise.</p>
     *
     * @param  context  context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
     * @param  value    the locale to convert to a country code, or {@code null}.
     * @return the country code, or {@code null} if the given value was null or does not contains a country code.
     * @throws MissingResourceException if no country code can be found for the given locale.
     *
     * @see Locale#getISO3Country()
     * @see Locale#getCountry()
     */
    public String toCountryCode(final MarshalContext context, final Locale value) throws MissingResourceException {
        if (value != null) {
            String code = value.getCountry();
            if (!code.isEmpty()) {
                return code;
            }
        }
        return null;
    }

    /**
     * Converts the given character set to a code.
     *
     * <p>The default implementation first invokes {@link Charset#name()}. Then if marshalling to legacy ISO 19139:2007,
     * this method converts the <a href="http://www.iana.org/assignments/character-sets">IANA</a> name to a
     * ISO 19115:2003 {@code MD_CharacterSetCode} using the following equivalence table:</p>
     *
     * <table class="sis">
     *   <caption>IANA to ISO 19115:2003 character set code</caption>
     *   <tr>
     *     <td><table class="compact">
     *       <caption>From ISO codes</caption>
     *       <tr><td style="width: 90px"><b>IANA</b></td><td><b>ISO 19115:2003</b></td></tr>
     *       <tr><td>{@code ISO-8859-1}</td>  <td>{@code 8859part1}</td></tr>
     *       <tr><td>{@code ISO-8859-2}</td>  <td>{@code 8859part2}</td></tr>
     *       <tr><td>{@code ISO-8859-3}</td>  <td>{@code 8859part3}</td></tr>
     *       <tr><td>{@code ISO-8859-4}</td>  <td>{@code 8859part4}</td></tr>
     *       <tr><td>{@code ISO-8859-5}</td>  <td>{@code 8859part5}</td></tr>
     *       <tr><td>{@code ISO-8859-6}</td>  <td>{@code 8859part6}</td></tr>
     *       <tr><td>{@code ISO-8859-7}</td>  <td>{@code 8859part7}</td></tr>
     *       <tr><td>{@code ISO-8859-8}</td>  <td>{@code 8859part8}</td></tr>
     *       <tr><td>{@code ISO-8859-9}</td>  <td>{@code 8859part9}</td></tr>
     *       <tr><td>{@code ISO-8859-10}</td> <td>{@code 8859part10}</td></tr>
     *       <tr><td>{@code ISO-8859-11}</td> <td>{@code 8859part11}</td></tr>
     *       <tr><td>{@code ISO-8859-12}</td> <td>{@code 8859part12}</td></tr>
     *       <tr><td>{@code ISO-8859-13}</td> <td>{@code 8859part13}</td></tr>
     *       <tr><td>{@code ISO-8859-14}</td> <td>{@code 8859part14}</td></tr>
     *       <tr><td>{@code ISO-8859-15}</td> <td>{@code 8859part15}</td></tr>
     *       <tr><td>{@code ISO-8859-16}</td> <td>{@code 8859part16}</td></tr>
     *     </table></td>
     *     <td class="sep"><table class="compact">
     *       <caption>Others</caption>
     *       <tr><td style="width: 90px"><b>IANA</b></td><td><b>ISO 19115:2003</b></td></tr>
     *       <tr><td>{@code UCS-2}</td>     <td>{@code ucs2}</td></tr>
     *       <tr><td>{@code UCS-4}</td>     <td>{@code ucs4}</td></tr>
     *       <tr><td>{@code UTF-7}</td>     <td>{@code utf7}</td></tr>
     *       <tr><td>{@code UTF-8}</td>     <td>{@code utf8}</td></tr>
     *       <tr><td>{@code UTF-16}</td>    <td>{@code utf16}</td></tr>
     *       <tr><td>{@code JIS_X0201}</td> <td>{@code jis}</td></tr>
     *       <tr><td>{@code Shift_JIS}</td> <td>{@code shiftJIS}</td></tr>
     *       <tr><td>{@code EUC-JP}</td>    <td>{@code eucJP}</td></tr>
     *       <tr><td>{@code US-ASCII}</td>  <td>{@code usAscii}</td></tr>
     *       <tr><td>{@code EBCDIC}</td>    <td>{@code ebcdic}</td></tr>
     *       <tr><td>{@code EUC-KR}</td>    <td>{@code eucKR}</td></tr>
     *       <tr><td>{@code Big5}</td>      <td>{@code big5}</td></tr>
     *       <tr><td>{@code GB2312}</td>    <td>{@code GB2312}</td></tr>
     *     </table></td>
     *   </tr>
     * </table>
     *
     * @param  context  context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
     * @param  value    the locale to convert to a character set code, or {@code null}.
     * @return the country code, or {@code null} if the given value was null.
     *
     * @see Charset#name()
     *
     * @since 0.5
     */
    public String toCharsetCode(final MarshalContext context, final Charset value) {
        if (value != null) {
            return LegacyCodes.fromIANA(value.name());
        }
        return null;
    }

    /**
     * Converts the given string to a locale. The string is the language code either as the 2
     * letters or the 3 letters ISO code. It can optionally be followed by the {@code '_'}
     * character and the country code (again either as 2 or 3 letters), optionally followed
     * by {@code '_'} and the variant.
     *
     * @param  context  context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
     * @param  value    the string to convert to a locale, or {@code null}.
     * @return the converted locale, or {@code null} if the given value was null or empty, or
     *         if an exception was thrown and {@code exceptionOccured(…)} returned {@code true}.
     * @throws IllformedLocaleException if the given string can not be converted to a locale.
     *
     * @see Locales#parse(String)
     */
    public Locale toLocale(final MarshalContext context, String value) throws IllformedLocaleException {
        value = trimWhitespaces(value);
        if (value != null && !value.isEmpty()) try {
            return Locales.parse(value);
        } catch (IllformedLocaleException e) {
            if (!exceptionOccured(context, value, String.class, Locale.class, e)) {
                throw e;
            }
        }
        return null;
    }

    /**
     * Converts the given string to a character set. The string can be either a
     * <a href="http://www.iana.org/assignments/character-sets">IANA</a> identifier,
     * or one of the ISO 19115:2003 {@code MD_CharacterSetCode} identifier.
     *
     * @param  context  context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
     * @param  value    the string to convert to a character set, or {@code null}.
     * @return the converted character set, or {@code null} if the given value was null or empty, or
     *         if an exception was thrown and {@code exceptionOccured(…)} returned {@code true}.
     * @throws IllegalCharsetNameException if the given string can not be converted to a character set.
     *
     * @see Charset#forName(String)
     *
     * @since 0.5
     */
    public Charset toCharset(final MarshalContext context, String value) throws IllegalCharsetNameException {
        value = trimWhitespaces(value);
        if (value != null && !value.isEmpty()) {
            value = LegacyCodes.toIANA(value);
            try {
                return Charset.forName(value);
            } catch (IllegalCharsetNameException e) {
                if (!exceptionOccured(context, value, String.class, Charset.class, e)) {
                    throw e;
                }
            }
        }
        return null;
    }

    /**
     * Converts the given string to a unit. The default implementation is as below, omitting
     * the check for null value and the call to {@link #exceptionOccured exceptionOccured(…)}
     * in case of error:
     *
     * {@preformat java
     *     return Units.valueOf(value);
     * }
     *
     * @param  context  context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
     * @param  value    the string to convert to a unit, or {@code null}.
     * @return the converted unit, or {@code null} if the given value was null or empty, or
     *         if an exception was thrown and {@code exceptionOccured(…)} returned {@code true}.
     * @throws IllegalArgumentException if the given string can not be converted to a unit.
     *
     * @see Units#valueOf(String)
     */
    public Unit<?> toUnit(final MarshalContext context, String value) throws IllegalArgumentException {
        value = trimWhitespaces(value);
        if (value != null && !value.isEmpty()) try {
            return Units.valueOf(value);
        } catch (ParserException e) {
            if (!exceptionOccured(context, value, String.class, Unit.class, e)) {
                throw e;
            }
        }
        return null;
    }

    /**
     * Converts the given string to a Universal Unique Identifier. The default implementation
     * is as below, omitting the check for null value and the call to {@link #exceptionOccured
     * exceptionOccured(…)} in case of error:
     *
     * {@preformat java
     *     return UUID.fromString(value);
     * }
     *
     * @param  context  context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
     * @param  value    the string to convert to a UUID, or {@code null}.
     * @return the converted UUID, or {@code null} if the given value was null or empty, or
     *         if an exception was thrown and {@code exceptionOccured(…)} returned {@code true}.
     * @throws IllegalArgumentException if the given string can not be converted to a UUID.
     *
     * @see UUID#fromString(String)
     */
    public UUID toUUID(final MarshalContext context, String value) throws IllegalArgumentException {
        value = trimWhitespaces(value);
        if (value != null && !value.isEmpty()) try {
            return UUID.fromString(value);
        } catch (IllegalArgumentException e) {
            if (!exceptionOccured(context, value, String.class, UUID.class, e)) {
                throw e;
            }
        }
        return null;
    }

    /**
     * Converts the given string to a URI. The default performs the following work
     * (omitting the check for null value and the call to {@link #exceptionOccured
     * exceptionOccured(…)} in case of error):
     *
     * {@preformat java
     *     return new URI(value);
     * }
     *
     * @param  context  context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
     * @param  value    the string to convert to a URI, or {@code null}.
     * @return the converted URI, or {@code null} if the given value was null or empty, or if
     *         an exception was thrown and {@code exceptionOccured(…)} returned {@code true}.
     * @throws URISyntaxException if the given string can not be converted to a URI.
     *
     * @see URI#URI(String)
     */
    public URI toURI(final MarshalContext context, String value) throws URISyntaxException {
        value = trimWhitespaces(value);
        if (value != null && !value.isEmpty()) try {
            return new URI(value);
        } catch (URISyntaxException e) {
            if (!exceptionOccured(context, value, String.class, URI.class, e)) {
                throw e;
            }
        }
        return null;
    }

    /**
     * Converts the given URL to a URI. The default implementation is as below, omitting
     * the check for null value and the call to {@link #exceptionOccured exceptionOccured(…)}
     * in case of error:
     *
     * {@preformat java
     *     return value.toURI();
     * }
     *
     * @param  context  context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
     * @param  value    the URL to convert to a URI, or {@code null}.
     * @return the converted URI, or {@code null} if the given value was null or if an
     *         exception was thrown and {@code exceptionOccured(…)} returned {@code true}.
     * @throws URISyntaxException if the given URL can not be converted to a URI.
     *
     * @see URL#toURI()
     */
    public URI toURI(final MarshalContext context, final URL value) throws URISyntaxException {
        if (value != null) try {
            return value.toURI();
        } catch (URISyntaxException e) {
            if (!exceptionOccured(context, value, URL.class, URI.class, e)) {
                throw e;
            }
        }
        return null;
    }

    /**
     * Converts the given URI to a URL. The default implementation is as below, omitting
     * the check for null value and the call to {@link #exceptionOccured exceptionOccured(…)}
     * in case of error:
     *
     * {@preformat java
     *     return value.toURL();
     * }
     *
     * @param  context  context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
     * @param  value    the URI to convert to a URL, or {@code null}.
     * @return the converted URL, or {@code null} if the given value was null or if an
     *         exception was thrown and {@code exceptionOccured(…)} returned {@code true}.
     * @throws MalformedURLException if the given URI can not be converted to a URL.
     *
     * @see URI#toURL()
     */
    public URL toURL(final MarshalContext context, final URI value) throws MalformedURLException {
        if (value != null) try {
            return value.toURL();
        } catch (MalformedURLException | IllegalArgumentException e) {
            if (!exceptionOccured(context, value, URI.class, URL.class, e)) {
                throw e;
            }
        }
        return null;
    }

    /**
     * Converts the given string to a {@code NilReason}. The default implementation is as below,
     * omitting the check for null value and the call to {@link #exceptionOccured exceptionOccured(…)}
     * in case of error:
     *
     * {@preformat java
     *     return NilReason.valueOf(value);
     * }
     *
     * @param  context  context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
     * @param  value    the string to convert to a nil reason, or {@code null}.
     * @return the converted nil reason, or {@code null} if the given value was null or empty, or
     *         if an exception was thrown and {@code exceptionOccured(…)} returned {@code true}.
     * @throws URISyntaxException if the given string can not be converted to a nil reason.
     *
     * @see NilReason#valueOf(String)
     */
    public NilReason toNilReason(final MarshalContext context, String value) throws URISyntaxException {
        value = trimWhitespaces(value);
        if (value != null && !value.isEmpty()) try {
            return NilReason.valueOf(value);
        } catch (URISyntaxException e) {
            if (!exceptionOccured(context, value, String.class, URI.class, e)) {
                throw e;
            }
        }
        return null;
    }
}