package org.csc.phynixx.xa;

/*
 * #%L
 * phynixx-xa
 * %%
 * Copyright (C) 2014 csc
 * %%
 * 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.
 * #L%
 */

import java.util.Arrays;

import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;

import org.apache.commons.lang.Validate;
import org.csc.phynixx.common.exceptions.DelegatedRuntimeException;
import org.csc.phynixx.common.exceptions.ExceptionUtils;
import org.csc.phynixx.common.logger.IPhynixxLogger;
import org.csc.phynixx.common.logger.PhynixxLogManager;
import org.csc.phynixx.connection.IPhynixxConnection;
import org.csc.phynixx.connection.IPhynixxManagedConnection;
import org.csc.phynixx.connection.IPhynixxManagedConnectionFactory;

/**
 * keeps the XAresource's association to the transactional branch(es) (given via
 * XID) or manages the local transaction
 *
 * @author Christoph Schmidt-Casdorff
 */
class PhynixxManagedXAConnection<C extends IPhynixxConnection> implements IPhynixxXAConnection<C> {

    private static final IPhynixxLogger LOG = PhynixxLogManager.getLogger(PhynixxManagedXAConnection.class);

    /**
     * parent XAResource
     */
    private final PhynixxXAResource<C> xaResource;

    private IPhynixxManagedConnectionFactory<C> managedConnectionFactory;

    private final ITransactionBinding<C> transactionBinding = new TransactionBinding<C>();

    private TransactionManager transactionManager = null;

    /**
     * Global repository for XA-Branches associated with XAResource of the
     * current Resource
     */
    private final IXATransactionalBranchRepository<C> xaTransactionalBranchDictionary;

    private boolean enlisted;

    PhynixxManagedXAConnection(PhynixxXAResource<C> xaResource, TransactionManager transactionManager,
            IXATransactionalBranchRepository<C> xaTransactionalBranchDictionary,
            IPhynixxManagedConnectionFactory<C> managedConnectionFactory) {
        this.xaResource = xaResource;
        this.xaTransactionalBranchDictionary = xaTransactionalBranchDictionary;
        this.managedConnectionFactory = managedConnectionFactory;
        this.transactionManager = transactionManager;
    }

    @Override
    public XAResource getXAResource() {
        return xaResource;
    }

    /**
     * Ermittelt, ob zu zugehoeriger Connection bereits ein TX exitiert. This
     * tranbsaction binding is reused
     */
    @Override
    public IPhynixxManagedConnection<C> getManagedConnection() {

        // Connection wid in TX eingetragen ....
        this.checkTransactionBinding();

        return transactionBinding.getManagedConnection();
    }

    @Override
    public C getConnection() {
        return this.getManagedConnection().toConnection();
    }

    /**
     * @return !=null if an if the XAConnection is bound to a global transaction
     */
    XATransactionalBranch<C> toGlobalTransactionBranch() {
        if (this.transactionBinding != null && this.transactionBinding.isGlobalTransaction()) {
            GlobalTransactionProxy<C> activeXid = this.transactionBinding.getEnlistedGlobalTransaction();
            if (activeXid == null) {
                throw new IllegalStateException("No active XABranch");
            }
            // suche XABranch
            return activeXid.getGlobalTransactionalBranch();
        }
        return null;
    }

    /**
     * @return !=null if an if the XAConnection is bound to a global transaction
     */
    LocalTransactionProxy<C> toLocalTransaction() {
        if (this.transactionBinding != null && this.transactionBinding.isLocalTransaction()) {
            LocalTransactionProxy<C> lid = this.transactionBinding.getEnlistedLocalTransaction();
            if (lid == null) {
                throw new IllegalStateException("No active LocalTransaction");
            }
            // suche XABranch
            return lid;
        }
        return null;
    }

    /**
     * call by the XAResource when
     * {@link javax.transaction.xa.XAResource#start(javax.transaction.xa.Xid, int)}
     * is called. A TransactionalBranch for the given XID ist expected to be
     * created.
     *
     * @param xid
     */
    void startTransactionalBranch(Xid xid) {

        cleanupTransactionBinding();

        final TransactionBindingType transactionBindingType = this.transactionBinding.getTransactionBindingType();

        // already associated to a local transaction
        if (transactionBindingType == TransactionBindingType.LocalTransaction) {
            LocalTransactionProxy<C> localTransactionProxy = this.toLocalTransaction();
            Validate.isTrue(!localTransactionProxy.hasTransactionalData(),
                    "Connection is associated to a local transaction and has uncommitted transactional data");
            if (localTransactionProxy.hasTransactionalData()) {
                throw new IllegalStateException(
                        "Connection is associated to a local transaction and has uncommitted transactional data");
            }

            XATransactionalBranch<C> xaTransactionalBranch = 
                    this.xaTransactionalBranchDictionary.findTransactionalBranch(xid);
            // xaTransactionalBranch!=null => joining a existing transactional branch
            if (xaTransactionalBranch == null) {
                IPhynixxManagedConnection<C> connection = localTransactionProxy.getConnection();
                if( connection==null || connection.isClosed()) {
                    connection= this.createPhysicalConnection();
                }
                xaTransactionalBranch = this.xaTransactionalBranchDictionary.instanciateTransactionalBranch(xid, connection);
            } else {
                localTransactionProxy.close();
            }
            // forget the Local transaction
            this.transactionBinding.release();

            XATransactionalBranch<C> transactionalBranch = this.xaTransactionalBranchDictionary
                    .findTransactionalBranch(xid);
            Validate.isTrue(xaTransactionalBranch != null, "Expect a transactional branch to be created");

            this.transactionBinding.activateGlobalTransaction(new GlobalTransactionProxy<C>(transactionalBranch));

            // no transaction binding
        } else if (transactionBindingType == TransactionBindingType.NoTransaction) {
            XATransactionalBranch<C> transactionalBranch = this.xaTransactionalBranchDictionary
                    .findTransactionalBranch(xid);

            // / xaTransactionalBranch!=null => joining a existing transactional
            // branch
            if (transactionalBranch == null) {
                IPhynixxManagedConnection<C> physicalConnection = this.createPhysicalConnection();
                transactionalBranch = this.xaTransactionalBranchDictionary.instanciateTransactionalBranch(xid,
                        physicalConnection);
                this.xaTransactionalBranchDictionary.instanciateTransactionalBranch(xid, physicalConnection);
            }
            this.transactionBinding.activateGlobalTransaction(new GlobalTransactionProxy<C>(transactionalBranch));

            // if bound to a global TX , check if same XID
        } else if (transactionBindingType == TransactionBindingType.GlobalTransaction) {

            XATransactionalBranch<C> transactionalBranch = this.xaTransactionalBranchDictionary
                    .findTransactionalBranch(xid);

            // xaTransactionalBranch!=null => joining a existing transactional
            // branch
            if (transactionalBranch == null) {
                IPhynixxManagedConnection<C> physicalConnection = this.createPhysicalConnection();
                transactionalBranch = this.xaTransactionalBranchDictionary.instanciateTransactionalBranch(xid,
                        physicalConnection);
                this.xaTransactionalBranchDictionary.instanciateTransactionalBranch(xid, physicalConnection);
            }

            // Check if previous XID are compatible to the current
            GlobalTransactionProxy<C> previousGlobalTransactionProxy = this.transactionBinding
                    .getEnlistedGlobalTransaction();
            if (previousGlobalTransactionProxy != null) {
                Xid previousXid = previousGlobalTransactionProxy.getXid();
                byte[] currentTransactionId = previousXid.getGlobalTransactionId();
                if (!Arrays.equals(xid.getGlobalTransactionId(), currentTransactionId)) {
                    LOG.warn("TransactionId of the new Transaction doenn't corresponds to the transactionId of the previous Ids");
                }
                previousGlobalTransactionProxy.release();
            }

            this.transactionBinding.activateGlobalTransaction(new GlobalTransactionProxy<C>(transactionalBranch));

        }

    }

    private void cleanupTransactionBinding() {
        // cleanup
        if (this.transactionBinding.getTransactionBindingType() == TransactionBindingType.LocalTransaction) {
            LocalTransactionProxy<C> localTransactionProxy = this.transactionBinding.getEnlistedLocalTransaction();
            if (localTransactionProxy != null) {
                if (localTransactionProxy.isClosed()) {
                    localTransactionProxy.release();
                }
            }
        }
    }

    private IPhynixxManagedConnection<C> createPhysicalConnection() {
        return this.managedConnectionFactory.getManagedConnection();
    }

    void resumeTransactionalBranch(Xid xid) {

        this.cleanupTransactionBinding();

        TransactionBindingType transactionBindingType = this.transactionBinding.getTransactionBindingType();
        if (transactionBindingType == TransactionBindingType.GlobalTransaction) {
            throw new IllegalStateException("XAConnection associated to a global transaction and can not be resumed");
        }
        XATransactionalBranch<C> transactionalBranch = findTransactionalBranch(xid);
        transactionalBranch.resume();

        // Check if previous XID are compatible to the current
        if (transactionBinding.isGlobalTransaction()) {
            GlobalTransactionProxy<C> previousGlobalTransactionProxy = this.transactionBinding.getEnlistedGlobalTransaction();
            LOG.warn("Resume meets an unrelease Transactional branch. It is released");
            previousGlobalTransactionProxy.close();
        }
        this.transactionBinding.activateGlobalTransaction(new GlobalTransactionProxy<C>(transactionalBranch));
    }

    private XATransactionalBranch<C> findTransactionalBranch(Xid xid) {
        XATransactionalBranch<C> transactionalBranch = this.xaTransactionalBranchDictionary
                .findTransactionalBranch(xid);
        if (transactionalBranch == null) {
            throw new IllegalStateException("No suspended branch for XID " + xid);
        }
        return transactionalBranch;
    }

    void suspendTransactionalBranch(Xid xid) {

        this.cleanupTransactionBinding();

        GlobalTransactionProxy<C> previousGlobalTransactionProxy = this.transactionBinding
                .getEnlistedGlobalTransaction();
        if (previousGlobalTransactionProxy == null) {
            LOG.warn("suspends meets not active Transactional branch.");
        } else {
            previousGlobalTransactionProxy.release();
        }

        XATransactionalBranch<C> transactionalBranch = findTransactionalBranch(xid);
        transactionalBranch.suspend();
        delistTransaction();

    }

    void joinTransactionalBranch(Xid xid) {

        cleanupTransactionBinding();

        TransactionBindingType transactionBindingType = this.transactionBinding.getTransactionBindingType();
        if (transactionBindingType == TransactionBindingType.GlobalTransaction) {
            throw new IllegalStateException(
                    "XAConnection already associated to a global transaction and can not be joined to XID " + xid);
        }

        this.transactionBinding.release();

    }

    /**
     * call by the XCAResource when
     * {@link javax.transaction.xa.XAResource#end(javax.transaction.xa.Xid, int)}
     * is called
     *
     * @param xid
     */
    void closeTransactionalBranch(Xid xid) {

        if (this.transactionBinding.getTransactionBindingType() != TransactionBindingType.GlobalTransaction) {
            throw new IllegalStateException("XAConnection not associated to a global transaction");
        }

        GlobalTransactionProxy<C> globalTransactionProxy = this.transactionBinding.getEnlistedGlobalTransaction();
        globalTransactionProxy.close();

        delistTransaction();
    }

    @Override
    public void close() {
       this.xaResource.close();
    }
    
    void doClose() {
        if(LOG.isDebugEnabled()) {
            LOG.debug("Closing "+this);
        }
        if (this.transactionBinding != null) {
            this.transactionBinding.close();
        }
        this.enlisted=false;
    }
    private void delistTransaction() {
        if (this.transactionBinding != null) {
            this.transactionBinding.release();
        }
        this.enlisted=false;
        this.transactionBinding.release();
    }

    boolean isInGlobalTransaction() {
        try {
            return transactionManager != null && this.transactionManager.getTransaction() != null;
        } catch (SystemException e) {
            throw new DelegatedRuntimeException(e);
        }
    }

    /**
     * if necessary the current xa resource is enlisted in the current TX.

     * The enlistment calls the
     * {@link javax.transaction.xa.XAResource#start(javax.transaction.xa.Xid, int)}
     * . This call associates the Xid with the current instance
     */
    private void enlistTransaction() {

        this.cleanupTransactionBinding();

        TransactionBindingType transactionBindingType = this.transactionBinding.getTransactionBindingType();

        // not associated to global transaction, try to associate a global
        // transaction
        if (this.isInGlobalTransaction() && transactionBindingType != TransactionBindingType.GlobalTransaction) {
            try {
                Transaction ntx = this.transactionManager.getTransaction();
                
                // Bitronix calls start on reaction of enlist --- check if cycle
                if (!enlisted && ntx != null) {
                    this.enlisted = true;
                    // enlisted makes startTransaactionalBranch calling
                    this.enlisted = ntx.enlistResource(this.xaResource);
                    if (!enlisted) {
                        LOG.error("Enlisting " + xaResource + " failed");
                    } else {

                    }
                } else {
                    LOG.debug("SampleXAConnection:connectionRequiresTransaction (no globalTransaction found)");
                }
            } catch (RollbackException n) {
                LOG.error("SampleXAConnection:prevokeAction enlistResource exception : " + n.toString());
            } catch (SystemException n) {
                LOG.error("SampleXAConnection:connectionRequiresTransaction " + n + "\n"
                        + ExceptionUtils.getStackTrace(n));
                throw new DelegatedRuntimeException(n);
            }
        } else if (transactionBindingType == TransactionBindingType.NoTransaction) {
            this.transactionBinding.activateLocalTransaction(new LocalTransactionProxy<C>(this.managedConnectionFactory
                    .getManagedConnection()));
        } else { // In Global Transaction and associated to a global transaction
                 // => nothing to do
            if (this.isInGlobalTransaction() && transactionBindingType == TransactionBindingType.GlobalTransaction) {
                // Not in Global Transaction and associated to a local
                // transaction => nothing to do
            } else if (!this.isInGlobalTransaction()
                    && transactionBindingType == TransactionBindingType.LocalTransaction) {
            }
        }

    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;

        PhynixxManagedXAConnection<C> that = (PhynixxManagedXAConnection) o;

        return xaResource.equals(that.xaResource);

    }

    @Override
    public int hashCode() {
        return xaResource.hashCode();
    }

    @Override
    public String toString() {
        return "PhynixxManagedXAConnection{" + "xaResource=" + xaResource + ", transactionBinding="
                + this.transactionBinding + '}';
    }

    void checkTransactionBinding() {

        if (this.isInGlobalTransaction()) {
            this.enlistTransaction();
        }
        if (this.transactionBinding == null
                || this.transactionBinding.getTransactionBindingType() == TransactionBindingType.NoTransaction) {
            this.transactionBinding.activateLocalTransaction(new LocalTransactionProxy<C>(this.createPhysicalConnection()) );
       }
    }

}
