/*
 * Copyright 2014 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed 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.kie.spring.annotations;

import org.drools.core.util.StringUtils;
import org.kie.api.KieBase;
import org.kie.api.KieServices;
import org.kie.api.builder.ReleaseId;
import org.kie.api.cdi.KBase;
import org.kie.api.cdi.KContainer;
import org.kie.api.cdi.KReleaseId;
import org.kie.api.cdi.KSession;
import org.kie.api.runtime.CommandExecutor;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.StatelessKieSession;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.InjectionMetadata;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.util.ClassUtils;

import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

class AnnotationsPostProcessor implements InstantiationAwareBeanPostProcessor,
        MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware, Serializable {

    public static final String KIE_ANNOTATIONS_ARE_NOT_SUPPORTED_ON_STATIC_METHODS = "Kie Annotations are not supported on static methods";
    public static final String INJECTION_OF_KIE_DEPENDENCIES_FAILED = "Injection of kie dependencies failed";
    private final transient Map<Class<?>, InjectionMetadata> injectionMetadataCache = new ConcurrentHashMap<Class<?>, InjectionMetadata>();
    private int order = Ordered.LOWEST_PRECEDENCE - 4;
    private transient ListableBeanFactory beanFactory;
    private ReleaseId releaseId;
    private Map<ReleaseId, KieContainer> kieContainerMap = new HashMap<ReleaseId, KieContainer>();

    public ReleaseId getReleaseId() {
        return releaseId;
    }

    public void setReleaseId(ReleaseId releaseId) {
        this.releaseId = releaseId;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    public int getOrder() {
        return this.order;
    }

    public void setBeanFactory(BeanFactory beanFactory) {
        if (beanFactory instanceof ListableBeanFactory) {
            this.beanFactory = (ListableBeanFactory) beanFactory;
        }
    }

    public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) {
        if (beanType != null) {
            InjectionMetadata metadata = findAnnotationMetadata(beanType);
            metadata.checkConfigMembers(beanDefinition);
        }
    }

    public Object postProcessBeforeInstantiation(Class beanClass, String beanName) throws BeansException {
        return null;
    }

    public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
        return true;
    }

    public PropertyValues postProcessPropertyValues(
            PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {

        InjectionMetadata metadata = findAnnotationMetadata(bean.getClass());
        try {
            metadata.inject(bean, beanName, pvs);
        }
        catch (Throwable ex) {
            throw new BeanCreationException(beanName, INJECTION_OF_KIE_DEPENDENCIES_FAILED, ex);
        }

        return pvs;
    }

    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    private InjectionMetadata findAnnotationMetadata(final Class clazz) {
        // Quick check on the concurrent map first, with minimal locking.
        InjectionMetadata metadata = this.injectionMetadataCache.get(clazz);
        if (metadata == null) {
            synchronized (this.injectionMetadataCache) {
                metadata = this.injectionMetadataCache.get(clazz);
                if (metadata == null) {
                    LinkedList<InjectionMetadata.InjectedElement> elements = new LinkedList<InjectionMetadata.InjectedElement>();
                    Class<?> targetClass = clazz;

                    do {
                        LinkedList<InjectionMetadata.InjectedElement> currElements = new LinkedList<InjectionMetadata.InjectedElement>();
                        checkForFieldInjections(targetClass, currElements);
                        checkForMethodInjections(targetClass, currElements);
                        elements.addAll(0, currElements);
                        targetClass = targetClass.getSuperclass();
                    }
                    while (targetClass != null && targetClass != Object.class);

                    metadata = new InjectionMetadata(clazz, elements);
                    this.injectionMetadataCache.put(clazz, metadata);
                }
            }
        }
        return metadata;
    }

    private void checkForMethodInjections(Class<?> targetClass, LinkedList<InjectionMetadata.InjectedElement> currElements) {
        for (Method method : targetClass.getDeclaredMethods()) {
            KSession kSession = method.getAnnotation(KSession.class);
            KBase kBase = method.getAnnotation(KBase.class);
            KContainer kContainer = method.getAnnotation(KContainer.class);
            if ((kSession != null || kBase != null || kContainer !=null) &&
                    method.equals(ClassUtils.getMostSpecificMethod(method, targetClass))) {
                if (Modifier.isStatic(method.getModifiers())) {
                    throw new IllegalStateException(KIE_ANNOTATIONS_ARE_NOT_SUPPORTED_ON_STATIC_METHODS);
                }
                if (method.getParameterTypes().length != 1) {
                    throw new IllegalStateException("Kie Annotation requires a single-arg method: " + method);
                }
                PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method);
                if ( kSession != null ) {
                    currElements.add(new KSessionInjectedElement(method, pd, releaseId));
                } else if (kBase != null ) {
                    currElements.add(new KBaseInjectedElement(method, pd,releaseId));
                } else if (kContainer != null ) {
                    currElements.add(new KContainerInjectedElement(method, pd, releaseId));
                }
            }
        }
    }

    private void checkForFieldInjections(Class<?> targetClass, LinkedList<InjectionMetadata.InjectedElement> currElements) {
        KieServices ks = KieServices.Factory.get();
        for (Field field : targetClass.getDeclaredFields()) {

            KReleaseId kReleaseId = field.getAnnotation(KReleaseId.class);
            ReleaseId specificReleaseId = null;
            if ( kReleaseId != null ) {
                specificReleaseId = ks.newReleaseId(kReleaseId.groupId(),
                        kReleaseId.artifactId(),
                        kReleaseId.version());
            } else {
                specificReleaseId = this.releaseId;
            }

            KBase kBase = field.getAnnotation(KBase.class);
            if (kBase != null) {
                throwExceptionIfStatic(field);
                currElements.add(new KBaseInjectedElement(field, null, specificReleaseId));
            }
            KSession kSession = field.getAnnotation(KSession.class);
            if (kSession != null) {
                throwExceptionIfStatic(field);
                currElements.add(new KSessionInjectedElement(field, null, specificReleaseId));
            }
            KContainer kContainer = field.getAnnotation(KContainer.class);
            if (kContainer != null) {
                throwExceptionIfStatic(field);
                currElements.add(new KContainerInjectedElement(field, null, specificReleaseId));
            }
        }
    }

    private void throwExceptionIfStatic(Field field) {
        if (Modifier.isStatic(field.getModifiers())) {
            throw new IllegalStateException(KIE_ANNOTATIONS_ARE_NOT_SUPPORTED_ON_STATIC_METHODS);
        }
    }

    private class KieElementInjectedElement extends InjectionMetadata.InjectedElement {
        protected String name;
        protected ReleaseId releaseId;
        public KieElementInjectedElement(Member member, PropertyDescriptor pd, ReleaseId releaseId) {
            super(member, pd);
            setReleaseId(releaseId);
        }

        public KieElementInjectedElement(Member member, PropertyDescriptor pd) {
            this(member, pd, null);
        }

        protected Object getResourceToInject(Object target, String requestingBeanName) {
            return beanFactory.getBean(name);
        }

        public ReleaseId getReleaseId() {
            return releaseId;
        }

        public void setReleaseId(ReleaseId releaseId) {
            this.releaseId = releaseId;
        }
    }

    private class KBaseInjectedElement extends KieElementInjectedElement {

        public KBaseInjectedElement(Member member, PropertyDescriptor pd, ReleaseId releaseId) {
            super(member, pd, releaseId);
            AnnotatedElement ae = (AnnotatedElement) member;
            KBase aeAnnotation = ae.getAnnotation(KBase.class);
            name = aeAnnotation.value();
            checkResourceType(KieBase.class);
        }

        protected Object getResourceToInject(Object target, String requestingBeanName) {
            if (StringUtils.isEmpty(name)) {
                //check for default KieBase in the current KieContainer
                KieContainer kieContainer = kieContainerMap.get(getReleaseId());
                if  ( kieContainer == null){
                    kieContainer = KieServices.Factory.get().newKieContainer(getReleaseId());
                    kieContainerMap.put(releaseId, kieContainer);
                }
                return kieContainer.getKieBase();
            }
            if( getReleaseId().equals(AnnotationsPostProcessor.this.getReleaseId())) {
                return beanFactory.getBean(name);
            } else {
                KieContainer kieContainer = kieContainerMap.get(getReleaseId());
                if  ( kieContainer == null){
                    kieContainer = KieServices.Factory.get().newKieContainer(getReleaseId());
                    kieContainerMap.put(releaseId, kieContainer);
                }
                return kieContainer.getKieBase(name);
            }
        }

    }

    private class KSessionInjectedElement extends KieElementInjectedElement {

        String type;
        public KSessionInjectedElement(Member member, PropertyDescriptor pd, ReleaseId releaseId) {
            super(member, pd, releaseId);
            AnnotatedElement ae = (AnnotatedElement) member;
            KSession kSessionAnnotation = ae.getAnnotation(KSession.class);
            name = kSessionAnnotation.value();

            checkResourceType(CommandExecutor.class);
        }

        protected Object getResourceToInject(Object target, String requestingBeanName) {
            if( getReleaseId().equals(AnnotationsPostProcessor.this.getReleaseId())) {
                return beanFactory.getBean(name);
            } else {
                KieContainer kieContainer = kieContainerMap.get(getReleaseId());
                if  (kieContainer == null){
                    kieContainer = KieServices.Factory.get().newKieContainer(getReleaseId());
                    kieContainerMap.put(releaseId, kieContainer);
                }
                String type = "stateful";
                if ( member instanceof Field){
                    if(((Field)member).getGenericType() instanceof StatelessKieSession){
                        type = "stateless";
                    }
                } else if (member instanceof Method) {
                    if(((Method)member).getParameterTypes()[0].getName().equalsIgnoreCase(StatelessKieSession.class.getName())){
                        type = "stateless";
                    }
                }
                if (type.equalsIgnoreCase("stateful")) {
                    return kieContainer.newKieSession(name);
                } else {
                    return kieContainer.newStatelessKieSession(name);
                }
            }
        }
    }

    private class KContainerInjectedElement extends KieElementInjectedElement {

        public KContainerInjectedElement(Member member, PropertyDescriptor pd, ReleaseId releaseId) {
            super(member, pd, releaseId);
            checkResourceType(KieContainer.class);
        }

        protected Object getResourceToInject(Object target, String requestingBeanName) {
            KieContainer kieContainer = kieContainerMap.get(getReleaseId());
            if  ( kieContainer == null){
                kieContainer = KieServices.Factory.get().newKieContainer(getReleaseId());
                kieContainerMap.put(releaseId, kieContainer);
            }
            return kieContainer;
        }
    }
}
