/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2011, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.pojo.service;

import org.jboss.as.pojo.logging.PojoLogger;
import org.jboss.as.pojo.descriptor.BeanMetaDataConfig;
import org.jboss.as.pojo.descriptor.ConstructorConfig;
import org.jboss.as.pojo.descriptor.FactoryConfig;
import org.jboss.as.pojo.descriptor.LifecycleConfig;
import org.jboss.as.pojo.descriptor.PropertyConfig;
import org.jboss.as.pojo.descriptor.ValueConfig;
import org.jboss.as.server.deployment.reflect.DeploymentReflectionIndex;
import org.jboss.modules.Module;
import org.jboss.msc.service.StartException;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * Bean utils.
 *
 * @author <a href="mailto:ales.justin@jboss.org">Ales Justin</a>
 */
public final class BeanUtils {

    /**
     * Instantiate bean.
     *
     * @param beanConfig the bean metadata config, must not be null
     * @param beanInfo the bean info, can be null if enough info
     * @param index the reflection index, must not be null
     * @param module the current CL module, must not be null
     * @return new bean instance
     * @throws Throwable for any error
     */
    public static Object instantiateBean(BeanMetaDataConfig beanConfig, BeanInfo beanInfo, DeploymentReflectionIndex index, Module module) throws Throwable {
        Joinpoint instantiateJoinpoint = null;
        ValueConfig[] parameters = new ValueConfig[0];
        String[] types = Configurator.NO_PARAMS_TYPES;
        ConstructorConfig ctorConfig = beanConfig.getConstructor();
        if (ctorConfig != null) {
            parameters = ctorConfig.getParameters();
            types = Configurator.getTypes(parameters);

            String factoryClass = ctorConfig.getFactoryClass();
            FactoryConfig factory = ctorConfig.getFactory();
            if (factoryClass != null || factory != null) {
                String factoryMethod = ctorConfig.getFactoryMethod();
                if (factoryMethod == null)
                    throw PojoLogger.ROOT_LOGGER.missingFactoryMethod(beanConfig);

                if (factoryClass != null) {
                    // static factory
                    Class<?> factoryClazz = Class.forName(factoryClass, false, module.getClassLoader());
                    Method method = Configurator.findMethod(index, factoryClazz, factoryMethod, types, true, true, true);
                    MethodJoinpoint mj = new MethodJoinpoint(method);
                    mj.setTarget(() -> null); // null, since this is static call
                    mj.setParameters(parameters);
                    instantiateJoinpoint = mj;
                } else if (factory != null) {
                    ReflectionJoinpoint rj = new ReflectionJoinpoint(factory.getBeanInfo(), factoryMethod, types);
                    // null type is ok, as this should be plain injection
                    rj.setTarget(() -> factory.getValue(null));
                    rj.setParameters(parameters);
                    instantiateJoinpoint = rj;
                }
            }
        }
        // plain bean's ctor
        if (instantiateJoinpoint == null) {
            if (beanInfo == null)
                throw new StartException(PojoLogger.ROOT_LOGGER.missingBeanInfo(beanConfig));

            Constructor ctor = (types.length == 0) ? beanInfo.getConstructor() : beanInfo.findConstructor(types);
            ConstructorJoinpoint constructorJoinpoint = new ConstructorJoinpoint(ctor);
            constructorJoinpoint.setParameters(parameters);
            instantiateJoinpoint = constructorJoinpoint;
        }

        return instantiateJoinpoint.dispatch();
    }

    /**
     * Configure bean.
     *
     * @param beanConfig the bean metadata config, must not be null
     * @param beanInfo the bean info, can be null if enough info
     * @param module the current CL module, must not be null
     * @param bean the bean instance
     * @param nullify do we nullify property
     * @throws Throwable for any error
     */
    public static void configure(BeanMetaDataConfig beanConfig, BeanInfo beanInfo, Module module, Object bean, boolean nullify) throws Throwable {
        Set<PropertyConfig> properties = beanConfig.getProperties();
        if (properties != null) {
            List<PropertyConfig> used = new ArrayList<PropertyConfig>();
            for (PropertyConfig pc : properties) {
                try {
                    configure(beanInfo, module, bean, pc, nullify);
                    used.add(pc);
                } catch (Throwable t) {
                    if (nullify == false) {
                        for (PropertyConfig upc : used) {
                            try {
                                configure(beanInfo, module, bean,upc, true);
                            } catch (Throwable ignored) {
                            }
                        }
                        throw new StartException(t);
                    }
                }
            }
        }
    }

    /**
     * Dispatch lifecycle joinpoint.
     *
     * @param beanInfo the bean info
     * @param bean the bean instance
     * @param config the lifecycle config
     * @param defaultMethod the default method
     * @throws Throwable for any error
     */
    public static void dispatchLifecycleJoinpoint(BeanInfo beanInfo, Object bean, LifecycleConfig config, String defaultMethod) throws Throwable {
        if (config != null && config.isIgnored())
            return;

        Joinpoint joinpoint = createJoinpoint(beanInfo, bean, config, defaultMethod);
        if (joinpoint != null)
            joinpoint.dispatch();
    }

    private static Joinpoint createJoinpoint(BeanInfo beanInfo, Object bean, LifecycleConfig config, String defaultMethod) {
        Method method;
        ValueConfig[] params = null;
        if (config == null) {
            try {
                method = beanInfo.getMethod(defaultMethod);
            } catch (Exception t) {
                PojoLogger.ROOT_LOGGER.tracef(t, "Ignoring default %s invocation.", defaultMethod);
                return null;
            }
        } else {
            String methodName = config.getMethodName();
            if (methodName == null) {
                methodName = defaultMethod;
            }
            ValueConfig[] parameters = config.getParameters();
            String[] types = Configurator.getTypes(parameters);
            method = beanInfo.findMethod(methodName, types);
            params = parameters;
        }
        MethodJoinpoint joinpoint = new MethodJoinpoint(method);
        joinpoint.setTarget(() -> bean);
        joinpoint.setParameters(params);
        return joinpoint;
    }

    private static void configure(BeanInfo beanInfo, Module module, Object bean, PropertyConfig pc, boolean nullify) throws Throwable {
        ValueConfig value = pc.getValue();
        Class<?> clazz = null;
        String type = pc.getType(); // check property
        if (type == null)
            type = value.getType(); // check value
        if (type != null)
            clazz = module.getClassLoader().loadClass(type);

        Method setter = beanInfo.getSetter(pc.getPropertyName(), clazz);
        MethodJoinpoint joinpoint = new MethodJoinpoint(setter);
        ValueConfig param = (nullify == false) ? value : null;
        joinpoint.setParameters(new ValueConfig[]{param});
        joinpoint.setTarget(() -> bean);
        joinpoint.dispatch();
    }

}
