package de.saumya.mojo.gem;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.sql.Date;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.resolver.ArtifactResolutionRequest;
import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.StringUtils;

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

/**
 * goal to convert that artifact into a gem or uses a given gemspec to build a gem.
 * 
 * @goal package
 * @requiresDependencyResolution test
 */
public class PackageMojo extends AbstractGemMojo {

    /**
     * @parameter expression="${project.build.directory}"
     */
    File                              buildDirectory;

    /**
     * the gemspec to use for building the gem
     * <br/>
     * Command line -Dgemspec=...
     * 
     * @parameter default-value="${gemspec}"
     */
    File                              gemspec;

    /**
     * use the pom to generate a gemspec and overwrite the one in lauchDirectory.
     * <br/>
     * Command line -Dgemspec.overwrite=...
     * 
     * @parameter default-value="${gemspec.overwrite}"
     */
    boolean                           gemspecOverwrite = false;

    /** @parameter */
    private String                    date;

    /** @parameter */
    private String                    extraRdocFiles;

    /** @parameter */
    private String                    extraFiles;

    /** @parameter */
    private String                    rdocOptions;

    /** @parameter */
    private String                    requirePaths;

    /** @parameter */
    private String                    rubyforgeProject;

    /** @parameter */
    private String                    rubygemsVersion;

    /** @parameter */
    private String                    requiredRubygemsVersion;

    /** @parameter */
    private String                    bindir;

    /** @parameter */
    private String                    requiredRubyVersion;

    /** @parameter */
    private String                    postInstallMessage;

    /** @parameter */
    private String                    executables;

    /** @parameter */
    private String                    extensions;

    /** @parameter */
    private String                    platform;

    /** @parameter default-value="gem_hook.rb" */
    private String                    gemHook;

    /**
     * @parameter default-value="false"
     */
    boolean                           includeDependencies;

    /**
     * use repository layout for included dependencies
     * @parameter default-value="false"
     */
    boolean                           useRepositoryLayout;

    private void generatePom(File source, File target) throws ScriptException, IOException {
        URL url = Thread.currentThread().getContextClassLoader().getResource("maven/tools/pom.rb");
        String baseUrl = url.toExternalForm().replace("/maven/tools/pom.rb", "");
        // this assumes to get an url JRuby understands !!
        this.factory.newScript("def warn(*args);end;$LOAD_PATH << '" + baseUrl + "';"
                + "require 'maven/tools/pom';"
                + "puts Maven::Tools::POM.new('" + source.getAbsolutePath() + "').to_s")
            .executeIn(launchDirectory(), target);
    }
    
    @Override
    public void executeJRuby() throws MojoExecutionException,
            MojoFailureException, ScriptException {
        final MavenProject project = this.project;
        final GemArtifact artifact = new GemArtifact(project);
        try {
            // first case no gemspec file given but there is pom => use pom to generate gemspec
            if (this.gemspec == null && project.getBasedir() != null
                    && project.getBasedir().exists()) {
                // TODO generate the gemspec in the prepare-package phase so we
                // can use it separately
                buildFromPom(project, artifact);

                // use POM generated by polyglot maven instead of project
                // file (which can be 'Gemfile' or a gemspec file) to allow 
                // to install it into the local repository and be
                // usable for proper maven
                // maybe that is a bug in polyglot maven ?
                final File pom = new File(this.project.getFile()
                        .getAbsolutePath()
                        + ".pom");
                if (pom.exists()) {
                    this.project.setFile(pom);
                }

            }
            else {
                // no gemspec file given => find a gemspec file in launchDirectory
                if (this.gemspec == null) {
                    for (final File f : launchDirectory().listFiles()) {
                        if (f.getName().endsWith(".gemspec")) {
                            if (this.gemspec == null) {
                                this.gemspec = f;
                            }
                            else {
                                throw new MojoFailureException("more than one gemspec file found, use -Dgemspec=... to specifiy one");
                            }
                        }
                    }
                    if (this.gemspec == null) {
                        throw new MojoFailureException("no gemspec file or pom found, use -Dgemspec=... to specifiy a gemspec file or '-f ...' to use a pom file");
                    }
                    else {
                        getLog().info("use gemspec: " + this.gemspec);
                    }
                }
                
                ArtifactResolutionResult jarDependencyArtifacts = includeDependencies(  project,
                                                                                        artifact );

                if (jarDependencyArtifacts != null ) {
                    getLog().info("use repository layout? " + this.useRepositoryLayout);
                    for (final Object element : jarDependencyArtifacts.getArtifacts()) {
                        final Artifact dependency = (Artifact) element;
                        getLog().info(" -- include -- " + dependency);
                        File target;
                        if ( useRepositoryLayout )
                        {   
                            StringBuilder path = new StringBuilder( libDirectory.getAbsolutePath() );
                            path.append( File.separatorChar ).append( dependency.getGroupId().replace( ".", File.separator ) );
                            path.append( File.separatorChar ).append( dependency.getArtifactId() );
                            path.append( File.separatorChar ).append( dependency.getVersion() );
                            target = new File( path.toString(), dependency.getFile().getName() );
                            target.getParentFile().mkdirs();
                        }
                        else
                        {
                            target = new File( libDirectory, dependency.getFile().getName() );
                        }
                        FileUtils.copyFile( dependency.getFile(), target );
                    }
                }
                
                // now we have a gemspec file - either found or given
                this.factory.newScriptFromJRubyJar("gem")
                        .addArg("build", this.gemspec)
                        .executeIn(launchDirectory());

                File newPom = new File( this.gemspec.getParentFile(), "pom." + this.gemspec.getName() + ".xml");
                generatePom(this.gemspec, newPom);
                project.setFile(newPom);
                
                // find file with biggest lastModified
                File gemFile = null;
                for (final File f : launchDirectory().listFiles()) {
                    if (f.getName().endsWith(".gem")) {
                        if (gemFile == null || gemFile.lastModified() < f.lastModified() )
                        {
                            gemFile = f;
                        }
                    }
                }
                if (project.getFile() != null && artifact.isGem()) {
                    // only when the pom exist there will be an artifact
                    FileUtils.copyFileIfModified(gemFile, artifact.getFile());
                    gemFile.delete();
                }
                else {
                    // keep the gem where it is when there is no buildDirectory
                    if (this.buildDirectory.exists()) {
                        FileUtils.copyFileIfModified(gemFile,
                                                     new File(this.buildDirectory,
                                                             gemFile.getName()));
                        gemFile.deleteOnExit();
                    }
                }
            }
        }
        catch (final IOException e) {
            throw new MojoExecutionException("error gemifing pom", e);
        }
    }

    private void buildFromPom(final MavenProject project, final GemArtifact artifact)
            throws MojoExecutionException, IOException, ScriptException {
        getLog().info("building gem for " + artifact + " . . .");
        final File gemDir = new File(this.buildDirectory, artifact.getGemName());
        final File gemSpec = new File(gemDir, artifact.getGemName()
                + ".gemspec");
        final GemspecWriter gemSpecWriter = new GemspecWriter(gemSpec,
                project,
                artifact);

        if (this.date != null) {
            gemSpecWriter.append("date", Date.valueOf(this.date).toString());
        }
        gemSpecWriter.append("rubygems_version", this.rubygemsVersion);
        gemSpecWriter.append("required_rubygems_version",
                             this.requiredRubygemsVersion);
        gemSpecWriter.append("required_ruby_version", this.requiredRubyVersion);
        gemSpecWriter.append("bindir", this.bindir);
        gemSpecWriter.append("post_install_message", this.postInstallMessage);

        gemSpecWriter.append("rubyforge_project", this.rubyforgeProject);
        gemSpecWriter.appendRdocFiles(this.extraRdocFiles);
        gemSpecWriter.appendFiles(this.extraFiles);
        gemSpecWriter.appendList("executables", this.executables);
        gemSpecWriter.appendList("extensions", this.extensions);
        gemSpecWriter.appendList("rdoc_options", this.rdocOptions);
        gemSpecWriter.appendList("require_paths", this.requirePaths);
        final File rubyFile;
        if (artifact.hasJarFile()) {
            gemSpecWriter.appendPlatform(this.platform == null
                    ? "java"
                    : this.platform);
            gemSpecWriter.appendJarfile(artifact.getJarFile(),
                                        artifact.getJarFile().getName());
            final File lib = new File(gemDir, "lib");
            lib.mkdirs();
            // need relative filename here
            rubyFile = new File(lib.getName(), artifact.getGemName() + ".rb");
            gemSpecWriter.appendFile(rubyFile);
        }
        else {
            rubyFile = null;
            gemSpecWriter.appendPlatform(this.platform);
        }

        ArtifactResolutionResult jarDependencyArtifacts = includeDependencies(  project,
                                                                                artifact );
        if (jarDependencyArtifacts != null ) {
            for (final Object element : jarDependencyArtifacts.getArtifacts()) {
                final Artifact dependency = (Artifact) element;
                getLog().info(" -- include -- " + dependency);
                gemSpecWriter.appendJarfile(dependency.getFile(),
                                            dependency.getFile().getName());
            }
        }
        
        // TODO make it the maven way (src/main/ruby + src/test/ruby) or the
        // ruby way (lib + spec + test)
        // TODO make a loop or so ;-)
        final File binDir = new File(project.getBasedir(), "bin");
        final File libDir = new File(project.getBasedir(), "lib");
        final File generatorsDir = new File(project.getBasedir(), "generators");
        final File specDir = new File(project.getBasedir(), "spec");
        final File testDir = new File(project.getBasedir(), "test");

        if (binDir.exists()) {
            gemSpecWriter.appendPath("bin");
            for (final File file : binDir.listFiles()) {
                // if ( file.canExecute() ) { // java1.6 feature which will fail
                // on jre1.5 runtimes
                gemSpecWriter.appendExecutable(file.getName());
                // }
            }
        }
        if (libDir.exists()) {
            gemSpecWriter.appendPath("lib");
        }
        if (generatorsDir.exists()) {
            gemSpecWriter.appendPath("generators");
        }
        if (specDir.exists()) {
            gemSpecWriter.appendPath("spec");
            gemSpecWriter.appendTestPath("spec");
        }
        if (testDir.exists()) {
            gemSpecWriter.appendPath("test");
            gemSpecWriter.appendTestPath("test");
        }

        for (final Dependency dependency : project.getDependencies()) {
            if (!dependency.isOptional()
                    && dependency.getType().contains("gem")) {

                final String prefix = dependency.getGroupId()
                        .equals("rubygems") ? "" : dependency.getGroupId()
                        + ".";
                if ((Artifact.SCOPE_COMPILE + Artifact.SCOPE_RUNTIME).contains(dependency.getScope())) {
                    gemSpecWriter.appendDependency(prefix
                                                           + dependency.getArtifactId(),
                                                   dependency.getVersion());
                }
                else if ((Artifact.SCOPE_PROVIDED + Artifact.SCOPE_TEST).contains(dependency.getScope())) {
                    gemSpecWriter.appendDevelopmentDependency(prefix
                                                                      + dependency.getArtifactId(),
                                                              dependency.getVersion());
                }
                else {
                    // TODO put things into "requirements"
                }
            }
        }

        gemSpecWriter.close();

        gemSpecWriter.copy(gemDir);

        if (artifact.hasJarFile() && !rubyFile.exists()) {
            FileWriter writer = null;
            try {
                // need absolute filename here
                writer = new FileWriter(new File(gemDir, rubyFile.getPath()));

                writer.append("module ")
                        .append(titleizedClassname(project.getArtifactId()))
                        .append("\n");
                writer.append("  VERSION = '")
                        .append(artifact.getGemVersion())
                        .append("'\n");
                writer.append("  MAVEN_VERSION = '")
                        .append(project.getVersion())
                        .append("'\n");
                writer.append("end\n");
                writer.append("begin\n");
                writer.append("  require 'java'\n");
                writer.append("  require File.dirname(__FILE__) + '/")
                        .append(artifact.getJarFile().getName())
                        .append("'\n");
                if (jarDependencyArtifacts != null) {
                    for (final Object element : jarDependencyArtifacts.getArtifacts()) {
                        final Artifact dependency = (Artifact) element;
                        writer.append("  require File.dirname(__FILE__) + '/")
                                .append(dependency.getFile().getName())
                                .append("'\n");
                    }

                }
                writer.append("rescue LoadError\n");
                writer.append("  puts 'JAR-based gems require JRuby to load. Please visit www.jruby.org.'\n");
                writer.append("  raise\n");
                writer.append("end\n");
                writer.append("\n");
                writer.append("load File.dirname(__FILE__) + '/" + this.gemHook
                        + "' if File.exists?( File.dirname(__FILE__) + '/"
                        + this.gemHook + "')\n");
            }
            catch (final IOException e) {
                throw new MojoExecutionException("error writing ruby file", e);
            }
            finally {
                if (writer != null) {
                    try {
                        writer.close();
                    }
                    catch (final IOException ignore) {
                    }
                }
            }
        }

        final File localGemspec = new File(launchDirectory(), gemSpec.getName());

        this.factory.newScriptFromJRubyJar("gem")
                .addArg("build", gemSpec)
                .executeIn(gemDir);

        if ((!localGemspec.exists() || !FileUtils.contentEquals(localGemspec,
                                                                gemSpec))
                && this.gemspecOverwrite) {
            getLog().info("overwrite gemspec '" + localGemspec.getName() + "'");
            FileUtils.copyFile(gemSpec, localGemspec);
        }

        final StringBuilder gemFilename = new StringBuilder("rubygems".equals(artifact.getGroupId())
                ? ""
                : artifact.getGroupId() + ".").append(artifact.getArtifactId())
                .append("-")
                .append(artifact.getGemVersion())
                .append("java-gem".equals(artifact.getType()) || "java".equals(this.platform) ? "-java" : "")
                .append(".gem");

        FileUtils.copyFile(new File(gemDir, gemFilename.toString()),
                           artifact.getFile());
    }

    private ArtifactResolutionResult includeDependencies( MavenProject project, final GemArtifact artifact)
            throws IOException {
        ArtifactResolutionResult jarDependencyArtifacts = null;
        getLog().info("include dependencies? " + this.includeDependencies);
        if (this.includeDependencies) {
            final ArtifactFilter filter = new ArtifactFilter() {
                public boolean include(final Artifact candidate) {
                    if (candidate == artifact) {
                        return true;
                    }
                    final boolean result = (candidate.getType().equals("jar") && ("compile".equals(candidate.getScope()) || "runtime".equals(candidate.getScope())));
                    return result;
                }

            };

            // remember file location since resolve will set it to
            // local-repository location
            final File artifactFile = artifact.getFile();
            final ArtifactResolutionRequest request = new ArtifactResolutionRequest().setArtifact(project.getArtifact())
                    .setResolveRoot(false)
                    .setLocalRepository(this.localRepository)
                    .setRemoteRepositories(project.getRemoteArtifactRepositories())
                    .setCollectionFilter(filter)
                    .setManagedVersionMap(project.getManagedVersionMap())
                    .setArtifactDependencies(project.getDependencyArtifacts());
            jarDependencyArtifacts = this.repositorySystem.resolve(request);
            // keep the artifactFile on build directory
            artifact.setFile(artifactFile);
        }
        return jarDependencyArtifacts;
    }

    private String titleizedClassname(final String artifactId) {
        final StringBuilder name = new StringBuilder();
        for (final String part : artifactId.split("-")) {
            name.append(StringUtils.capitalise(part));
        }
        return name.toString();
    }

    @Override
    protected void executeWithGems() throws MojoExecutionException,
            ScriptException, IOException {
        // nothing to do here since we override executeJRuby
    }
}
