/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyrighted [year] [name of copyright owner]".
 *
 * Copyright © 2010–2011 ApexIdentity Inc. All rights reserved.
 * Portions Copyrighted 2011-2016 ForgeRock AS.
 */

package org.forgerock.json;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.forgerock.util.Function;
import org.forgerock.util.Utils;
import org.forgerock.util.time.Duration;

/**
 * This class contains the utility functions to convert a {@link JsonValue} to another type.
 */
public final class JsonValueFunctions {

    private JsonValueFunctions() {
    }

    //@Checkstyle:off
    private static final Function<JsonValue, Charset, JsonValueException> TO_CHARSET =
            new Function<JsonValue, Charset, JsonValueException>() {
                @Override
                public Charset apply(JsonValue value) throws JsonValueException {
                    try {
                        return value.isNull() ? null : Charset.forName(value.asString());
                    } catch (final IllegalCharsetNameException | UnsupportedCharsetException e) {
                        throw new JsonValueException(value, e);
                    }
                }
            };

    private static final Function<JsonValue, Duration, JsonValueException> TO_DURATION =
            new Function<JsonValue, Duration, JsonValueException>() {
                @Override
                public Duration apply(JsonValue value) throws JsonValueException {
                    try {
                        return value.isNull() ? null : Duration.duration(value.asString());
                    } catch (final IllegalArgumentException iae) {
                        throw new JsonValueException(value, iae);
                    }
                }
            };

    private static final Function<JsonValue, File, JsonValueException> TO_FILE =
            new Function<JsonValue, File, JsonValueException>() {
                @Override
                public File apply(JsonValue value) throws JsonValueException {
                    return value.isNull() ? null : new File(value.asString());
                }
            };

    private static final Function<JsonValue, Pattern, JsonValueException> TO_PATTERN =
            new Function<JsonValue, Pattern, JsonValueException>() {
                @Override
                public Pattern apply(JsonValue value) throws JsonValueException {
                    try {
                        return value.isNull() ? null : Pattern.compile(value.asString());
                    } catch (final PatternSyntaxException pse) {
                        throw new JsonValueException(value, pse);
                    }
                }
            };

    private static final Function<JsonValue, JsonPointer, JsonValueException> TO_POINTER =
            new Function<JsonValue, JsonPointer, JsonValueException>() {
                @Override
                public JsonPointer apply(JsonValue value) throws JsonValueException {
                    try {
                        return value.isNull() ? null : new JsonPointer(value.asString());
                    } catch (final JsonValueException jve) {
                        throw jve;
                    } catch (final JsonException je) {
                        throw new JsonValueException(value, je);
                    }
                }
            };

    private static final Function<JsonValue, URL, JsonValueException> TO_URL =
            new Function<JsonValue, URL, JsonValueException>() {
                @Override
                public URL apply(JsonValue value) throws JsonValueException {
                    try {
                        return value.isNull() ? null : new URL(value.asString());
                    } catch (final MalformedURLException e) {
                        throw new JsonValueException(value, e);
                    }
                }
            };

    private static final Function<JsonValue, URI, JsonValueException> TO_URI =
            new Function<JsonValue, URI, JsonValueException>() {
                @Override
                public URI apply(JsonValue value) throws JsonValueException {
                    try {
                        return value.isNull() ? null : new URI(value.asString());
                    } catch (final URISyntaxException use) {
                        throw new JsonValueException(value, use);
                    }
                }
            };

    private static final Function<JsonValue, UUID, JsonValueException> TO_UUID =
            new Function<JsonValue, UUID, JsonValueException>() {
                @Override
                public UUID apply(JsonValue value) throws JsonValueException {
                    try {
                        return value.isNull() ? null : UUID.fromString(value.asString());
                    } catch (final IllegalArgumentException iae) {
                        throw new JsonValueException(value, iae);
                    }
                }
            };
    //@Checkstyle:on

    /**
     * Returns the JSON string value as a character set used for byte
     * encoding/decoding. If the JSON value is {@code null}, this function returns
     * {@code null}.
     *
     * @return the character set represented by the string value.
     * @throws JsonValueException
     *         if the JSON value is not a string or the character set
     *         specified is invalid.
     */
    public static Function<JsonValue, Charset, JsonValueException> charset() {
        return TO_CHARSET;
    }

    /**
     * Returns the JSON string value as a {@link Duration}. If the JSON value is {@code null}, this method returns
     * {@code null}.
     *
     * @return the duration represented by the string value.
     * @throws JsonValueException
     *         if the JSON value is not a string or the duration
     *         specified is invalid.
     */
    public static Function<JsonValue, Duration, JsonValueException> duration() {
        return TO_DURATION;
    }

    /**
     * Returns the JSON string value as an enum constant of the specified enum
     * type. The string value and enum constants are compared, ignoring case
     * considerations. If the JSON value is {@code null}, this method returns
     * {@code null}.
     *
     * @param <T>
     *         the enum type sub-class.
     * @param type
     *         the enum type to match constants with the value.
     * @return the enum constant represented by the string value.
     * @throws IllegalArgumentException
     *         if {@code type} does not represent an enum type. or
     *         if the JSON value does not match any of the enum's constants.
     * @throws NullPointerException
     *         if {@code type} is {@code null}.
     */
    public static <T extends Enum<T>> Function<JsonValue, T, JsonValueException> enumConstant(final Class<T> type) {
        return new Function<JsonValue, T, JsonValueException>() {
            @Override
            public T apply(JsonValue value) throws JsonValueException {
                return Utils.asEnum(value.asString(), type);
            }
        };
    }

    /**
     * Returns the JSON string value as a {@code File} object. If the JSON value
     * is {@code null}, this method returns {@code null}.
     *
     * @return a file represented by the string value.
     * @throws JsonValueException
     *         if the JSON value is not a string.
     */
    public static Function<JsonValue, File, JsonValueException> file() {
        return TO_FILE;
    }

    /**
     * Returns the JSON string value as a regular expression pattern. If the
     * JSON value is {@code null}, this method returns {@code null}.
     *
     * @return the compiled regular expression pattern.
     * @throws JsonValueException
     *         if the pattern is not a string or the value is not a valid
     *         regular expression pattern.
     */
    public static Function<JsonValue, Pattern, JsonValueException> pattern() {
        return TO_PATTERN;
    }

    /**
     * Returns the JSON string value as a JSON pointer. If the JSON value is
     * {@code null}, this method returns {@code null}.
     *
     * @return the JSON pointer represented by the JSON value string.
     * @throws JsonValueException
     *         if the JSON value is not a string or valid JSON pointer.
     */
    public static Function<JsonValue, JsonPointer, JsonValueException> pointer() {
        return TO_POINTER;
    }

    /**
     * Returns the JSON string value as a uniform resource identifier. If the
     * JSON value is {@code null}, this method returns {@code null}.
     *
     * @return the URI represented by the string value.
     * @throws JsonValueException
     *         if the given string violates URI syntax.
     */
    public static Function<JsonValue, URI, JsonValueException> uri() {
        return TO_URI;
    }

    /**
     * Returns the JSON string value as a uniform resource locator. If the
     * JSON value is {@code null}, this method returns {@code null}.
     *
     * @return the URL represented by the string value.
     * @throws JsonValueException
     *         if the given string violates URL syntax.
     */
    public static Function<JsonValue, URL, JsonValueException> url() {
        return TO_URL;
    }

    /**
     * Returns the JSON string value as a universally unique identifier (UUID).
     * If the JSON value is {@code null}, this method returns {@code null}.
     *
     * @return the UUID represented by the JSON value string.
     * @throws JsonValueException
     *         if the JSON value is not a string or valid UUID.
     */
    public static Function<JsonValue, UUID, JsonValueException> uuid() {
        return TO_UUID;
    }

    /**
     * Returns the JSON value as a {@link List} containing objects whose type
     * (and value) is specified by a transformation function. If the value is
     * {@code null}, this method returns {@code null}. It is up to to the
     * transformation function to transform/enforce source types of the elements
     * in the Json source collection.  If any of the elements of the list are not of
     * the appropriate type, or the type-transformation cannot occur,
     * the exception specified by the transformation function is thrown.
     *
     * @param <V>
     *            the type of elements in this list
     * @param <E>
     *            the type of exception thrown by the transformation function
     * @param transformFunction
     *            a {@link Function} to transform an element of the JsonValue list
     *            to the desired type
     * @return the list value, or {@code null} if no value.
     * @throws E
     *             if the JSON value is not a {@code List}, not a {@code Set}, contains an
     *             unexpected type, or contains an element that cannot be transformed
     * @throws NullPointerException
     *             if {@code transformFunction} is {@code null}.
     */
    public static <V, E extends Exception> Function<JsonValue, List<V>, E> listOf(
            final Function<JsonValue, V, E> transformFunction) throws E {
        return new Function<JsonValue, List<V>, E>() {
            @Override
            public List<V> apply(JsonValue value) throws E {
                if (value.isCollection()) {
                    final List<V> list = new ArrayList<>(value.size());
                    for (JsonValue elem : value) {
                        list.add(elem.as(transformFunction));
                    }
                    return list;
                }
                return null;
            }
        };
    }

    /**
     * Returns the JSON value as a {@link Set} containing objects whose type
     * (and value) is specified by a transformation function. If the value is
     * {@code null}, this method returns {@code null}. It is up to to the
     * transformation function to transform/enforce source types of the elements
     * in the Json source collection.  If called on an object which wraps a List,
     * this method will drop duplicates performing element comparisons using
     * equals/hashCode. If any of the elements of the collection are not of
     * the appropriate type, or the type-transformation cannot occur, the
     * exception specified by the transformation function is thrown.
     *
     * @param <V>
     *            the type of elements in this set
     * @param <E>
     *            the type of exception thrown by the transformation function
     * @param transformFunction
     *            a {@link Function} to transform an element of the JsonValue set
     *            to the desired type
     * @return the set value, or {@code null} if no value.
     * @throws E
     *             if the JSON value is not a {@code Set}, contains an
     *             unexpected type, or contains an element that cannot be
     *             transformed
     * @throws NullPointerException
     *             if {@code transformFunction} is {@code null}.
     */
    public static <V, E extends Exception> Function<JsonValue, Set<V>, E> setOf(
            final Function<JsonValue, V, E> transformFunction) throws E {
        return new Function<JsonValue, Set<V>, E>() {
            @Override
            public Set<V> apply(JsonValue value) throws E {
                if (value.isCollection()) {
                    final Set<V> set = new LinkedHashSet<>(value.size());
                    for (JsonValue elem : value) {
                        set.add(elem.as(transformFunction));
                    }
                    return set;
                }
                return null;
            }
        };
    }
}
