/*
 * Copyright 2011 SpringSource
 *
 * Licensed 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 org.codehaus.groovy.grails.compiler;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.tools.ant.BuildException;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.MultipleCompilationErrorsException;
import org.codehaus.groovy.grails.cli.agent.GrailsPluginManagerReloadPlugin;
import org.codehaus.groovy.grails.commons.ClassPropertyFetcher;
import org.codehaus.groovy.grails.plugins.DefaultGrailsPluginManager;
import org.codehaus.groovy.grails.plugins.GrailsPlugin;
import org.codehaus.groovy.grails.plugins.GrailsPluginInfo;
import org.codehaus.groovy.grails.plugins.GrailsPluginManager;
import org.codehaus.groovy.grails.plugins.support.WatchPattern;
import org.springframework.core.io.Resource;
import org.springframework.util.ClassUtils;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * Watches a Grails projects and re-compiles sources when they change or fires events to the pluginManager
 *
 * @author Graeme Rocher
 * @since 1.4
 */
public class GrailsProjectWatcher extends DirectoryWatcher{
    private static final Log LOG = LogFactory.getLog(GrailsProjectWatcher.class);
    private static final Queue<Runnable> classChangeEventQueue = new ConcurrentLinkedQueue<Runnable>();
    private static boolean active = false;
    public static final String SPRING_LOADED_PLUGIN_CLASS = "com.springsource.loaded.Plugins";


    private List<String> compilerExtensions;
    private GrailsPluginManager pluginManager;
    private GrailsProjectCompiler compiler;
    private Map<File, GrailsPlugin> descriptorToPluginMap = new ConcurrentHashMap<File, GrailsPlugin>();

    public GrailsProjectWatcher(final GrailsProjectCompiler compiler, GrailsPluginManager pluginManager) {
        super();


        this.pluginManager = pluginManager;
        this.compilerExtensions = compiler.getCompilerExtensions();
        this.compiler = compiler;
        if(ClassUtils.isPresent(SPRING_LOADED_PLUGIN_CLASS, getClass().getClassLoader())) {
            GrailsPluginManagerReloadPlugin.register();
        }
    }

    /**
     * Whether the watcher is active
     * @return True if it is
     */
    public static boolean isActive() {
        return active;
    }

    /**
     * Fire any pending class change events
     */
    public static void firePendingClassChangeEvents() {
        for (Runnable runnable : classChangeEventQueue) {
            runnable.run();
        }
        classChangeEventQueue.clear();
    }

    @Override
    public void run() {
        active = true;
        for (String directory : compiler.getSrcDirectories()) {
            addWatchDirectory(new File(directory), compilerExtensions);
        }
        Resource[] pluginSourceFiles = compiler.getPluginSettings().getPluginSourceFiles();
        for (Resource pluginSourceFile : pluginSourceFiles) {
            try {
                if(pluginSourceFile.getFile().isDirectory()) {
                    addWatchDirectory(pluginSourceFile.getFile(), compilerExtensions);
                }
            } catch (IOException e) {
                // ignore
            }
        }

        addListener(new FileChangeListener() {
            public void onChange(File file) {
                LOG.info("File ["+file+"] changed. Applying changes to application.");
                if(descriptorToPluginMap.containsKey(file)) {
                    reloadPlugin(file);
                }
                else {
                    compileIfSource(file);
                    informPluginManager(file, false);
                }

            }

            public void onNew(File file) {
                LOG.info("File ["+file+"] added. Applying changes to application.");
                sleep(5000);
                compileIfSource(file);
                informPluginManager(file, true);
            }
        });

        GrailsPlugin[] allPlugins = pluginManager.getAllPlugins();

        for (GrailsPlugin plugin : allPlugins) {
            // watch the plugin descriptor for changes
            GrailsPluginInfo info = compiler.getPluginSettings().getPluginInfoForName(plugin.getFileSystemShortName());

            if(info != null && info.getDescriptor() != null) {
                try {
                    Resource descriptor = info.getDescriptor();
                    plugin.setDescriptor(descriptor);
                    File pluginFile = descriptor.getFile();
                    descriptorToPluginMap.put(pluginFile, plugin);
                    addWatchFile(pluginFile);
                } catch (IOException e) {
                    // ignore
                }
            }
            List<WatchPattern> watchPatterns = plugin.getWatchedResourcePatterns();
            if(watchPatterns != null) {
                for (WatchPattern watchPattern : watchPatterns) {
                    if(watchPattern.getFile() != null) {
                        addWatchFile(watchPattern.getFile());
                    }
                    else if(watchPattern.getDirectory() != null) {
                        addWatchDirectory(watchPattern.getDirectory(),watchPattern.getExtension());
                    }
                }
            }

        }
        super.run();
    }

    private void reloadPlugin(File file) {
        GrailsPlugin grailsPlugin = descriptorToPluginMap.get(file);
        grailsPlugin.refresh();
        if(pluginManager instanceof DefaultGrailsPluginManager) {
            ((DefaultGrailsPluginManager)pluginManager).reloadPlugin(grailsPlugin);
        }
    }

    private void informPluginManager(final File file, boolean isNew) {
        if(!isSourceFile(file) || isNew) {
            try {
                pluginManager.informOfFileChange(file);
            } catch (Exception e) {
                LOG.error("Failed to reload file ["+file+"] with error: " + e.getMessage());
            }
        }
        else {
            // add to class change event queue
            classChangeEventQueue.add(new Runnable() {
                public void run() {
                    try {
                        pluginManager.informOfFileChange(file);
                    } catch (Exception e) {
                        LOG.error("Failed to reload file ["+file+"] with error: " + e.getMessage());
                    }
                }
            });
        }
    }

    private void compileIfSource(File file) {
        try {
            if(isSourceFile(file)) {
                compiler.compileAll();
                ClassPropertyFetcher.clearClassPropertyFetcherCache();
            }
        }
        catch (MultipleCompilationErrorsException e) {
            LOG.error("Compilation Error: " + e.getMessage());
        }
        catch(CompilationFailedException e) {
            LOG.error("Compilation Error: " + e.getMessage());
        }
        catch(BuildException e) {
            LOG.error("Compilation Error: " + e.getCause().getMessage());
        }
    }


    private void sleep(int time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            // ignore
        }
    }

    private boolean isSourceFile(File file) {
        for (String compilerExtension : compilerExtensions) {
            if(file.getName().endsWith(compilerExtension))
                return true;
        }
        return false;
    }
}

@@ -3145,24 +3145,42 @@
 eEvents() %7B%0A
+        try %7B%0A    
         for 
@@ -3237,16 +3237,20 @@
         
+    
 runnable
@@ -3257,34 +3257,62 @@
 .run();%0A        
+    
 %7D%0A
+        %7D finally %7B%0A    
         classCha
@@ -3330,24 +3330,34 @@
 ue.clear();%0A
+        %7D%0A
     %7D%0A%0A    @

clear the class change queue in a finally block to avoid running again on errors