/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
 *
 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, the
 * "License"). You may not use this file except in compliance with the
 * License. You can obtain a copy of the License at
 * http://www.netbeans.org/cddl-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * Contributor(s):
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 *
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 */

package org.apache.tools.ant.module.bridge.impl;

import java.beans.Introspector;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DemuxOutputStream;
import org.apache.tools.ant.IntrospectionHelper;
import org.apache.tools.ant.Main;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelper;
import org.apache.tools.ant.module.AntModule;
import org.apache.tools.ant.module.AntSettings;
import org.apache.tools.ant.module.api.IntrospectedInfo;
import org.apache.tools.ant.module.bridge.AntBridge;
import org.apache.tools.ant.module.bridge.BridgeInterface;
import org.apache.tools.ant.module.bridge.IntrospectionHelperProxy;
import org.apache.tools.ant.types.EnumeratedAttribute;
import org.apache.tools.ant.types.Path;
import org.netbeans.api.progress.ProgressHandle;
import org.openide.ErrorManager;
import org.openide.awt.StatusDisplayer;
import org.openide.filesystems.FileUtil;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.util.NbCollections;
import org.openide.util.RequestProcessor;
import org.openide.windows.InputOutput;
import org.openide.windows.OutputWriter;

/**
 * Implements the BridgeInterface using the current version of Ant.
 * @author Jesse Glick
 */
public class BridgeImpl implements BridgeInterface {

    private static final Logger LOG = Logger.getLogger(BridgeImpl.class.getName());

    private static final RequestProcessor RP = new RequestProcessor(BridgeImpl.class);
    
    /** Number of milliseconds to wait before forcibly halting a runaway process. */
    private static final int STOP_TIMEOUT = 10000;
    
    private final AtomicBoolean classpathInitialized = new AtomicBoolean();
    
    /**
     * Index of loggers by active thread.
     * @see #stop
     */
    private static final Map<Thread,NbBuildLogger> loggersByThread = new WeakHashMap<Thread,NbBuildLogger>();
    
    public BridgeImpl() {
    }
    
    public String getAntVersion() {
        try {
            return Main.getAntVersion();
        } catch (BuildException be) {
            AntModule.err.notify(ErrorManager.INFORMATIONAL, be);
            return NbBundle.getMessage(BridgeImpl.class, "LBL_ant_version_unknown");
        }
    }
    
    public boolean isAnt16() {
        try {
            Class.forName("org.apache.tools.ant.taskdefs.Antlib"); // NOI18N
            return true;
        } catch (ClassNotFoundException e) {
            // Fine, 1.5
            return false;
        }
    }
    
    public IntrospectionHelperProxy getIntrospectionHelper(Class<?> clazz) {
        return new IntrospectionHelperImpl(clazz);
    }
    
    public boolean toBoolean(String val) {
        return Project.toBoolean(val);
    }
    
    public String[] getEnumeratedValues(Class<?> c) {
        if (EnumeratedAttribute.class.isAssignableFrom(c)) {
            try {
                return ((EnumeratedAttribute)c.newInstance()).getValues();
            } catch (Exception e) {
                AntModule.err.notify(ErrorManager.INFORMATIONAL, e);
            }
        } else if (Enum.class.isAssignableFrom(c)) { // Ant 1.7.0 (#41058)
            try {
                Enum<?>[] vals = (Enum<?>[]) c.getMethod("values").invoke(null);
                String[] names = new String[vals.length];
                for (int i = 0; i < vals.length; i++) {
                    names[i] = vals[i].name();
                }
                return names;
            } catch (Exception x) {
                Exceptions.printStackTrace(x);
            }
        }
        return null;
    }
        
    @Override
    public boolean run(File buildFile, List<String> targets, InputStream in, OutputWriter out, OutputWriter err, Map<String,String> properties,
            Set<? extends String> concealedProperties, int verbosity, String displayName, Runnable interestingOutputCallback, ProgressHandle handle, InputOutput io) {
        if (classpathInitialized.compareAndSet(false, true)) {
            // #46171: Ant expects this path to have itself and whatever else you loaded with it,
            // or AntClassLoader.getResources will not be able to find anything in the Ant loader.
            //Proabably not needed anymore: https://bz.apache.org/bugzilla/show_bug.cgi?id=30161
            Path.systemClasspath = new Path(null, AntBridge.getMainClassPath());
        }
        
        boolean ok = false;
        
        // Important for various other stuff.
        final boolean ant16 = isAnt16();
        
        // Make sure "main Ant loader" is used as context loader for duration of the
        // run. Otherwise some code, e.g. JAXP, will accidentally pick up NB classes,
        // which can cause various undesirable effects.
        ClassLoader oldCCL = Thread.currentThread().getContextClassLoader();
        ClassLoader newCCL = Project.class.getClassLoader();
        LOG.log(Level.FINER, "Fixing CCL: {0} -> {1}", new Object[] {oldCCL, newCCL});
        Thread.currentThread().setContextClassLoader(newCCL);
        try {
        
        final Project project;
        
        // first use the ProjectHelper to create the project object
        // from the given build file.
        final NbBuildLogger logger = new NbBuildLogger(buildFile, out, err, verbosity, displayName, properties, concealedProperties, interestingOutputCallback, handle, io);
        Vector<String> targs;
        try {
            project = new Project();
            project.addBuildListener(logger);
            project.init();
            project.addTaskDefinition("java", ForkedJavaOverride.class); // #56341
            project.addTaskDefinition("input", InputOverride.class); // #155056
            try {
                addCustomDefs(project);
            } catch (IOException e) {
                throw new BuildException(e);
            }
            project.setUserProperty("ant.file", buildFile.getAbsolutePath()); // NOI18N
            // #14993:
            project.setUserProperty("ant.version", Main.getAntVersion()); // NOI18N
            File antHome = AntSettings.getAntHome();
            if (antHome != null) {
                project.setUserProperty("ant.home", antHome.getAbsolutePath()); // NOI18N
            }
            String ENABLE_TESTLISTENER_EVENTS = "ant.junit.enabletestlistenerevents"; // NOI18N; since 1.8.2 in JUnitTask
            project.setProperty(ENABLE_TESTLISTENER_EVENTS, "true"); // NOI18N
            for (Map.Entry<String,String> entry : properties.entrySet()) {
                project.setUserProperty(entry.getKey(), entry.getValue());
            }
            if (in != null && ant16) {
                try {
                    Method m = Project.class.getMethod("setDefaultInputStream", InputStream.class); // NOI18N
                    m.invoke(project, in);
                } catch (Exception e) {
                    AntModule.err.notify(ErrorManager.INFORMATIONAL, e);
                }
            }
            LOG.log(Level.FINER, "CCL when configureProject is called: {0}", Thread.currentThread().getContextClassLoader());
            ProjectHelper projhelper = ProjectHelper.getProjectHelper();
            // Cf. Ant #32668 & #32216; ProjectHelper.configureProject undeprecated in 1.7
            project.addReference("ant.projectHelper", projhelper); // NOI18N
            projhelper.parse(project, buildFile);
            
            project.setInputHandler(new NbInputHandler(interestingOutputCallback));
            
            if (targets != null) {
                targs = new Vector<String>(targets);
            } else {
                targs = new Vector<String>(1);
                targs.add(project.getDefaultTarget());
            }
            logger.setActualTargets(targets != null ? targets.toArray(new String[targets.size()]) : null);
        }
        catch (BuildException be) {
            logger.buildInitializationFailed(be);
            logger.shutdown();
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    AntModule.err.notify(e);
                }
            }
            return false;
        }
        
        project.fireBuildStarted();
        
        // Save & restore system output streams.
        InputStream is = System.in;
        if (in != null && ant16) {
            try {
                Class<? extends InputStream> dis = Class.forName("org.apache.tools.ant.DemuxInputStream").asSubclass(InputStream.class); // NOI18N
                Constructor<? extends InputStream> c = dis.getConstructor(Project.class);
                is = c.newInstance(project);
            } catch (Exception e) {
                AntModule.err.notify(ErrorManager.INFORMATIONAL, e);
            }
        }
        AntBridge.pushSystemInOutErr(is,
                                     new PrintStream(new DemuxOutputStream(project, false)),
                                     new PrintStream(new DemuxOutputStream(project, true)));

        Thread currentThread = Thread.currentThread();
        synchronized (loggersByThread) {
            assert !loggersByThread.containsKey(currentThread);
            loggersByThread.put(currentThread, logger);
        }
        try {
            if (Thread.interrupted()) {
                logger.shutdown();
                return false;
            }
            // Execute the configured project
            //writer.println("#4"); // NOI18N
            project.executeTargets(targs);
            //writer.println("#5"); // NOI18N
            project.fireBuildFinished(null);
            ok = true;
        } catch (Throwable t) {
            // Really need to catch everything, else AntClassLoader.cleanup may
            // not be called, resulting in a memory leak and/or locked JARs (#42431).
            project.fireBuildFinished(t);
        } finally {
            AntBridge.restoreSystemInOutErr();
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    AntModule.err.notify(e);
                }
            }
            synchronized (loggersByThread) {
                loggersByThread.remove(currentThread);
            }
        }
        
        // Now check to see if the Project defined any cool new custom tasks.
        RP.post(new PostRun(project, logger));
        
        } finally {
            LOG.log(Level.FINER, "Restoring CCL: {0}", oldCCL);
            Thread.currentThread().setContextClassLoader(oldCCL);
        }
        
        return ok;
    }

    private static final RequestProcessor.Task refreshFilesystemsTask = RP.create(new Runnable() {
        public @Override void run() {
            LOG.log(Level.FINE, "Refreshing filesystems");
            FileUtil.refreshAll(); 
        }
    });

    public void stop(final Thread process) {
        NbBuildLogger logger;
        synchronized (loggersByThread) {
            logger = loggersByThread.get(process);
        }
        if (logger != null) {
            // Try stopping at a safe point.
            StatusDisplayer.getDefault().setStatusText(NbBundle.getMessage(BridgeImpl.class, "MSG_stopping", logger.getDisplayNameNoLock()));
            logger.stop();
        }
        process.interrupt();
        // But if that doesn't do it, double-check later...
        // Yes Thread.stop() is deprecated; that is why we try to avoid using it.
        RP.create(new StopProcess(process)).schedule(STOP_TIMEOUT);
    }
    private static class StopProcess implements Runnable {
        private final Thread process;
        StopProcess(Thread process) {
            this.process = process;
        }
        public @Override void run() {
            if (process.isAlive()) {
                StatusDisplayer.getDefault().setStatusText(NbBundle.getMessage(BridgeImpl.class, "MSG_halting"));
                stopThread();
            }
        }
        @SuppressWarnings("deprecation")
        private void stopThread() {
            process.stop();
        }
    }

    private static void addCustomDefs(Project project) throws BuildException, IOException {
        long start = System.currentTimeMillis();
        if (AntBridge.getInterface().isAnt16()) {
            Map<String,ClassLoader> antlibLoaders = AntBridge.getCustomDefClassLoaders();
            for (Map.Entry<String,ClassLoader> entry : antlibLoaders.entrySet()) {
                String cnb = entry.getKey();
                ClassLoader l = entry.getValue();
                String resource = cnb.replace('.', '/') + "/antlib.xml"; // NOI18N
                URL antlib = l.getResource(resource);
                if (antlib == null) {
                    throw new IOException("Could not find " + resource + " in ant/nblib/" + cnb.replace('.', '-') + ".jar"); // NOI18N
                }
                // Once with no namespaces.
                NbAntlib.process(project, antlib, null, l);
                // Once with.
                String antlibUri = "antlib:" + cnb; // NOI18N
                NbAntlib.process(project, antlib, antlibUri, l);
            }
        } else {
            // For Ant 1.5, just dump in old-style defs in the simplest manner.
            Map<String,Map<String,Class>> customDefs = AntBridge.getCustomDefsNoNamespace();
            for (Map.Entry<String,Class> entry : customDefs.get("task").entrySet()) { // NOI18N
                project.addTaskDefinition(entry.getKey(), entry.getValue());
            }
            for (Map.Entry<String,Class> entry : customDefs.get("type").entrySet()) { // NOI18N
                project.addDataTypeDefinition(entry.getKey(), entry.getValue());
            }
        }
        if (AntModule.err.isLoggable(ErrorManager.INFORMATIONAL)) {
            AntModule.err.log("addCustomDefs took " + (System.currentTimeMillis() - start) + "msec");
        }
    }
    
    private static boolean doGutProject = !Boolean.getBoolean("org.apache.tools.ant.module.bridge.impl.BridgeImpl.doNotGutProject");
    /**
     * Try to break up as many references in a project as possible.
     * Helpful to mitigate the effects of unsolved memory leaks: at
     * least one project will not hold onto all subprojects, and a
     * taskdef will not hold onto its siblings, etc.
     */
    private static void gutProject(Project p) {
        if (!doGutProject) {
            return;
        }
        // XXX should ideally try to wait for all other threads in this thread group
        // to finish - see e.g. #51962 for example of what can happen otherwise.
        try {
            String s = p.getName();
            AntModule.err.log("Gutting extra references in project \"" + s + "\"");
            for (Field f : Project.class.getDeclaredFields()) {
                if (Modifier.isStatic(f.getModifiers())) {
                    continue;
                }
                if (f.getType().isPrimitive()) {
                    continue;
                }
                f.setAccessible(true);
                Object o = f.get(p);
                if (o == null) {
                    continue;
                }
                try {
                    if (o instanceof Collection<?>) {
                        ((Collection<?>) o).clear();
                        // #69727: do not null out the field (e.g. Project.listeners) in this case.
                        continue;
                    } else if (o instanceof Map<?,?>) {
                        ((Map<?,?>) o).clear();
                        continue;
                    }
                } catch (UnsupportedOperationException e) {
                    // ignore
                }
                if (Modifier.isFinal(f.getModifiers())) {
                    continue;
                }
                if (o.getClass().isArray()) {
                    f.set(p, Array.newInstance(o.getClass().getComponentType(), 0));
                    continue;
                }
                f.set(p, null);
            }
            // #43113: IntrospectionHelper can hold strong refs to dynamically loaded classes
            Field helpersF;
            try {
                helpersF = IntrospectionHelper.class.getDeclaredField("helpers");
            } catch (NoSuchFieldException x) { // Ant 1.7.0
                helpersF = IntrospectionHelper.class.getDeclaredField("HELPERS");
            }
            helpersF.setAccessible(true);
            Object helpersO = helpersF.get(null);
            Map<?,?> helpersM = (Map<?,?>) helpersO;
            helpersM.clear();
            // #46532: java.beans.Introspector caches not cleared well in all cases.
            Introspector.flushCaches();
        } catch (Exception e) {
            // Oh well.
            AntModule.err.notify(ErrorManager.INFORMATIONAL, e);
            doGutProject = false;
        }
    }

    private static class PostRun implements Runnable {
        private Project project;
        private NbBuildLogger logger;
        public PostRun(Project project, NbBuildLogger logger) {
            this.project = project;
            this.logger = logger;
        }
        public @Override void run() {
            IntrospectedInfo custom = AntSettings.getCustomDefs();
            @SuppressWarnings("rawtypes")
            Map<String,Map<String,Class>> defs = new HashMap<String, Map<String, Class>>();
            try {
                defs.put("task", NbCollections.checkedMapByCopy(project.getTaskDefinitions(), String.class, Class.class, true));
                defs.put("type", NbCollections.checkedMapByCopy(project.getDataTypeDefinitions(), String.class, Class.class, true));
            } catch (ThreadDeath t) {
                // #137883: late clicks on Stop which can be ignored.
            }
            custom.scanProject(defs);
            AntSettings.setCustomDefs(custom);
            logger.shutdown();
            // #85698: do not invoke multiple refreshes at once
            refreshFilesystemsTask.schedule(0);
            gutProject(project);
            // #185853: make sure these fields do not stick around
            project = null;
            logger = null;
        }
    }
    
}
