/*
 *  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.bval.jsr.valueextraction;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.validation.ConstraintDeclarationException;
import javax.validation.valueextraction.ValueExtractor;
import javax.validation.valueextraction.ValueExtractorDeclarationException;
import javax.validation.valueextraction.ValueExtractorDefinitionException;

import org.apache.bval.jsr.metadata.ContainerElementKey;
import org.apache.bval.util.Exceptions;
import org.apache.bval.util.Lazy;
import org.apache.bval.util.StringUtils;
import org.apache.bval.util.Validate;
import org.apache.bval.util.reflection.Reflection;
import org.apache.bval.util.reflection.TypeUtils;

public class ValueExtractors {
    public static final ValueExtractors DEFAULT;

    static {
        DEFAULT = new ValueExtractors(null) {
            {
                final Properties defaultExtractors = new Properties();
                try {
                    defaultExtractors.load(ValueExtractors.class.getResourceAsStream("DefaultExtractors.properties"));
                } catch (IOException e) {
                    throw new IllegalStateException(e);
                }
                split(defaultExtractors.getProperty(ValueExtractor.class.getName())).map(cn -> {
                    try {
                        @SuppressWarnings("unchecked")
                        final Class<? extends ValueExtractor<?>> result =
                            (Class<? extends ValueExtractor<?>>) Reflection.toClass(cn)
                                .asSubclass(ValueExtractor.class);
                        return result;
                    } catch (Exception e) {
                        throw new IllegalStateException(e);
                    }
                }).map(ValueExtractors::newInstance).forEach(super::add);

                split(defaultExtractors.getProperty(ValueExtractor.class.getName() + ".container"))
                    .flatMap(ValueExtractors::loadValueExtractors).forEach(super::add);
            }

            @Override
            public void add(ValueExtractor<?> extractor) {
                throw new UnsupportedOperationException();
            }
        };
    }

    public static Class<?> getExtractedType(ValueExtractor<?> extractor, Type target) {
        final ContainerElementKey key = ContainerElementKey.forValueExtractor(extractor);
        Type result = key.getAnnotatedType().getType();
        if (result instanceof TypeVariable<?>) {
            result = TypeUtils.getTypeArguments(target, key.getContainerClass()).get(result);
        }
        Exceptions.raiseUnless(result instanceof Class<?>, ValueExtractorDefinitionException::new,
            "%s did not resolve to a %s relative to %s", key, Class.class.getName(), target);
        return (Class<?>) result;
    }

    private static Stream<String> split(String s) {
        return Stream.of(StringUtils.split(s, ','));
    }

    private static <T> T newInstance(Class<T> t) {
        try {
            return t.getConstructor().newInstance();
        } catch (NoSuchMethodException | InstantiationException | IllegalAccessException e) {
            throw new IllegalStateException(e);
        } catch (InvocationTargetException e) {
            throw new IllegalStateException(e.getTargetException());
        }
    }

    private static Stream<ValueExtractor<?>> loadValueExtractors(String containerClassName) {
        try {
            final Class<? extends BooleanSupplier> activation =
                Reflection.toClass(containerClassName + "$Activation").asSubclass(BooleanSupplier.class);
            if (!newInstance(activation).getAsBoolean()) {
                return Stream.empty();
            }
        } catch (ClassNotFoundException e) {
            // always active
        }
        final Class<?> containerClass;
        try {
            containerClass = Reflection.toClass(containerClassName);
        } catch (ClassNotFoundException e) {
            throw new IllegalStateException(e);
        }
        return Stream.of(containerClass.getClasses()).filter(ValueExtractor.class::isAssignableFrom).map(c -> {
            @SuppressWarnings("unchecked")
            final Class<? extends ValueExtractor<?>> result =
                (Class<? extends ValueExtractor<?>>) c.asSubclass(ValueExtractor.class);
            return result;
        }).map(ValueExtractors::newInstance);
    }

    private final Lazy<Map<ContainerElementKey, ValueExtractor<?>>> valueExtractors = new Lazy<>(HashMap::new);
    private final ValueExtractors parent;

    public ValueExtractors() {
        this(DEFAULT);
    }

    private ValueExtractors(ValueExtractors parent) {
        this.parent = parent;
    }

    public ValueExtractors createChild() {
        return new ValueExtractors(this);
    }

    public void add(ValueExtractor<?> extractor) {
        Validate.notNull(extractor);
        valueExtractors.get().compute(ContainerElementKey.forValueExtractor(extractor), (k, v) -> {
            Exceptions.raiseIf(v != null, ValueExtractorDeclarationException::new,
                "Multiple context-level %ss specified for %s", ValueExtractor.class.getSimpleName(), k);
            return extractor;
        });
    }

    public Map<ContainerElementKey, ValueExtractor<?>> getValueExtractors() {
        final Lazy<Map<ContainerElementKey, ValueExtractor<?>>> result = new Lazy<>(HashMap::new);
        populate(result);
        return result.optional().orElseGet(Collections::emptyMap);
    }

    public ValueExtractor<?> find(ContainerElementKey key) {
        final Map<ContainerElementKey, ValueExtractor<?>> allValueExtractors = getValueExtractors();
        if (allValueExtractors.containsKey(key)) {
            return allValueExtractors.get(key);
        }
        // search for assignable ContainerElementKey:
        Set<ContainerElementKey> assignableKeys = key.getAssignableKeys();
        while (!assignableKeys.isEmpty()) {
            final Set<ValueExtractor<?>> results = assignableKeys.stream().filter(allValueExtractors::containsKey)
                .map(allValueExtractors::get).collect(Collectors.toSet());

            final int rz = results.size();
            if (rz == 1) {
                return results.iterator().next();
            }
            Exceptions.raiseIf(rz > 1, ConstraintDeclarationException::new, "%d maximally specific %ss found for %s",
                rz, ValueExtractor.class.getSimpleName(), key);

            assignableKeys = assignableKeys.stream().map(ContainerElementKey::getAssignableKeys)
                .flatMap(Collection::stream).collect(Collectors.toSet());
        }
        return null;
    }

    protected void populate(Supplier<Map<ContainerElementKey, ValueExtractor<?>>> target) {
        Optional.ofNullable(parent).ifPresent(p -> p.populate(target));
        valueExtractors.optional().ifPresent(m -> target.get().putAll(m));
    }
}

@@ -1339,64 +1339,8 @@
 m;%0A%0A
-import javax.validation.ConstraintDeclarationException;%0A
 impo
@@ -7134,19 +7134,24 @@
   final 
-Set
+Optional
 %3CValueEx
@@ -7162,23 +7162,21 @@
 tor%3C?%3E%3E 
-results
+found
  = assig
@@ -7251,16 +7251,36 @@
        .
+%3CValueExtractor%3C?%3E%3E 
 map(allV
@@ -7304,354 +7304,107 @@
 et).
-collect(Collectors.toSet());%0A%0A            final int rz = results.size();%0A            if (rz == 1) %7B%0A                return results.iterator().next();%0A            %7D%0A            Exceptions.raiseIf(rz %3E 1, ConstraintDeclarationException::new, %22%25d maximally specific %25ss found for %25s%22,%0A                rz, ValueExtractor.class.getSimpleName(), key);%0A
+findFirst();%0A            if (found.isPresent()) %7B%0A                return found.get();%0A            %7D
 %0A   

revert bad go at detecting multiple maximally specific ValueExtractors