/*
 * Copyright PMEase (c) 2005-2008,
 * Date: Feb 24, 2008
 * Time: 4:29:05 PM
 * All rights reserved.
 * 
 * Revision: $$Id: AntBuildStep.java 1550 2008-10-24 08:24:21Z robin $$ 
 */
package com.pmease.quickbuild.plugin.builder.ant;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

import org.apache.commons.lang.SystemUtils;
import org.apache.tools.ant.types.Environment;
import org.dom4j.Element;
import org.hibernate.validator.constraints.NotEmpty;

import com.pmease.quickbuild.Context;
import com.pmease.quickbuild.Property;
import com.pmease.quickbuild.QuickbuildException;
import com.pmease.quickbuild.annotation.Advanced;
import com.pmease.quickbuild.annotation.Editable;
import com.pmease.quickbuild.annotation.ScriptApi;
import com.pmease.quickbuild.annotation.Scriptable;
import com.pmease.quickbuild.execution.Commandline;
import com.pmease.quickbuild.execution.LineConsumer;
import com.pmease.quickbuild.log.Log.LogLevel;
import com.pmease.quickbuild.migration.VersionedDocument;
import com.pmease.quickbuild.stepsupport.Step;
import com.pmease.quickbuild.util.FileUtils;
import com.pmease.quickbuild.util.StringUtils;

@Editable(name="Ant", order=20, category="Build", description=
	"Configure a Ant based build step here. By default, QuickBuild executes \"ant\" " +
	"(or ant.bat on Windows) to run this build step, and expects this file to be on the " +
	"system path. If not, you will need to specify path to this file by configuring the " +
	"Ant plugin through the plugin management page.")
@ScriptApi("This step calls Apache Ant to build projects.")
public class AntBuildStep extends Step {
	private static final long serialVersionUID = 1L;
	
	private String workingPath;
    
	private String buildScriptPath;
    
	private String buildTargets;
    
	private List<Property> buildProperties = new ArrayList<Property>();
    
	private String extraAntOptions;
	
    private List<Property> environments = new ArrayList<Property>();
    
    private int returnCode;
    
	@Editable(order=1100, name="Ant Build File", description=
    	"Specify the path to the Ant build file. A non-absolute path is considered to be " +
    	"relative to the workspace directory."
    	)
    @NotEmpty
    @ScriptApi("Get Ant build file path.")
	@Scriptable
    public String getBuildScriptPath() {
        return buildScriptPath;
    }

    public void setBuildScriptPath(String buildScriptPath) {
        this.buildScriptPath = buildScriptPath;
    }

    @Editable(order=1200, description=
    	"Specify the targets to build. Use space to separate different targets " +
    	"(target name containing spaces should be quoted in order not to be " +
    	"interpreted as multiple targets)"
    	)
    @ScriptApi("Get build targets.")
	@Scriptable
    public String getBuildTargets() {
        return buildTargets;
    }

    public void setBuildTargets(String buildTargets) {
        this.buildTargets = buildTargets;
    }

    @Editable(order=1300, description=
    	"Define build properties here to pass into the build script. For example, " +
    	"you may pass version of current build as property <i>buildVersion</i> " +
    	"by specifying property name as <i>buildVersion</i> and property value as " +
    	"<i>${build.version}</code></i>.<br>" +
    	"<b>NOTE:</b> Properties with blank value will be ignored."
    	)
    @ScriptApi("Get build properties passed to Ant.")
    public List<Property> getBuildProperties() {
        return buildProperties;
    }

    public void setBuildProperties(List<Property> buildProperties) {
        this.buildProperties = buildProperties;
    }

    @Editable(order=1400, name="Working Directory", description=
          "Optionally specify working directory for Ant. A non-absolute path is considered " +
          "to be relative to the directory containing the build script specified above. If " +
          "not specified, the directory containing the build script will be used."
    	)
    @ScriptApi("Get working directory of Ant. Null if the directory containing the build " +
    		"script is used as working directory.")
	@Scriptable
	@Advanced
    public String getWorkingPath() {
        return workingPath;
    }

    public void setWorkingPath(String workingPath) {
        this.workingPath = workingPath;
    }

    @Editable(order=1500, description=
    	"Specify extra Ant options. If specified, it will be appended to the Ant " +
    	"global options specified in Ant plugin setting. Please note that you should NOT specify " +
    	"below options as they'll be determined by QuickBuild:<br>" +
    	"<b>-f, -buildfile, -file, -logfile, -l, -quiet, -q, -verbose, -v, -debug, -d, " +
    	"-logger, -emacs, -e, -D</b>"
    	)
    @ScriptApi("Get extra options to run Ant. Null if not specified.")
	@Scriptable
	@Advanced
    public String getExtraAntOptions() {
        return extraAntOptions;
    }

    public void setExtraAntOptions(String extraAntOptions) {
        this.extraAntOptions = extraAntOptions;
    }

    @Editable(name="Environment Variables", order=1600, description=
    	"Specify environment variables for Ant execution. For example, " +
    	"you may store version of current build into environment variable <i>buildVersion</i> " +
    	"by specifying variable name as <i>buildVersion</i> and variable value as " +
    	"<i>${build.version}</code></i>.<br>" +
        "<b>NOTE:</b> " +
        "<ul><li>You may define environment variable <i>JAVA_HOME</i> here in order to build " +
        "with your choosed JDK." +
        "<li>Environment variables with blank value will be ignored.</li></ul>"
    	)
    @ScriptApi("Get defined environment variables when executing the build command.")
    public List<Property> getEnvironments() {
        return environments;
    }

    public void setEnvironments(List<Property> environments) {
        this.environments = environments;
    }

	@ScriptApi("Get return code of Ant command.")
	public int getReturnCode() {
		return returnCode;
	}

	private Commandline getBuildCmd(AntSetting antSetting) {
    	Commandline cmdline = new Commandline();
    	cmdline.setExecutable(getAntExecutable(antSetting));

    	String globalOpts = ((AntSetting) getPlugin().getSetting(true)).getAntOptions();
    	if (StringUtils.isNotBlank(globalOpts))
    		cmdline.createArgument().setLine(StringUtils.remove(globalOpts, "-noclasspath"));
    	
    	String extraAntOptions = getExtraAntOptions();
        if (extraAntOptions != null)
            cmdline.createArgument().setLine(StringUtils.remove(extraAntOptions, "-noclasspath"));

        for (Property property: getBuildProperties()) {
            if (StringUtils.isNotBlank(property.getValue()))
            	cmdline.createArgument().setValue("-D" + property.getName()+ "=" + property.getValue());
        }

        if (Context.getLogger().isTraceEnabled())
        	cmdline.createArgument().setValue("-d");
        else if (Context.getLogger().isDebugEnabled())
        	cmdline.createArgument().setValue("-v");
        else if (!Context.getLogger().isInfoEnabled())
            cmdline.createArgument().setValue("-q");
        
        File buildScriptFile = FileUtils.resolvePath(Context.getConfiguration().getWorkspaceDir(), 
        		getBuildScriptPath());
        cmdline.createArgument().setValue("-buildfile");
        cmdline.createArgument().setValue(buildScriptFile.getAbsolutePath());
        if (StringUtils.isNotBlank(getBuildTargets()))
            cmdline.createArgument().setLine(getBuildTargets());

        cmdline.createArgument().setLine("-logger com.pmease.quickbuild.plugin.builder.ant.backport.AntLogger");
        return cmdline;
    }

    private File getWorkingDir() {
    	File buildScriptFile = FileUtils.resolvePath(Context.getConfiguration().getWorkspaceDir(), 
    			getBuildScriptPath());
    	return FileUtils.resolvePath(buildScriptFile.getParentFile(), getWorkingPath());
    }
    
    private String getAntExecutable(AntSetting antSetting) {
    	String antExe = antSetting.getAntExecutablePath();
    	if (antExe == null) {
    		String antHome = antSetting.getAntHome();
    		if (antHome == null) {
	    		if (SystemUtils.IS_OS_WINDOWS)
	    			antExe = "ant.bat";
	    		else
	    			antExe = "ant";
    		} else {
    			antHome = StringUtils.stripEnd(antHome, "/\\");
	    		if (SystemUtils.IS_OS_WINDOWS)
	    			antExe = antHome + "\\bin\\ant.bat";
	    		else
	    			antExe = antHome + "/bin/ant";
    		}
    	}
    	return antExe;
    }
    
	@Override
	public void run() {
		AntSetting antSetting = ((AntSetting) getPlugin().getSetting(true));		
		
        List<Property> actualEnvironments = getActualEnvironments(getEnvironments());
		
        Commandline cmdline = getBuildCmd(antSetting);
        Environment env = new Environment();
        String antHome = antSetting.getAntHome();
        if (antHome != null) {
            Environment.Variable var = new Environment.Variable();
            var.setKey("ANT_HOME");
            var.setValue(antHome);
            env.addVariable(var);
        }
        if (StringUtils.contains(getExtraAntOptions(), "-noclasspath")) {
            for (Property each: actualEnvironments) {
                if (StringUtils.isNotBlank(each.getValue()) && !each.getName().equals("CLASSPATH")) {
                    Environment.Variable var = new Environment.Variable();
                    var.setKey(each.getName());
                    var.setValue(each.getValue());
                    env.addVariable(var);
                }
            }
            Environment.Variable var = new Environment.Variable();
            var.setKey("CLASSPATH");
            var.setValue(getPlugin().getClasspath());
            env.addVariable(var);
        } else {
        	String classpath = null;
            for (Property each: actualEnvironments) {
            	if (StringUtils.isNotBlank(each.getValue())) {
	                if (!each.getName().equals("CLASSPATH")) {
	                    Environment.Variable var = new Environment.Variable();
	                    var.setKey(each.getName());
	                    var.setValue(each.getValue());
	                    env.addVariable(var);
	                } else {
	                	classpath = each.getValue();
	                }
            	}
            }
            if (classpath == null)
            	classpath = System.getenv("CLASSPATH");
            if (classpath == null)
            	classpath = "";
            else
            	classpath = classpath.trim();
            String sep = System.getProperty("path.separator");
            if (classpath.endsWith(sep) || classpath.length() == 0)
            	classpath += getPlugin().getClasspath();
            else
            	classpath += sep + getPlugin().getClasspath();
            Environment.Variable var = new Environment.Variable();
            var.setKey("CLASSPATH");
            var.setValue(classpath);
            env.addVariable(var);
        }

        final StringBuffer errorMessages = new StringBuffer();
        final boolean[] successful = new boolean[]{false};
        final LogLevel[] logLevel = new LogLevel[]{LogLevel.INFO};
        
        returnCode = cmdline.execute(getWorkingDir(), env, new LineConsumer() {

        	private void log(String message) {
        		if (logLevel[0] == LogLevel.ERROR) {
        			Context.getLogger().error(message);
    				if (errorMessages.length() == 0)
    					errorMessages.append(message);
    				else
    					errorMessages.append("\n" + message);
        		} else if (logLevel[0] == LogLevel.WARN) {
        			Context.getLogger().warn(message);
        		} else if (logLevel[0] == LogLevel.INFO) {
        			Context.getLogger().info(message);
        		} else if (logLevel[0] == LogLevel.DEBUG) {
        			Context.getLogger().debug(message);
        		} else { 
        			Context.getLogger().trace(message);
        		}
        	}

        	@Override
			public void consume(String line) {
				if (line.startsWith("ERROR ")) {
					logLevel[0] = LogLevel.ERROR;
					log(line.substring("ERROR ".length()));
				} else if (line.startsWith("INFO ")) {
					logLevel[0] = LogLevel.INFO;
					log(line.substring("INFO ".length()));
				} else if (line.startsWith("WARN ")) {
					logLevel[0] = LogLevel.WARN;
					log(line.substring("WARN ".length()));
				} else if (line.startsWith("DEBUG ")) {
					logLevel[0] = LogLevel.DEBUG;
					log(line.substring("DEBUG ".length()));
				} else if (line.startsWith("TRACE ")) {
					logLevel[0] = LogLevel.TRACE;
					log(line.substring("TRACE ".length()));
				} else if (line.startsWith(" ")) {
					log(line.substring(1));
				} else if (line.startsWith("BUILD SUCCESSFUL")) {
					successful[0] = true;
					Context.getLogger().info(line);
				} else {
					Context.getLogger().info(line);
				}
			}
        	
        }, new LineConsumer() {

			@Override
			public void consume(String line) {
				Context.getLogger().error(line);
				if (errorMessages.length() == 0)
					errorMessages.append(line);
				else
					errorMessages.append("\n" + line);
			}
        	
        }).getReturnCode();
        
        if (!successful[0]) {
        	String exceptionMessage = "Ant build failed.";
        	if (errorMessages.length() != 0)
        		exceptionMessage += "\n" + errorMessages.toString();
        	throw new QuickbuildException(exceptionMessage);
        }
	}

	@SuppressWarnings("unused")
	private void migrate1(VersionedDocument dom, Stack<Integer> versions) {
		Element element = dom.getRootElement().element("antWorkingPath");
		if (element != null) {
			element.detach();
			dom.getRootElement().addElement("workingPath").setText(element.getTextTrim());
		}
	}

	@SuppressWarnings("unused")
	private void migrate2(VersionedDocument dom, Stack<Integer> versions) {
		Element root = dom.getRootElement();
		root.element("commandSuccessCondition").detach();
		root.element("environmentVariables").setName("environments");
		if (!versions.empty()) {
			versions.pop();
		} else {
			versions.push(0);
			versions.push(0);
		}
	}
}
