package cn.edu.seu.ise.dca.analysis;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.log4j.Logger;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLogEntry;
import org.tmatesoft.svn.core.SVNLogEntryPath;
import org.tmatesoft.svn.core.SVNNodeKind;
import org.tmatesoft.svn.core.io.SVNRepository;

import cn.edu.seu.ise.dca.app.php.model.PHPFile;
import cn.edu.seu.ise.dca.app.php.model.PHPFileChunk;
import cn.edu.seu.ise.dca.app.php.parser.PHPParser;
import cn.edu.seu.ise.dca.global.Global;
import cn.edu.seu.ise.dca.model.ChangedFile;
import cn.edu.seu.ise.dca.model.Revision;
import cn.edu.seu.ise.dca.util.FileComparator;
import cn.edu.seu.ise.dca.util.ListUtil;
import cn.edu.seu.ise.dca.util.URLUtil;

public class SVNCollector {
	private SVNRepository repository;
	
	static Logger logger = Logger.getLogger(SVNCollector.class);

	public SVNCollector(SVNRepository repository) {
		this.repository = repository;
	}
	
	@SuppressWarnings("rawtypes")
	public List<Revision> getRevisions(String[] targetPaths,  long startRevision, long endRevision, List<Long> selectedRevisions,
			List<String> includedUrl, List<String> excludedUrl,  boolean selectedPostFix) {
		if(selectedRevisions == null || selectedRevisions.size() == 0) {
			return getRevisions(targetPaths, startRevision, endRevision, includedUrl, excludedUrl, selectedPostFix);
		} else {
			final List<Revision> revisions = new ArrayList<Revision>();
			Collection logEntries = null;
			try {
				logEntries = repository.log(targetPaths, null, startRevision, endRevision, true, true);		
			} catch (SVNException e) {
				logger.error(e);
			}
			
			for (Iterator entries = logEntries.iterator(); entries.hasNext();) {
				SVNLogEntry logEntry = (SVNLogEntry) entries.next();
				if(ListUtil.isInList(logEntry.getRevision(), selectedRevisions)) {
					logger.info("Start to get information from repository: revision " + logEntry.getRevision());
					//create a new revision object to store information
					Revision revision = getRevision(logEntry, includedUrl, excludedUrl, selectedPostFix);
					//Add the constructed revision into the list
					revisions.add(revision);
				}
			}
			return revisions;
		}
		
	}

	
	/**
	 * 
	 * @param url specifies the file url in repository 
	 * @param revision specifies the revision id to check out
	 * @return
	 */
	public String checkoutFile(String url, long revision) {
		//String filePath = Global.outputDir + URLUtil.generateFileName(url, revision);
		String filePath = URLUtil.generateFilePath(Global.DOWNLOAD_DIR, url, revision);
		File f = new File(filePath);
		//Check whether the file has been checked out
		if(!f.exists()) {
			OutputStream os = null;
			try {
				 os = new FileOutputStream(filePath);
				 repository.getFile(url, revision, null, os);
				 //Log the checkout information to the log file 
				 //logger.info("Checking out files from repository: revision " + revision + " " + url);
			} catch (FileNotFoundException e) {
				logger.error(e);
			} catch (SVNException e) {
				logger.error(e);
			}
		}
		return filePath;
	}
	
	/**
	 * Get the number of revision related to the target paths
	 * @param targetPaths
	 * @param startRevision
	 * @param endRevision
	 * @return
	 */
	@SuppressWarnings("rawtypes")
	public long sizeOfRevisions(String[] targetPaths, long startRevision, long endRevision) {
		Collection logEntries = null;
		//Set the paths that mean only those revisions at which they were changed
		try {
			logEntries = repository.log(targetPaths, null, startRevision, endRevision, true, true);		
		} catch (SVNException e) {
			logger.error(e);
		}
		
		return logEntries.size();
	}
	
	/**
	 * Print all the number of revisions related to the target paths
	 * @param targetPaths
	 * @param startRevision
	 * @param endRevision
	 */
	@SuppressWarnings("rawtypes")
	public void printSelectedRevisions(String[] targetPaths, long startRevision, long endRevision) {
		Collection logEntries = null;
		//Set the paths that mean only those revisions at which they were changed
		try {
			logEntries = repository.log(targetPaths, null, startRevision, endRevision, true, true);	
			for (Iterator entries = logEntries.iterator(); entries.hasNext();) {
				SVNLogEntry logEntry = (SVNLogEntry) entries.next();
				System.out.println(logEntry.getRevision());
			}
		} catch (SVNException e) {
			logger.error(e);
		}
	}
	
	
	/**
	 * Get all informations stored in SVN for each revision (including 
	 * revision id, commit author, commit date, and list of changed files.
	 * We store all the information in a list for the further analysis)
	 */
	@SuppressWarnings("rawtypes")
	private List<Revision> getRevisions(String[] targetPaths, long startRevision, long endRevision, List<String> includedUrls, List<String> excludedUrls, boolean selectedPostFix) {
		final List<Revision> revisions = new ArrayList<Revision>();
		Collection logEntries = null;
		/*
		 * Set the paths that mean only those revisions at which they were
		 * changed
		 */
		try {
			logEntries = repository.log(targetPaths, null, startRevision, endRevision, true, true);		
		} catch (SVNException e) {
			logger.error(e);
		}
		
		for (Iterator entries = logEntries.iterator(); entries.hasNext();) {
			SVNLogEntry logEntry = (SVNLogEntry) entries.next();
			logger.info("Start to get information from repository: revision " + logEntry.getRevision());
			//create a new revision object to store information
			Revision revision = getRevision(logEntry, includedUrls, excludedUrls, selectedPostFix);
			//Add the constructed revision into the list
			revisions.add(revision);
		}
		return revisions;
	}
	

	/**
	 * Get the revision information according to the SVNLogEntry 
	 * @param logEntry
	 * @param includedUrls
	 * @param excludedUrls
	 * @param selectedPostFix
	 * @return
	 */
	@SuppressWarnings("rawtypes")
	private Revision getRevision(SVNLogEntry logEntry, List<String> includedUrls, List<String> excludedUrls, boolean selectedPostFix) {
		Revision revision = new Revision(logEntry.getRevision(), logEntry.getAuthor(), logEntry.getDate());
		
		// Get all changed paths
		Set changedPathsSet = logEntry.getChangedPaths().keySet();
		Iterator changedPathsItor = changedPathsSet.iterator();
		
		while (changedPathsItor.hasNext()) {
			//A boolean to show that whether the file are PHP-related files
			Boolean isConsidered = true;
			//obtains a next SVNLogEntryPath
			SVNLogEntryPath entryPath = (SVNLogEntryPath) logEntry.getChangedPaths().get(changedPathsItor.next());
			//Check whether the file that entryPath represents are PHP-related file
			if(selectedPostFix) {
				String postFix = URLUtil.getFileSuffix(entryPath.getPath());
				if(!ListUtil.isInArray(postFix, Global.POSTFIX)) {
					isConsidered = false;
				}
			} 
			
			if(includedUrls != null && includedUrls.size() > 0) {
				if(!ListUtil.isStartWith(entryPath.getPath(), includedUrls)) {
					isConsidered = false;
				}
			}
			
			if(includedUrls != null && includedUrls.size() > 0) {
				if(ListUtil.isStartWith(entryPath.getPath(), excludedUrls)) {
					isConsidered = false;
				}
			}
			
			//If the file is php_related files, create changedFile
			if(isConsidered) {
				//create a changedFile object to store information
				ChangedFile changedFile = new ChangedFile(entryPath.getPath(), entryPath.getType());
				//Set the file type of changed files (file or directory)
				SVNNodeKind nodeKind = null;
				try {
					nodeKind = repository.checkPath(entryPath.getPath(), revision.getId());
				} catch (SVNException svne) {
					logger.error(svne);
				}
				changedFile.setFileType(nodeKind);
				//If the changed path is file
				if(changedFile.getFileType() == SVNNodeKind.FILE) {
					//file is modified
					if(changedFile.getChangeType() == SVNLogEntryPath.TYPE_MODIFIED || changedFile.getChangeType() == SVNLogEntryPath.TYPE_REPLACED) {
						String original = checkoutFile(entryPath.getPath(), revision.getId() - 1);
						String revised = checkoutFile(entryPath.getPath(), revision.getId());
						FileComparator comparator = new FileComparator(original, revised);
						changedFile.setPatch(comparator.getPatch());
						//Parse Both the original file and revised file and pass the reference to the changed files
						logger.info("Start to parse revision " + (revision.getId()-1) + ": " + changedFile.getUrl());
						PHPFile originalPHPFile = PHPParser.getPHPFile(changedFile.getUrl(), revision.getId() - 1);
						logger.info("Start to parse revision " + revision.getId() + ": " + changedFile.getUrl());
						PHPFile revisedPHPFile = PHPParser.getPHPFile(changedFile.getUrl(), revision.getId());
						changedFile.setPHPFileChunk(new PHPFileChunk(originalPHPFile, revisedPHPFile));
						//revision.addChangedFile(changedFile);
					}
					//file is newly added
					if(changedFile.getChangeType() == SVNLogEntryPath.TYPE_ADDED) {
						//Create a null file for original version
						String original = URLUtil.generateFilePath(Global.DOWNLOAD_DIR, changedFile.getUrl(), revision.getId() - 1);
						try {
							File oriFile = new File(original);
							oriFile.createNewFile();
						} catch (IOException e) {
							logger.error(e);
						}
						//Check out revised version from repository
						String revised = checkoutFile(entryPath.getPath(), revision.getId());
						FileComparator comparator = new FileComparator(original, revised);
						changedFile.setPatch(comparator.getPatch());
						//Parse Both the original file and revised file and pass the reference to the changed files
						logger.info("Start to parse revision " + (revision.getId()-1) + ": " + changedFile.getUrl());
						PHPFile originalPHPFile = PHPParser.getPHPFile(changedFile.getUrl(), revision.getId() - 1);
						logger.info("Start to parse revision " + revision.getId() + ": " + changedFile.getUrl());
						PHPFile revisedPHPFile = PHPParser.getPHPFile(changedFile.getUrl(), revision.getId());
						changedFile.setPHPFileChunk(new PHPFileChunk(originalPHPFile, revisedPHPFile));
					}
					revision.addChangedFile(changedFile);
				}
				//file is deleted
				if(changedFile.getFileType() == SVNNodeKind.NONE) {
					if(changedFile.getChangeType() == SVNLogEntryPath.TYPE_DELETED) {
						//Check out original version from repository
						String original = checkoutFile(entryPath.getPath(), revision.getId() - 1);
						//Create a null file for revised version
						String revised = URLUtil.generateFilePath(Global.DOWNLOAD_DIR, changedFile.getUrl(), revision.getId());
						try {
							File revFile = new File(revised);
							revFile.createNewFile();
						} catch (IOException e) {
							logger.error(e);
						}
						FileComparator comparator = new FileComparator(original, revised);
						changedFile.setPatch(comparator.getPatch());
						//Parse Both the original file and revised file and pass the reference to the changed files
						logger.info("Start to parse revision " + (revision.getId()-1) + ": " + changedFile.getUrl());
						PHPFile originalPHPFile = PHPParser.getPHPFile(changedFile.getUrl(), revision.getId() - 1);
						logger.info("Start to parse revision " + revision.getId() + ": " + changedFile.getUrl());
						PHPFile revisedPHPFile = PHPParser.getPHPFile(changedFile.getUrl(), revision.getId());
						changedFile.setPHPFileChunk(new PHPFileChunk(originalPHPFile, revisedPHPFile));
					}
					revision.addChangedFile(changedFile);
				}
				//The file type is directory
				if(changedFile.getFileType() == SVNNodeKind.DIR) {
					//Do nothing and do not add it to the changed file list
				}
				//revision.addChangedFile(changedFile);
			} /* The end of if(isConsidered)*/
		}
		return revision;
	}
	
	
}
