/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.openejb.core.singleton;

import org.apache.openejb.ApplicationException;
import org.apache.openejb.BeanContext;
import org.apache.openejb.BeanType;
import org.apache.openejb.OpenEJBException;
import org.apache.openejb.cdi.CdiEjbBean;
import org.apache.openejb.core.InstanceContext;
import org.apache.openejb.core.Operation;
import org.apache.openejb.core.ThreadContext;
import org.apache.openejb.core.interceptor.InterceptorData;
import org.apache.openejb.core.interceptor.InterceptorStack;
import org.apache.openejb.core.timer.TimerServiceWrapper;
import org.apache.openejb.core.transaction.EjbTransactionUtil;
import org.apache.openejb.core.transaction.TransactionPolicy;
import org.apache.openejb.core.transaction.TransactionType;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.monitoring.LocalMBeanServer;
import org.apache.openejb.monitoring.ManagedMBean;
import org.apache.openejb.monitoring.ObjectNameBuilder;
import org.apache.openejb.monitoring.StatsInterceptor;
import org.apache.openejb.spi.ContainerSystem;
import org.apache.openejb.spi.SecurityService;
import org.apache.openejb.util.LogCategory;
import org.apache.openejb.util.Logger;

import javax.ejb.EJBContext;
import javax.ejb.NoSuchEJBException;
import javax.ejb.SessionBean;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.xml.ws.WebServiceContext;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class SingletonInstanceManager {
    private static final Logger logger = Logger.getInstance(LogCategory.OPENEJB, "org.apache.openejb.util.resources");

    private final SecurityService securityService;
    private final SingletonContext sessionContext;
    private final WebServiceContext webServiceContext;

    public SingletonInstanceManager(final SecurityService securityService) {
        this.securityService = securityService;
        sessionContext = new SingletonContext(this.securityService);
        webServiceContext = new EjbWsContext(sessionContext);
    }

    protected void start(final BeanContext beanContext) throws OpenEJBException {
        if (beanContext.isLoadOnStartup()) {
            initialize(beanContext);
        }
    }

    private void initialize(final BeanContext beanContext) throws OpenEJBException {
        try {
            final ThreadContext callContext = new ThreadContext(beanContext, null);
            final ThreadContext old = ThreadContext.enter(callContext);
            try {
                getInstance(callContext);
            } finally {
                ThreadContext.exit(old);
            }
        } catch (final OpenEJBException e) {
            throw new OpenEJBException("Singleton startup failed: " + beanContext.getDeploymentID(), e);
        }
    }

    public Instance getInstance(final ThreadContext callContext) throws OpenEJBException {
        final BeanContext beanContext = callContext.getBeanContext();
        final Data data = (Data) beanContext.getContainerData();
        final AtomicReference<Future<Instance>> singleton = data.singleton;
        try {
            // Has the singleton been created yet?
            // If there is a Future object in the AtomicReference, then
            // it's either been created or is being created now.
            Future<Instance> singletonFuture = singleton.get();
            if (singletonFuture != null) {
                return singletonFuture.get();
            }

            // The singleton has not been created nor is being created
            // We will construct this FutureTask and compete with the
            // other threads for the right to create the singleton
            final FutureTask<Instance> task = new FutureTask<Instance>(new Callable<Instance>() {
                public Instance call() throws Exception {
                    return createInstance(callContext, beanContext);
                }
            });

            do {
                // If our FutureTask was the one to win the slot
                // than we are the ones responsible for creating
                // the singleton while the others wait.
                if (singleton.compareAndSet(null, task)) {
                    task.run();
                }

                // If we didn't win the slot and no other FutureTask
                // has been set by a different thread, than we need
                // to try again.
            } while ((singletonFuture = singleton.get()) == null);


            // At this point we can safely return the singleton
            return singletonFuture.get();

        } catch (final InterruptedException e) {
            Thread.interrupted();
            throw new ApplicationException(new NoSuchEJBException("Singleton initialization interrupted").initCause(e));
        } catch (final ExecutionException e) {
            final Throwable throwable = e.getCause();
            if (throwable instanceof ApplicationException) {
                throw (ApplicationException) throwable;
            }

            throw new ApplicationException(new NoSuchEJBException("Singleton initialization failed").initCause(e.getCause()));
        }
    }

    private void initializeDependencies(final BeanContext beanContext) throws OpenEJBException {
        final SystemInstance systemInstance = SystemInstance.get();
        final ContainerSystem containerSystem = systemInstance.getComponent(ContainerSystem.class);
        for (final String dependencyId : beanContext.getDependsOn()) {
            final BeanContext dependencyContext = containerSystem.getBeanContext(dependencyId);
            if (dependencyContext == null) {
                throw new OpenEJBException("Deployment does not exist. Deployment(id='" + dependencyContext + "')");
            }

            final Object containerData = dependencyContext.getContainerData();

            // Bean may not be a singleton or may be a singleton
            // managed by a different container implementation
            if (containerData instanceof Data) {
                final Data data = (Data) containerData;

                data.initialize();
            }
        }
    }

    private Instance createInstance(final ThreadContext callContext, final BeanContext beanContext) throws ApplicationException {
        try {
            initializeDependencies(beanContext);

            final InstanceContext context = beanContext.newInstance();

            if (context.getBean() instanceof SessionBean) {

                final Operation originalOperation = callContext.getCurrentOperation();
                try {
                    callContext.setCurrentOperation(Operation.CREATE);
                    final Method create = beanContext.getCreateMethod();
                    final InterceptorStack ejbCreate = new InterceptorStack(context.getBean(), create, Operation.CREATE, new ArrayList<InterceptorData>(), new HashMap());
                    ejbCreate.invoke();
                } finally {
                    callContext.setCurrentOperation(originalOperation);
                }
            }

            final ReadWriteLock lock;
            if (beanContext.isBeanManagedConcurrency()) {
                // Bean-Managed Concurrency
                lock = new BeanManagedLock();
            } else {
                // Container-Managed Concurrency
                lock = new ReentrantReadWriteLock();
            }

            return new Instance(context.getBean(), context.getInterceptors(), context.getCreationalContext(), lock);
        } catch (Throwable e) {
            if (e instanceof InvocationTargetException) {
                e = ((InvocationTargetException) e).getTargetException();
            }
            final String t = "The bean instance " + beanContext.getDeploymentID() + " threw a system exception:" + e;
            logger.error(t, e);
            throw new ApplicationException(new NoSuchEJBException("Singleton failed to initialize").initCause(e));
        }
    }

    public void freeInstance(final ThreadContext callContext) {
        final BeanContext beanContext = callContext.getBeanContext();
        final Data data = (Data) beanContext.getContainerData();
        final Future<Instance> instanceFuture = data.singleton.get();

        // Possible the instance was never created
        if (instanceFuture == null) {
            return;
        }

        final Instance instance;
        try {
            instance = instanceFuture.get();
        } catch (final InterruptedException e) {
            Thread.interrupted();
            logger.error("Singleton shutdown failed because the thread was interrupted: " + beanContext.getDeploymentID(), e);
            return;
        } catch (final ExecutionException e) {
            // Instance was never initialized
            return;
        }

        try {
            callContext.setCurrentOperation(Operation.PRE_DESTROY);
            callContext.setCurrentAllowedStates(null);

            final Method remove = instance.bean instanceof SessionBean ? beanContext.getCreateMethod() : null;

            final List<InterceptorData> callbackInterceptors = beanContext.getCallbackInterceptors();
            final InterceptorStack interceptorStack = new InterceptorStack(instance.bean, remove, Operation.PRE_DESTROY, callbackInterceptors, instance.interceptors);

            //Transaction Demarcation for Singleton PostConstruct method
            TransactionType transactionType;

            if (beanContext.getComponentType() == BeanType.SINGLETON) {
                final Set<Method> callbacks = callbackInterceptors.get(callbackInterceptors.size() - 1).getPreDestroy();
                if (callbacks.isEmpty()) {
                    transactionType = TransactionType.RequiresNew;
                } else {
                    transactionType = beanContext.getTransactionType(callbacks.iterator().next());
                    if (transactionType == TransactionType.Required) {
                        transactionType = TransactionType.RequiresNew;
                    }
                }
            } else {
                transactionType = beanContext.isBeanManagedTransaction() ? TransactionType.BeanManaged : TransactionType.NotSupported;
            }
            final TransactionPolicy transactionPolicy = EjbTransactionUtil.createTransactionPolicy(transactionType, callContext);
            try {
                //Call the chain
                final CdiEjbBean<Object> bean = beanContext.get(CdiEjbBean.class);
                if (bean != null) { // TODO: see if it should be called before or after next call
                    bean.getInjectionTarget().preDestroy(instance.bean);
                }

                interceptorStack.invoke();
                if (instance.creationalContext != null) {
                    instance.creationalContext.release();
                }
            } catch (final Throwable e) {
                //RollBack Transaction
                EjbTransactionUtil.handleSystemException(transactionPolicy, e, callContext);
            } finally {
                EjbTransactionUtil.afterInvoke(transactionPolicy, callContext);
            }

        } catch (final Throwable re) {
            logger.error("Singleton shutdown failed: " + beanContext.getDeploymentID(), re);
        }
    }

    /**
     * This method has no work to do as all instances are removed from
     * the pool on getInstance(...) and not returned via poolInstance(...)
     * if they threw a system exception.
     *
     * @param callContext
     * @param bean
     */
    public void discardInstance(final ThreadContext callContext, final Object bean) {

    }

    public void deploy(final BeanContext beanContext) throws OpenEJBException {
        final Data data = new Data(beanContext);
        beanContext.setContainerData(data);

        beanContext.set(EJBContext.class, this.sessionContext);

        // Create stats interceptor
        if (StatsInterceptor.isStatsActivated()) {
            final StatsInterceptor stats = new StatsInterceptor(beanContext.getBeanClass());
            beanContext.addFirstSystemInterceptor(stats);

            final ObjectNameBuilder jmxName = new ObjectNameBuilder("openejb.management");
            jmxName.set("J2EEServer", "openejb");
            jmxName.set("J2EEApplication", null);
            jmxName.set("EJBModule", beanContext.getModuleID());
            jmxName.set("SingletonSessionBean", beanContext.getEjbName());
            jmxName.set("name", beanContext.getEjbName());
            jmxName.set("j2eeType", "Invocations");

            // register the invocation stats interceptor
            final MBeanServer server = LocalMBeanServer.get();
            try {
                final ObjectName objectName = jmxName.build();
                if (server.isRegistered(objectName)) {
                    server.unregisterMBean(objectName);
                }
                server.registerMBean(new ManagedMBean(stats), objectName);
                data.add(objectName);
            } catch (final Exception e) {
                logger.error("Unable to register MBean ", e);
            }
        }

        try {
            final Context context = beanContext.getJndiEnc();
            context.bind("comp/EJBContext", sessionContext);
            context.bind("comp/WebServiceContext", webServiceContext);
            context.bind("comp/TimerService", new TimerServiceWrapper());
        } catch (final NamingException e) {
            throw new OpenEJBException("Failed to bind EJBContext/WebServiceContext/TimerService", e);
        }
    }

    public void undeploy(final BeanContext beanContext) {
        final Data data = (Data) beanContext.getContainerData();
        if (data == null) {
            return;
        }

        final MBeanServer server = LocalMBeanServer.get();
        for (final ObjectName objectName : data.jmxNames) {
            try {
                server.unregisterMBean(objectName);
            } catch (final Exception e) {
                logger.error("Unable to unregister MBean " + objectName);
            }
        }

        beanContext.setContainerData(null);
    }

    private final class Data {
        private final AtomicReference<Future<Instance>> singleton = new AtomicReference<Future<Instance>>();
        private final List<ObjectName> jmxNames = new ArrayList<ObjectName>();
        private final BeanContext info;

        public Data(final BeanContext info) {
            this.info = info;
        }

        public ObjectName add(final ObjectName name) {
            jmxNames.add(name);
            return name;
        }


        public void initialize() throws OpenEJBException {
            SingletonInstanceManager.this.initialize(info);
        }
    }


    private static class BeanManagedLock implements ReadWriteLock {
        private final Lock lock = new Lock() {
            public void lock() {
            }

            public void lockInterruptibly() {
            }

            public Condition newCondition() {
                throw new UnsupportedOperationException("newCondition()");
            }

            public boolean tryLock() {
                return true;
            }

            public boolean tryLock(final long time, final TimeUnit unit) {
                return true;
            }

            public void unlock() {
            }
        };

        public Lock readLock() {
            return lock;
        }

        public Lock writeLock() {
            return lock;
        }
    }
}
