/** 
 * 
 * Copyright 2004 Protique Ltd
 * 
 * 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. 
 * 
 **/
package org.codehaus.activemq.service.impl;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.activemq.broker.Broker;
import org.codehaus.activemq.message.ActiveMQXid;
import org.codehaus.activemq.service.Transaction;
import org.codehaus.activemq.store.PreparedTransactionStore;
import org.codehaus.activemq.util.SerializationHelper;

import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Map;

/**
 * @version $Revision: 1.3 $
 */
public class XATransactionCommand extends AbstractTransaction {
    private static final Log log = LogFactory.getLog(TransactionManagerImpl.class);

    private ActiveMQXid xid;
    private transient Map xaTxs;
    private transient PreparedTransactionStore preparedTransactions;

    /**
     * This constructor is only used when deserializing from disk
     */
    public XATransactionCommand() {
        super(null);
    }

    public XATransactionCommand(Broker broker, ActiveMQXid xid, Map xaTxs, PreparedTransactionStore preparedTransactions) {
        super(broker);
        this.xid = xid;
        this.xaTxs = xaTxs;
        this.preparedTransactions = preparedTransactions;
    }


    /**
     * Called after the transaction command has been recovered from disk
     *
     * @param xaTxs
     * @param preparedTransactions
     */
    public void initialise(Map xaTxs, PreparedTransactionStore preparedTransactions) {
        this.xaTxs = xaTxs;
        this.preparedTransactions = preparedTransactions;
    }

    public void commit(boolean onePhase) throws XAException {
    	if(log.isDebugEnabled())
    		log.debug("XA Transaction commit: "+xid);

        switch (getState()) {
            case START_STATE:
                // 1 phase commit, no work done.
                checkForPreparedState(onePhase);
                setStateFinished();
                break;
            case IN_USE_STATE:
                // 1 phase commit, work done.
                checkForPreparedState(onePhase);
                doPrePrepare();
                setStateFinished();
                doPostCommit();
                break;
            case PREPARED_STATE:
                // 2 phase commit, work done.
                // We would record commit here.
                setStateFinished();
                doPostCommit();
                break;
            default:
                illegalStateTransition("commit");
        }
    }

    private void illegalStateTransition(String callName) throws XAException {
        XAException xae = new XAException("Cannot call " + callName + " now.");
        xae.errorCode = XAException.XAER_PROTO;
        throw xae;
    }

    private void checkForPreparedState(boolean onePhase) throws XAException {
        if (!onePhase) {
            XAException xae = new XAException("Cannot do 2 phase commit if the transaction has not been prepared.");
            xae.errorCode = XAException.XAER_PROTO;
            throw xae;
        }
    }

    private void doPrePrepare() throws XAException {
        try {
            prePrepare();
        } catch (XAException e) {
            throw e;
        } catch (Throwable e) {
            log.warn("PRE-PREPARE FAILED: ", e);
            rollback();
            XAException xae = new XAException("PRE-PREPARE FAILED: Transaction rolled back.");
            xae.errorCode = XAException.XA_RBOTHER;
            xae.initCause(e);
            throw xae;
        }
    }

    private void doPostCommit() throws XAException {
        try {
            postCommit();
        }
        catch (Throwable e) {
            // I guess this could happen.  Post commit task failed
            // to execute properly.
            log.warn("POST COMMIT FAILED: ", e);
            XAException xae = new XAException("POST COMMIT FAILED");
            xae.errorCode = XAException.XAER_RMERR;
            xae.initCause(e);
            throw xae;
        }
    }

    public void rollback() throws XAException {
    	
    	if(log.isDebugEnabled())
    		log.debug("XA Transaction rollback: "+xid);

        switch (getState()) {
            case START_STATE:
                // 1 phase rollback no work done.
                setStateFinished();
                break;
            case IN_USE_STATE:
                // 1 phase rollback work done.
                setStateFinished();
                doPostRollback();
                break;
            case PREPARED_STATE:
                // 2 phase rollback work done.
                preparedTransactions.remove(xid);
                setStateFinished();
                doPostRollback();
                break;
        }

    }

    private void doPostRollback() throws XAException {
        try {
            postRollback();
        }
        catch (Throwable e) {
            // I guess this could happen.  Post commit task failed
            // to execute properly.
            log.warn("POST ROLLBACK FAILED: ", e);
            XAException xae = new XAException("POST ROLLBACK FAILED");
            xae.errorCode = XAException.XAER_RMERR;
            xae.initCause(e);
            throw xae;
        }
    }

    public int prepare() throws XAException {
    	if(log.isDebugEnabled())
    		log.debug("XA Transaction prepare: "+xid);
    	
        switch (getState()) {
            case START_STATE:
                // No work done.. no commit/rollback needed.
                setStateFinished();
                return XAResource.XA_RDONLY;
            case IN_USE_STATE:
                // We would record prepare here.
                doPrePrepare();
                setState(AbstractTransaction.PREPARED_STATE);
                preparedTransactions.put(xid, this);
                return XAResource.XA_OK;
            default :
                illegalStateTransition("prepare");
                return XAResource.XA_RDONLY;
        }
    }

    private void setStateFinished() {
        setState(AbstractTransaction.FINISHED_STATE);
        xaTxs.remove(xid);
    }
    
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        super.readExternal(in);
        xid = ActiveMQXid.read(in);
    }

    public void writeExternal(ObjectOutput out) throws IOException {
        super.writeExternal(out);
        xid.writeExternal(out);
    }

    public static Transaction fromBytes(byte[] data) throws IOException, ClassNotFoundException {
        return (Transaction) SerializationHelper.deserialize(data);
    }

    public byte[] toBytes() throws IOException {
        return SerializationHelper.serialize(this);
    }
}