/***************************************************************************
 * Copyright 2014 Kieker Project (http://kieker-monitoring.net)
 *
 * 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 kieker.webgui.persistence.impl;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import kieker.common.logging.Log;
import kieker.common.logging.LogFactory;
import kieker.webgui.common.exception.DataAccessException;
import kieker.webgui.common.exception.UserAlreadyExistingException;
import kieker.webgui.domain.Role;
import kieker.webgui.domain.User;
import kieker.webgui.persistence.IUserDAO;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * This service uses Apache Derby to persist and manage the available users. A transaction manager makes sure that all operations are atomically performed. The
 * connection to the data base, the actual data source, and the transaction manager are managed by the Spring framework. The data source uses pools for the
 * connections and the prepared statements. The configuration can be found in the file {@code spring-database-config.xml}.
 * 
 * @author Nils Christian Ehmke
 */
@Service
public final class DerbyUserDAOImpl implements IUserDAO {

	private static final Log LOG = LogFactory.getLog(DerbyUserDAOImpl.class);

	/**
	 * We use this state to determine that a transaction failed due to a duplicate key. The class containing this constant is unfortunately not packed in the Apache
	 * Derby jar.
	 */
	private static final String SQL_STATE_DUPLICATE_KEY = "23505";

	@Autowired
	private DataSource dataSource;

	/**
	 * Default constructor. <b>Do not use this constructor. This bean is Spring managed.</b>
	 */
	public DerbyUserDAOImpl() {
		// No code necessary
	}

	@Override
	@Transactional
	public void addUser(final User user) throws DataAccessException {
		Connection connection = null;
		PreparedStatement statement = null;

		try {
			connection = this.dataSource.getConnection();
			statement = connection.prepareStatement("INSERT INTO Users (name, password, isGuest, isUser, isAdministrator, isEnabled) VALUES (?, ?, ?, ?, ?, ?)");

			// Use all properties of the given object
			statement.setString(1, user.getName());
			statement.setString(2, user.getPassword());
			statement.setBoolean(3, user.getRole() == Role.GUEST);
			statement.setBoolean(4, user.getRole() == Role.USER);
			statement.setBoolean(5, user.getRole() == Role.ADMINISTRATOR);
			statement.setBoolean(6, user.isEnabled());

			statement.execute();
		} catch (final SQLException ex) {
			// Check whether the execution failed due to a duplicate key
			if (SQL_STATE_DUPLICATE_KEY.equals(ex.getSQLState())) {
				throw new UserAlreadyExistingException("A user with the given name exists already", ex);
			}
			// Something else went wrong
			throw new DataAccessException("Could not add user to the database", ex);
		} finally {
			// Try to close the statement. If this is not possible, then log the problem. It is not necessary to inform the calling method about a fail though.
			if (statement != null) {
				try {
					statement.close();
				} catch (final SQLException ex) {
					LOG.error("Could not close prepared statement", ex);
				}
			}
			if (connection != null) {
				try {
					connection.close();
				} catch (final SQLException ex) {
					LOG.error("Could not close connection", ex);
				}
			}
		}
	}

	@Override
	@Transactional
	public void deleteUser(final User user) throws DataAccessException {
		Connection connection = null;
		PreparedStatement statement = null;

		try {
			connection = this.dataSource.getConnection();
			statement = connection.prepareStatement("DELETE FROM Users WHERE name=?");

			statement.setString(1, user.getName());

			statement.execute();
		} catch (final SQLException ex) {
			// Something went wrong. Inform the calling method.
			throw new DataAccessException("Could not delete user from the database", ex);
		} finally {
			// Try to close the statement. If this is not possible, then log the problem. It is not necessary to inform the calling method about a fail though.
			if (statement != null) {
				try {
					statement.close();
				} catch (final SQLException ex) {
					LOG.error("Could not close prepared statement", ex);
				}
			}
			if (connection != null) {
				try {
					connection.close();
				} catch (final SQLException ex) {
					LOG.error("Could not close connection", ex);
				}
			}
		}
	}

	@Override
	@Transactional
	public void editUserWithPassword(final User user) throws DataAccessException {
		Connection connection = null;
		PreparedStatement statement = null;

		try {
			connection = this.dataSource.getConnection();
			statement = connection.prepareStatement("UPDATE Users SET name=?, isGuest=?, isUser=?, isAdministrator=?, isEnabled=?, password=? WHERE name=?");

			statement.setString(1, user.getName());
			statement.setBoolean(2, user.getRole() == Role.GUEST);
			statement.setBoolean(3, user.getRole() == Role.USER);
			statement.setBoolean(4, user.getRole() == Role.ADMINISTRATOR);
			statement.setBoolean(5, user.isEnabled());
			statement.setString(6, user.getPassword());
			statement.setString(7, user.getName());

			statement.execute();
		} catch (final SQLException ex) {
			throw new DataAccessException("Could not modify user within the database", ex);
		} finally {
			// Try to close the statement. If this is not possible, then log the problem. It is not necessary to inform the calling method about a fail though.
			if (statement != null) {
				try {
					statement.close();
				} catch (final SQLException ex) {
					LOG.error("Could not close prepared statement", ex);
				}
			}
			if (connection != null) {
				try {
					connection.close();
				} catch (final SQLException ex) {
					LOG.error("Could not close connection", ex);
				}
			}
			DataSourceUtils.releaseConnection(connection, this.dataSource);
		}
	}

	@Override
	@Transactional
	public void editUserWithoutPassword(final User user) throws DataAccessException {
		Connection connection = null;
		PreparedStatement statement = null;

		try {
			connection = this.dataSource.getConnection();
			statement = connection.prepareStatement("UPDATE Users SET name=?, isGuest=?, isUser=?, isAdministrator=?, isEnabled=? WHERE name=?");

			statement.setString(1, user.getName());
			statement.setBoolean(2, user.getRole() == Role.GUEST);
			statement.setBoolean(3, user.getRole() == Role.USER);
			statement.setBoolean(4, user.getRole() == Role.ADMINISTRATOR);
			statement.setBoolean(5, user.isEnabled());
			statement.setString(6, user.getName());

			statement.execute();
		} catch (final SQLException ex) {
			throw new DataAccessException("Could not modify user within the database", ex);
		} finally {
			// Try to close the statement. If this is not possible, then log the problem. It is not necessary to inform the calling method about a fail though.
			if (statement != null) {
				try {
					statement.close();
				} catch (final SQLException ex) {
					LOG.error("Could not close prepared statement", ex);
				}
			}
			if (connection != null) {
				try {
					connection.close();
				} catch (final SQLException ex) {
					LOG.error("Could not close connection", ex);
				}
			}
		}
	}

	@Override
	@Transactional(readOnly = true)
	public List<User> getUsers() throws DataAccessException {
		final List<User> result = new ArrayList<User>();

		ResultSet queryResult = null;
		Connection connection = null;
		PreparedStatement statement = null;

		try {
			connection = this.dataSource.getConnection();
			// Get all properties from all users - except the password
			statement = connection.prepareStatement("SELECT name, isGuest, isUser, isAdministrator, isEnabled FROM Users");

			queryResult = statement.executeQuery();

			DerbyUserDAOImpl.queryToUsers(queryResult, result);
		} catch (final SQLException ex) {
			throw new DataAccessException("Could not receive user list", ex);
		} finally {
			// Try to close everything. If this is not possible, then log the problem. It is not necessary to inform the calling method about a fail though.
			if (queryResult != null) {
				try {
					queryResult.close();
				} catch (final SQLException ex) {
					LOG.error("Could not close query result", ex);
				}
			}
			if (statement != null) {
				try {
					statement.close();
				} catch (final SQLException ex) {
					LOG.error("Could not close prepared statement", ex);
				}
			}
			if (connection != null) {
				try {
					connection.close();
				} catch (final SQLException ex) {
					LOG.error("Could not close connection", ex);
				}
			}
		}

		return result;
	}

	private static void queryToUsers(final ResultSet queryResult, final List<User> result) throws SQLException {
		// Run through all results
		while (queryResult.next()) {
			final String username = queryResult.getString("name");
			final boolean isGuest = queryResult.getBoolean("isGuest");
			final boolean isUser = queryResult.getBoolean("isUser");
			final boolean isAdministrator = queryResult.getBoolean("isAdministrator");
			final boolean isEnabled = queryResult.getBoolean("isEnabled");

			// The case that the user has no role cannot happen, as the database should make sure that this is not possible
			final Role role;
			if (isAdministrator) {
				role = Role.ADMINISTRATOR;
			} else if (isUser) {
				role = Role.USER;
			} else if (isGuest) {
				role = Role.GUEST;
			} else {
				role = null;
			}

			result.add(new User(username, null, role, isEnabled));
		}
	}
}
