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

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.tmatesoft.svn.core.SVNNodeKind;

import cn.edu.seu.ise.dca.app.php.model.PHPClass;
import cn.edu.seu.ise.dca.app.php.model.PHPFileChunk;
import cn.edu.seu.ise.dca.app.php.model.PHPFunction;
import cn.edu.seu.ise.dca.util.ListUtil;
import cn.edu.seu.ise.dca.util.StringUtil;
import difflib.Delta;
import difflib.Patch;

public class ChangedFile {
	private String url;
	private char changeType;
	private SVNNodeKind fileType;
	private Patch patch;
	private PHPFileChunk phpFileChunk;

	public ChangedFile(String url, char changeType) {
		this.url = url;
		this.changeType = changeType;
		this.patch = new Patch();
	}

	public ChangedFile(String url, char changeType, SVNNodeKind fileType) {
		this(url, changeType);
		this.fileType = fileType;
	}

	public String getUrl() {
		return url;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	public char getChangeType() {
		return changeType;
	}

	public void setChangeType(char changeType) {
		this.changeType = changeType;
	}

	public List<Delta> getDeltas() {
		return patch.getDeltas();
	}
	
	public Patch getPatch() {
		return patch;
	}

	public void setPatch(Patch patch) {
		List<Delta> deltas = patch.getDeltas();
		for(Delta delta : deltas) {
			this.patch.addDelta(delta);
		}
	}
	
	public PHPFileChunk getPHPFileChunk() {
		return phpFileChunk;
	}

	public void setPHPFileChunk(PHPFileChunk phpFIleChunk) {
		this.phpFileChunk = phpFIleChunk;
	}

	public SVNNodeKind getFileType() {
		return fileType;
	}

	public void setFileType(SVNNodeKind fileType) {
		this.fileType = fileType;
	}
	
	/**
	 * Add delta into the delta list
	 * @param delta
	 */
	public void addDeltas(Delta delta) {
		this.patch.addDelta(delta);
	}
	
	/**
	 * Return the code lines of the current file
	 * @param isRevised specifies whether lines are of original or revised versions
	 * @return
	 */
	public long sizeOfLines(boolean isRevised) {
		if(isRevised) {
			return this.phpFileChunk.sizeOfRevisedLines();
		} else {
			return this.phpFileChunk.sizeOfOriginalLines();
		}
	}
	
	/**
	 * Return the actual code lines of the current file
	 * @param isRevised specifies whether lines are of original or revised versions
	 * @return
	 */
	public long sizeOfActLines(boolean isRevised) {
		if(isRevised) {
			return this.phpFileChunk.sizeOfRevisedActLines();
		} else {
			return this.phpFileChunk.sizeOfOriginalActLines();
		}
	}
	
	/**
	 * Return the number of all classes in the current file
	 * @param isRevised specifies whether lines are of original or revised versions
	 * @return
	 */
	public long sizeOfClasses(boolean isRevised) {
		if(isRevised) {
			return this.phpFileChunk.getRevised().sizeOfClasses();
		} else {
			return this.phpFileChunk.getOriginal().sizeOfClasses();
		}
	}
	
	/**
	 * Return the number of all global functions in the current file
	 * @param isRevised specifies whether lines are of original or revised versions
	 * @return
	 */
	public long sizeOfGlobalFunctions(boolean isRevised) {
		if(isRevised) {
			return this.phpFileChunk.getRevised().sizeOfGlobalFunctions();
		} else {
			return this.phpFileChunk.getOriginal().sizeOfGlobalFunctions();
		}
	}
	
	/**
	 * Return the number of all member functions in the current file
	 * @param isRevised specifies whether lines are of original or revised versions
	 * @return
	 */
	public long sizeOfMemberFunctions(boolean isRevised) {
		if(isRevised) {
			return this.phpFileChunk.getRevised().sizeOfMemberFunctions();
		} else {
			return this.phpFileChunk.getOriginal().sizeOfMemberFunctions();
		}
	}
	
	/**
	 * Calculate the added lines in changed file.
	 * @param isActual specifies whether actual code lines (without comment and blanks) are calculate
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public long sizeOfAddedLines(boolean isActual) {
		long lines = 0;
		List<Delta> deltas = patch.getDeltas();
		for(Delta delta : deltas) {
			if(delta.getType() == Delta.TYPE.INSERT) 
				if(isActual) {
					for(String line : (List<String>)delta.getRevised().getLines()) {
						if(StringUtil.isActualLine(line)) {
							lines ++;
						}
					}
				} else {
					lines += delta.getRevised().size();
				}
		}
		return lines;
	}
	
	/**
	 * Calculate the deleted lines in changed file.
	 * @param isActual specifies whether actual code lines (without comment and blanks) are calculate
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public long sizeOfDeletedLines(boolean isActual) {
		long lines = 0;
		List<Delta> deltas = patch.getDeltas();
		for(Delta delta : deltas) {
			if(delta.getType() == Delta.TYPE.DELETE) 
				if(isActual) {
					for(String line : (List<String>)delta.getOriginal().getLines()) {
						if(StringUtil.isActualLine(line)) {
							lines ++;
						}
					}
				} else {
					lines += delta.getOriginal().size();
				}
				
		}
		return lines;
	} 
	
	/**
	 * Calculate the modified lines in changed file according to the selected version
	 * @param isRevised specifies the revised version if isRevised is true
	 * @param isActual specifies whether actual code lines (without comment and blanks) are calculate
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public long sizeOfModifiedLines(boolean isRevised, boolean isActual) {
		long lines = 0;
		List<Delta> deltas = patch.getDeltas();
		for(Delta delta : deltas) {
			if(delta.getType() == Delta.TYPE.CHANGE) {
				if(isRevised) {
					if(isActual) {
						for(String line : (List<String>)delta.getRevised().getLines()) {
							if(StringUtil.isActualLine(line)) {
								lines ++;
							}
						}
					} else {
						lines += delta.getRevised().size();
					}
				} else {
					if(isActual) {
						for(String line : (List<String>)delta.getOriginal().getLines()) {
							if(StringUtil.isActualLine(line)) {
								lines ++;
							}
						}
					} else {
						lines += delta.getOriginal().size();
					}
					
				}
			}
		}
		return lines;
	}
	
	/**
	 * Calculate the changed lines in changed file. Respect to the original version 
	 * (which is isRevised is false), return the line number of lines to be changed 
	 * and deleted. Respect to the revised version (which is isRevised is true),
	 * return the number of lines that have been added and changed
	 * @param isRevised isRevised specifies the revised version if isRevised is true
	 * @param isActual specifies whether actual code lines (without comment and blanks) are calculate
	 * @return
	 */
	public long sizeOfChangedLines(boolean isRevised, boolean isActual) {
		long lines = 0;
		if(isRevised) {
			lines += sizeOfAddedLines(isActual) ;
		} else {
			lines += sizeOfDeletedLines(isActual);
		}
		lines += sizeOfModifiedLines(isRevised, isActual);
		return lines;
	}
	
	/**
	 * Return the number of totally changed classes(including added, deleted and modified)
	 * @return
	 */
	public int sizeOfChangedClasses() {
		return sizeOfAddedClasses() + sizeOfDeletedClasses() + sizeOfModifiedClasses();
	}
	
	/**
	 * Return the number of newly added classes
	 * @return
	 */
	public int sizeOfAddedClasses() {
		return this.phpFileChunk.sizeOfAddedClasses();
	}
	
	/**
	 * Return the number of deleted classes
	 * @return
	 */
	public int sizeOfDeletedClasses() {
		return this.phpFileChunk.sizeOfDeletedClasses();
	}
	
	/**
	 * Return the number of modified classes
	 * @return
	 */
	public int sizeOfModifiedClasses() {
		if(this.changeType == 'M') {
			List<PHPClass> phpClasses = this.phpFileChunk.getRemainingClasses(true);
			List<Delta> deltas  = this.getDeltas();
			//modifiedClasses is used to store the full name of classes that are modified
			Set<String> modifiedClasses = new HashSet<String>();
			
			for(PHPClass phpClass : phpClasses) {
				for(Delta delta : deltas) {
					long startLine = delta.getRevised().getPosition() + 1;
					long endLine = delta.getRevised().last() + 1;
					if( (startLine >= phpClass.getStartLine() && startLine <= phpClass.getEndLine()) || 
							(endLine >= phpClass.getStartLine() && endLine <= phpClass.getEndLine())) {
						modifiedClasses.add(phpClass.getFullName());
					}
				}
			}
			return modifiedClasses.size();
		} else {
			//If the file is newly added or deleted, the number of modified classes is zero 
			return 0;
		}
	}
	
	/**
	 * Return the number of totally changed functions (including added, deleted and modified)
	 * @return
	 */
	public int sizeOfChangedFunctions() {
		return sizeOfAddedFunctions() + sizeOfDeletedFunctions() + sizeOfModifiedFunctions();
	}
	
	/**
	 * Return the number of newly added functions
	 * @return
	 */
	public int sizeOfAddedFunctions() {
		return phpFileChunk.sizeOfAddedFunctions();
	}
	
	/**
	 * Return the number of deleted functions
	 * @return
	 */
	public int sizeOfDeletedFunctions() {
		return phpFileChunk.sizeOfDeletedFunctions();
	}
	
	/**
	 * Return the number of modified functions
	 * @return
	 */
	public int sizeOfModifiedFunctions() {
		return sizeOfModifiedGlobalFunctions() + sizeOfModifiedMemberFunctions();
	}
	
	/**
	 * Return the number of all changed member functions in the changed file
	 * @return
	 */
	public int sizeOfChangedMemberFunctions() {
		return sizeOfAddedMemberFunctions() + sizeOfDeletedMemberFunctions() + sizeOfModifiedMemberFunctions();
	}
	
	/**
	 * Return the number of all added member functions in the changed file 
	 * @return
	 */
	public int sizeOfAddedMemberFunctions() {
		return this.phpFileChunk.sizeOfAddedMemberFunctions();
	}
	
	/**
	 * Return the number of all deleted member functions in the changed file 
	 * @return
	 */
	public int sizeOfDeletedMemberFunctions() {
		return this.phpFileChunk.sizeOfDeletedMemberFunctions();
	}
	
	/**
	 * Return the number of all modified member functions in the changed file
	 * @return
	 */
	public int sizeOfModifiedMemberFunctions() {
		if(this.changeType == 'M') {
			List<PHPFunction> phpFunctions = this.phpFileChunk.getRemainingMemberFunctions(true);
			List<Delta> deltas  = this.getDeltas();
			//modifiedClasses is used to store the full name of classes that are modified
			Set<String> modifiedMemberFunctions = new HashSet<String>();
			
			for(PHPFunction phpFunction : phpFunctions) {
				for(Delta delta : deltas) {
					long startLine = delta.getRevised().getPosition() + 1;
					long endLine = delta.getRevised().last() + 1;
					if( (startLine >= phpFunction.getStartLine() && startLine <= phpFunction.getEndLine()) || 
							(endLine >= phpFunction.getStartLine() && endLine <= phpFunction.getEndLine())) {
						modifiedMemberFunctions.add(phpFunction.getFullName());
					}
				}
			}
			return modifiedMemberFunctions.size();
		} else {
			//If the file is newly added or deleted, the number of modified classes is zero 
			return 0;
		}
	}
	
	/**
	 * Return the number of all changed member functions in the changed file
	 * @return
	 */
	public int sizeOfChangedGlobalFunctions() {
		return sizeOfAddedGlobalFunctions() + sizeOfDeletedGlobalFunctions() + sizeOfModifiedGlobalFunctions();
	}
	
	/**
	 * Return the number of all added member functions in the changed file 
	 * @return
	 */
	public int sizeOfAddedGlobalFunctions() {
		return this.phpFileChunk.sizeOfAddedGlobalFunctions();
	}
	
	/**
	 * Return the number of all deleted member functions in the changed file 
	 * @return
	 */
	public int sizeOfDeletedGlobalFunctions() {
		return this.phpFileChunk.sizeOfDeletedGlobalFunctions();
	}
	
	/**
	 * Return the number of all modified Global functions in the changed file
	 * @return
	 */
	public int sizeOfModifiedGlobalFunctions() {
		if(this.changeType == 'M') {
			List<PHPFunction> phpFunctions = this.phpFileChunk.getRemainingGlobalFunctions(true);
			List<Delta> deltas  = this.getDeltas();
			//modifiedClasses is used to store the full name of classes that are modified
			Set<String> modifiedGlobalFunctions = new HashSet<String>();
			
			for(PHPFunction phpFunction : phpFunctions) {
				for(Delta delta : deltas) {
					long startLine = delta.getRevised().getPosition() + 1;
					long endLine = delta.getRevised().last() + 1;
					if( (startLine >= phpFunction.getStartLine() && startLine <= phpFunction.getEndLine()) || 
							(endLine >= phpFunction.getStartLine() && endLine <= phpFunction.getEndLine())) {
						modifiedGlobalFunctions.add(phpFunction.getFullName());
					}
				}
			}
			return modifiedGlobalFunctions.size();
		} else {
			//If the file is newly added or deleted, the number of modified classes is zero 
			return 0;
		}
	}
	
	/**
	 * Get the list of modified classes
	 * @return
	 */
	public List<PHPClass> getModifiedClasses() {
		//Get the list of classes that are remaining in both original and revised versions
		List<PHPClass> remainingClasses = this.phpFileChunk.getRemainingClasses(true);
		//Create a member function list to store the functions that are modified
		List<PHPClass> modifiedClasses = new ArrayList<PHPClass>();
		//Get the modified deltas in the file
		List<Delta> deltas  = this.getDeltas();
		for(PHPClass phpClass : remainingClasses) {
			for(Delta delta : deltas) {
				long startLine = delta.getRevised().getPosition() + 1;
				long endLine = delta.getRevised().last() + 1;
				if( (startLine >= phpClass.getStartLine() && startLine <= phpClass.getEndLine()) || 
						(endLine >= phpClass.getStartLine() && endLine <= phpClass.getEndLine())) {
					//If the modified member function is not in the list, add it
					if(!ListUtil.isInList(phpClass, modifiedClasses)) {
						modifiedClasses.add(phpClass);
					}
					
				}
			}
		}
		return modifiedClasses;
	}
	
	/**
	 * Get the list of the added classes (The method is implemented in PHPFileChunk)
	 * @return
	 */
	public List<PHPClass> getAddedClasses() {
		return this.phpFileChunk.getAddedClasses();
	}
	
	/**
	 * Get the list of the deleted classes (The method is implemented in PHPFileChunk)
	 * @return
	 */
	public List<PHPClass> getDeletedClasses() {
		return this.phpFileChunk.getDeletedClasses();
	}
	
	/**
	 * Get the list of the modified member functions
	 * @return
	 */
	public List<PHPFunction> getModifiedMemberFunctions() {
		//Get the list of member functions that are remaining in both original and revised versions
		List<PHPFunction> remainingMemberFunctions = this.phpFileChunk.getRemainingMemberFunctions(true);
		//Create a member function list to store the functions that are modified
		List<PHPFunction> modifiedMemberFunctions = new ArrayList<PHPFunction>();
		//Get the modified deltas in the file
		List<Delta> deltas  = this.getDeltas();
		for(PHPFunction phpFunction : remainingMemberFunctions) {
			for(Delta delta : deltas) {
				long startLine = delta.getRevised().getPosition() + 1;
				long endLine = delta.getRevised().last() + 1;
				if( (startLine >= phpFunction.getStartLine() && startLine <= phpFunction.getEndLine()) || 
						(endLine >= phpFunction.getStartLine() && endLine <= phpFunction.getEndLine())) {
					//If the modified member function is not in the list, add it
					if(!ListUtil.isInList(phpFunction, modifiedMemberFunctions)) {
						modifiedMemberFunctions.add(phpFunction);
					}
					
				}
			}
		}
		return modifiedMemberFunctions;
	}
	
	/**
	 * Get the list of the added member functions (The method is implemented in PHPFileChunk)
	 * @return
	 */
	public List<PHPFunction> getAddedMemberFunctions() {
		return this.phpFileChunk.getAddedMemberFunctions();
	}
	
	/**
	 * Get the list of the deleted member functions (The method is implemented in PHPFileChunk)
	 * @return
	 */
	public List<PHPFunction> getDeletedMemberFunctions() {
		return this.phpFileChunk.getDeletedMemberFunctions();
	}
	
	/**
	 * Get the list of the modified global functions
	 * @return
	 */
	public List<PHPFunction> getModifiedGlobalFunctions() {
		//Get the list of global functions that are remaining in both original and revised versions
		List<PHPFunction> remainingGlobalFunctions = this.phpFileChunk.getRemainingGlobalFunctions(true);
		//Create a member function list to store the functions that are modified
		List<PHPFunction> modifiedGlobalFunctions = new ArrayList<PHPFunction>();
		//Get the modified deltas in the file
		List<Delta> deltas  = this.getDeltas();
		for(PHPFunction phpFunction : remainingGlobalFunctions) {
			for(Delta delta : deltas) {
				long startLine = delta.getRevised().getPosition() + 1;
				long endLine = delta.getRevised().last() + 1;
				if( (startLine >= phpFunction.getStartLine() && startLine <= phpFunction.getEndLine()) || 
						(endLine >= phpFunction.getStartLine() && endLine <= phpFunction.getEndLine())) {
					//If the modified member function is not in the list, add it
					if(!ListUtil.isInList(phpFunction, modifiedGlobalFunctions)) {
						modifiedGlobalFunctions.add(phpFunction);
					}
				}
			}
		}
		return modifiedGlobalFunctions;
	}
	
	/**
	 * Get the list of the added global functions
	 * @return
	 */
	public List<PHPFunction> getAddedGlobalFunctions() {
		return this.phpFileChunk.getAddedGlobalFunctions();
	}
	
	/**
	 * Get the list of the deleted global functions
	 * @return
	 */
	public List<PHPFunction> getDeletedGlobalFunctions() {
		return this.phpFileChunk.getDeletedGlobalFunctions();
	}
	
	/**
	 * Return the report of the changed line 
	 * @return
	 */
	public String reportChangedLines(boolean isActual) {
		StringBuffer buffer = new StringBuffer();
		if(!isActual) {
			buffer.append("LOCs [Original-").append(this.phpFileChunk.getOriginal().getLines())
			.append(" Revised-").append(this.phpFileChunk.getRevised().getLines());
		} else {
			buffer.append("LOCs [Original-").append(this.phpFileChunk.getOriginal().getActualLines())
			.append(" Revised-").append(this.phpFileChunk.getRevised().getActualLines());
		}
		buffer.append(" Added-").append(this.sizeOfAddedLines(isActual))
			.append(" Deleted-").append(this.sizeOfDeletedLines(isActual))
			.append(" Modifie-").append(this.sizeOfModifiedLines(false, isActual))
			.append(" Modifie(R)-").append(this.sizeOfModifiedLines(true, isActual))
			.append(" TChanges(O)-").append(this.sizeOfChangedLines(false, isActual))
			.append("TChanges(R)").append(this.sizeOfChangedLines(true, isActual))
			.append("]");
		return buffer.toString();
	}
	
	/**
	 * Return the report of the changed classes
	 * @return
	 */
	public String reportChangedClasses() {
		StringBuffer buffer = new StringBuffer();
		buffer.append("Class [Original-").append(this.phpFileChunk.getOriginal().sizeOfClasses())
		.append(" Revised-").append(this.phpFileChunk.getRevised().sizeOfClasses()).append(" Added-")
		.append(this.sizeOfAddedClasses()).append(" Deleted-").append(this.sizeOfDeletedClasses())
		.append(" Modifie-").append(this.sizeOfModifiedClasses()).append(" Total Changes-")
		.append(this.sizeOfChangedClasses()).append("]");
		
		return buffer.toString();	
	}
	
	/**
	 * Return the report of the changed global functions
	 * @return
	 */
	public String reportChangedGlobalFunctions() {
		StringBuffer buffer = new StringBuffer();
		buffer.append("GFunc [Original-").append(this.phpFileChunk.getOriginal().sizeOfGlobalFunctions())
		.append(" Revised-").append(this.phpFileChunk.getRevised().sizeOfGlobalFunctions()).append(" Added-")
		.append(this.sizeOfAddedGlobalFunctions()).append(" Deleted-").append(this.sizeOfDeletedGlobalFunctions())
		.append(" Modifie-").append(this.sizeOfModifiedGlobalFunctions()).append(" Total Changes-")
		.append(this.sizeOfChangedGlobalFunctions()).append("]");
		
		return buffer.toString();	
	}
	
	
	/**
	 * Return the report of the changed member functions
	 * @return
	 */
	public String reportChangedMemberFunctions() {
		StringBuffer buffer = new StringBuffer();
		buffer.append("MFunc [Original-").append(this.phpFileChunk.getOriginal().sizeOfMemberFunctions())
		.append(" Revised-").append(this.phpFileChunk.getRevised().sizeOfMemberFunctions()).append(" Added-")
		.append(this.sizeOfAddedMemberFunctions()).append(" Deleted-").append(this.sizeOfDeletedMemberFunctions())
		.append(" Modifie-").append(this.sizeOfModifiedMemberFunctions()).append(" Total Changes-")
		.append(this.sizeOfChangedMemberFunctions()).append("]");
		
		return buffer.toString();	
	}
	
	/*@Override
	public String toString() {
		StringBuffer buffer = new StringBuffer();
		buffer.append(this.changeType).append("\t").append(this.url).append("\t").append(this.sizeOfChangedLines());
		if(this.sizeOfChangedLines() > 1) {
			buffer.append(" lines changed");
		} else {
			buffer.append(" line changed");
		}
		return buffer.toString();
	}*/
}
