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

import static java.util.Arrays.asList;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import javax.json.JsonObject;
import javax.json.JsonValue;

import org.apache.johnzon.jsonschema.regex.JavaRegex;
import org.apache.johnzon.jsonschema.regex.JavascriptRegex;
import org.apache.johnzon.jsonschema.spi.ValidationContext;
import org.apache.johnzon.jsonschema.spi.ValidationExtension;
import org.apache.johnzon.jsonschema.spi.builtin.ContainsValidation;
import org.apache.johnzon.jsonschema.spi.builtin.EnumValidation;
import org.apache.johnzon.jsonschema.spi.builtin.ExclusiveMaximumValidation;
import org.apache.johnzon.jsonschema.spi.builtin.ExclusiveMinimumValidation;
import org.apache.johnzon.jsonschema.spi.builtin.IntegerValidation;
import org.apache.johnzon.jsonschema.spi.builtin.ItemsValidation;
import org.apache.johnzon.jsonschema.spi.builtin.MaxItemsValidation;
import org.apache.johnzon.jsonschema.spi.builtin.MaxLengthValidation;
import org.apache.johnzon.jsonschema.spi.builtin.MaxPropertiesValidation;
import org.apache.johnzon.jsonschema.spi.builtin.MaximumValidation;
import org.apache.johnzon.jsonschema.spi.builtin.MinItemsValidation;
import org.apache.johnzon.jsonschema.spi.builtin.MinLengthValidation;
import org.apache.johnzon.jsonschema.spi.builtin.MinPropertiesValidation;
import org.apache.johnzon.jsonschema.spi.builtin.MinimumValidation;
import org.apache.johnzon.jsonschema.spi.builtin.MultipleOfValidation;
import org.apache.johnzon.jsonschema.spi.builtin.PatternValidation;
import org.apache.johnzon.jsonschema.spi.builtin.RequiredValidation;
import org.apache.johnzon.jsonschema.spi.builtin.TypeValidation;
import org.apache.johnzon.jsonschema.spi.builtin.UniqueItemsValidation;

public class JsonSchemaValidatorFactory implements AutoCloseable {
    private static final String[] ROOT_PATH = new String[0];
    private static final Function<JsonValue, Stream<ValidationResult.ValidationError>> NO_VALIDATION = new Function<JsonValue, Stream<ValidationResult.ValidationError>>() {
        @Override
        public Stream<ValidationResult.ValidationError> apply(JsonValue jsonValue) {
            return Stream.empty();
        }

        @Override
        public String toString() {
            return "NoValidation";
        }
    };

    private final List<ValidationExtension> extensions = new ArrayList<>();

    // js is closer to default and actually most used in the industry
    private final AtomicReference<Function<String, Predicate<CharSequence>>> regexFactory = new AtomicReference<>(this::newRegexFactory);

    private Predicate<CharSequence> newRegexFactory(final String regex) {
        try {
            return new JavascriptRegex(regex);
        } catch (final RuntimeException re) {
            return new JavaRegex(regex);
        }
    }

    public JsonSchemaValidatorFactory() {
        extensions.addAll(createDefaultValidations());
        extensions.addAll(new ArrayList<>(StreamSupport.stream(ServiceLoader.load(ValidationExtension.class).spliterator(), false)
                .collect(toList())));
    }

    // see http://json-schema.org/latest/json-schema-validation.html
    public List<ValidationExtension> createDefaultValidations() {
        return asList(
                new RequiredValidation(),
                new TypeValidation(),
                new IntegerValidation(),
                new EnumValidation(),
                new MultipleOfValidation(),
                new MaximumValidation(),
                new MinimumValidation(),
                new ExclusiveMaximumValidation(),
                new ExclusiveMinimumValidation(),
                new MaxLengthValidation(),
                new MinLengthValidation(),
                new PatternValidation(regexFactory.get()),
                new ItemsValidation(this),
                new MaxItemsValidation(),
                new MinItemsValidation(),
                new UniqueItemsValidation(),
                new ContainsValidation(this),
                new MaxPropertiesValidation(),
                new MinPropertiesValidation()
                // TODO: dependencies, propertyNames, if/then/else, allOf/anyOf/oneOf/not,
                //       format validations
        );
    }

    public JsonSchemaValidatorFactory appendExtensions(final ValidationExtension... extensions) {
        this.extensions.addAll(asList(extensions));
        return this;
    }

    public JsonSchemaValidatorFactory setExtensions(final ValidationExtension... extensions) {
        this.extensions.clear();
        return appendExtensions(extensions);
    }

    public JsonSchemaValidatorFactory setRegexFactory(final Function<String, Predicate<CharSequence>> factory) {
        regexFactory.set(factory);
        return this;
    }

    public JsonSchemaValidator newInstance(final JsonObject schema) {
        return new JsonSchemaValidator(buildValidator(ROOT_PATH, schema, null));
    }

    @Override
    public void close() {
        // no-op for now
    }

    private Function<JsonValue, Stream<ValidationResult.ValidationError>> buildValidator(final String[] path,
                                                                                         final JsonObject schema,
                                                                                         final Function<JsonValue, JsonValue> valueProvider) {
        final List<Function<JsonValue, Stream<ValidationResult.ValidationError>>> directValidations = buildDirectValidations(path, schema, valueProvider).collect(toList());
        final Function<JsonValue, Stream<ValidationResult.ValidationError>> nestedValidations = buildPropertiesValidations(path, schema, valueProvider);
        final Function<JsonValue, Stream<ValidationResult.ValidationError>> dynamicNestedValidations = buildPatternPropertiesValidations(path, schema, valueProvider);
        final Function<JsonValue, Stream<ValidationResult.ValidationError>> fallbackNestedValidations = buildAdditionalPropertiesValidations(path, schema, valueProvider);
        return new ValidationsFunction(
                Stream.concat(
                        directValidations.stream(),
                        Stream.of(nestedValidations, dynamicNestedValidations, fallbackNestedValidations))
                    .collect(toList()));
    }

    private Stream<Function<JsonValue, Stream<ValidationResult.ValidationError>>> buildDirectValidations(final String[] path,
                                                                                                         final JsonObject schema,
                                                                                                         final Function<JsonValue, JsonValue> valueProvider) {
        final ValidationContext model = new ValidationContext(path, schema, valueProvider);
        return extensions.stream()
                .map(e -> e.create(model))
                .filter(Optional::isPresent)
                .map(Optional::get);
    }

    private Function<JsonValue, Stream<ValidationResult.ValidationError>> buildPropertiesValidations(final String[] path,
                                                                                                     final JsonObject schema,
                                                                                                     final Function<JsonValue, JsonValue> valueProvider) {
        return ofNullable(schema.get("properties"))
                .filter(it -> it.getValueType() == JsonValue.ValueType.OBJECT)
                .map(it -> it.asJsonObject().entrySet().stream()
                        .filter(obj -> obj.getValue().getValueType() == JsonValue.ValueType.OBJECT)
                        .map(obj -> {
                            final String key = obj.getKey();
                            final String[] fieldPath = Stream.concat(Stream.of(path), Stream.of(key)).toArray(String[]::new);
                            return buildValidator(fieldPath, obj.getValue().asJsonObject(), new ChainedValueAccessor(valueProvider, key));
                        })
                        .collect(toList()))
                .map(this::toFunction)
                .orElse(NO_VALIDATION);
    }

    // not the best impl but is it really an important case?
    private Function<JsonValue, Stream<ValidationResult.ValidationError>> buildPatternPropertiesValidations(final String[] path,
                                                                                                     final JsonObject schema,
                                                                                                     final Function<JsonValue, JsonValue> valueProvider) {
        return ofNullable(schema.get("patternProperties"))
                .filter(it -> it.getValueType() == JsonValue.ValueType.OBJECT)
                .map(it -> it.asJsonObject().entrySet().stream()
                        .filter(obj -> obj.getValue().getValueType() == JsonValue.ValueType.OBJECT)
                        .map(obj -> {
                            final Predicate<CharSequence> pattern = regexFactory.get().apply(obj.getKey());
                            final JsonObject currentSchema = obj.getValue().asJsonObject();
                            // no cache cause otherwise it could be in properties
                            return (Function<JsonValue, Stream<ValidationResult.ValidationError>>) validable -> {
                                if (validable.getValueType() != JsonValue.ValueType.OBJECT) {
                                    return Stream.empty();
                                }
                                return validable.asJsonObject().entrySet().stream()
                                        .filter(e -> pattern.test(e.getKey()))
                                        .flatMap(e -> {
                                            final String[] subPath = Stream.concat(Stream.of(path), Stream.of(e.getKey())).toArray(String[]::new);
                                            final Function<JsonValue, JsonValue> provider = new ChainedValueAccessor(valueProvider, e.getKey());
                                            return buildValidator(subPath, currentSchema, provider).apply(validable);
                                        });
                            };
                        })
                        .collect(toList()))
                .map(this::toFunction)
                .orElse(NO_VALIDATION);
    }

    private Function<JsonValue, Stream<ValidationResult.ValidationError>> buildAdditionalPropertiesValidations(final String[] path,
                                                                                                     final JsonObject schema,
                                                                                                     final Function<JsonValue, JsonValue> valueProvider) {
        return ofNullable(schema.get("additionalProperties"))
                .filter(it -> it.getValueType() == JsonValue.ValueType.OBJECT)
                .map(it -> {
                    Predicate<String> excluded = s -> false;
                    if (schema.containsKey("properties")) {
                        final Set<String> properties = schema.getJsonObject("properties").keySet();
                        excluded = excluded.and(s -> !properties.contains(s));
                    }
                    if (schema.containsKey("patternProperties")) {
                        final List<Predicate<CharSequence>> properties = schema.getJsonObject("patternProperties").keySet().stream()
                                                                               .map(regexFactory.get())
                                                                               .collect(toList());
                        excluded = excluded.and(s -> properties.stream().noneMatch(p -> p.test(s)));
                    }
                    final Predicate<String> excludeAttrRef = excluded;
                    final JsonObject currentSchema = it.asJsonObject();
                    return (Function<JsonValue, Stream<ValidationResult.ValidationError>>) validable -> {
                        if (validable.getValueType() != JsonValue.ValueType.OBJECT) {
                            return Stream.empty();
                        }
                        return validable.asJsonObject().entrySet().stream()
                                        .filter(e -> excludeAttrRef.test(e.getKey()))
                                        .flatMap(e -> buildValidator(
                                                Stream.concat(Stream.of(path), Stream.of(e.getKey())).toArray(String[]::new),
                                                currentSchema,
                                                new ChainedValueAccessor(valueProvider, e.getKey())).apply(validable));
                    };
                })
                .orElse(NO_VALIDATION);
    }

    private Function<JsonValue, Stream<ValidationResult.ValidationError>> toFunction(
            final List<Function<JsonValue, Stream<ValidationResult.ValidationError>>> validations) {
        return new ValidationsFunction(validations);
    }

    private static class ValidationsFunction implements Function<JsonValue, Stream<ValidationResult.ValidationError>> {
        private final List<Function<JsonValue, Stream<ValidationResult.ValidationError>>> delegates;

        private ValidationsFunction(final List<Function<JsonValue, Stream<ValidationResult.ValidationError>>> validations) {
            // unwrap when possible to simplify the stack and make toString readable (debug)
            this.delegates = validations.stream()
                    .flatMap(it -> ValidationsFunction.class.isInstance(it) ? ValidationsFunction.class.cast(it).delegates.stream() : Stream.of(it))
                    .filter(it -> it != NO_VALIDATION)
                    .collect(toList());
        }

        @Override
        public Stream<ValidationResult.ValidationError> apply(final JsonValue jsonValue) {
            return delegates.stream().flatMap(v -> v.apply(jsonValue));
        }

        @Override
        public String toString() {
            return delegates.toString();
        }
    }

    private static class ChainedValueAccessor implements Function<JsonValue, JsonValue> {
        private final Function<JsonValue, JsonValue> parent;
        private final String key;

        private ChainedValueAccessor(final Function<JsonValue, JsonValue> valueProvider, final String key) {
            this.parent = valueProvider;
            this.key = key;
        }

        @Override
        public JsonValue apply(final JsonValue value) {
            final JsonValue root = parent == null ? value : parent.apply(value);
            if (root != null && root.getValueType() != JsonValue.ValueType.NULL && root.getValueType() == JsonValue.ValueType.OBJECT) {
                return root.asJsonObject().get(key);
            }
            return JsonValue.NULL;
        }

        @Override
        public String toString() {
            return "ChainedValueAccessor{" +
                    "parent=" + parent +
                    ", key='" + key + '\'' +
                    '}';
        }
    }
}
