/*
 * Copyright 2004-2014 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.LinkedList;
import java.util.List;
import java.util.Map;

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.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.seasar.extension.jta.xa.XidImpl;

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

    /** VOTE_READONLY */
    private static final int VOTE_READONLY = 0;
    /** VOTE_COMMIT */
    private static final int VOTE_COMMIT = 1;
    /** VOTE_ROLLBACK */
    private static final int VOTE_ROLLBACK = 2;

    /** LOGGER */
    private static final Logger LOGGER = LogManager.getLogger(TransactionImpl.class);

    /** xaResourceWrappers */
    private final List<XAResourceWrapper> xaResourceWrappers = new ArrayList<>();
    /** synchronizations */
    private final List<Synchronization> synchronizations = new ArrayList<>();
    /** interposedSynchronizations */
    private final List<Synchronization> interposedSynchronizations = new ArrayList<>();
    /** resourceMap */
    private final Map<Object, Object> resourceMap = new HashMap<>();

    /** Xid */
    private Xid xid;
    /** status */
    private int status = Status.STATUS_NO_TRANSACTION;
    /** suspended */
    private boolean suspended = false;
    /** branchId */
    private int branchId = 0;

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

    /**
     * トランザクションを開始します。
     *
     */
    @Override
    public void begin() {
        this.status = Status.STATUS_ACTIVE;
        init();
        LOGGER.debug(this);
    }

    /**
     * トランザクションを中断します。
     *
     * @throws SystemException <code>XAResource</code>を中断できなかった場合にスローされます
     */
    @Override
    public void suspend() throws SystemException {
        assertNotSuspended();
        assertActiveOrMarkedRollback();
        for (final XAResourceWrapper xarw : this.xaResourceWrappers) {
            try {
                xarw.end(XAResource.TMSUSPEND);
            } catch (final XAException ex) {
                throw new SystemException(ex.getMessage());
            }
        }
        this.suspended = true;
    }

    /**
     * assertNotSuspended
     * @throws IllegalStateException IllegalStateException
     */
    private void assertNotSuspended() {
        if (this.suspended) {
            throw new IllegalStateException("suspended");
        }
    }

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

    /**
     * throwIllegalStateException
     * @throws IllegalStateException IllegalStateException
     */
    private void throwIllegalStateException() {
        switch (this.status) {
            case Status.STATUS_PREPARING:
                throw new IllegalStateException("status is PREPARING.");
            case Status.STATUS_PREPARED:
                throw new IllegalStateException("status is PREPARED.");
            case Status.STATUS_COMMITTING:
                throw new IllegalStateException("status is COMMITTING.");
            case Status.STATUS_COMMITTED:
                throw new IllegalStateException("status is COMMITTED.");
            case Status.STATUS_MARKED_ROLLBACK:
                throw new IllegalStateException("status is MARKED_ROLLBACK.");
            case Status.STATUS_ROLLING_BACK:
                throw new IllegalStateException("status is ROLLING_BACK.");
            case Status.STATUS_ROLLEDBACK:
                throw new IllegalStateException("status is ROLLEDBACK.");
            case Status.STATUS_NO_TRANSACTION:
                throw new IllegalStateException("status is NO_TRANSACTION.");
            case Status.STATUS_UNKNOWN:
                throw new IllegalStateException("status is UNKNOWN.");
            default:
                throw new IllegalStateException(String.valueOf(this.status));
        }
    }

    /**
     * トランザクションを再開します。
     *
     * @throws SystemException <code>XAResource</code>を再開できなかった場合にスローされます
     */
    @Override
    public void resume() throws SystemException {
        assertSuspended();
        assertActiveOrMarkedRollback();
        for (final XAResourceWrapper xarw : this.xaResourceWrappers) {
            try {
                xarw.start(XAResource.TMRESUME);
            } catch (final XAException ex) {
                throw new SystemException(ex.getMessage());
            }
        }
        this.suspended = false;
    }

    /**
     * assertSuspended
     * @throws IllegalStateException IllegalStateException
     */
    private void assertSuspended() {
        if (!this.suspended) {
            throw new IllegalStateException("not suspended.");
        }
    }

    /**
     * @see javax.transaction.Transaction#commit()
     */
    @Override
    public void commit() throws RollbackException {
        try {
            assertNotSuspended();
            assertActive();
            beforeCompletion();
            if (this.status == Status.STATUS_ACTIVE) {
                endResources(XAResource.TMSUCCESS);
                if (this.xaResourceWrappers.isEmpty()) {
                    this.status = Status.STATUS_COMMITTED;
                } else if (this.xaResourceWrappers.size() == 1) {
                    commitOnePhase();
                } else {
                    switch (prepareResources()) {
                        case VOTE_READONLY:
                            this.status = Status.STATUS_COMMITTED;
                            break;
                        case VOTE_COMMIT:
                            commitTwoPhase();
                            break;
                        case VOTE_ROLLBACK:
                            rollbackForVoteOK();
                            break;
                        default:
                            break;
                    }
                }
                if (this.status == Status.STATUS_COMMITTED) {
                    LOGGER.debug(this);
                }
            }

            final boolean rolledBack = this.status != Status.STATUS_COMMITTED;
            afterCompletion();
            if (rolledBack) {
                throw new RollbackException(toString());
            }
        } finally {
            destroy();
        }
    }

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

    /**
     * beforeCompletion
     * @param sync Synchronization
     */
    private void beforeCompletion(final Synchronization sync) {
        try {
            sync.beforeCompletion();
        } catch (final Throwable t) {
            LOGGER.info(t);
            this.status = Status.STATUS_MARKED_ROLLBACK;
            endResources(XAResource.TMFAIL);
            rollbackResources();
        }
    }

    /**
     * endResources
     * @param flag int
     */
    private void endResources(final int flag) {
        for (final XAResourceWrapper xarw : this.xaResourceWrappers) {
            try {
                xarw.end(flag);
            } catch (final Throwable t) {
                LOGGER.info(t);
                this.status = Status.STATUS_MARKED_ROLLBACK;
            }
        }
    }

    /**
     * commitOnePhase
     */
    private void commitOnePhase() {
        this.status = Status.STATUS_COMMITTING;
        final XAResourceWrapper xari = this.xaResourceWrappers.get(0);
        try {
            xari.commit(true);
            this.status = Status.STATUS_COMMITTED;
        } catch (final Throwable t) {
            LOGGER.info(t);
            this.status = Status.STATUS_UNKNOWN;
        }
    }

    /**
     * prepareResources
     * @return vote
     */
    private int prepareResources() {
        this.status = Status.STATUS_PREPARING;
        int vote = VOTE_READONLY;
        final LinkedList<XAResourceWrapper> xarwList = new LinkedList<>();
        for (final XAResourceWrapper xarw : this.xaResourceWrappers) {
            if (xarw.isCommitTarget()) {
                xarwList.addFirst(xarw);
            }
        }
        for (int i = 0; i < xarwList.size(); ++i) {
            final XAResourceWrapper xarw = 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 (final Throwable t) {
                LOGGER.info(t);
                xarw.setVoteOk(false);
                this.status = Status.STATUS_MARKED_ROLLBACK;
                return VOTE_ROLLBACK;
            }
        }
        if (this.status == Status.STATUS_PREPARING) {
            this.status = Status.STATUS_PREPARED;
        }
        return vote;
    }

    /**
     * commitTwoPhase
     */
    private void commitTwoPhase() {
        this.status = Status.STATUS_COMMITTING;
        for (final XAResourceWrapper xarw : this.xaResourceWrappers) {
            if (xarw.isCommitTarget() && xarw.isVoteOk()) {
                try {
                    xarw.commit(false);
                } catch (final Throwable t) {
                    LOGGER.info(t);
                    this.status = Status.STATUS_UNKNOWN;
                }
            }
        }
        if (this.status == Status.STATUS_COMMITTING) {
            this.status = Status.STATUS_COMMITTED;
        }
    }

    /**
     * rollbackForVoteOK
     */
    private void rollbackForVoteOK() {
        this.status = Status.STATUS_ROLLING_BACK;
        for (final XAResourceWrapper xarw : this.xaResourceWrappers) {
            if (xarw.isVoteOk()) {
                try {
                    xarw.rollback();
                } catch (final Throwable t) {
                    LOGGER.info(t);
                    this.status = Status.STATUS_UNKNOWN;
                }
            }
        }
        if (this.status == Status.STATUS_ROLLING_BACK) {
            this.status = Status.STATUS_ROLLEDBACK;
        }
    }

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

    /**
     * afterCompletion
     * @param sts status
     * @param sync Synchronization
     */
    private void afterCompletion(final int sts, final Synchronization sync) {
        try {
            sync.afterCompletion(sts);
        } catch (final Throwable t) {
            LOGGER.info(t);
        }
    }

    /**
     * @return size
     */
    private int getSynchronizationSize() {
        return this.synchronizations.size();
    }

    /**
     * @param index index
     * @return Synchronization
     */
    private Synchronization getSynchronization(final int index) {
        return this.synchronizations.get(index);
    }

    /**
     * @return size
     */
    private int getInterposedSynchronizationSize() {
        return this.interposedSynchronizations.size();
    }

    /**
     * @param index index
     * @return Synchronization
     */
    private Synchronization getInterposedSynchronization(final int index) {
        return this.interposedSynchronizations.get(index);
    }

    /**
     * @see javax.transaction.Transaction#rollback()
     */
    @Override
    public void rollback() throws SecurityException {
        try {
            assertNotSuspended();
            assertActiveOrMarkedRollback();
            endResources(XAResource.TMFAIL);
            rollbackResources();
            LOGGER.debug(this);
            afterCompletion();
        } finally {
            destroy();
        }
    }

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

    /**
     * rollbackResources
     */
    private void rollbackResources() {
        this.status = Status.STATUS_ROLLING_BACK;
        for (final XAResourceWrapper xarw : this.xaResourceWrappers) {
            try {
                if (xarw.isCommitTarget()) {
                    xarw.rollback();
                }
            } catch (final Throwable t) {
                LOGGER.info(t);
                this.status = Status.STATUS_UNKNOWN;
            }
        }
        if (this.status == Status.STATUS_ROLLING_BACK) {
            this.status = Status.STATUS_ROLLEDBACK;
        }
    }

    /**
     * @see javax.transaction.Transaction#setRollbackOnly()
     */
    @Override
    public void setRollbackOnly() {
        assertNotSuspended();
        assertActiveOrPreparingOrPrepared();
        this.status = Status.STATUS_MARKED_ROLLBACK;
    }

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

    /**
     * @see javax.transaction.Transaction#enlistResource(javax.transaction.xa.XAResource)
     */
    @Override
    public boolean enlistResource(final XAResource xaResource) {
        final boolean oracled = xaResource.getClass().getName().startsWith("oracle");
        assertNotSuspended();
        assertActive();
        Xid newXid = null;
        for (final XAResourceWrapper xarw : this.xaResourceWrappers) {
            if (xaResource.equals(xarw.getXAResource())) {
                return false;
            } else if (!oracled) {
                try {
                    if (xaResource.isSameRM(xarw.getXAResource())) {
                        newXid = xarw.getXid();
                        break;
                    }
                } catch (final XAException ex) {
                    throw new IllegalStateException(ex.toString());
                }
            }
        }
        final int flag = (newXid == null) ? XAResource.TMNOFLAGS : XAResource.TMJOIN;
        final boolean commitTarget = newXid == null;
        if (newXid == null) {
            newXid = createXidBranch();
        }
        try {
            xaResource.start(newXid, flag);
            this.xaResourceWrappers.add(new XAResourceWrapper(xaResource, newXid, commitTarget));
            return true;
        } catch (final XAException ex) {
            throw new IllegalStateException(ex);
        }
    }

    /**
     * createXidBranch
     * @return Xid
     */
    private Xid createXidBranch() {
        return new XidImpl(this.xid, ++this.branchId);
    }

    /**
     * @see javax.transaction.Transaction#delistResource(javax.transaction.xa.XAResource, int)
     */
    @Override
    public boolean delistResource(final XAResource xaResource, final int flag) {
        assertNotSuspended();
        assertActiveOrMarkedRollback();
        for (final XAResourceWrapper xarw : this.xaResourceWrappers) {
            if (xaResource.equals(xarw.getXAResource())) {
                try {
                    xarw.end(flag);
                    return true;
                } catch (final XAException ex) {
                    LOGGER.info(ex);
                    this.status = Status.STATUS_MARKED_ROLLBACK;
                    return false;
                }
            }
        }
        throw new IllegalStateException("ESSR0313", null);
    }

    /**
     * @see javax.transaction.Transaction#getStatus()
     */
    @Override
    public int getStatus() {
        return this.status;
    }

    /**
     * @see javax.transaction.Transaction#registerSynchronization(javax.transaction.Synchronization)
     */
    @Override
    public void registerSynchronization(final Synchronization sync) {
        assertNotSuspended();
        assertActive();
        this.synchronizations.add(sync);
    }

    /**
     * @see org.seasar.extension.jta.SynchronizationRegister
     * #registerInterposedSynchronization(javax.transaction.Synchronization)
     */
    @Override
    public void registerInterposedSynchronization(final Synchronization sync) {
        assertNotSuspended();
        assertActive();
        this.interposedSynchronizations.add(sync);
    }

    /**
     * @see org.seasar.extension.jta.SynchronizationRegister
     * #putResource(java.lang.Object, java.lang.Object)
     */
    @Override
    public void putResource(final Object key, final Object value) {
        assertNotSuspended();
        this.resourceMap.put(key, value);
    }

    /**
     * @see org.seasar.extension.jta.SynchronizationRegister#getResource(java.lang.Object)
     */
    @Override
    public Object getResource(final Object key) {
        assertNotSuspended();
        return this.resourceMap.get(key);
    }

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

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

    /**
     * init
     */
    private void init() {
        this.xid = new XidImpl();
    }

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

    /**
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return this.xid.toString();
    }

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

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