/*
 * 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.resource.jdbc.managed.local;

import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LocalXAResource implements XAResource {
    private final Connection connection;
    private Xid currentXid;
    private boolean originalAutoCommit;
    private final Lock lock = new ReentrantLock();

    public LocalXAResource(final Connection localTransaction) {
        connection = localTransaction;
    }

    public Xid getXid() {
        checkLock();
        return currentXid;
    }

    @Override
    public void start(final Xid xid, final int flag) throws XAException {
        try {
            lock.tryLock(10, TimeUnit.MINUTES);
        } catch (final InterruptedException e) {
            throw (XAException) new XAException("can't get lock").initCause(cantGetLock());
        }

        if (flag == XAResource.TMNOFLAGS) {
            if (currentXid != null) {
                throw new XAException("Already enlisted in another transaction with xid " + xid);
            }

            // save off the current auto commit flag so it can be restored after the transaction completes
            try {
                originalAutoCommit = connection.getAutoCommit();
            } catch (final SQLException ignored) {
                originalAutoCommit = true;
            }

            try {
                connection.setAutoCommit(false);
            } catch (final SQLException e) {
                throw (XAException) new XAException("Count not turn off auto commit for a XA transaction").initCause(e);
            }

            this.currentXid = xid;
        } else if (flag == XAResource.TMRESUME) {
            if (xid != currentXid) {
                throw new XAException("Attempting to resume in different transaction: expected " + currentXid + ", but was " + xid);
            }
        } else {
            throw new XAException("Unknown start flag " + flag);
        }
    }

    private RuntimeException cantGetLock() {
        return new IllegalStateException("can't get lock on resource with Xid " + currentXid + " from thread " + Thread.currentThread().getName());
    }

    @Override
    public void end(final Xid xid, final int flag) throws XAException {
        try {
            if (xid == null) {
                throw new NullPointerException("xid is null");
            }
            if (!this.currentXid.equals(xid)) {
                throw new XAException("Invalid Xid: expected " + this.currentXid + ", but was " + xid);
            }
        } finally {
            lock.unlock();
        }
    }

    @Override
    public int prepare(final Xid xid) {
        checkLock();

        try {
            if (connection.isReadOnly()) {
                connection.setAutoCommit(originalAutoCommit);
                return XAResource.XA_RDONLY;
            }
        } catch (final SQLException ignored) {
            // no-op
        }

        return XAResource.XA_OK;
    }

    @Override
    public void commit(final Xid xid, final boolean flag) throws XAException {
        checkLock();

        if (xid == null) {
            throw new NullPointerException("xid is null");
        }
        if (!currentXid.equals(xid)) {
            throw new XAException("Invalid Xid: expected " + currentXid + ", but was " + xid);
        }

        try {
            if (connection.isClosed()) {
                throw new XAException("Conection is closed");
            }

            if (!connection.isReadOnly()) {
                connection.commit();
            }
        } catch (final SQLException e) {
            throw (XAException) new XAException().initCause(e);
        } finally {
            try {
                connection.setAutoCommit(originalAutoCommit);
            } catch (final SQLException e) {
                // no-op
            }
            currentXid = null;
        }
    }

    @Override
    public void rollback(final Xid xid) throws XAException {
        checkLock();

        if (xid == null) {
            throw new NullPointerException("xid is null");
        }
        if (!currentXid.equals(xid)) {
            throw new XAException("Invalid Xid: expected " + currentXid + ", but was " + xid);
        }

        try {
            connection.rollback();
        } catch (final SQLException e) {
            throw (XAException) new XAException().initCause(e);
        } finally {
            try {
                connection.setAutoCommit(originalAutoCommit);
            } catch (final SQLException e) {
                // no-op
            }
            this.currentXid = null;
        }
    }

    @Override
    public boolean isSameRM(final XAResource xaResource) {
        return this == xaResource;
    }

    @Override
    public void forget(final Xid xid) {
        checkLock();
        if (xid != null && currentXid.equals(xid)) {
            currentXid = null;
        }
    }

    @Override
    public Xid[] recover(final int flag) {
        return new Xid[0];
    }

    @Override
    public int getTransactionTimeout() {
        return 0;
    }

    @Override
    public boolean setTransactionTimeout(final int transactionTimeout) {
        return false;
    }

    private void checkLock() {
        if (!lock.tryLock()) {
            throw cantGetLock();
        }
    }
}
