/*
 * 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.ChunkChecksum;
import org.syncany.database.FileContent;
import org.syncany.database.FileContent.FileChecksum;
import org.syncany.database.VectorClock;

/**
 * The file content data access object (DAO) writes and queries the SQL database for information
 * on {@link FileContent}s. It translates the relational data in the <i>filecontent</i> table to
 * Java objects.
 * 
 * @author Philipp C. Heckel <philipp.heckel@gmail.com>
 */
public class FileContentSqlDao extends AbstractSqlDao {
	public FileContentSqlDao(Connection connection) {
		super(connection);
	}

	/**
	 * Writes a list of {@link FileContent}s to the database using <tt>INSERT</tt>s and the given connection.
	 * It fills two tables, the <i>filecontent</i> table ({@link FileContent}) and the <i>filecontent_chunk</i> 
	 * table ({@link ChunkChecksum}).
	 * 
	 * <p>To do the latter (write chunk references), this method calls
	 * {@link #writeFileContentChunkRefs(Connection, FileContent) writeFileContentChunkRefs()} for every 
	 * {@link FileContent}. 
	 * 
	 * <p><b>Note:</b> This method executes, but does not commit the queries.
	 * 
	 * @param connection The connection used to execute the statements
	 * @param databaseVersionId 
	 * @param fileContents List of {@link FileContent}s to be inserted in the database
	 * @throws SQLException If the SQL statement fails
	 */
	public void writeFileContents(Connection connection, long databaseVersionId, Collection<FileContent> fileContents) throws SQLException {
		for (FileContent fileContent : fileContents) {
			PreparedStatement preparedStatement = getStatement(connection, "filecontent.insert.all.writeFileContents.sql");

			preparedStatement.setString(1, fileContent.getChecksum().toString());
			preparedStatement.setLong(2, databaseVersionId);
			preparedStatement.setLong(3, fileContent.getSize());
			
			preparedStatement.executeUpdate();
			preparedStatement.close();	
			
			// Write chunk references
			writeFileContentChunkRefs(connection, fileContent);			
		}
	}
	
	private void writeFileContentChunkRefs(Connection connection, FileContent fileContent) throws SQLException {
		PreparedStatement preparedStatement = getStatement(connection, "filecontent.insert.all.writeFileContentChunkRefs.sql");
		int order = 0;
		
		for (ChunkChecksum chunkChecksum : fileContent.getChunks()) {
			preparedStatement.setString(1, fileContent.getChecksum().toString());
			preparedStatement.setString(2, chunkChecksum.toString());
			preparedStatement.setInt(3, order);

			preparedStatement.addBatch();
			
			order++;				
		}
		
		preparedStatement.executeBatch();
		preparedStatement.close();
	}

	/**
	 * Removes unreferenced {@link FileContent}s from the database table <i>filecontent</i>,
	 * as well as the corresponding chunk references (list of {@link ChunkChecksum}s) from the
	 * table <i>filecontent_chunk</i>. 
	 * 
	 * <p><b>Note:</b> This method executes, but <b>does not commit</b> the query.
	 * 
	 * @throws SQLException If the SQL statement fails
	 */
	public void removeUnreferencedFileContents() throws SQLException {
		// Note: Chunk references (filcontent_chunk) must be removed first, because
		//       of the foreign key constraints. 
		
		removeUnreferencedFileContentChunkRefs();
		removeUnreferencedFileContentsInt();
	}
	
	private void removeUnreferencedFileContentsInt() throws SQLException {
		PreparedStatement preparedStatement = getStatement("filecontent.delete.all.removeUnreferencedFileContents.sql");
		preparedStatement.executeUpdate();	
		preparedStatement.close();
	}
	
	private void removeUnreferencedFileContentChunkRefs() throws SQLException {
		PreparedStatement preparedStatement = getStatement("filecontent.delete.all.removeUnreferencedFileContentRefs.sql");
		preparedStatement.executeUpdate();	
		preparedStatement.close();
	}
	
	/**
	 * Queries the database for a particular {@link FileContent}, either with or without the
	 * corresponding chunk references (list of {@link ChunkChecksum}). 
	 * 
	 * @param fileChecksum {@link FileContent}-identifying file checksum
	 * @param includeChunkChecksums If <tt>true</tt>, the resulting {@link FileContent} will contain its chunk references  
	 * @return Returns a {@link FileContent} either with or without chunk references, or <tt>null</tt> if it does not exist. 
	 */
	public FileContent getFileContent(FileChecksum fileChecksum, boolean includeChunkChecksums) {
		if (fileChecksum == null) {
			return null;
		}
		else if (includeChunkChecksums) {
			return getFileContentWithChunkChecksums(fileChecksum);			
		}
		else {
			return getFileContentWithoutChunkChecksums(fileChecksum);			
		}
	}

	/**
	 * Queries the SQL database for all {@link FileContent}s 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 file contents that are referenced
	 * in the database version. In particular, it <b>does not return</b> file contents that appeared
	 * in previous other database versions.
	 * 
	 * @param vectorClock Vector clock that identifies the database version
	 * @return Returns all {@link FileContent}s that originally belong to a database version
	 */
	public Map<FileChecksum, FileContent> getFileContents(VectorClock vectorClock) {
		try (PreparedStatement preparedStatement = getStatement("filecontent.select.master.getFileContentsWithChunkChecksumsForDatabaseVersion.sql")) {
			preparedStatement.setString(1, vectorClock.toString());

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

	private FileContent getFileContentWithoutChunkChecksums(FileChecksum fileChecksum) {
		try (PreparedStatement preparedStatement = getStatement("filecontent.select.all.getFileContentByChecksumWithoutChunkChecksums.sql")) {
			preparedStatement.setString(1, fileChecksum.toString());

			try (ResultSet resultSet = preparedStatement.executeQuery()) {
				if (resultSet.next()) {
					FileContent fileContent = new FileContent();
	
					fileContent.setChecksum(FileChecksum.parseFileChecksum(resultSet.getString("checksum")));
					fileContent.setSize(resultSet.getLong("size"));
	
					return fileContent;
				}
			}

			return null;
		}
		catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}

	private FileContent getFileContentWithChunkChecksums(FileChecksum fileChecksum) {
		try (PreparedStatement preparedStatement = getStatement("filecontent.select.all.getFileContentByChecksumWithChunkChecksums.sql")) {
			preparedStatement.setString(1, fileChecksum.toString());

			try (ResultSet resultSet = preparedStatement.executeQuery()) {
				FileContent fileContent = null;
				
				while (resultSet.next()) {
					if (fileContent == null) {
						fileContent = new FileContent();
						
						fileContent.setChecksum(FileChecksum.parseFileChecksum(resultSet.getString("checksum")));
						fileContent.setSize(resultSet.getLong("size"));
					}
					
					// Add chunk references
					ChunkChecksum chunkChecksum = ChunkChecksum.parseChunkChecksum(resultSet.getString("chunk_checksum"));
					fileContent.addChunk(chunkChecksum);
				}
	
				return fileContent;
			}
		}
		catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}
	
	private Map<FileChecksum, FileContent> createFileContents(ResultSet resultSet) throws SQLException {
		Map<FileChecksum, FileContent> fileContents = new HashMap<FileChecksum, FileContent>();	
		FileChecksum currentFileChecksum = null;
		
		while (resultSet.next()) {		
			FileChecksum fileChecksum = FileChecksum.parseFileChecksum(resultSet.getString("checksum"));
			FileContent fileContent = null;
			
			if (currentFileChecksum != null && currentFileChecksum.equals(fileChecksum)) {
				fileContent = fileContents.get(fileChecksum);	
			}
			else {
				fileContent = new FileContent();
				
				fileContent.setChecksum(fileChecksum);
				fileContent.setSize(resultSet.getLong("size"));
			}
			
			ChunkChecksum chunkChecksum = ChunkChecksum.parseChunkChecksum(resultSet.getString("chunk_checksum"));
			fileContent.addChunk(chunkChecksum);

			fileContents.put(fileChecksum, fileContent); 
			currentFileChecksum = fileChecksum;
		}
		
		return fileContents;
	}

	/**
	 * no commit
	 */
	public void updateDirtyFileContentsNewDatabaseId(long newDatabaseVersionId) {
		try (PreparedStatement preparedStatement = getStatement("filecontent.update.dirty.updateDirtyFileContentsNewDatabaseId.sql")) {
			preparedStatement.setLong(1, newDatabaseVersionId);
			preparedStatement.executeUpdate();
		}
		catch (SQLException e) {
			throw new RuntimeException(e);
		}		
	}
}
