/*
 * Copyright (C) 2017
 *
 * [SpringVerticleFactory.java]
 * vertx-spring-mod (https://github.com/amoAHCP/spring-vertx-mod)
 * All rights reserved.
 *
 * 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.jacpfx.vertx.spring;


import io.vertx.core.AbstractVerticle;
import io.vertx.core.Verticle;
import io.vertx.core.Vertx;
import io.vertx.core.impl.verticle.CompilingClassLoader;
import io.vertx.core.spi.VerticleFactory;
import java.security.InvalidParameterException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;

/**
 * A Vertx Spring factory that creates a spring verticle and loads for each verticle instance a spring context
 * @author Andy Moncsek , Johannes Schüth
 *
 */
public class SpringVerticleFactory implements VerticleFactory {
    private Vertx vertx;

    public static final String PREFIX = "java-spring";
    public static final String SUFFIX = ".java";

    @Override
    public String prefix() {
        return PREFIX;
    }

    
    @Override
    public void init(Vertx vertx) {
        this.vertx = vertx;
    }

    @Override
    public synchronized Verticle createVerticle(final String verticleName, final ClassLoader classLoader) throws Exception {
        final String className = VerticleFactory.removePrefix(verticleName);
        Class<?> clazz;
        if (className.endsWith(SUFFIX)) {
            CompilingClassLoader compilingLoader = new CompilingClassLoader(classLoader, className);
            clazz = compilingLoader.loadClass(compilingLoader.resolveMainClassName());
        } else {
            clazz = classLoader.loadClass(className);
        }
        return createVerticle(clazz, classLoader);
    }

    private Verticle createVerticle(final Class<?> clazz, ClassLoader classLoader) throws Exception {

        if (clazz.isAnnotationPresent(SpringVerticle.class)) {
            return createSpringVerticle(clazz, classLoader);
        } else if (Verticle.class.isAssignableFrom(clazz)) {
            // init a non spring verticle, but this should not happen
            final Verticle verticle = Verticle.class.cast(clazz.newInstance());
            verticle.init(vertx, vertx.getOrCreateContext());
            return verticle;
        }

        return null;
    }

    private Verticle createSpringVerticle(final Class<?> currentVerticleClass, ClassLoader classLoader) {
        final SpringVerticle annotation = currentVerticleClass.getAnnotation(SpringVerticle.class);
        final Class<?> springConfigClass = annotation.springConfig();
        
        // Create the parent context  
        final GenericApplicationContext genericApplicationContext = getGenericApplicationContext(
            classLoader);

        // 1. Create a new context for each verticle and use the specified spring configuration class if possible
        AnnotationConfigApplicationContext annotationConfigApplicationContext = getAnnotationConfigApplicationContext(
            springConfigClass, genericApplicationContext);

        // 2. Register the Vertx instance as a singleton in spring context
        annotationConfigApplicationContext.getBeanFactory().registerSingleton(vertx.getClass().getSimpleName(), vertx);

        // 3. Register a bean definition for this verticle
        annotationConfigApplicationContext.registerBeanDefinition(currentVerticleClass.getSimpleName(), new VerticleBeanDefinition(currentVerticleClass));
        
        // 4. Add a bean factory post processor to avoid configuration issues
        addPostprocessorAndUpdateContext(currentVerticleClass, annotationConfigApplicationContext);
        
        // 5. Return the verticle by fetching the bean from the context
        return (Verticle) annotationConfigApplicationContext.getBeanFactory().getBean(currentVerticleClass.getSimpleName());
    }




    /**
     * Initialize a Spring Context for given Verticle instance. A Verticle MUST be annotated with {@link SpringVerticle}
     * @param verticle The Verticle Instance where to start the Spring Context
     */
    public static void initSpring(AbstractVerticle verticle) {
        final Class<?> currentVerticleClass = verticle.getClass();
        final Vertx vertx = verticle.getVertx();
        if(!currentVerticleClass.isAnnotationPresent(SpringVerticle.class)) {
            throw new InvalidParameterException("no @SpringVerticle annotation found");
        }
        final SpringVerticle annotation = currentVerticleClass.getAnnotation(SpringVerticle.class);
        final Class<?> springConfigClass = annotation.springConfig();

        // Create the parent context
        final GenericApplicationContext genericApplicationContext = getGenericApplicationContext(
            vertx.getClass().getClassLoader());

        // 1. Create a new context for each verticle and use the specified spring configuration class if possible
        AnnotationConfigApplicationContext annotationConfigApplicationContext = getAnnotationConfigApplicationContext(
            springConfigClass, genericApplicationContext);

        // 2. Register the Vertx instance as a singleton in spring context
        annotationConfigApplicationContext.getBeanFactory().registerSingleton(vertx.getClass().getSimpleName(), vertx);

        // 3. Register a bean definition for this verticle
        annotationConfigApplicationContext.getBeanFactory().registerSingleton(verticle.getClass().getSimpleName(), verticle);

        // 4. Add a bean factory post processor to avoid configuration issues
        addPostprocessorAndUpdateContext(currentVerticleClass, annotationConfigApplicationContext);

        // 5. perform autowiring
        annotationConfigApplicationContext.getAutowireCapableBeanFactory().autowireBean(verticle);

    }

    private static void addPostprocessorAndUpdateContext(Class<?> currentVerticleClass,
        AnnotationConfigApplicationContext annotationConfigApplicationContext) {
        annotationConfigApplicationContext.addBeanFactoryPostProcessor(new SpringSingleVerticleConfiguration(currentVerticleClass));
        annotationConfigApplicationContext.refresh();
        annotationConfigApplicationContext.start();
        annotationConfigApplicationContext.registerShutdownHook();
    }

    private static AnnotationConfigApplicationContext getAnnotationConfigApplicationContext(
        Class<?> springConfigClass, GenericApplicationContext genericApplicationContext) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext();
        annotationConfigApplicationContext.setParent(genericApplicationContext);
        annotationConfigApplicationContext.register(SpringContextConfiguration.class, springConfigClass);
        return annotationConfigApplicationContext;
    }

    private static GenericApplicationContext getGenericApplicationContext(ClassLoader classLoader) {
        final GenericApplicationContext genericApplicationContext = new GenericApplicationContext();
        genericApplicationContext.setClassLoader(classLoader);
        genericApplicationContext.refresh();
        genericApplicationContext.start();
        return genericApplicationContext;
    }

    public void close() {
    }


}