package de.saumya.mojo.jruby;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactResolutionRequest;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.apache.maven.repository.RepositorySystem;
import org.codehaus.classworlds.ClassRealm;
import org.codehaus.classworlds.NoSuchRealmException;
import org.codehaus.plexus.util.Scanner;
import org.sonatype.plexus.build.incremental.BuildContext;

import de.saumya.mojo.ruby.Logger;
import de.saumya.mojo.ruby.script.ScriptException;
import de.saumya.mojo.ruby.script.ScriptFactory;

/**
 * Base for all JRuby mojos.
 *
 * @requiresProject false
 */
public abstract class AbstractJRubyMojo extends AbstractMojo {

    protected static final String JRUBY_COMPLETE = "jruby-complete";

    protected static final String JRUBY_CORE = "jruby-core";

    protected static final String JRUBY_STDLIB = "jruby-stdlib";

    protected static final String DEFAULT_JRUBY_VERSION = "1.7.19";


    /**
     * common arguments
     * <br/>
     * Command line -Dargs=...
     *
     * @parameter expression="${args}"
     */
    protected String args;

    /**
     * jvm arguments for the java command executing jruby
     * <br/>
     * Command line -Djruby.jvmargs=...
     *
     * @parameter expression="${jruby.jvmargs}"
     */
    protected String jrubyJvmArgs;

    /**
     * switches for the jruby command, like '--1.9'
     * <br/>
     * Command line -Djruby.switches=...
     *
     * @parameter expression="${jruby.switches}"
     */
    protected String jrubySwitches;

    /**
     * environment values passed on to the jruby process. needs jrubyFork true.
     * <br/>
     * @parameter
     */
    protected Map<String, String> env;

    /**
     * if the pom.xml has no runtime dependency to a jruby-complete.jar then
     * this version is used to resolve the jruby-complete dependency from the
     * local/remote maven repository. it overwrites the jruby version from
     * the dependencies if any. i.e. you can easily switch jruby version from the commandline !
     * <br/>
     * default: 1.7.19
     * <br/>
     * Command line -Djruby.version=...
     *
     * @parameter expression="${jruby.version}"
     */
    private String jrubyVersion;

    /**
     * fork the JRuby execution.
     * <br/>
     * Command line -Djruby.fork=...
     *
     * @parameter expression="${jruby.fork}" default-value="true"
     */
    protected boolean jrubyFork;

    /**
     * verbose jruby related output
     * <br/>
     * Command line -Djruby.verbose=...
     *
     * @parameter expression="${jruby.verbose}" default-value="false"
     */
    protected boolean jrubyVerbose;

    /**
     * directory with ruby sources - added to java classpath and ruby loadpath
     * <br/>
     * Command line -Djruby.sourceDirectory=...
     *
     * @parameter expression="${jruby.sourceDirectory}" default-value="src/main/ruby"
     */
    protected File rubySourceDirectory;

    /**
     * directory with ruby sources - added to ruby loadpath only
     * <br/>
     * Command line -Djruby.lib=...
     *
     * @parameter expression="${jruby.lib}" default-value="lib"
     */
    protected File libDirectory;

    /**
     * the launch directory for the JRuby execution.
     * <br/>
     * Command line -Djruby.launchDirectory=...
     *
     * @parameter default-value="${project.basedir}" expression="${jruby.launchDirectory}"
     */
    private File launchDirectory;

    /**
     * reference to maven project for internal use.
     *
     * @parameter expression="${project}"
     * @required
     * @readOnly
     */
    protected MavenProject project;

    /**
     * add project test class path to JVM classpath.
     * @parameter default-value=true expression="${gem.addProjectClasspath}"
     */
    protected boolean addProjectClasspath;
    
    /**
     * local repository for internal use.
     *
     * @parameter default-value="${localRepository}"
     * @required
     * @readonly
     */
    protected ArtifactRepository localRepository;

    /**
     * classrealm for internal use.
     *
     * @parameter expression="${dummyExpression}"
     * @readonly
     */
    protected ClassRealm classRealm;

    /** @component */
    protected RepositorySystem repositorySystem;

    protected Logger logger;

    protected ScriptFactory factory;

    private JRubyVersion jRubyVersion;

    /** @component */
    private BuildContext buildContext;
    
    
    /** @parameter expression="${m2e.jruby.watch}" */
    protected List<String> eclipseWatches = new ArrayList<String>();
    
    /** @parameter expression="${m2e.jruby.refresh}" */
    protected List<String> eclipseRefresh = new ArrayList<String>(); 
    
    protected JRubyVersion getJrubyVersion()
    {
        if (jRubyVersion == null )
        {
            this.jRubyVersion = new JRubyVersion( jrubyVersion == null ? DEFAULT_JRUBY_VERSION : jrubyVersion );
        }
        return jRubyVersion;
    }
    
    private ScriptFactory newScriptFactory() throws MojoExecutionException {
    	ScriptFactory factory = createScriptFactory();
    	if( env != null ){
    		for( Map.Entry<String, String> entry: env.entrySet() ){
    			factory.addEnv( entry.getKey(), entry.getValue() );
    		}
    	}
    	return factory;
    }

	private ScriptFactory createScriptFactory() throws MojoExecutionException {
        try
        {
            classRealm.getWorld().disposeRealm("jruby-all");
        }
        catch( NoSuchRealmException ignore )
        {
            // ignore
        }
        try {
			ClassRealm realm = classRealm.getWorld().newRealm("jruby-all");
			for (String path : getProjectClasspath()) {
				realm.addConstituent(new File(path).toURI().toURL());
			}
			if (this.jrubyVersion != null) {
	            // preference to command line or property version
	            return newScriptFactory( resolveJRubyCompleteArtifact(this.jrubyVersion) );
	        } 
	        // check if there is jruby present
			Class<?> clazz = realm.loadClass("org.jruby.runtime.Constants");
			if ( jrubyVerbose ){
				String version = clazz.getField( "VERSION" ).get(clazz).toString();
				getLog().info("found jruby on classpath");
				getLog().info("jruby version   : " + version);
			}
			this.classRealm = realm;
			return newScriptFactory( null );
		} catch (final Exception e) {
		    //TODO debug
		    //e.printStackTrace();
			try {
				return newScriptFactory(resolveJRubyArtifact());
			} catch (final DependencyResolutionRequiredException ee) {
				throw new MojoExecutionException("could not resolve jruby", e);
			}
		}
	}

    protected ScriptFactory newScriptFactory(Artifact artifact) throws MojoExecutionException {
        try {
            final ScriptFactory factory = 
            		artifact == null ? 
                            new ScriptFactory(this.logger,
                                    this.classRealm, 
                                    null,
                                    getProjectClasspath(), 
                                    this.jrubyFork):
            			(JRUBY_CORE.equals(artifact.getArtifactId()) ?
                    new ScriptFactory(this.logger,
                            this.classRealm, 
                            artifact.getFile(),
                            resolveJRubyStdlibArtifact(artifact).getFile(),
                            getProjectClasspath(), 
                            this.jrubyFork) :
                    new ScriptFactory(this.logger,
                            this.classRealm, 
                            artifact.getFile(),
                            getProjectClasspath(), 
                            this.jrubyFork) );

            if(libDirectory != null && libDirectory.exists()){
                if(jrubyVerbose){
                    getLog().info("add to ruby loadpath: " + libDirectory.getAbsolutePath());
                }
                // add it to the load path for all scripts using that factory
                factory.addSwitch("-I", libDirectory.getAbsolutePath());
            }
            if(rubySourceDirectory != null && rubySourceDirectory.exists()){
                if(jrubyVerbose){
                    getLog().info("add to ruby loadpath: " + rubySourceDirectory.getAbsolutePath());
                }
                // add it to the load path for all scripts using that factory
                factory.addSwitch("-I", rubySourceDirectory.getAbsolutePath());
            }
            return factory;
        } catch (final DependencyResolutionRequiredException e) {
            throw new MojoExecutionException("could not resolve jruby", e);
        } catch (final ScriptException e) {
            throw new MojoExecutionException(
                    "could not initialize script factory", e);
        } catch (final IOException e) {
            throw new MojoExecutionException(
                    "could not initialize script factory", e);
        }
    }

    public void execute() throws MojoExecutionException, MojoFailureException {
    	boolean shouldCheckChanges = buildContext.isIncremental() && !eclipseWatches.isEmpty();    	
    	if (shouldCheckChanges && !buildContext.hasDelta(eclipseWatches)) {
    		return;
    	}
    	
    	 
        System.setProperty("jbundle.skip", "true");
        this.logger = new MojoLogger(this.jrubyVerbose, getLog());
        this.factory = newScriptFactory();
        // skip installing jars via jbundler
        this.factory.addEnv("JBUNDLE_SKIP", "true");
        // skip installing jars via jar-dependencies
        this.factory.addEnv("JARS_SKIP", "true");
        this.factory.addJvmArgs(this.jrubyJvmArgs);
        this.factory.addSwitches(this.jrubySwitches);

        if(rubySourceDirectory != null && rubySourceDirectory.exists()){
            if(jrubyVerbose){
                getLog().info("add to java classpath: " + rubySourceDirectory.getAbsolutePath());
            }
            // add it to the classpath so java classes can find the ruby files
            Resource resource = new Resource();
            resource.setDirectory(rubySourceDirectory.getAbsolutePath());
            project.getBuild().getResources().add(resource);
        }

        try {

            executeJRuby();

        } catch (final IOException e) {
            throw new MojoExecutionException("error in executing jruby", e);
        } catch (final ScriptException e) {
            throw new MojoExecutionException("error in executing jruby", e);
        } finally {
        	// ensure that eclipse update any changes, include errors reports
        	if (!eclipseRefresh.isEmpty()) {
        		for (String fileName : eclipseRefresh) {
        			File file = new File(fileName);
        			buildContext.refresh(file);;
        		}
        	}
        }
    }

    abstract protected void executeJRuby() throws MojoExecutionException,
            MojoFailureException, IOException, ScriptException;

    protected File launchDirectory() {
        if (this.launchDirectory == null) {
            this.launchDirectory = this.project.getBasedir();
            if (this.launchDirectory == null || !this.launchDirectory.exists()) {
                this.launchDirectory = new File(System.getProperty("user.dir"));
            }
        }
        return this.launchDirectory;
    }

    protected Artifact resolveJRubyCompleteArtifact(final String version)
            throws DependencyResolutionRequiredException {
        getLog().debug("resolve jruby for version " + version);
        final Artifact artifact = this.repositorySystem.createArtifact(
                "org.jruby", JRUBY_COMPLETE, version, "jar");
        return resolveJRubyArtifact(artifact);
    }

    private Artifact resolveJRubyArtifact(final Artifact artifact)
            throws DependencyResolutionRequiredException {
        final ArtifactResolutionRequest request = new ArtifactResolutionRequest();
        request.setArtifact(artifact);
        request.setLocalRepository(this.localRepository);
        request.setRemoteRepositories(this.project.getRemoteArtifactRepositories());
        this.repositorySystem.resolve(request);

        if (this.jrubyVerbose) {
            getLog().info("jruby version   : " + artifact.getVersion());
        }
        // set it so other plugins can retrieve the version in use
        this.jrubyVersion = artifact.getVersion();
        return artifact;
    }

    protected Artifact resolveJRubyArtifact() throws DependencyResolutionRequiredException,
            MojoExecutionException {
        if (this.jrubyVersion != null) {
            // preference to command line or property version
            return resolveJRubyCompleteArtifact(this.jrubyVersion);
        } 
        else {
            // then take jruby from the dependencies either jruby-complete or jruby-core
            for (final Dependency artifact : this.project.getDependencies()) {
                if ((artifact.getArtifactId().equals(JRUBY_COMPLETE)
                      ||  artifact.getArtifactId().equals(JRUBY_CORE))
                      // TODO this condition is not needed ?
                            && !artifact.getScope().equals(Artifact.SCOPE_PROVIDED)
                        && !artifact.getScope().equals(Artifact.SCOPE_SYSTEM)) {
                    return resolveJRubyArtifact(this.repositorySystem
                            .createArtifact(artifact.getGroupId(), artifact
                                    .getArtifactId(), artifact.getVersion(),
                                    artifact.getType()));
                }
            }
        }
        // finally fall back on the default version of jruby
        return resolveJRubyCompleteArtifact(DEFAULT_JRUBY_VERSION);
    }

    protected Artifact resolveJRubyStdlibArtifact(Artifact jruby) throws DependencyResolutionRequiredException,
            MojoExecutionException {
        final ArtifactResolutionRequest request = new ArtifactResolutionRequest();
        for (final Dependency artifact : this.project.getDependencies()) {
            if (artifact.getArtifactId().equals(JRUBY_STDLIB)
                    // TODO this condition is not needed ?
                    && !artifact.getScope().equals(Artifact.SCOPE_PROVIDED)
                    && !artifact.getScope().equals(Artifact.SCOPE_SYSTEM)) {
                request.setArtifact(this.repositorySystem
                        .createArtifact(artifact.getGroupId(), artifact
                                .getArtifactId(), artifact.getVersion(),
                                artifact.getType()));
                break;
            }
        }
        if (request.getArtifact() == null){
            request.setResolveTransitively(true);
            request.setArtifact(jruby);
        }
        request.setLocalRepository(this.localRepository);
        request.setRemoteRepositories(this.project.getRemoteArtifactRepositories());
        Set<Artifact> set = this.repositorySystem.resolve(request).getArtifacts();
        for (Artifact a: set){
            if (JRUBY_STDLIB.equals(a.getArtifactId())) {
                return a;
            }
        }
        throw new MojoExecutionException("failed to resolve jruby stdlib artifact");
    }
    
    protected List<String> getProjectClasspath() throws DependencyResolutionRequiredException {
    	if (addProjectClasspath) {
    		return project.getTestClasspathElements();
    	} else {
    		return new ArrayList<String>();
    	}
    }
    
}
