/**
 *
 */
package de.saumya.mojo.gem;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.maven.model.Contributor;
import org.apache.maven.model.Developer;
import org.apache.maven.model.License;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.FileUtils;

class GemspecWriter {

    final MavenProject      project;
    final Writer            writer;
    final String            excludes         = ".*~$|^[.][a-zA-Z].*";
    final List<File>        dirs             = new ArrayList<File>();
    final List<File>        files            = new ArrayList<File>();
    final List<URL>         licenses         = new ArrayList<URL>();
    final Map<String, File> jarFiles         = new HashMap<String, File>();
    long                    latestModified   = 0;
    final File              gemspec;
    private boolean         firstAuthor      = true;
    private boolean         firstFile        = true;
    private boolean         platformAppended = false;
    private boolean         firstTestFile;

    // private List<String> executables = new ArrayList<String>();

    GemspecWriter(final File gemspec, final MavenProject project,
            final GemArtifact artifact) throws IOException {
        this.latestModified = project.getFile() == null ? 0 : project.getFile()
                .lastModified();
        this.gemspec = gemspec;
        this.gemspec.getParentFile().mkdirs();
        this.writer = new FileWriter(gemspec);
        this.project = project;

        append("# create by maven - leave it as is");
        append("Gem::Specification.new do |s|");
        append("name", artifact.getGemName());
        appendRaw("version", "'"
                + GemArtifact.getGemVersion(project.getVersion()) + "'");
        append();
        append("summary", project.getName());
        append("description", project.getDescription());
        append("homepage", project.getUrl());
        append();

        for (final Developer developer : project.getDevelopers()) {
            appendAuthor(developer.getName(), developer.getEmail());
        }
        for (final Contributor contributor : project.getContributors()) {
            appendAuthor(contributor.getName(), contributor.getEmail());
        }
        if (project.getDevelopers().isEmpty() && project.getContributors().isEmpty()){
            appendList("authors", "dummy");
        }

        append();

        for (final License license : project.getLicenses()) {
            appendLicense(license.getUrl(), license.getName());
        }
    }

    boolean isUptodate() {
        return this.gemspec.lastModified() > this.latestModified;
    }

    private String gemVersion(String version) {
        version = version.replaceAll("-SNAPSHOT", "").replace("-", ".");
        if (version.matches("^[\\[\\(].*[\\]\\)]$")) {
            final int comma = version.indexOf(",");
            final String first = version.substring(1, comma);
            final String second = version.substring(comma + 1,
                                                    version.length() - 1);
            if (version.matches("\\[.*99999.99999\\)$")) {
                // out of '[1.2.0, 1.99999.99999]' make '1.2'
                final String prefix = second.replaceFirst("99999.99999$", "");
                return "'~> "
                        + prefix
                        + first.substring(prefix.length())
                                .replaceFirst("[.].*", "") + "'";
            }
            else if (version.matches("\\[.*,\\)$")) {
                final StringBuilder buf = new StringBuilder("'>");
                buf.append(version.charAt(0) == '[' ? "=" : "");
                buf.append(first).append("'");
                return buf.toString();
            }
            else {
                final StringBuilder buf = new StringBuilder("['>");
                buf.append(version.charAt(0) == '[' ? "=" : "");
                buf.append(first).append("','<");
                buf.append(version.charAt(version.length() - 1) == '['
                        ? "="
                        : "");
                buf.append(second);
                buf.append("']");
                return buf.toString();
            }
        }
        else {
            return "'" + version + "'";
        }
    }

    private void append() throws IOException {
        this.writer.append("\n");
    }

    private void append(final String line) throws IOException {
        this.writer.append(line).append("\n");
    }

    private void appendAuthor(final String name, final String email)
            throws IOException {
        if (name != null && email != null) {
            if (this.firstAuthor) {
                this.writer.append("  s.authors = ['")
                        .append(name)
                        .append("']\n");
                this.writer.append("  s.email = ['")
                        .append(email)
                        .append("']\n");
                this.firstAuthor = false;
            }
            else {
                this.writer.append("  s.authors << '")
                        .append(name)
                        .append("'\n");
                this.writer.append("  s.email << '")
                        .append(email)
                        .append("'\n");
            }
        }
    }

    void append(final String key, final String value) throws IOException {
        if (value != null) {
            this.writer.append("  s.")
                    .append(key)
                    .append(" = '")
                    .append(value.replaceAll("'", "\""))
                    .append("'\n");
        }
    }

    private void appendRaw(final String key, final String value)
            throws IOException {
        if (value != null) {
            this.writer.append("  s.")
                    .append(key)
                    .append(" = ")
                    .append(value)
                    .append("\n");
        }
    }

    void appendDependency(final String name, final String version)
            throws IOException {
        this.writer.append("  s.add_dependency '")
                .append(name)
                .append("', ")
                .append(gemVersion(version))
                .append("\n");
    }

    void appendDevelopmentDependency(final String name, final String version)
            throws IOException {
        this.writer.append("  s.add_development_dependency '")
                .append(name)
                .append("', ")
                .append(gemVersion(version))
                .append("\n");
    }

    void appendPath(final String path) throws IOException {
        if (this.firstFile) {
            this.writer.append("  s.files = Dir['")
                    .append(path)
                    .append("/**/*']\n");
            this.firstFile = false;
        }
        else {
            this.writer.append("  s.files += Dir['")
                    .append(path)
                    .append("/**/*']\n");
        }
        final File file = new File(this.project.getBasedir(), path);
        if (file.lastModified() > this.latestModified) {
            this.latestModified = file.lastModified();
        }
        this.dirs.add(new File(path));
    }

    void appendTestPath(final String path) throws IOException {
        if (this.firstTestFile) {
            this.writer.append("  s.test_files = Dir['")
                    .append(path)
                    .append("/**/*_" + path + ".rb']\n");
            this.firstTestFile = false;
        }
        else {
            this.writer.append("  s.test_files += Dir['")
                    .append(path)
                    .append("/**/*_" + path + ".rb']\n");
        }
        final File file = new File(this.project.getBasedir(), path);
        if (file.lastModified() > this.latestModified) {
            this.latestModified = file.lastModified();
        }
        this.dirs.add(new File(path));
    }

    void appendPlatform(final String platform) throws IOException {
        if (!this.platformAppended && platform != null) {
            append("platform", platform);
            this.platformAppended = true;
        }
    }

    void appendJarfile(final File jar, final String jarfileName)
            throws IOException {
        final File f = new File("lib", jarfileName);
        this.jarFiles.put(f.toString(), jar);
        appendFile(f);
    }

    void appendFile(final File file) throws IOException {
        if (this.firstFile) {
            this.writer.append("  s.files = Dir['")
                    .append(file.toString())
                    .append("']\n");
            this.firstFile = false;
        }
        else {
            this.writer.append("  s.files += Dir['")
                    .append(file.toString())
                    .append("']\n");
        }

        if (file.lastModified() > this.latestModified) {
            this.latestModified = file.lastModified();
        }
    }

    void appendFile(final String file) throws IOException {
        final File f = new File(file);
        appendFile(f);
        this.files.add(f);
    }

    void appendExecutable(final String executable) throws IOException {
        this.writer.append("  s.executables << '" + executable + "'\n");
    }

    private void appendLicense(final String url, final String name)
            throws IOException {
        URL u;
        try {
            u = new URL(url);
        }
        catch (final MalformedURLException e) {
            u = new URL("file:." + url);
        }
        this.licenses.add(u);
        final URLConnection con = u.openConnection();
        if (this.latestModified < con.getLastModified()) {
            this.latestModified = con.getLastModified();
        }
        // omit the first slash
        final File license = new File(u.getFile()
                .substring(1)
                .replaceFirst("^./", ""));
        appendFile(license);
        if ("file".equals(u.getProtocol())) {
            // make a nice File without unix style prefix "./"
            this.files.add(new File(license.getPath().replaceFirst("^./", "")));
        }
        if (name != null) {
            append("  s.licenses << '" + name.replaceFirst("^./", "") + "'");
        }
    }

    void copy(final File target) throws IOException {
        target.mkdirs();
        copyJarFiles(target);
        copyFiles(target);
        copyLicenses(target);
    }

    private void copyLicenses(final File target) throws IOException {
        OutputStream writer = null;
        InputStream reader = null;
        for (final URL url : this.licenses) {
            try {
                try {
                    reader = new BufferedInputStream(url.openStream());
                }
                catch (final IOException e) {
                    // TODO log it but otherwise ignore
                    break;
                }
                final File licenseFile = new File(target, url.getFile());
                licenseFile.getParentFile().mkdirs();
                writer = new BufferedOutputStream(new FileOutputStream(licenseFile));
                int b = reader.read();
                while (b != -1) {
                    writer.write(b);
                    b = reader.read();
                }
            }
            finally {
                if (reader != null) {
                    try {
                        reader.close();
                    }
                    catch (final IOException ignore) {
                    }
                }
                if (writer != null) {
                    try {
                        writer.close();
                    }
                    catch (final IOException ignore) {
                    }
                }
            }
        }
    }

    private void copyJarFiles(final File target) throws IOException {
        new File(target, "lib").mkdirs();
        for (final Map.Entry<String, File> entry : this.jarFiles.entrySet()) {
            FileUtils.copyFile(entry.getValue(), new File(target,
                    entry.getKey()));
        }
    }

    private void copyFiles(final File target) throws IOException {
        for (final File file : this.files) {
            if (file.exists()) {
                FileUtils.copyFile(file, new File(target, file.getPath()));
            }
        }
        for (final File dir : this.dirs) {
            copyDir(target, dir);
        }
    }

    private void copyDir(final File target, final File dir) throws IOException {
        final File realDir = new File(this.project.getBasedir(), dir.getPath());
        if (realDir.isDirectory()) {
            for (final String file : realDir.list()) {
                copyDir(target, new File(dir, file));
            }
        }
        else {
            if (realDir.exists() && !realDir.getName().matches(this.excludes)) {
                final File targetFile = new File(target, dir.getPath());
                FileUtils.copyFile(realDir, targetFile);
            }
        }
    }

    void close() throws IOException {
        try {
            this.writer.append("end");
        }
        finally {
            this.writer.close();
        }
    }

    void appendList(final String name, final String list) throws IOException {
        if (list != null) {
            final StringBuilder buf = new StringBuilder("[");
            boolean first = true;
            for (final String part : list.split(",")) {
                if (first) {
                    first = false;
                }
                else {
                    buf.append(",");
                }
                final char quoteChar = part.contains("'") ? '"' : '\'';
                buf.append(quoteChar).append(part.trim()).append(quoteChar);
            }
            buf.append("]");
            appendRaw(name, buf.toString());
        }
    }

    void appendRdocFiles(final String extraRdocFiles) throws IOException {
        if (extraRdocFiles != null) {
            for (final String f : extraRdocFiles.split(",")) {
                appendFile(f.trim());
            }
            appendList("extra_rdoc_files", extraRdocFiles);
        }
    }

    void appendFiles(final String files) throws IOException {
        if (files != null) {
            for (final String f : files.split(",")) {
                appendFile(f.trim());
            }
        }
    }
}
