package org.kie.dmn.feel.lang.impl;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Stream;

import org.kie.dmn.feel.lang.CompositeType;
import org.kie.dmn.feel.lang.FEELProperty;
import org.kie.dmn.feel.lang.FEELType;
import org.kie.dmn.feel.lang.Type;
import org.kie.dmn.feel.lang.types.BuiltInType;
import org.kie.dmn.feel.parser.feel11.ParserHelper;
import org.kie.dmn.feel.util.EvalHelper;

public class JavaBackedType implements CompositeType {
    private static Map<Class<?>, JavaBackedType> cache = new HashMap<>();
    
    private static Set<Method> javaObjectMethods = new HashSet<>( Arrays.asList( Object.class.getMethods() ) );
    
    private Class<?> wrapped;
    private Map<String, Type> properties = new LinkedHashMap<>();

    private JavaBackedType(Class<?> class1) {
        this.wrapped = class1;
        
        Stream.of( class1.getMethods() )
            .filter( m -> Modifier.isPublic( m.getModifiers() ) || Modifier.isProtected( m.getModifiers() ) )
            .filter( m -> ! javaObjectMethods.contains(m) )
            .flatMap( m -> Stream.<Function<Method, Optional<String>>>of(JavaBackedType::methodToCustomProperty, EvalHelper::propertyFromAccessor)
                            .map(f -> f.apply( m ))
                            .filter(Optional::isPresent)
                            .map(p -> new Property( p.get(), ParserHelper.determineTypeFromClass( m.getReturnType() ) ) ) )
            .forEach( p -> properties.put( p.name , p.type ) );
    }

    /**
     * If method m is annotated with FEELProperty, will return FEELProperty.value, otherwise empty.
     */
    private static Optional<String> methodToCustomProperty(Method m) {
        return Optional.ofNullable(m.getAnnotation(FEELProperty.class)).map(a->a.value());
    }
    
    /**
     * If clazz can be represented as a JavaBackedType, returns a JavaBackedType for representing clazz.
     * If clazz can not be represented as a JavaBackedType, returns BuiltInType.UNKNOWN.
     * This method performs memoization when necessary.
     * @param clazz the class to be represented as JavaBackedType
     * @return JavaBackedType representing clazz or BuiltInType.UNKNOWN
     */
    public static Type of(Class<?> clazz) {
        return Optional.ofNullable( (Type) cache.computeIfAbsent( clazz, JavaBackedType::createIfAnnotated ) ).orElse( BuiltInType.UNKNOWN );
    }
    
    /**
     * For internal use, returns a new JavaBackedType if clazz can be represented as such, returns null otherwise.
     */
    private static JavaBackedType createIfAnnotated(Class<?> clazz) {
        if (clazz.isAnnotationPresent(FEELType.class) || Stream.of(clazz.getMethods()).anyMatch(m->m.getAnnotation(FEELProperty.class)!=null)) {
            return new JavaBackedType(clazz) ;
        }
        return null;
    }

    @Override
    public String getName() {
        return wrapped.getName();
    }

    public Class<?> getWrapped() {
        return wrapped;
    }

    @Override
    public Map<String, Type> getFields() {
        return this.properties;
    }

    private static class Property {
        public final String name;
        public final Type type;

        public Property( String name, Type type ) {
            this.name = name;
            this.type = type;
        }
    }

    @Override
    public boolean isInstanceOf(Object o) {
        return wrapped.getClass().isInstance(o);
    }

    @Override
    public boolean isAssignableValue(Object value) {
        return value == null || wrapped.getClass().isAssignableFrom(value.getClass());
    }
}

@@ -115,17 +115,228 @@
 va.util.
-*
+Arrays;%0Aimport java.util.Collections;%0Aimport java.util.HashSet;%0Aimport java.util.LinkedHashMap;%0Aimport java.util.Map;%0Aimport java.util.Optional;%0Aimport java.util.Set;%0Aimport java.util.concurrent.ConcurrentHashMap
 ;%0Aimport
@@ -756,16 +756,17 @@
 eType %7B%0A
+%0A
     priv
@@ -814,24 +814,34 @@
 cache = new 
+Concurrent
 HashMap%3C%3E();
@@ -893,24 +893,52 @@
 ctMethods = 
+Collections.unmodifiableSet(
 new HashSet%3C
@@ -939,17 +939,16 @@
 shSet%3C%3E(
- 
 Arrays.a
@@ -953,17 +953,16 @@
 .asList(
- 
 Object.c
@@ -978,19 +978,18 @@
 ethods()
- ) 
+))
 );%0A    %0A

fix JavaBackendTypeTest.testPerson fails on ConcurrentModificationExcptn (#1583)