/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package groovy.ant;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import groovy.namespace.QName;
import groovy.util.BuilderSupport;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.BuildLogger;
import org.apache.tools.ant.DemuxInputStream;
import org.apache.tools.ant.DemuxOutputStream;
import org.apache.tools.ant.Location;
import org.apache.tools.ant.NoBannerLogger;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelper;
import org.apache.tools.ant.RuntimeConfigurable;
import org.apache.tools.ant.Target;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.UnknownElement;
import org.apache.tools.ant.dispatch.DispatchUtils;
import org.apache.tools.ant.helper.AntXMLContext;
import org.apache.tools.ant.helper.ProjectHelper2;
import org.codehaus.groovy.ant.FileScanner;
import org.codehaus.groovy.reflection.ReflectionUtils;
import org.codehaus.groovy.runtime.DefaultGroovyMethodsSupport;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.AttributesImpl;

import java.io.InputStream;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Allows <a href="http://ant.apache.org/manual/coretasklist.html">Ant tasks</a> to
 * be used with a Groovy builder-style markup. Requires that {{ant.jar}} is on your classpath which will
 * happen automatically if you are using the Groovy distribution but will be up
 * to you to organize if you are embedding Groovy. If you wish to use the
 * <a href="http://ant.apache.org/manual/install#optionalTasks">optional tasks</a>
 * you will need to add one or more additional jars from the ant distribution to
 * your classpath - see the <a href="http://ant.apache.org/manual/install.html#librarydependencies">library
 * dependencies</a> for more details.
 */
public class AntBuilder extends BuilderSupport {

    private final Logger log = Logger.getLogger(getClass().getName());
    private final Project project;
    private final AntXMLContext antXmlContext;
    private final ProjectHelper2.ElementHandler antElementHandler = new ProjectHelper2.ElementHandler();
    private final ProjectHelper2.TargetHandler antTargetHandler = new ProjectHelper2.TargetHandler();
    private final Target collectorTarget;
    private final Target implicitTarget;
    private Target definingTarget;
    private Object lastCompletedNode;
    // true when inside a task so special ant.target handling occurs just at top level
    boolean insideTask;

    private boolean saveStreams = true;
    private static Integer streamCount = 0;
    private static InputStream savedIn;
    private static PrintStream savedErr;
    private static PrintStream savedOut;
    private static DemuxInputStream demuxInputStream;
    private static DemuxOutputStream demuxOutputStream;
    private static DemuxOutputStream demuxErrorStream;
    private static InputStream savedProjectInputStream;

    public AntBuilder() {
        this(createProject());
    }

    public AntBuilder(final Project project) {
        this(project, new Target());
    }

    public AntBuilder(final Project project, final Target owningTarget) {
        this.project = project;

        /*
         * GROOVY-4524: The following is not needed anymore as an ant Project already by default has inputhandler
         * set to DefaultInputHandler. And if it is again set here, it mistakenly overrides the custom input handler
         * if set using -inputhandler switch.
         */
        //this.project.setInputHandler(new DefaultInputHandler());

        collectorTarget = owningTarget;
        antXmlContext = new AntXMLContext(project);
        collectorTarget.setProject(project);
        antXmlContext.setCurrentTarget(collectorTarget);
        antXmlContext.setLocator(new AntBuilderLocator());
        antXmlContext.setCurrentTargets(new HashMap<String, Target>());

        implicitTarget = new Target();
        implicitTarget.setProject(project);
        implicitTarget.setName("");
        antXmlContext.setImplicitTarget(implicitTarget);

        // FileScanner is a Groovy utility
        project.addDataTypeDefinition("fileScanner", FileScanner.class);
    }

    public AntBuilder(final Task parentTask) {
        this(parentTask.getProject(), parentTask.getOwningTarget());

        // define "owning" task as wrapper to avoid having tasks added to the target
        // but it needs to be an UnknownElement and no access is available from
        // task to its original UnknownElement
        final UnknownElement ue = new UnknownElement(parentTask.getTaskName());
        ue.setProject(parentTask.getProject());
        ue.setTaskType(parentTask.getTaskType());
        ue.setTaskName(parentTask.getTaskName());
        ue.setLocation(parentTask.getLocation());
        ue.setOwningTarget(parentTask.getOwningTarget());
        ue.setRuntimeConfigurableWrapper(parentTask.getRuntimeConfigurableWrapper());
        parentTask.getRuntimeConfigurableWrapper().setProxy(ue);
        antXmlContext.pushWrapper(parentTask.getRuntimeConfigurableWrapper());
    }

    /**
     * #
     * Gets the Ant project in which the tasks are executed
     *
     * @return the project
     */
    public Project getProject() {
        return project;
    }

    /**
     * Gets the xml context of Ant used while creating tasks
     *
     * @return the Ant xml context
     */
    public AntXMLContext getAntXmlContext() {
        return antXmlContext;
    }

    /**
     * Whether stdin, stdout, stderr streams are saved.
     *
     * @return true if we are saving streams
     * @see #setSaveStreams(boolean)
     */
    public boolean isSaveStreams() {
        return saveStreams;
    }

    /**
     * Indicates that we save stdin, stdout, stderr and replace them
     * while AntBuilder is executing tasks with
     * streams that funnel the normal streams into Ant's logs.
     *
     * @param saveStreams set to false to disable this behavior
     */
    public void setSaveStreams(boolean saveStreams) {
        this.saveStreams = saveStreams;
    }

    /**
     * @return Factory method to create new Project instances
     */
    protected static Project createProject() {
        final Project project = new Project();

        final ProjectHelper helper = ProjectHelper.getProjectHelper();
        project.addReference(ProjectHelper.PROJECTHELPER_REFERENCE, helper);
        helper.getImportStack().addElement("AntBuilder"); // import checks that stack is not empty

        final BuildLogger logger = new NoBannerLogger();

        logger.setMessageOutputLevel(org.apache.tools.ant.Project.MSG_INFO);
        logger.setOutputPrintStream(System.out);
        logger.setErrorPrintStream(System.err);

        project.addBuildListener(logger);

        project.init();
        project.getBaseDir();
        return project;
    }

    @Override
    protected void setParent(Object parent, Object child) {
    }

    /**
     * We don't want to return the node as created in {@link #createNode(Object, Map, Object)}
     * but the one made ready by {@link #nodeCompleted(Object, Object)}
     *
     * @see groovy.util.BuilderSupport#doInvokeMethod(java.lang.String, java.lang.Object, java.lang.Object)
     */
    @Override
    protected Object doInvokeMethod(String methodName, Object name, Object args) {
        super.doInvokeMethod(methodName, name, args);


        // return the completed node
        return lastCompletedNode;
    }

    /**
     * Determines, when the ANT Task that is represented by the "node" should perform.
     * Node must be an ANT Task or no "perform" is called.
     * If node is an ANT Task, it performs right after complete construction.
     * If node is nested in a TaskContainer, calling "perform" is delegated to that
     * TaskContainer.
     *
     * @param parent note: null when node is root
     * @param node   the node that now has all its children applied
     */
    @Override
    @SuppressFBWarnings(value = "DLS_DEAD_LOCAL_STORE", justification = "False positive: bytecode needlessly but harmlessly assigns local to JVM register")
    protected void nodeCompleted(final Object parent, final Object node) {
        if (parent == null) insideTask = false;
        antElementHandler.onEndElement(null, null, antXmlContext);

        lastCompletedNode = node;
        if (parent != null && !(parent instanceof Target)) {
            log.finest("parent is not null: no perform on nodeCompleted");
            return; // parent will care about when children perform
        }
        if (definingTarget != null && definingTarget == parent && node instanceof Task) return; // inside defineTarget
        if (definingTarget == node) {
            definingTarget = null;
        }

        // as in Target.execute()
        if (node instanceof Task) {
            Task task = (Task) node;
            final String taskName = task.getTaskName();

            if ("antcall".equals(taskName) && parent == null) {
                throw new BuildException("antcall not supported within AntBuilder, consider using 'ant.project.executeTarget('targetName')' instead.");
            }

            if (saveStreams) {
                // save original streams
                synchronized (AntBuilder.class) {
                    int currentStreamCount = streamCount++; // SuppressFBWarnings: DLS_DEAD_LOCAL_STORE
                    if (currentStreamCount == 0) {
                        // we are first, save the streams
                        savedProjectInputStream = project.getDefaultInputStream();
                        savedIn = System.in;
                        savedErr = System.err;
                        savedOut = System.out;

                        if (!(savedIn instanceof DemuxInputStream)) {
                            project.setDefaultInputStream(savedIn);
                            demuxInputStream = new DemuxInputStream(project);
                            System.setIn(demuxInputStream);
                        }
                        demuxOutputStream = new DemuxOutputStream(project, false);
                        System.setOut(printStream(demuxOutputStream));
                        demuxErrorStream = new DemuxOutputStream(project, true);
                        System.setErr(printStream(demuxErrorStream));
                    }
                }
            }

            try {
                lastCompletedNode = performTask(task);
            } finally {
                if (saveStreams) {
                    synchronized (AntBuilder.class) {
                        int currentStreamCount = --streamCount;
                        if (currentStreamCount == 0) {
                            // last to leave, turn out the lights: restore original streams
                            project.setDefaultInputStream(savedProjectInputStream);
                            System.setOut(savedOut);
                            System.setErr(savedErr);
                            if (demuxInputStream != null) {
                                System.setIn(savedIn);
                                DefaultGroovyMethodsSupport.closeQuietly(demuxInputStream);
                                demuxInputStream = null;
                            }
                            DefaultGroovyMethodsSupport.closeQuietly(demuxOutputStream);
                            DefaultGroovyMethodsSupport.closeQuietly(demuxErrorStream);
                            demuxOutputStream = null;
                            demuxErrorStream = null;
                        }
                    }
                }
            }

            // restore dummy collector target
            if ("import".equals(taskName)) {
                antXmlContext.setCurrentTarget(collectorTarget);
            }
        } else if (node instanceof Target) {
            // restore dummy collector target
            antXmlContext.setCurrentTarget(collectorTarget);
        } else {
            final RuntimeConfigurable r = (RuntimeConfigurable) node;
            r.maybeConfigure(project);
        }
    }

    @SuppressFBWarnings(value = "DM_DEFAULT_ENCODING", justification = "We are only wrapping stdout/stderr which are likely created with default encoding")
    private PrintStream printStream(DemuxOutputStream outputStream) {
        // we could make a saveStreamEncoding but seems like a rare scenario
        return new PrintStream(outputStream);
    }

    // Copied from org.apache.tools.ant.Task, since we need to get a real thing before it gets nulled in DispatchUtils.execute

    private Object performTask(Task task) {

        Throwable reason = null;
        try {
            // Have to call fireTestStared/fireTestFinished via reflection as they unfortunately have protected access in Project
            final Method fireTaskStarted = Project.class.getDeclaredMethod("fireTaskStarted", Task.class);
            ReflectionUtils.trySetAccessible(fireTaskStarted);
            fireTaskStarted.invoke(project, task);

            Object realThing;
            realThing = task;
            task.maybeConfigure();
            if (task instanceof UnknownElement) {
                realThing = ((UnknownElement) task).getRealThing();
            }

            DispatchUtils.execute(task);

            return realThing != null ? realThing : task;
        } catch (BuildException ex) {
            if (ex.getLocation() == Location.UNKNOWN_LOCATION) {
                ex.setLocation(task.getLocation());
            }
            reason = ex;
            throw ex;
        } catch (Exception ex) {
            reason = ex;
            BuildException be = new BuildException(ex);
            be.setLocation(task.getLocation());
            throw be;
        } catch (Error ex) {
            reason = ex;
            throw ex;
        } finally {
            try {
                final Method fireTaskFinished = Project.class.getDeclaredMethod("fireTaskFinished", Task.class, Throwable.class);
                ReflectionUtils.trySetAccessible(fireTaskFinished);
                fireTaskFinished.invoke(project, task, reason);
            } catch (Exception e) {
                BuildException be = new BuildException(e);
                be.setLocation(task.getLocation());
                throw be;
            }
        }
    }

    @Override
    protected Object createNode(Object tagName) {
        return createNode(tagName, Collections.EMPTY_MAP);
    }

    @Override
    protected Object createNode(Object name, Object value) {
        Object task = createNode(name);
        setText(task, value.toString());
        return task;
    }

    @Override
    protected Object createNode(Object name, Map attributes, Object value) {
        Object task = createNode(name, attributes);
        setText(task, value.toString());
        return task;
    }

    /**
     * Builds an {@link Attributes} from a {@link Map}
     *
     * @param attributes the attributes to wrap
     * @return the wrapped attributes
     */
    protected static Attributes buildAttributes(final Map attributes) {
        final AttributesImpl attr = new AttributesImpl();
        for (Object o : attributes.entrySet()) {
            final Map.Entry entry = (Map.Entry) o;
            final String attributeName = (String) entry.getKey();
            final String attributeValue = String.valueOf(entry.getValue());
            attr.addAttribute(null, attributeName, attributeName, "CDATA", attributeValue);
        }
        return attr;
    }

    @Override
    protected Object createNode(final Object name, final Map attributes) {

        final Attributes attrs = buildAttributes(attributes);
        String tagName = name.toString();
        String ns = "";

        if (name instanceof QName) {
            QName q = (QName) name;
            tagName = q.getLocalPart();
            ns = q.getNamespaceURI();
        }

        // import can be used only as top level element
        if ("import".equals(name)) {
            antXmlContext.setCurrentTarget(implicitTarget);
        } else if ("target".equals(name) && !insideTask) {
            return onStartTarget(attrs, tagName, ns);
        } else if ("defineTarget".equals(name) && !insideTask) {
            return onDefineTarget(attrs, "target", ns);
        }

        try {
            antElementHandler.onStartElement(ns, tagName, tagName, attrs, antXmlContext);
        } catch (final SAXParseException e) {
            log.log(Level.SEVERE, "Caught: " + e, e);
        }

        insideTask = true;
        final RuntimeConfigurable wrapper = antXmlContext.getWrapperStack().lastElement();
        return wrapper.getProxy();
    }

    private Target onDefineTarget(final Attributes attrs, String tagName, String ns) {
        try {
            antTargetHandler.onStartElement(ns, tagName, tagName, attrs, antXmlContext);
            final Target newTarget = getProject().getTargets().get(attrs.getValue("name"));
            antXmlContext.setCurrentTarget(newTarget);
            definingTarget = newTarget;
            return newTarget;
        } catch (final SAXParseException e) {
            log.log(Level.SEVERE, "Caught: " + e, e);
            return null;
        }
    }

    private Target onStartTarget(final Attributes attrs, String tagName, String ns) {
        try {
            antTargetHandler.onStartElement(ns, tagName, tagName, attrs, antXmlContext);
            final Target newTarget = getProject().getTargets().get(attrs.getValue("name"));

            // execute dependencies (if any)
            final Vector<Target> targets = new Vector<Target>();
            for (final Enumeration<String> deps = newTarget.getDependencies(); deps.hasMoreElements(); ) {
                final String targetName = deps.nextElement();
                targets.add(project.getTargets().get(targetName));
            }
            getProject().executeSortedTargets(targets);

            antXmlContext.setCurrentTarget(newTarget);
            return newTarget;
        } catch (final SAXParseException e) {
            log.log(Level.SEVERE, "Caught: " + e, e);
            return null;
        }
    }

    protected void setText(Object task, String text) {
        final char[] characters = text.toCharArray();
        try {
            antElementHandler.characters(characters, 0, characters.length, antXmlContext);
        } catch (final SAXParseException e) {
            log.log(Level.WARNING, "SetText failed: " + task + ". Reason: " + e, e);
        }
    }

    public Project getAntProject() {
        return project;
    }
}

/**
 * Would be nice to retrieve location information (from AST?).
 * In a first time, without info
 */
class AntBuilderLocator implements Locator {
    @Override
    public int getColumnNumber() {
        return 0;
    }

    @Override
    public int getLineNumber() {
        return 0;
    }

    @Override
    public String getPublicId() {
        return "";
    }

    @Override
    public String getSystemId() {
        return "";
    }
}
