/*
 * Copyright 2017 requery.io
 *
 * 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 io.requery.sql;

import io.requery.EntityCache;
import io.requery.Transaction;
import io.requery.TransactionException;
import io.requery.TransactionIsolation;
import io.requery.TransactionListener;
import io.requery.meta.Type;
import io.requery.proxy.EntityProxy;
import io.requery.util.Objects;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collection;

/**
 * {@link Transaction} implementation using JDBC {@link Connection} operations
 * {@link Connection#commit()} and {@link Connection#rollback()}.
 *
 * @author Nikhil Purushe
 */
class ConnectionTransaction implements EntityTransaction, ConnectionProvider {

    private final ConnectionProvider connectionProvider;
    private final TransactionEntitiesSet entities;
    private final TransactionListener transactionListener;
    private final boolean supportsTransaction;
    private Connection connection;
    private Connection uncloseableConnection;
    private boolean committed;
    private boolean rolledBack;
    private int previousIsolationLevel;

    ConnectionTransaction(TransactionListener transactionListener,
                          ConnectionProvider connectionProvider,
                          EntityCache cache,
                          boolean supportsTransaction) {
        this.transactionListener = Objects.requireNotNull(transactionListener);
        this.connectionProvider = Objects.requireNotNull(connectionProvider);
        this.supportsTransaction = supportsTransaction;
        this.entities = new TransactionEntitiesSet(cache);
        this.previousIsolationLevel = -1;
    }

    @Override
    public Connection getConnection() {
        return uncloseableConnection;
    }

    @Override
    public Transaction begin() {
        return begin(null);
    }

    @Override
    public Transaction begin(TransactionIsolation isolation) {
        if (active()) {
            throw new IllegalStateException("transaction already active");
        }
        try {
            transactionListener.beforeBegin(isolation);
            connection = connectionProvider.getConnection();
            uncloseableConnection = new UncloseableConnection(connection);
            if (supportsTransaction) {
                connection.setAutoCommit(false);
                if (isolation != null) {
                    previousIsolationLevel = connection.getTransactionIsolation();
                    int level;
                    switch (isolation) {
                        case NONE:
                            level = Connection.TRANSACTION_NONE;
                            break;
                        case READ_UNCOMMITTED:
                            level = Connection.TRANSACTION_READ_UNCOMMITTED;
                            break;
                        case READ_COMMITTED:
                            level = Connection.TRANSACTION_READ_COMMITTED;
                            break;
                        case REPEATABLE_READ:
                            level = Connection.TRANSACTION_REPEATABLE_READ;
                            break;
                        case SERIALIZABLE:
                            level = Connection.TRANSACTION_SERIALIZABLE;
                            break;
                        default:
                            throw new UnsupportedOperationException();
                    }
                    connection.setTransactionIsolation(level);
                }
            }
            committed = false;
            rolledBack = false;
            entities.clear();
            transactionListener.afterBegin(isolation);
        } catch (SQLException e) {
            throw new TransactionException(e);
        }
        return this;
    }

    @Override
    public void close() {
        if (connection != null) {
            if (!committed && !rolledBack) {
                try {
                    rollback();
                } catch (Exception ignored) {
                }
            }
            try {
                connection.close();
            } catch (SQLException e) {
                throw new TransactionException(e);
            } finally {
                connection = null;
            }
        }
    }

    @Override
    public void commit() {
        try {
            transactionListener.beforeCommit(entities.types());
            if (supportsTransaction) {
                connection.commit();
                committed = true;
            }
            transactionListener.afterCommit(entities.types());
            entities.clear();
        } catch (SQLException e) {
            throw new TransactionException(e);
        } finally {
            resetConnection();
            close();
        }
    }

    @Override
    public void rollback() {
        try {
            transactionListener.beforeRollback(entities.types());
            if (supportsTransaction) {
                connection.rollback();
                rolledBack = true;
                entities.clearAndInvalidate();
            }
            transactionListener.afterRollback(entities.types());
            entities.clear();
        } catch (SQLException e) {
            throw new TransactionException(e);
        } finally {
            resetConnection();
        }
    }

    @Override
    public boolean active() {
        try {
            return connection != null && !connection.getAutoCommit();
        } catch (SQLException e) {
            return false;
        }
    }

    @Override
    public void addToTransaction(EntityProxy<?> proxy) {
        entities.add(proxy);
    }

    @Override
    public void addToTransaction(Collection<Type<?>> types) {
        entities.types().addAll(types);
    }

    private void resetConnection() {
        if (supportsTransaction) {
            try {
                connection.setAutoCommit(true);
                // restore default isolation level
                if (previousIsolationLevel != -1) {
                    connection.setTransactionIsolation(previousIsolationLevel);
                }
            } catch (SQLException ignored) {
            }
        }
    }
}