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

import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Supplier;
import java.util.logging.Logger;

import javax.validation.Configuration;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ParameterNameProvider;
import javax.validation.Validation;
import javax.validation.ValidationException;
import javax.validation.ValidationProviderResolver;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.executable.ExecutableValidator;
import javax.validation.metadata.MethodDescriptor;
import javax.validation.spi.ValidationProvider;

import org.apache.cxf.common.logging.LogUtils;

import static java.util.Collections.emptySet;

public class BeanValidationProvider implements AutoCloseable {
    private static final Logger LOG = LogUtils.getL7dLogger(BeanValidationProvider.class);

    private final Runnable close;
    private final Supplier<Validator> factory;
    private final RuntimeCache runtimeCache; // /!must only be created when a single factory is used

    public BeanValidationProvider() {
        try {
            final ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
            this.factory = vf::getValidator;
            this.close = vf::close;
            this.runtimeCache = new RuntimeCache();
        } catch (final ValidationException ex) {
            LOG.severe("Bean Validation provider can not be found, no validation will be performed");
            throw ex;
        }
    }

    public BeanValidationProvider(ParameterNameProvider parameterNameProvider) {
        this(new ValidationConfiguration(parameterNameProvider));
    }

    public BeanValidationProvider(ValidationConfiguration cfg) {
        try {
            Configuration<?> factoryCfg = Validation.byDefaultProvider().configure();
            initFactoryConfig(factoryCfg, cfg);
            final ValidatorFactory vf = factoryCfg.buildValidatorFactory();
            this.factory = vf::getValidator;
            this.close = vf::close;
            this.runtimeCache = new RuntimeCache();
        } catch (final ValidationException ex) {
            LOG.severe("Bean Validation provider can not be found, no validation will be performed");
            throw ex;
        }
    }

    public BeanValidationProvider(Validator validator) {
        if (validator == null) {
            throw new NullPointerException("Validator is null");
        }
        this.factory = () -> validator;
        this.close = () -> {
        };
        this.runtimeCache = new RuntimeCache();
    }

    public BeanValidationProvider(ValidatorFactory factory) {
        if (factory == null) {
            throw new NullPointerException("Factory is null");
        }
        this.factory = factory::getValidator;
        this.close = () -> {
        };
        this.runtimeCache = new RuntimeCache();
    }

    public BeanValidationProvider(ValidationProviderResolver resolver) {
        this(resolver, null);
    }

    public <T extends Configuration<T>, U extends ValidationProvider<T>> BeanValidationProvider(
        ValidationProviderResolver resolver,
        Class<U> providerType) {
        this(resolver, providerType, null);
    }

    public <T extends Configuration<T>, U extends ValidationProvider<T>> BeanValidationProvider(
        ValidationProviderResolver resolver,
        Class<U> providerType,
        ValidationConfiguration cfg) {
        try {
            Configuration<?> factoryCfg = providerType != null
                ? Validation.byProvider(providerType).providerResolver(resolver).configure()
                : Validation.byDefaultProvider().providerResolver(resolver).configure();
            initFactoryConfig(factoryCfg, cfg);
            final ValidatorFactory vf = factoryCfg.buildValidatorFactory();
            this.factory = vf::getValidator;
            this.close = () -> {
            };
            this.runtimeCache = new RuntimeCache();
        } catch (final ValidationException ex) {
            LOG.severe("Bean Validation provider can not be found, no validation will be performed");
            throw ex;
        }
    }

    private static void initFactoryConfig(Configuration<?> factoryCfg, ValidationConfiguration cfg) {
        if (cfg != null) {
            factoryCfg.parameterNameProvider(cfg.getParameterNameProvider());
            factoryCfg.messageInterpolator(cfg.getMessageInterpolator());
            factoryCfg.traversableResolver(cfg.getTraversableResolver());
            factoryCfg.constraintValidatorFactory(cfg.getConstraintValidatorFactory());
            for (Map.Entry<String, String> entry : cfg.getProperties().entrySet()) {
                factoryCfg.addProperty(entry.getKey(), entry.getValue());
            }
        }
    }

    public< T > void validateParameters(final T instance, final Method method, final Object[] arguments) {
        final Validator validator = factory.get();
        final ExecutableValidator methodValidator = validator.forExecutables();
        if (runtimeCache == null || runtimeCache.shouldValidateParameters(validator, method)) {
            final Set<ConstraintViolation<T>> violations = methodValidator.validateParameters(instance,
                    method, arguments);
            if (!violations.isEmpty()) {
                throw new ConstraintViolationException(violations);
            }
        }
    }

    public< T > void validateReturnValue(final T instance, final Method method, final Object returnValue) {
        final Validator validator = factory.get();
        final ExecutableValidator methodValidator = validator.forExecutables();
        if (runtimeCache == null || runtimeCache.shouldValidateReturnedValue(validator, method)) {
            final Set<ConstraintViolation<T>> violations = methodValidator.validateReturnValue(instance,
                    method, returnValue);
            if (!violations.isEmpty()) {
                throw new ResponseConstraintViolationException(violations);
            }
        }
    }

    public< T > void validateReturnValue(final T bean) {
        Validator validator = factory.get();
        if (runtimeCache != null && bean != null
                && !runtimeCache.shouldValidateBean(validator, bean.getClass())) {
            return;
        }
        final Set<ConstraintViolation< T > > violations = doValidateBean(validator, bean);
        if (!violations.isEmpty()) {
            throw new ResponseConstraintViolationException(violations);
        }
    }

    public< T > void validateBean(final T bean) {
        final Set<ConstraintViolation< T > > violations = doValidateBean(factory.get(), bean);
        if (!violations.isEmpty()) {
            throw new ConstraintViolationException(violations);
        }
    }

    private< T > Set<ConstraintViolation< T > > doValidateBean(final Validator validator, final T bean) {
        if (validator.getConstraintsForClass(bean.getClass()).isBeanConstrained()) {
            return validator.validate(bean);
        }
        return emptySet();
    }

    @Override
    public void close() {
        close.run();
    }

    // only created when there is a single validator/factory so it is safe to cache
    // note: the validator is passed as param to avoid to create useless ones
    private static class RuntimeCache {
        private final ConcurrentMap<Class<?>, Boolean> types = new ConcurrentHashMap<>();
        private final ConcurrentMap<Method, Boolean> params = new ConcurrentHashMap<>();
        private final ConcurrentMap<Method, Boolean> returnedValues = new ConcurrentHashMap<>();

        public boolean shouldValidateParameters(final Validator validator, final Method method) {
            return params.computeIfAbsent(method, m -> {
                final MethodDescriptor constraint = validator
                    .getConstraintsForClass(m.getDeclaringClass())
                    .getConstraintsForMethod(m.getName(), m.getParameterTypes());
                return constraint != null && constraint.hasConstrainedParameters();
            });
        }

        public boolean shouldValidateReturnedValue(final Validator validator, final Method method) {
            return returnedValues.computeIfAbsent(method, m -> {
                final MethodDescriptor constraint = validator
                    .getConstraintsForClass(m.getDeclaringClass())
                    .getConstraintsForMethod(m.getName(), method.getParameterTypes());
                return constraint != null && constraint.hasConstrainedReturnValue();
            });
        }

        public boolean shouldValidateBean(final Validator validator, final Class<?> clazz) {
            return types.computeIfAbsent(clazz, it -> validator.getConstraintsForClass(it).isBeanConstrained());
        }
    }
}
