/*
 * Copyright 2004-2013 the Seasar Foundation and the Others.
 *
 * 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.seasar.extension.jta;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;

import org.seasar.extension.jta.xa.XidImpl;
import org.seasar.framework.exception.SIllegalStateException;
import org.seasar.framework.exception.SRollbackException;
import org.seasar.framework.exception.SSystemException;
import org.seasar.framework.log.Logger;
import org.seasar.framework.util.SLinkedList;

/**
 * {@link javax.transaction.Transaction}の実装クラスです。
 * 
 * @author higa
 */
public class TransactionImpl implements ExtendedTransaction,
        SynchronizationRegister {

    private static final int VOTE_READONLY = 0;

    private static final int VOTE_COMMIT = 1;

    private static final int VOTE_ROLLBACK = 2;

    private static Logger logger = Logger.getLogger(TransactionImpl.class);

    private Xid xid;

    private int status = Status.STATUS_NO_TRANSACTION;

    private List xaResourceWrappers = new ArrayList();

    private List synchronizations = new ArrayList();

    private List interposedSynchronizations = new ArrayList();

    private Map resourceMap = new HashMap();

    private boolean suspended = false;

    private int branchId = 0;

    /**
     * <code>TransactionImpl</code>のインスタンスを構築します。
     * 
     */
    public TransactionImpl() {
    }

    /**
     * トランザクションを開始します。
     * 
     */
    public void begin() throws NotSupportedException, SystemException {
        status = Status.STATUS_ACTIVE;
        init();
        if (logger.isDebugEnabled()) {
            logger.log("DSSR0003", new Object[] { this });
        }
    }

    /**
     * トランザクションを中断します。
     * 
     * @throws XAException
     *             <code>XAResource</code>を中断できなかった場合にスローされます
     */
    public void suspend() throws SystemException {
        assertNotSuspended();
        assertActiveOrMarkedRollback();
        for (int i = 0; i < getXAResourceWrapperSize(); ++i) {
            XAResourceWrapper xarw = getXAResourceWrapper(i);
            try {
                xarw.end(XAResource.TMSUSPEND);
            } catch (final XAException ex) {
                throw new SSystemException("ESSR0363", new Object[] { ex }, ex);
            }
        }
        suspended = true;
    }

    private void assertNotSuspended() throws IllegalStateException {
        if (suspended) {
            throw new SIllegalStateException("ESSR0314", null);
        }
    }

    private void assertActive() throws IllegalStateException {
        switch (status) {
        case Status.STATUS_ACTIVE:
            break;
        default:
            throwIllegalStateException();
        }
    }

    private void throwIllegalStateException() throws IllegalStateException {
        switch (status) {
        case Status.STATUS_PREPARING:
            throw new SIllegalStateException("ESSR0304", null);
        case Status.STATUS_PREPARED:
            throw new SIllegalStateException("ESSR0305", null);
        case Status.STATUS_COMMITTING:
            throw new SIllegalStateException("ESSR0306", null);
        case Status.STATUS_COMMITTED:
            throw new SIllegalStateException("ESSR0307", null);
        case Status.STATUS_MARKED_ROLLBACK:
            throw new SIllegalStateException("ESSR0308", null);
        case Status.STATUS_ROLLING_BACK:
            throw new SIllegalStateException("ESSR0309", null);
        case Status.STATUS_ROLLEDBACK:
            throw new SIllegalStateException("ESSR0310", null);
        case Status.STATUS_NO_TRANSACTION:
            throw new SIllegalStateException("ESSR0311", null);
        case Status.STATUS_UNKNOWN:
            throw new SIllegalStateException("ESSR0312", null);
        default:
            throw new SIllegalStateException("ESSR0032", new Object[] { String
                    .valueOf(status) });
        }
    }

    private int getXAResourceWrapperSize() {
        return xaResourceWrappers.size();
    }

    private XAResourceWrapper getXAResourceWrapper(int index) {
        return (XAResourceWrapper) xaResourceWrappers.get(index);
    }

    /**
     * トランザクションを再開します。
     * 
     * @throws XAException
     *             <code>XAResource</code>を再開できなかった場合にスローされます
     */
    public void resume() throws SystemException {
        assertSuspended();
        assertActiveOrMarkedRollback();
        for (int i = 0; i < getXAResourceWrapperSize(); ++i) {
            XAResourceWrapper xarw = getXAResourceWrapper(i);
            try {
                xarw.start(XAResource.TMRESUME);
            } catch (final XAException ex) {
                throw new SSystemException("ESSR0364", new Object[] { ex }, ex);
            }
        }
        suspended = false;
    }

    private void assertSuspended() throws IllegalStateException {
        if (!suspended) {
            throw new SIllegalStateException("ESSR0315", null);
        }
    }

    public void commit() throws RollbackException, HeuristicMixedException,
            HeuristicRollbackException, SecurityException,
            IllegalStateException, SystemException {

        try {
            assertNotSuspended();
            assertActive();
            beforeCompletion();
            if (status == Status.STATUS_ACTIVE) {
                endResources(XAResource.TMSUCCESS);
                if (getXAResourceWrapperSize() == 0) {
                    status = Status.STATUS_COMMITTED;
                } else if (getXAResourceWrapperSize() == 1) {
                    commitOnePhase();
                } else {
                    switch (prepareResources()) {
                    case VOTE_READONLY:
                        status = Status.STATUS_COMMITTED;
                        break;
                    case VOTE_COMMIT:
                        commitTwoPhase();
                        break;
                    case VOTE_ROLLBACK:
                        rollbackForVoteOK();
                    }
                }
                if (status == Status.STATUS_COMMITTED) {
                    if (logger.isDebugEnabled()) {
                        logger.log("DSSR0004", new Object[] { this });
                    }
                }
            }
            final boolean rolledBack = status != Status.STATUS_COMMITTED;
            afterCompletion();
            if (rolledBack) {
                throw new SRollbackException("ESSR0303",
                        new Object[] { toString() });
            }
        } finally {
            destroy();
        }
    }

    private void beforeCompletion() {
        for (int i = 0; i < getSynchronizationSize()
                && status == Status.STATUS_ACTIVE; ++i) {
            beforeCompletion(getSynchronization(i));
        }
        for (int i = 0; i < getInterposedSynchronizationSize()
                && status == Status.STATUS_ACTIVE; ++i) {
            beforeCompletion(getInterposedSynchronization(i));
        }
    }

    private void beforeCompletion(Synchronization sync) {
        try {
            sync.beforeCompletion();
        } catch (Throwable t) {
            logger.log(t);
            status = Status.STATUS_MARKED_ROLLBACK;
            endResources(XAResource.TMFAIL);
            rollbackResources();
        }
    }

    private void endResources(int flag) {
        for (int i = 0; i < getXAResourceWrapperSize(); ++i) {
            XAResourceWrapper xarw = getXAResourceWrapper(i);
            try {
                xarw.end(flag);
            } catch (Throwable t) {
                logger.log(t);
                status = Status.STATUS_MARKED_ROLLBACK;
            }
        }
    }

    private void commitOnePhase() {
        status = Status.STATUS_COMMITTING;
        XAResourceWrapper xari = getXAResourceWrapper(0);
        try {
            xari.commit(true);
            status = Status.STATUS_COMMITTED;
        } catch (Throwable t) {
            logger.log(t);
            status = Status.STATUS_UNKNOWN;
        }
    }

    private int prepareResources() {
        status = Status.STATUS_PREPARING;
        int vote = VOTE_READONLY;
        SLinkedList xarwList = new SLinkedList();
        for (int i = 0; i < getXAResourceWrapperSize(); ++i) {
            XAResourceWrapper xarw = getXAResourceWrapper(i);
            if (xarw.isCommitTarget()) {
                xarwList.addFirst(xarw);
            }
        }
        for (int i = 0; i < xarwList.size(); ++i) {
            XAResourceWrapper xarw = (XAResourceWrapper) xarwList.get(i);
            try {
                if (i == xarwList.size() - 1) {
                    // last resource commit optimization
                    xarw.commit(true);
                    xarw.setVoteOk(false);
                    vote = VOTE_COMMIT;
                } else if (xarw.prepare() == XAResource.XA_OK) {
                    vote = VOTE_COMMIT;
                } else {
                    xarw.setVoteOk(false);
                }
            } catch (Throwable t) {
                logger.log(t);
                xarw.setVoteOk(false);
                status = Status.STATUS_MARKED_ROLLBACK;
                return VOTE_ROLLBACK;
            }
        }
        if (status == Status.STATUS_PREPARING) {
            status = Status.STATUS_PREPARED;
        }
        return vote;
    }

    private void commitTwoPhase() {
        status = Status.STATUS_COMMITTING;
        for (int i = 0; i < getXAResourceWrapperSize(); ++i) {
            XAResourceWrapper xarw = getXAResourceWrapper(i);
            if (xarw.isCommitTarget() && xarw.isVoteOk()) {
                try {
                    xarw.commit(false);
                } catch (Throwable t) {
                    logger.log(t);
                    status = Status.STATUS_UNKNOWN;
                }
            }
        }
        if (status == Status.STATUS_COMMITTING) {
            status = Status.STATUS_COMMITTED;
        }
    }

    private void rollbackForVoteOK() {
        status = Status.STATUS_ROLLING_BACK;
        for (int i = 0; i < getXAResourceWrapperSize(); ++i) {
            XAResourceWrapper xarw = getXAResourceWrapper(i);
            if (xarw.isVoteOk()) {
                try {
                    xarw.rollback();
                } catch (Throwable t) {
                    logger.log(t);
                    status = Status.STATUS_UNKNOWN;
                }
            }
        }
        if (status == Status.STATUS_ROLLING_BACK) {
            status = Status.STATUS_ROLLEDBACK;
        }
    }

    private void afterCompletion() {
        final int status = this.status;
        this.status = Status.STATUS_NO_TRANSACTION;
        for (int i = 0; i < getInterposedSynchronizationSize(); ++i) {
            afterCompletion(status, getInterposedSynchronization(i));
        }
        for (int i = 0; i < getSynchronizationSize(); ++i) {
            afterCompletion(status, getSynchronization(i));
        }
    }

    private void afterCompletion(final int status, final Synchronization sync) {
        try {
            sync.afterCompletion(status);
        } catch (Throwable t) {
            logger.log(t);
        }
    }

    private int getSynchronizationSize() {
        return synchronizations.size();
    }

    private Synchronization getSynchronization(int index) {
        return (Synchronization) synchronizations.get(index);
    }

    private int getInterposedSynchronizationSize() {
        return interposedSynchronizations.size();
    }

    private Synchronization getInterposedSynchronization(int index) {
        return (Synchronization) interposedSynchronizations.get(index);
    }

    public void rollback() throws IllegalStateException, SecurityException,
            SystemException {

        try {
            assertNotSuspended();
            assertActiveOrMarkedRollback();
            endResources(XAResource.TMFAIL);
            rollbackResources();
            if (logger.isDebugEnabled()) {
                logger.log("DSSR0005", new Object[] { this });
            }
            afterCompletion();
        } finally {
            destroy();
        }
    }

    private void assertActiveOrMarkedRollback() throws IllegalStateException {
        switch (status) {
        case Status.STATUS_ACTIVE:
        case Status.STATUS_MARKED_ROLLBACK:
            break;
        default:
            throwIllegalStateException();
        }
    }

    private void rollbackResources() {
        status = Status.STATUS_ROLLING_BACK;
        for (int i = 0; i < getXAResourceWrapperSize(); ++i) {
            XAResourceWrapper xarw = getXAResourceWrapper(i);
            try {
                if (xarw.isCommitTarget()) {
                    xarw.rollback();
                }
            } catch (Throwable t) {
                logger.log(t);
                status = Status.STATUS_UNKNOWN;
            }
        }
        if (status == Status.STATUS_ROLLING_BACK) {
            status = Status.STATUS_ROLLEDBACK;
        }
    }

    public void setRollbackOnly() throws IllegalStateException, SystemException {

        assertNotSuspended();
        assertActiveOrPreparingOrPrepared();
        status = Status.STATUS_MARKED_ROLLBACK;
    }

    private void assertActiveOrPreparingOrPrepared()
            throws IllegalStateException {
        switch (status) {
        case Status.STATUS_ACTIVE:
        case Status.STATUS_PREPARING:
        case Status.STATUS_PREPARED:
            break;
        default:
            throwIllegalStateException();
        }
    }

    public boolean enlistResource(XAResource xaResource)
            throws RollbackException, IllegalStateException, SystemException {

        boolean oracled = xaResource.getClass().getName().startsWith("oracle");
        assertNotSuspended();
        assertActive();
        Xid xid = null;
        for (int i = 0; i < getXAResourceWrapperSize(); ++i) {
            XAResourceWrapper xarw = getXAResourceWrapper(i);
            if (xaResource.equals(xarw.getXAResource())) {
                return false;
            } else if (oracled) {
                continue;
            } else {
                try {
                    if (xaResource.isSameRM(xarw.getXAResource())) {
                        xid = xarw.getXid();
                        break;
                    }
                } catch (XAException ex) {
                    throw new IllegalStateException(ex.toString());
                }
            }
        }
        int flag = xid == null ? XAResource.TMNOFLAGS : XAResource.TMJOIN;
        boolean commitTarget = xid == null ? true : false;
        if (xid == null) {
            xid = createXidBranch();
        }
        try {
            xaResource.start(xid, flag);
            xaResourceWrappers.add(new XAResourceWrapper(xaResource, xid,
                    commitTarget));
            return true;
        } catch (XAException ex) {
            IllegalStateException ise = new IllegalStateException(ex.toString());
            ise.initCause(ex);
            throw ise;
        }
    }

    private Xid createXidBranch() {
        return new XidImpl(xid, ++branchId);
    }

    public boolean delistResource(XAResource xaResource, int flag)
            throws IllegalStateException, SystemException {

        assertNotSuspended();
        assertActiveOrMarkedRollback();
        for (int i = 0; i < getXAResourceWrapperSize(); ++i) {
            XAResourceWrapper xarw = getXAResourceWrapper(i);
            if (xaResource.equals(xarw.getXAResource())) {
                try {
                    xarw.end(flag);
                    return true;
                } catch (XAException ex) {
                    logger.log(ex);
                    status = Status.STATUS_MARKED_ROLLBACK;
                    return false;
                }
            }
        }
        throw new SIllegalStateException("ESSR0313", null);
    }

    public int getStatus() {
        return status;
    }

    public void registerSynchronization(Synchronization sync)
            throws RollbackException, IllegalStateException, SystemException {

        assertNotSuspended();
        assertActive();
        synchronizations.add(sync);
    }

    public void registerInterposedSynchronization(Synchronization sync)
            throws IllegalStateException {

        assertNotSuspended();
        assertActive();
        interposedSynchronizations.add(sync);
    }

    public void putResource(Object key, Object value)
            throws IllegalStateException {
        assertNotSuspended();
        resourceMap.put(key, value);
    }

    public Object getResource(Object key) throws IllegalStateException {
        assertNotSuspended();
        return resourceMap.get(key);
    }

    /**
     * トランザクションIDを返します。
     * 
     * @return トランザクションID
     */
    public Xid getXid() {
        return xid;
    }

    /**
     * トランザクションが中断されている場合は<code>true</code>を、それ以外の場合は<code>false</code>を返します。
     * 
     * @return トランザクションが中断されている場合は<code>true</code>
     */
    public boolean isSuspended() {
        return suspended;
    }

    private void init() {
        xid = new XidImpl();
    }

    private void destroy() {
        status = Status.STATUS_NO_TRANSACTION;
        xaResourceWrappers.clear();
        synchronizations.clear();
        interposedSynchronizations.clear();
        resourceMap.clear();
        suspended = false;
    }

    public String toString() {
        return xid.toString();
    }

    /**
     * {@link Synchronization}のリストを返します。
     * 
     * @return
     */
    public List getSynchronizations() {
        return synchronizations;
    }

    /**
     * {@link Synchronization}のリストを返します。
     * 
     * @return
     */
    public List getInterposedSynchronizations() {
        return interposedSynchronizations;
    }
}
