package com.tandbergtv.watchpoint.pmm.core;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

import org.apache.log4j.Logger;

import com.tandbergtv.workflow.core.service.ServiceRegistry;
import com.tandbergtv.workflow.sanmanager.SANManagement;
import com.tandbergtv.workflow.sanmanager.SANManagementException;
import com.tandbergtv.workflow.sanmanager.entities.SANDrive;

/**
 * Manages unmapped assets.
 * Make sure to demarcate a transaction with {@link #beginTransaction()} and
 *  {@link #commitTransaction()} or {@link #rollbackTransaction()}.
 * The transaction is committed by default.
 * 
 * @author Raj Prakash
 */
public class AssetsManager {
	private static final Logger logger = Logger.getLogger(AssetsManager.class);
	
	private static final String UNMAPPED_FILES_DRIVE = "Unmapped Assets";
	private static final String MAPPED_FILES_DRIVE = "Mapped Assets";

	private static class Record {
		private File src, dest;
		
		Record(File src, File dest) {
			this.src = src;
			this.dest = dest;
		}
		
		@Override
		public String toString() {
			return "Source: " + src + " | Destination: " + dest;
		}
	}
	
	private static boolean initialized = false;
	private static File unmappedFilesDirectory, mappedFilesDirectory;
	private static ThreadLocal<AssetsManager> instances = new ThreadLocal<AssetsManager>();
	private Stack<Record> records = new Stack<Record>();
	
	static {
		try {
			unmappedFilesDirectory = getDriveRoot(UNMAPPED_FILES_DRIVE);
			mappedFilesDirectory = getDriveRoot(MAPPED_FILES_DRIVE);
			initialized = true;
		} catch (SANManagementException e) {
			logger.error("Failed to initialize.", e);
		}
	}
	
	/**
	 * Constructor.
	 * 
	 * @throws RuntimeException
	 */
	private AssetsManager() {
		if(!initialized)
			throw new RuntimeException("Not initialized properly. Check previous errors.");
	}
	
	/**
	 * Gets thread-local instance of this class.
	 * 
	 * @return
	 * @throws RuntimeException
	 */
	public static AssetsManager getThreadLocalInstance() {
		AssetsManager instance = instances.get();
		if(instance == null) {
			instance = new AssetsManager();
			instances.set(instance);
		}
		return instance;
	}
	
	/**
	 * Gets unmapped files directory
	 */
	public File getUnmappedFilesDirectory() {
		return unmappedFilesDirectory;
	}

	/**
	 * Gets mapped files directory
	 */
	public File getMappedFilesDirectory() {
		return mappedFilesDirectory;
	}
	
	/**
	 * Gets unmapped files.
	 * 
	 * @throws IOException	for any file system access exceptions
	 */
	public List<File> getUnmappedFiles() throws IOException {
		return getAllFiles(unmappedFilesDirectory); 
	}
	
	/**
	 * Gets unmapped file of the given name.
	 * Returns null if not found.
	 * 
	 * @throws IOException	for any file system access exceptions
	 */
	public File getUnmappedFile(String fileName) throws IOException {
		return getFile(unmappedFilesDirectory, fileName); 
	}

	/**
	 * Moves the given file to mapped files directory.
	 * Also, deletes the parent directory of the given file, if empty. 
	 * 
	 * @param file				the file to move
	 * @return					the file that refers to the moved location
	 * @throws IOException		when file not found or any file system exceptions
	 * @throws RuntimeException	when the given file is not a normal file
	 */
	public File moveToMappedFilesDirectory(File file) throws IOException, RuntimeException  {
		//validation
		if(!file.exists())
			throw new FileNotFoundException("File does not exist. File: " + file);
		if(!file.isFile())
			throw new RuntimeException("File is not a normal file. File: " + file);
		
		//create a File instance for target file 
		String targetDirName = file.getName() + "_" + System.currentTimeMillis();
		File targetDir = new File(mappedFilesDirectory,  targetDirName);
		File targetFile = new File(targetDir, file.getName());
		
		//move from file to targetFile
		move(file, targetFile);

		//record
		records.push(new Record(file, targetFile));
		
		return targetFile;
	}
	
	/**
	 * Moves the given file to unmapped files directory.
	 * Also, deletes the parent directory of the given file, if empty. 
	 * 
	 * @param file				the file to move
	 * @return					the file that refers to the moved location
	 * @throws IOException		when file not found or any file system exceptions
	 * @throws RuntimeException	when the given file is not a normal file
	 */
	public File moveToUnmappedFilesDirectory(File file) throws IOException, RuntimeException  {
		//validation
		if(!file.exists())
			throw new FileNotFoundException("File does not exist. File: " + file);
		if(!file.isFile())
			throw new RuntimeException("File is not a normal file. File: " + file);
		
		//create a File instance for target file 
		String targetDirName = file.getName() + "_" + System.currentTimeMillis();
		File targetDir = new File(unmappedFilesDirectory,  targetDirName);
		File targetFile = new File(targetDir, file.getName());
		
		//move from file to targetFile
		move(file, targetFile);

		//record
		records.push(new Record(file, targetFile));
		
		return targetFile;
	}
	
	/**
	 * Begins a new transaction.
	 */
	public void beginTransaction() {
		logger.debug("Begining transaction");
		records.clear();
	}
	
	/**
	 * Commits the given transaction.
	 */
	public void commitTransaction() {
		logger.debug("Committing transaction");
		records.clear();
	}

	/**
	 * Rolls back the transaction to the last begun/committed point.
	 * 
	 * @throws RuntimeException	when there is a failure
	 */
	public void rollbackTransaction() {
		logger.debug("Rolling back transaction. Records: " + records);
		List<Record> failedRecords = new ArrayList<Record>();
		while(!records.isEmpty()) {
			Record r = records.pop();
			try {
				move(r.dest, r.src);
			} catch(Exception e) {
				failedRecords.add(r);
			}
		}
		records.clear();
		if(!failedRecords.isEmpty())
			throw new RuntimeException("Rollback failed for: " + failedRecords);
	}

	/*
	 * Create directories for dest.
	 * Move the file.
	 * Delete the parent directory of src if move successful and if empty.
	 * Delete the parent directory of dest if move failed and if empty.
	 */
	private void move(File src, File dest) throws IOException {
		//create destination directory path
		dest.getParentFile().mkdirs();
		
		//move
		if(src.renameTo(dest)) {
			logger.debug("Moved file: " + src + " to: " + dest);
			deleteIfEmpty(src.getParentFile());
		} else {
			deleteIfEmpty(dest.getParentFile());
			throw new IOException("Failed to move file: " + src + " to: " + dest);	
		}
	}
	
	private void deleteIfEmpty(File dir) {
		String[] contents = dir.list();
		if(contents == null) {
			logger.warn("Failed to delete directory: " + dir
					+ " | Reason: Failed to get list of files in the directory");
			return;
		}

		if(contents.length == 0) {
			if(dir.delete())
				logger.debug("Deleted directory: " + dir);
			else
				logger.warn("Failed to delete directory: " + dir);
		}
	}
	
	/**
	 * Gets the root directory of the drive with the given name.
	 * 
	 * @throws SANManagementException	when the unmapped files drive does not exist
	 */
	private static File getDriveRoot(String driveName) throws SANManagementException {
		SANManagement sanmanager = ServiceRegistry.getDefault().lookup(SANManagement.class);
		SANDrive drive = sanmanager.getSANDrive(driveName);
		return new File(drive.getPath());
	}
	
	/**
	 * Gets list of files from the given directory and any of its descendant
	 * sub directories.
	 * 
	 * @throws IOException	for any file system access exceptions
	 */
	private List<File> getAllFiles(File dirFile) throws IOException {
		List<File> files = new ArrayList<File>();
		
		File[] dirContents = dirFile.listFiles();

		if(dirContents == null)
			throw new IOException("IO Exception occurred while trying to get list of files " +
					"for directory: " + dirFile.getAbsolutePath());
		
		for(File file : dirContents) {
			if(file.isFile())
				files.add(file);
			else
				files.addAll(getAllFiles(file));
		}
		
		return files;
	}

	/**
	 * Gets the file of the given name from the given directory and any of its descendant
	 * sub directories.
	 * 
	 * @throws IOException	for any file system access exceptions
	 */
	private File getFile(File dirFile, String fileName) throws IOException {
		
		File[] dirContents = dirFile.listFiles();

		if(dirContents == null)
			throw new IOException("IO Exception occurred while trying to get list of files " +
					"for directory: " + dirFile.getAbsolutePath());
		
		for(File file : dirContents) {
			if(file.isFile()) {
				if(file.getName().equals(fileName))
					return file;
			}
			else {
				File foundFile = getFile(file, fileName);
				if(foundFile != null)
					return foundFile;
			}
		}
		
		return null;
	}

}
