/*
 * 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 java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import org.jboss.as.pojo.BeanState;
import org.jboss.as.pojo.logging.PojoLogger;
import org.jboss.as.pojo.descriptor.BeanMetaDataConfig;
import org.jboss.as.pojo.descriptor.CallbackConfig;
import org.jboss.as.pojo.descriptor.ConfigVisitor;
import org.jboss.as.pojo.descriptor.DefaultConfigVisitor;
import org.jboss.as.pojo.descriptor.InstallConfig;
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.Service;
import org.jboss.msc.service.ServiceBuilder;
import org.jboss.msc.service.ServiceName;
import org.jboss.msc.service.ServiceRegistry;
import org.jboss.msc.service.ServiceTarget;
import org.jboss.msc.service.StartContext;
import org.jboss.msc.service.StartException;
import org.jboss.msc.service.StopContext;
import org.jboss.msc.value.Value;
import org.wildfly.security.manager.WildFlySecurityManager;

/**
 * Abstract pojo phase; it handles install/uninstall
 *
 * @author <a href="mailto:ales.justin@jboss.org">Ales Justin</a>
 */
public abstract class AbstractPojoPhase implements Service<Object> {

    private Module module;
    private BeanMetaDataConfig beanConfig;
    private DeploymentReflectionIndex index;
    private BeanInfo beanInfo;
    private Object bean;

    protected abstract BeanState getLifecycleState();

    protected abstract AbstractPojoPhase createNextPhase();

    public void start(StartContext context) throws StartException {
        if (module != null) {
            final ClassLoader previous = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
            WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(module.getClassLoader());
            try {
                startInternal(context);
            } finally {
                WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(previous);
            }
        } else {
            startInternal(context);
        }
    }

    protected void startInternal(StartContext context) throws StartException {
        try {
            executeInstalls();

            // only after describe do we have a bean
            if (getLifecycleState().isAfter(BeanState.DESCRIBED)) {
                addCallbacks(true);
                addCallbacks(false);

                ServiceRegistry registry = context.getController().getServiceContainer();
                InstancesService.addInstance(registry, context.getChildTarget(), getLifecycleState(), getBean());
            }

            final AbstractPojoPhase nextPhase = createNextPhase(); // do we have a next phase
            if (nextPhase != null) {
                final BeanState state = getLifecycleState();
                final BeanState next = state.next();
                final BeanMetaDataConfig beanConfig = getBeanConfig();
                final ServiceName name = BeanMetaDataConfig.toBeanName(beanConfig.getName(), next);
                final ServiceTarget serviceTarget = context.getChildTarget();
                final ServiceBuilder serviceBuilder = serviceTarget.addService(name, nextPhase);
                registerAliases(serviceBuilder, next);
                final ConfigVisitor visitor = new DefaultConfigVisitor(serviceBuilder, state, getModule(), getIndex(), getBeanInfo());
                beanConfig.visit(visitor);
                nextPhase.setModule(getModule());
                nextPhase.setBeanConfig(getBeanConfig());
                nextPhase.setIndex(getIndex());
                nextPhase.setBeanInfo(getBeanInfo());
                nextPhase.setBean(getBean());
                serviceBuilder.install();
            }
        } catch (Throwable t) {
            throw new StartException(t);
        }
    }

    protected void registerAliases(ServiceBuilder serviceBuilder, BeanState next) {
        final Set<String> aliases = beanConfig.getAliases();
        if (aliases != null) {
            for (String alias : aliases) {
                ServiceName asn = BeanMetaDataConfig.toBeanName(alias, next);
                serviceBuilder.addAliases(asn);
            }
        }
    }

    @Override
    public Object getValue() throws IllegalStateException, IllegalArgumentException {
        return getBean();
    }

    public void stop(StopContext context) {
        if (module != null) {
            final ClassLoader previous = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
            WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(module.getClassLoader());
            try {
                stopInternal(context);
            } finally {
                WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(previous);
            }
        } else {
            stopInternal(context);
        }
    }

    protected void stopInternal(StopContext context) {
        if (getLifecycleState().isAfter(BeanState.DESCRIBED)) {
            InstancesService.removeInstance(context.getController().getServiceContainer(), getLifecycleState(), getBean());

            removeCallbacks(true);
            removeCallbacks(false);
        }
        executeUninstalls();
    }

    private List<Joinpoint> getInstalls() {
        List<InstallConfig> installs = getBeanConfig().getInstalls();
        return (installs != null) ? toJoinpoints(installs) : Collections.<Joinpoint>emptyList();
    }

    private List<Joinpoint> getUninstalls() {
        List<InstallConfig> uninstalls = getBeanConfig().getUninstalls();
        return (uninstalls != null) ? toJoinpoints(uninstalls) : Collections.<Joinpoint>emptyList();
    }

    private List<Joinpoint> toJoinpoints(List<InstallConfig> installs) {
        List<Joinpoint> joinpoints = new ArrayList<Joinpoint>();
        for (InstallConfig ic : installs) {
            if (ic.getWhenRequired() == getLifecycleState())
                joinpoints.add(createJoinpoint(ic));
        }
        return joinpoints;
    }

    protected Joinpoint createJoinpoint(InstallConfig config) {
        String methodName = config.getMethodName();
        if (methodName == null)
            throw PojoLogger.ROOT_LOGGER.nullMethodName();

        ValueConfig[] parameters = config.getParameters();
        String[] types = Configurator.getTypes(parameters);
        String dependency = config.getDependency();
        Value<Object> target = (dependency != null) ? config.getBean() : () -> getBean();
        BeanInfo beanInfo = (dependency != null) ? config.getBeanInfo().getValue() : getBeanInfo();
        Method method = beanInfo.findMethod(methodName, types);
        MethodJoinpoint joinpoint = new MethodJoinpoint(method);
        joinpoint.setTarget(target);
        joinpoint.setParameters(parameters);
        return joinpoint;
    }

    protected void executeInstalls() throws StartException {
        List<Joinpoint> installs = getInstalls();
        if (installs.isEmpty())
            return;

        int i = 0;
        try {
            for (i = 0; i < installs.size(); i++)
                installs.get(i).dispatch();
        } catch (Throwable t) {
            considerUninstalls(getUninstalls(), i);
            throw new StartException(t);
        }
    }

    /**
     * Consider the uninstalls.
     * <p/>
     * This method is here to be able to override
     * the behavior after installs failed.
     * e.g. perhaps only running uninstalls from the index.
     * <p/>
     * By default we run all uninstalls in the case
     * at least one install failed.
     *
     * @param uninstalls the uninstalls
     * @param index      current installs index
     */
    protected void considerUninstalls(List<Joinpoint> uninstalls, int index) {
        if (uninstalls == null)
            return;

        for (int j = Math.min(index, uninstalls.size() - 1); j >= 0; j--) {
            try {
                uninstalls.get(j).dispatch();
            } catch (Throwable t) {
                PojoLogger.ROOT_LOGGER.ignoreUninstallError(uninstalls.get(j), t);
            }
        }
    }

    protected void executeUninstalls() {
        considerUninstalls(getUninstalls(), Integer.MAX_VALUE);
    }

    protected void addCallbacks(boolean install) {
        List<CallbackConfig> configs = (install ? getBeanConfig().getIncallbacks() : getBeanConfig().getUncallbacks());
        if (configs != null) {
            for (CallbackConfig cc : configs) {
                if (cc.getWhenRequired() == getLifecycleState()) {
                    Callback callback = new Callback(getBeanInfo(), getBean(), cc);
                    if (install)
                        InstancesService.addIncallback(callback);
                    else
                        InstancesService.addUncallback(callback);
                }
            }
        }
    }

    protected void removeCallbacks(boolean install) {
        List<CallbackConfig> configs = (install ? getBeanConfig().getIncallbacks() : getBeanConfig().getUncallbacks());
        if (configs != null) {
            for (CallbackConfig cc : configs) {
                if (cc.getWhenRequired() == getLifecycleState()) {
                    Callback callback = new Callback(getBeanInfo(), getBean(), cc);
                    if (install)
                        InstancesService.removeIncallback(callback);
                    else
                        InstancesService.removeUncallback(callback);
                }
            }
        }
    }

    protected Module getModule() {
        return module;
    }

    protected void setModule(Module module) {
        this.module = module;
    }

    protected BeanMetaDataConfig getBeanConfig() {
        return beanConfig;
    }

    protected DeploymentReflectionIndex getIndex() {
        return index;
    }

    protected void setBeanConfig(BeanMetaDataConfig beanConfig) {
        this.beanConfig = beanConfig;
    }

    protected void setIndex(DeploymentReflectionIndex index) {
        this.index = index;
    }

    protected BeanInfo getBeanInfo() {
        return beanInfo;
    }

    protected void setBeanInfo(BeanInfo beanInfo) {
        this.beanInfo = beanInfo;
    }

    protected Object getBean() {
        return bean;
    }

    protected void setBean(Object bean) {
        this.bean = bean;
    }
}
