/*******************************************************************************
 * Copyright (c) 2005, 2011 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.ant.internal.launching.debug;

import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Stack;
import java.util.Vector;

import org.apache.tools.ant.BuildEvent;
import org.apache.tools.ant.Location;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Target;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.MacroInstance;
import org.eclipse.ant.internal.core.IAntCoreConstants;
import org.eclipse.ant.internal.launching.debug.model.DebugMessageIds;

public class AntDebugState {
    
    private static final String fgAntTaskName= "ant"; //$NON-NLS-1$
    private static final String fgAntCallTaskName= "antcall"; //$NON-NLS-1$
    
	private IDebugBuildLogger fLogger;
	private Stack fTasks= new Stack();
	private Map fTaskToProxies= new HashMap();
	private Task fCurrentTask;
	private Task fStepOverTask;
	private Task fStepIntoTask;
	private Task fLastTaskFinished;
    
	//properties set before execution
    private Map fInitialProperties= null;
	private Map fProperties= null;
    
    private Map fProjectToTargetNames= null;
    private Map fProjectToMapOfTargetToBuildSequence= null;
    private Stack fTargetsToExecute= new Stack();
    private Stack fTargetsExecuting= new Stack();
	
	private boolean fConsiderTargetBreakpoints= false;
	private boolean fShouldSuspend;
	private boolean fClientSuspend= false;
	private boolean fStepIntoSuspend= false;
	private boolean fIsAfterTaskEvent= false;
	
	
	public AntDebugState(IDebugBuildLogger logger) {
		fLogger= logger;
	}

	public void buildStarted() {
        fProjectToTargetNames= new HashMap();
        fProjectToMapOfTargetToBuildSequence= new HashMap();
    }
    
    /**
     * Call-back from {@link org.eclipse.ant.internal.launching.runtime.logger.AntProcessDebugBuildLogger}
     * when the build has finished
     * @since 1.0.1
     */
    public void buildFinished() {
    	if(fProjectToTargetNames != null) {
    		fProjectToTargetNames.clear();
    	}
    	if(fProjectToMapOfTargetToBuildSequence != null) {
    		fProjectToMapOfTargetToBuildSequence.clear();
    	}
    	fTargetsExecuting.clear();
    	fTargetsToExecute.clear();
    	if(fInitialProperties != null) {
    		fInitialProperties.clear();
    	}
    	if(fProperties != null) {
    		fProperties.clear();
    	}
    	if(fTaskToProxies != null) {
    		fTaskToProxies.clear();
    	}
    	if(fTasks != null) {
    		fTasks.clear();
    	}
    }
	
	public void waitIfSuspended() {
		fLogger.waitIfSuspended();
	}

	public Task getLastTaskFinished() {
		return fLastTaskFinished;
	}

	private void setLastTaskFinished(Task lastTaskFinished) {
		fLastTaskFinished= lastTaskFinished;

	}

	public Task getCurrentTask() {
		return fCurrentTask;
	}

	public void setCurrentTask(Task currentTask) {
		fCurrentTask= currentTask;

	}

	private Map getInitialProperties() {
		return fInitialProperties;
	}

	public Task getStepOverTask() {
		return fStepOverTask;
	}

	public void setStepOverTask(Task stepOverTask) {
		fStepOverTask= stepOverTask;

	}

	private boolean considerTargetBreakpoints() {
		return fConsiderTargetBreakpoints;
	}

	private void setConsiderTargetBreakpoints(boolean considerTargetBreakpoints) {
		fConsiderTargetBreakpoints= considerTargetBreakpoints;
	}

	private Stack getTasks() {
		return fTasks;
	}
	
	public void setShouldSuspend(boolean shouldSuspend) {
		fShouldSuspend= shouldSuspend;
	}

	public boolean shouldSuspend() {
		return fShouldSuspend;
	}

	private Map getTargetToBuildSequence(Project project) {
		return (Map) fProjectToMapOfTargetToBuildSequence.get(project);
	}

	public void setTargetToExecute(Target target) {
        if (target == null) {
            fTargetsToExecute.pop();
        } else {
            fTargetsToExecute.push(target);
        }
	}

	public void setTargetExecuting(Target target) {
        if (target == null) {
        	if(!fTargetsExecuting.isEmpty()) {
        		fTargetsExecuting.pop();
        	}
        } else {
            fTargetsExecuting.push(target);
        }
	}

	private Target getTargetToExecute() {
	    if (fTargetsToExecute.isEmpty()) {
            return null;
        }
		return (Target) fTargetsToExecute.peek();
	}
	
	private Target getTargetExecuting() {
        if (fTargetsExecuting.isEmpty()) {
            return null;
        }
		return (Target) fTargetsExecuting.peek();
	}

	public boolean isStepIntoSuspend() {
		return isAfterTaskEvent() && fStepIntoSuspend;
	}

	public void setStepIntoSuspend(boolean stepIntoSuspend) {
		fStepIntoSuspend = stepIntoSuspend;
	}

	public boolean isClientSuspend() {
		return fClientSuspend;
	}

	public void setClientSuspend(boolean clientSuspend) {
		fClientSuspend = clientSuspend;
	}

	public Task getStepIntoTask() {
		return fStepIntoTask;
	}

	public void setStepIntoTask(Task stepIntoTask) {
		fStepIntoTask = stepIntoTask;
	}
	
	public void resume() {
		fLogger.notifyAll();
	}

	public Map getProperties() {
		return fProperties;
	}
	
	public Location getBreakpointLocation() {
		if (isAfterTaskEvent() && getCurrentTask() != null) {
			return getCurrentTask().getLocation();
		}
		if (considerTargetBreakpoints()) {
	        Target targetExecuting= getTargetExecuting();
	        if (targetExecuting != null) {
                return getLocation(targetExecuting);      
            }
		}
		return null;
	}

	private boolean isAfterTaskEvent() {
		return fIsAfterTaskEvent;
	}

	private void setAfterTaskEvent(boolean isAfterTaskEvent) {
		fIsAfterTaskEvent = isAfterTaskEvent;
	}
	
	public void taskStarted(BuildEvent event) {
		setAfterTaskEvent(true);
		if (getInitialProperties() == null) {//implicit or top level target does not fire targetStarted()
			fInitialProperties= event.getProject().getProperties();
		}
		
		setCurrentTask(event.getTask());
		setConsiderTargetBreakpoints(false);
		Stack tasks = getTasks();
		if (!tasks.isEmpty()) {
			//cache the parent task proxy as when that task is started or finished the
			//proxy is not yet available or is nulled out
			Task parentTask = (Task) tasks.peek();
			Object proxy = parentTask.getRuntimeConfigurableWrapper().getProxy();
			if (proxy != null) {
				fTaskToProxies.put(parentTask, proxy);
			}
		}
		tasks.push(getCurrentTask());
		waitIfSuspended();
	}
	

    public void taskFinished() {
    	Stack tasks = getTasks();
    	if(!tasks.empty()) {
	    	Task lastTask= (Task)tasks.pop();
	        setLastTaskFinished(lastTask);
	        setCurrentTask(null);
	        String taskName= lastTask.getTaskName();
	       
	        if (getStepOverTask() != null) {
	        	if ((fgAntCallTaskName.equals(taskName) || fgAntTaskName.equals(taskName)) && (!fgAntCallTaskName.equals(getStepOverTask().getTaskName()) && !fgAntTaskName.equals(getStepOverTask().getTaskName()))) {
	        		setShouldSuspend(true);
	        	} else if (fTaskToProxies.remove(lastTask) instanceof MacroInstance) {
	        		setShouldSuspend(true);
	        	}
	        }
    	}
    	 waitIfSuspended();
    }

    public void stepOver() {
       setStepOverTask(getCurrentTask());
        if (getCurrentTask() == null) {
            //stepping over target breakpoint
           setShouldSuspend(true);
        }
        resume();
    }

    public void targetStarted(BuildEvent event) {
		setAfterTaskEvent(false);
        Project eventProject = event.getProject();
        if (getInitialProperties() == null) {
            fInitialProperties= eventProject.getProperties();
        }
        if (fProjectToTargetNames.get(eventProject) == null) {
            Object ref= eventProject.getReference("eclipse.ant.targetVector"); //$NON-NLS-1$
            if (ref != null) {
                fProjectToTargetNames.put(eventProject, ref);
                Map targetToBuildSequence= new HashMap();
                setTargetToExecute(initializeBuildSequenceInformation(event, targetToBuildSequence));
                fProjectToMapOfTargetToBuildSequence.put(eventProject, targetToBuildSequence);
            }
        }
        
        setTargetExecuting(event.getTarget());
        if (event.getTarget().equals(getTargetToExecute())) {
            //the dependencies of the target to execute have been met
            //prepare for the next target
            Vector targets= (Vector) fProjectToTargetNames.get(eventProject);
            if (!targets.isEmpty()) {
                setTargetToExecute((Target) eventProject.getTargets().get(targets.remove(0)));
            } else {
                setTargetToExecute(null);
            }
        }
        setConsiderTargetBreakpoints(true);
    }

	public int getLineNumber(Location location) {
	    try { //succeeds with Ant newer than 1.6
	        return location.getLineNumber();
	    } catch (NoSuchMethodError e) {
	        //Ant before 1.6
	        String locationString= location.toString();
	        if (locationString.length() == 0) {
	            return 0;
	        }
	        //filename: lineNumber: ("c:\buildfile.xml: 12: ")
	        int lastIndex= locationString.lastIndexOf(':');
	        int index =locationString.lastIndexOf(':', lastIndex - 1);
	        if (index != -1) {
	            try {
	                return Integer.parseInt(locationString.substring(index+1, lastIndex));
	            } catch (NumberFormatException nfe) {
	                return 0;
	            }
	        }
	        return 0;
	    }
	}

	public static Location getLocation(Target target) {
	    try {//succeeds with Ant newer than 1.6.2
	        return target.getLocation();
	    } catch (NoSuchMethodError e) {
	        return Location.UNKNOWN_LOCATION;
	    }
	}

	public String getFileName(Location location) {
	    try {//succeeds with Ant newer than 1.6
	        return location.getFileName();
	    } catch (NoSuchMethodError e) {
	        //Ant before 1.6
	        String locationString= location.toString();
	        if (locationString.length() == 0) {
	            return null;
	        }
	        //filename: lineNumber: ("c:\buildfile.xml: 12: ")          
	        int lastIndex= locationString.lastIndexOf(':');
	        int index =locationString.lastIndexOf(':', lastIndex-1);
	        if (index == -1) {
	            index= lastIndex; //only the filename is known
	        }
	        if (index != -1) {
	        //bug 84403
	            //if (locationString.startsWith("file:")) {
	              //  return FileUtils.newFileUtils().fromURI(locationString);
	            //}
	            //remove file:
	            return locationString.substring(5, index);
	        }
	        return null;
	    }
	}

	private void appendToStack(StringBuffer stackRepresentation, String targetName, String taskName, Location location) {
	    stackRepresentation.append(targetName);
	    stackRepresentation.append(DebugMessageIds.MESSAGE_DELIMITER);
	    stackRepresentation.append(taskName);
	    stackRepresentation.append(DebugMessageIds.MESSAGE_DELIMITER);
	    
	    stackRepresentation.append(getFileName(location));
	    stackRepresentation.append(DebugMessageIds.MESSAGE_DELIMITER);
	    stackRepresentation.append(getLineNumber(location));
	    stackRepresentation.append(DebugMessageIds.MESSAGE_DELIMITER);
	}

	public void marshalStack(StringBuffer stackRepresentation) {
		Stack tasks= getTasks();
		
	    stackRepresentation.append(DebugMessageIds.STACK);
	    stackRepresentation.append(DebugMessageIds.MESSAGE_DELIMITER);
	    
		Target targetToExecute= getTargetToExecute();
		Target targetExecuting= getTargetExecuting();
      
        Project projectExecuting= null;
        if (targetExecuting != null) {
            projectExecuting= targetExecuting.getProject();
        } else if(!tasks.empty()) { //no target...must be a task
            Task task= (Task) tasks.peek();
            projectExecuting= task.getProject();
        }
        
		if (!isAfterTaskEvent()) {
			appendToStack(stackRepresentation, targetExecuting.getName(), IAntCoreConstants.EMPTY_STRING, getLocation(targetExecuting));
		}
		for (int i = tasks.size() - 1; i >= 0 ; i--) {
			Task task= (Task) tasks.get(i);
            if (task.getProject() == projectExecuting) {
                appendToStack(stackRepresentation, task.getOwningTarget().getName(), task.getTaskName(), task.getLocation());
            } else {
                //sub build target dependencies
                String targetName= task.getOwningTarget().getName();
                if (targetName != null && targetName.length() != 0) { //skip for implicit target
                    Iterator itr= fTargetsToExecute.iterator();
                    while (itr.hasNext()) {
                        Target target= (Target) itr.next();
                        if (target.getProject() != projectExecuting) {
                        	targetToExecute= target;
                            continue;
                        }
                        marshalTargetDependancyStack(stackRepresentation, target, targetExecuting);
                    }
                }
                projectExecuting= task.getProject();
                targetExecuting= task.getOwningTarget();
                appendToStack(stackRepresentation, targetExecuting.getName(), task.getTaskName(), task.getLocation());
            }
		}

		//target dependency stack 
		marshalTargetDependancyStack(stackRepresentation, targetToExecute, targetExecuting);
	}

    private void marshalTargetDependancyStack(StringBuffer stackRepresentation, Target targetToExecute, Target targetExecuting) {
        if (targetToExecute != null) {
	     	Vector buildSequence= (Vector) getTargetToBuildSequence(targetToExecute.getProject()).get(targetToExecute);
	     	int startIndex= buildSequence.indexOf(targetExecuting) + 1;
	     	int dependancyStackDepth= buildSequence.indexOf(targetToExecute);
	     	
	     	Target stackTarget;
	     	for (int i = startIndex; i <= dependancyStackDepth; i++) {
	     		stackTarget= (Target) buildSequence.get(i);
	            if (stackTarget.dependsOn(targetExecuting.getName())) {
	     		    appendToStack(stackRepresentation, stackTarget.getName(), IAntCoreConstants.EMPTY_STRING, getLocation(stackTarget));
	            }
	     	}
	     }
    }

	public void marshallProperties(StringBuffer propertiesRepresentation, boolean escapeLineSep) {
		Stack tasks = getTasks();
		if (tasks.isEmpty()) {
			return;
		}
	    propertiesRepresentation.append(DebugMessageIds.PROPERTIES);
	    propertiesRepresentation.append(DebugMessageIds.MESSAGE_DELIMITER);
		
		Project project= ((Task)tasks.peek()).getProject();
		Map lastProperties= getProperties(); 
		
	    Map currentProperties= project.getProperties();
	    if (lastProperties != null && currentProperties.size() == lastProperties.size()) {
	        //no new properties
	        return;
	    }
	    
		Map initialProperties= getInitialProperties();
	    Map currentUserProperties= project.getUserProperties();
	    Iterator iter= currentProperties.keySet().iterator();
	    String propertyName;
		String originalPropertyName;
	    String propertyValue;
	    while (iter.hasNext()) {
	        propertyName = (String) iter.next();
			originalPropertyName= propertyName;
	        if (lastProperties == null || lastProperties.get(propertyName) == null) { //new property
				if (escapeLineSep) {
					propertyName= escapeLineSeparator(propertyName);
				}
	            propertiesRepresentation.append(propertyName.length());
	            propertiesRepresentation.append(DebugMessageIds.MESSAGE_DELIMITER);
	            propertiesRepresentation.append(propertyName);
	            propertiesRepresentation.append(DebugMessageIds.MESSAGE_DELIMITER);
	            propertyValue= (String) currentProperties.get(originalPropertyName);
				if (escapeLineSep) {
					propertyValue= escapeLineSeparator(propertyValue);
				}
	            propertiesRepresentation.append(propertyValue.length());
	            propertiesRepresentation.append(DebugMessageIds.MESSAGE_DELIMITER);
				propertiesRepresentation.append(propertyValue);
	            propertiesRepresentation.append(DebugMessageIds.MESSAGE_DELIMITER);
	            propertiesRepresentation.append(getPropertyType(initialProperties, currentUserProperties, originalPropertyName));
	            propertiesRepresentation.append(DebugMessageIds.MESSAGE_DELIMITER);
	        }
	    }
	    
	    propertiesRepresentation.deleteCharAt(propertiesRepresentation.length() - 1);
		fProperties= currentProperties;
	}

	private int getPropertyType(Map initialProperties, Map currentUserProperties, String propertyName) {
		if (initialProperties.get(propertyName) != null) { //properties set before the start of the build
		    if (currentUserProperties.get(propertyName) == null) {
		        return DebugMessageIds.PROPERTY_SYSTEM;
		    } 
			return DebugMessageIds.PROPERTY_USER;
		} else if (currentUserProperties.get(propertyName) == null){
		    return DebugMessageIds.PROPERTY_RUNTIME;
		} else {
		    return DebugMessageIds.PROPERTY_USER;
		}
	}

	private String escapeLineSeparator(String stringToEscape) {
		if (!(stringToEscape.indexOf('\r') != -1 || stringToEscape.indexOf('\n') != -1 || stringToEscape.indexOf("\\r") != -1 || stringToEscape.indexOf("\\n") != -1)) { //$NON-NLS-1$ //$NON-NLS-2$
			return stringToEscape;
		}
		StringBuffer escapedValue= new StringBuffer(stringToEscape);		
		for (int i= 0; i < escapedValue.length(); i++) {
			switch (escapedValue.charAt(i)) {
			case '\r':
				escapedValue.replace(i, i+1, "\\r"); //$NON-NLS-1$
				i++;
				break;
			case '\n':
				escapedValue.replace(i, i+1, "\\n"); //$NON-NLS-1$
				i++;
				break;
			case '\\':
				if (escapedValue.charAt(i + 1) == 'r' || escapedValue.charAt(i + 1) == 'n') {
					escapedValue.replace(i, i+1, "\\\\"); //$NON-NLS-1$
					i++;
				}
				break;
			default:
				break;
			}
		}

		return escapedValue.toString();
	}

	private Target initializeBuildSequenceInformation(BuildEvent event, Map targetToBuildSequence) {
	    Project antProject= event.getProject();
	    Vector targets= (Vector) antProject.getReference("eclipse.ant.targetVector"); //$NON-NLS-1$
        if (targets == null) {
            return null;
        }
	    Iterator itr= targets.iterator();
	    Hashtable allTargets= antProject.getTargets();
	    String targetName;
	    Vector sortedTargets;
	    while (itr.hasNext()) {
	        targetName= (String) itr.next();
	        sortedTargets= antProject.topoSort(targetName, allTargets);
	        targetToBuildSequence.put(allTargets.get(targetName), sortedTargets);
	    }
	    //the target to execute
	    return (Target) allTargets.get(targets.remove(0));
	}
}
