/*
 * Syncany, www.syncany.org
 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> 
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.syncany.database.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.syncany.database.ChunkEntry;
import org.syncany.database.ChunkEntry.ChunkChecksum;
import org.syncany.database.VectorClock;

/**
 * The chunk data access object (DAO) writes and queries the SQL database for information
 * on {@link ChunkEntry}s. It translates the relational data in the "chunk" table to
 * Java objects.
 * 
 * @author Philipp C. Heckel <philipp.heckel@gmail.com>
 */
public class ChunkSqlDao extends AbstractSqlDao {
	private Map<ChunkChecksum, ChunkEntry> chunkCache;

	public ChunkSqlDao(Connection connection) {
		super(connection);
		this.chunkCache = null;
	}

	/**
	 * Writes a list of {@link ChunkEntry}s to the database using <tt>INSERT</tt>s and the given connection.
	 * 
	 * <p><b>Note:</b> This method executes, but <b>does not commit</b> the query.
	 * 
	 * @param connection The connection used to execute the statements
	 * @param databaseVersionId 
	 * @param chunks List of {@link ChunkEntry}s to be inserted in the database
	 * @throws SQLException If the SQL statement fails
	 */
	public void writeChunks(Connection connection, long databaseVersionId, Collection<ChunkEntry> chunks) throws SQLException {
		if (chunks.size() > 0) {
			PreparedStatement preparedStatement = getStatement(connection, "chunk.insert.all.writeChunks.sql");

			for (ChunkEntry chunk : chunks) {
				preparedStatement.setString(1, chunk.getChecksum().toString());
				preparedStatement.setLong(2, databaseVersionId);
				preparedStatement.setInt(3, chunk.getSize());

				preparedStatement.addBatch();
			}

			preparedStatement.executeBatch();
			preparedStatement.close();
		}
	}	

	/**
	 * Removes unreferenced chunks from the database. Unreferenced chunks are chunks
	 * that are not referenced by any file content or multichunk. 
	 * 
	 * <p>During the cleanup process, when file versions are deleted, unused chunks 
	 * are left over. This method removes these chunks from the database.
	 * 
	 * <p><b>Note:</b> This method executes, but <b>does not commit</b> the query. 
	 */
	public void removeUnreferencedChunks() {
		try (PreparedStatement preparedStatement = getStatement("chunk.delete.all.removeUnreferencesChunks.sql")) {
			preparedStatement.execute();
			preparedStatement.close();
		}
		catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}
	
	/**
	 * Queries the database of a chunk with the given checksum. 
	 * 
	 * <p>Note: When first called, this method loads the <b>chunk cache</b> and keeps
	 * this cache until it is cleared explicitly with {@link #clearCache()}. 
	 * 
	 * <p>Also note that this method will return <tt>null</tt> if the chunk has been
	 * added after the cache has been filled. 
	 * 
	 * @param chunkChecksum Chunk checksum of the chunk to be selected
	 * @return Returns the chunk entry, or <tt>null</tt> if the chunk does not exist.
	 */	
	public synchronized ChunkEntry getChunk(ChunkChecksum chunkChecksum) {
		if (chunkCache == null) {
			loadChunkCache();
		}

		return chunkCache.get(chunkChecksum);
	}
	
	/**
	 * Clears the chunk cache loaded by {@link #getChunk(ChunkChecksum) getChunk()}
	 * and resets the cache. If {@link #getChunk(ChunkChecksum) getChunk()} is called
	 * after the cache is cleared, it is re-populated.
	 */
	public synchronized void clearCache() {
		if (chunkCache != null) {
			chunkCache.clear();
			chunkCache = null;
		}
	}

	/**
	 * Queries the SQL database for all chunks that <b>originally appeared</b> in the
	 * database version identified by the given vector clock.
	 * 
	 * <p><b>Note:</b> This method does <b>not</b> select all the chunks that are referenced
	 * in the database version. In particular, it <b>does not return</b> chunks that appeared
	 * in previous other database versions.
	 * 
	 * @param vectorClock Vector clock that identifies the database version
	 * @return Returns all chunks that originally belong to a database version
	 */
	public Map<ChunkChecksum, ChunkEntry> getChunks(VectorClock vectorClock) {
		try (PreparedStatement preparedStatement = getStatement("chunk.select.all.getChunksForDatabaseVersion.sql")) {
			preparedStatement.setString(1, vectorClock.toString());

			try (ResultSet resultSet = preparedStatement.executeQuery()) {
				return createChunkEntries(resultSet);
			}
		}
		catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}

	protected Map<ChunkChecksum, ChunkEntry> createChunkEntries(ResultSet resultSet) throws SQLException {
		Map<ChunkChecksum, ChunkEntry> chunks = new HashMap<ChunkChecksum, ChunkEntry>();

		while (resultSet.next()) {
			ChunkEntry chunkEntry = createChunkEntryFromRow(resultSet);
			chunks.put(chunkEntry.getChecksum(), chunkEntry);
		}

		return chunks;
	}

	protected ChunkEntry createChunkEntryFromRow(ResultSet resultSet) throws SQLException {
		ChunkChecksum chunkChecksum = ChunkChecksum.parseChunkChecksum(resultSet.getString("checksum"));
		return new ChunkEntry(chunkChecksum, resultSet.getInt("size"));
	}
	
	protected void loadChunkCache() {
		try (PreparedStatement preparedStatement = getStatement("chunk.select.all.loadChunkCache.sql")) {
			try (ResultSet resultSet = preparedStatement.executeQuery()) {
				chunkCache = createChunkEntries(resultSet);
			}
		}
		catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}
	
	/**
	 * no commit
	 */
	public void updateDirtyChunksNewDatabaseId(long newDatabaseVersionId) {
		try (PreparedStatement preparedStatement = getStatement("chunk.update.dirty.updateDirtyChunksNewDatabaseId.sql")) {
			preparedStatement.setLong(1, newDatabaseVersionId);
			preparedStatement.executeUpdate();
		}
		catch (SQLException e) {
			throw new RuntimeException(e);
		}		
	}
}
