/*
 * 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;

import org.apache.openejb.BeanContext;
import org.apache.openejb.OpenEJBException;
import org.apache.openejb.core.ivm.IntraVmArtifact;
import org.apache.openejb.core.timer.EjbTimerService;
import org.apache.openejb.core.timer.TimerServiceImpl;
import org.apache.openejb.core.timer.Timers;
import org.apache.openejb.core.transaction.EjbUserTransaction;
import org.apache.openejb.core.transaction.TransactionPolicy;
import org.apache.openejb.core.transaction.TransactionType;
import org.apache.openejb.spi.SecurityService;
import org.apache.openejb.threads.task.CUTask;

import javax.ejb.EJBContext;
import javax.ejb.EJBException;
import javax.ejb.EJBHome;
import javax.ejb.EJBLocalHome;
import javax.ejb.Timer;
import javax.ejb.TimerService;
import javax.interceptor.InvocationContext;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.security.Identity;
import java.security.Principal;
import java.util.Collection;
import java.util.Map;
import java.util.Properties;


/**
 * @version $Rev$ $Date$
 */
public abstract class BaseContext implements EJBContext, Serializable {

    public static enum Call {
        getEJBObject, getEJBLocalObject, isCallerInRole, setRollbackOnly, getCallerPrincipal, getRollbackOnly, getTimerService, getUserTransaction, getBusinessObject, timerMethod, getInvokedBusinessInterface, UserTransactionMethod, getMessageContext, getPrimaryKey, getContextData

    }

    protected final SecurityService securityService;
    protected final UserTransaction userTransaction;

    protected BaseContext(final SecurityService securityService) {
        this(securityService, new EjbUserTransaction());
    }

    protected BaseContext(final SecurityService securityService, final UserTransaction userTransaction) {
        this.securityService = securityService;
        this.userTransaction = new UserTransactionWrapper(userTransaction);
    }

    private boolean isAsyncOperation(final ThreadContext threadContext) {
        if (threadContext.getCurrentOperation() == null
            && threadContext.get(CUTask.Context.class) != null) {
            return true;
        }
        return false;
    }

    protected abstract void check(ThreadContext context, Call call);

    protected IllegalStateException illegal(final Call call, final Operation operation) {
        return new IllegalStateException(call + " cannot be called in " + operation);
    }

    public Map<String, Object> getContextData() {
        doCheck(Call.getContextData);
        return ThreadContext.getThreadContext().get(InvocationContext.class).getContextData();
    }

    public void doCheck(final Call call) {
        final ThreadContext context = ThreadContext.getThreadContext();
        if (!isAsyncOperation(context)) {
            check(context, call);
        }
    }

    public EJBHome getEJBHome() {
        final ThreadContext threadContext = ThreadContext.getThreadContext();
        final BeanContext di = threadContext.getBeanContext();

        return di.getEJBHome();
    }

    public EJBLocalHome getEJBLocalHome() {
        final ThreadContext threadContext = ThreadContext.getThreadContext();
        final BeanContext di = threadContext.getBeanContext();

        return di.getEJBLocalHome();
    }

    public Principal getCallerPrincipal() {
        doCheck(Call.getCallerPrincipal);
        Principal callerPrincipal = getCallerPrincipal(securityService);
        if (callerPrincipal == null) {
            callerPrincipal = UnauthenticatedPrincipal.INSTANCE;
        }
        return callerPrincipal;
    }

    protected Principal getCallerPrincipal(final SecurityService securityService) {
        return securityService.getCallerPrincipal();
    }

    @Override
    public boolean isCallerInRole(final String s) {
        doCheck(Call.isCallerInRole);
        return isCallerInRole(securityService, s);
    }

    protected boolean isCallerInRole(final SecurityService securityService, final String roleName) {
        doCheck(Call.isCallerInRole);

        final ThreadContext threadContext = ThreadContext.getThreadContext();
        final BeanContext di = threadContext.getBeanContext();
        final String roleLink = di.getSecurityRoleReference(roleName);

        return securityService.isCallerInRole(roleLink);
    }

    @Override
    public UserTransaction getUserTransaction() throws IllegalStateException {
        doCheck(Call.getUserTransaction);
        return getUserTransaction(userTransaction);
    }

    public UserTransaction getUserTransaction(final UserTransaction userTransaction) throws IllegalStateException {

        final ThreadContext threadContext = ThreadContext.getThreadContext();
        final BeanContext di = threadContext.getBeanContext();

        if (di.isBeanManagedTransaction()) {
            return userTransaction;
        } else {
            throw new IllegalStateException("container-managed transaction beans can not access the UserTransaction");
        }
    }

    public void setRollbackOnly() throws IllegalStateException {
        doCheck(Call.setRollbackOnly);
        final ThreadContext threadContext = ThreadContext.getThreadContext();
        final BeanContext di = threadContext.getBeanContext();

        if (di.isBeanManagedTransaction()) {
            throw new IllegalStateException("bean-managed transaction beans can not access the setRollbackOnly() method");
        }

        final TransactionPolicy txPolicy = threadContext.getTransactionPolicy();
        if (txPolicy == null) {
            throw new IllegalStateException("ThreadContext does not contain a TransactionEnvironment");
        }
        if (txPolicy.getTransactionType() == TransactionType.Never
            || txPolicy.getTransactionType() == TransactionType.NotSupported
            || txPolicy.getTransactionType() == TransactionType.Supports) {
            throw new IllegalStateException("setRollbackOnly accessible only from MANDATORY, REQUIRED, or REQUIRES_NEW");
        }
        txPolicy.setRollbackOnly();
    }

    public boolean getRollbackOnly() throws IllegalStateException {
        doCheck(Call.getRollbackOnly);
        final ThreadContext threadContext = ThreadContext.getThreadContext();
        final BeanContext di = threadContext.getBeanContext();

        if (di.isBeanManagedTransaction()) {
            throw new IllegalStateException("bean-managed transaction beans can not access the getRollbackOnly() method: deploymentId=" + di.getDeploymentID());
        }

        final TransactionPolicy txPolicy = threadContext.getTransactionPolicy();
        if (txPolicy == null) {
            throw new IllegalStateException("ThreadContext does not contain a TransactionEnvironment");
        }
        if (txPolicy.getTransactionType() == TransactionType.Never
            || txPolicy.getTransactionType() == TransactionType.NotSupported
            || txPolicy.getTransactionType() == TransactionType.Supports) {
            throw new IllegalStateException("getRollbackOnly accessible only from MANDATORY, REQUIRED, or REQUIRES_NEW");
        }
        return txPolicy.isRollbackOnly();
    }

    public TimerService getTimerService() throws IllegalStateException {
        doCheck(Call.getTimerService);

        final ThreadContext threadContext = ThreadContext.getThreadContext();
        final BeanContext beanContext = threadContext.getBeanContext();
        final EjbTimerService timerService = beanContext.getEjbTimerService();
        if (timerService == null) {
            throw new IllegalStateException("This ejb does not support timers " + beanContext.getDeploymentID());
        } else if (!timerService.isStarted()) {
            try {
                timerService.start();
            } catch (final OpenEJBException e) {
                throw new IllegalStateException(e);
            }
        }
        return new TimerServiceImpl(timerService, threadContext.getPrimaryKey(), beanContext.getEjbTimeout()) {
            @Override
            public Collection<Timer> getAllTimers() throws IllegalStateException, EJBException {
                return Timers.all(); // allowed here
            }
        };
    }

    public boolean isTimerMethodAllowed() {
        return true;
    }

    public boolean isUserTransactionAccessAllowed() {
        final ThreadContext threadContext = ThreadContext.getThreadContext();
        final BeanContext di = threadContext.getBeanContext();

        doCheck(Call.UserTransactionMethod);
        return di.isBeanManagedTransaction();
    }


    public final Properties getEnvironment() {
        throw new UnsupportedOperationException();
    }

    public final Identity getCallerIdentity() {
        throw new UnsupportedOperationException();
    }

    public final boolean isCallerInRole(final Identity identity) {
        throw new UnsupportedOperationException();
    }

    public Object lookup(final String name) {
        final ThreadContext threadContext = ThreadContext.getThreadContext();
        final BeanContext beanContext = threadContext.getBeanContext();
        Context jndiEnc = beanContext.getJndiEnc();
        try {
            jndiEnc = (Context) jndiEnc.lookup("comp/env");
            return jndiEnc.lookup(name);
        } catch (final NamingException | RuntimeException e) {
            throw new IllegalArgumentException(e);
        }
    }

    public class UserTransactionWrapper implements UserTransaction {
        private final UserTransaction userTransaction;

        public UserTransactionWrapper(final UserTransaction userTransaction) {
            this.userTransaction = userTransaction;
        }

        public void begin() throws NotSupportedException, SystemException {
            if (!isUserTransactionAccessAllowed()) {
                throw new IllegalStateException();
            }
            userTransaction.begin();
        }

        public void commit() throws HeuristicMixedException, HeuristicRollbackException, IllegalStateException, RollbackException, SecurityException, SystemException {
            if (!isUserTransactionAccessAllowed()) {
                throw new IllegalStateException();
            }
            userTransaction.commit();
        }

        public int getStatus() throws SystemException {
            if (!isUserTransactionAccessAllowed()) {
                throw new IllegalStateException();
            }
            return userTransaction.getStatus();
        }

        public void rollback() throws IllegalStateException, SecurityException, SystemException {
            if (!isUserTransactionAccessAllowed()) {
                throw new IllegalStateException();
            }
            userTransaction.rollback();
        }

        public void setRollbackOnly() throws IllegalStateException, SystemException {
            if (!isUserTransactionAccessAllowed()) {
                throw new IllegalStateException();
            }
            userTransaction.setRollbackOnly();
        }

        public void setTransactionTimeout(final int i) throws SystemException {
            if (!isUserTransactionAccessAllowed()) {
                throw new IllegalStateException();
            }
            userTransaction.setTransactionTimeout(i);
        }
    }

    public static class State {
    }

    protected Object writeReplace() throws ObjectStreamException {
        return new IntraVmArtifact(this, true);
    }
}
