package de.saumya.mojo.proxy;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import de.saumya.mojo.proxy.Controller.FileLocation.Type;
import de.saumya.mojo.ruby.GemScriptingContainer;

public class Controller {
    
    private static final String SHA1 = ".sha1";

    private static final String RUBYGEMS_URL = "https://rubygems.org/gems";

    private static final String RUBYGEMS_S3_URL = "http://s3.amazonaws.com/production.s3.rubygems.org/gems";
    
    public static final String[] PLATFORMS = { "-universal-java-1.5",
                                               "-universal-java-1.6",
                                               "-universal-java-1.7",
                                               "-universal-java-1.8",
                                               "-universal-java",
                                               "-universal-jruby-1.2",
                                               "-jruby",
                                               "-java",
                                               "-universal-ruby-1.8.7",
                                               "-universal-ruby-1.9.2",
                                               "-universal-ruby-1.9.3",
                                               "-universal-ruby",
                                               "" };

    static final Map<String, Set<String>> BROKEN_GEMS = new HashMap<String, Set<String>>();
    
    static {
         // activeresource-2.0.0 does not exist !!!
        Set<String> rails = new TreeSet<String>();
        rails.add("2.0.0");
        BROKEN_GEMS.put("rails", rails);

	// juby-openssl-0.7.6 can not open gem with jruby-1.6.8
        Set<String> openssl = new TreeSet<String>();
        openssl.add("0.7.6"); 
        BROKEN_GEMS.put("jruby-openssl", openssl);
    }
 
    private final File localStorage;
    
    private final GemScriptingContainer script = new GemScriptingContainer();
    
    public static class FileLocation {
        enum Type { 
            XML_CONTENT, 
            HTML_CONTENT, 
            ASCII_FILE, 
            XML_FILE, 
            REDIRECT, 
            NOT_FOUND , 
            ASCII_CONTENT, 
            REDIRECT_TO_DIRECTORY, 
            TEMP_UNAVAILABLE }
        
        public FileLocation() {
            this(null, null, null, Type.REDIRECT_TO_DIRECTORY);
        }

        public FileLocation(String message) {
            this(null, null, message, Type.NOT_FOUND);
        }

        public FileLocation(String content, Type type) {
            this(null, null, content, type);
        }
        
        public FileLocation(File local, Type type) {
            this(local, null, null, type);
        }

        public FileLocation(URL remote) {
            this(null, remote, null, Type.REDIRECT);
        }
        
        private FileLocation(File localFile, URL remoteFile, String content, Type type) {
            this.content = content;
            this.remoteUrl = remoteFile;
            this.localFile = localFile;
            this.type = type;
        }
        
        final File localFile;
        final URL remoteUrl;
        final String content;
        final Type type;
    }
     
     private final Object createPom;

     // assume there will be only one instance of this class per servlet container
     private final Set<String> fileLocks = new HashSet<String>();

     public Controller(File storage) throws IOException{
         this.localStorage = storage;
         this.localStorage.mkdirs();
         this.createPom = script.runScriptletFromClassloader("create_pom.rb");
     }
     
     public FileLocation locate(String path) throws IOException{
        // release/rubygems/name/version
        // release/rubygems/name/version/
        // release/rubygems/name/version/name-version.gem
        // release/rubygems/name/version/name-version.gem.md5
        // release/rubygems/name/version/name-version.pom
        // release/rubygems/name/version/name-version.pom.md5
        // release/rubygems/name
        // release/rubygems/name/
        // release/rubygems/name/maven-metadata.xml
        
        path = path.replaceAll("/+", "/");
        if(path.endsWith("/")){
            path += "index.html";
        }
        
        String[] parts = path.split("/");
        
        if(parts.length == 0){
            // TODO make listing with two directories 'releases', 'prereleases'
            return new FileLocation("for maven", Type.ASCII_CONTENT);
        }
        else {
            boolean prereleases = parts[0].contains("pre");
            if(parts.length > 1 && !"rubygems".equals(parts[1])){
                return notFound("Only rubygems/ groupId is supported through this proxy.");
            }
            switch(parts.length){
            case 1:
            case 2:
                // TODO make listing with one directory 'rubygems'
                return notFound("directory listing not implemented");
            case 3:
                if("index.html".equals(parts[2])) {
                    return notFound("directory listing not implemented");
                }
                else {
                    return new FileLocation();
                }
            case 4:
                if("maven-metadata.xml".equals(parts[3])){
                    return metadata(parts[2], prereleases);
                }
                else if(("maven-metadata.xml" + SHA1).equals(parts[3])){
                    return metadataSha1(parts[2], prereleases);
                }
                else if("index.html".equals(parts[3])){
                    return versionListDirectory(parts[2], path, prereleases);
                }
                else {
                    return notFound("not found");
                }
            case 5:
                String filename = parts[4].replace("-SNAPSHOT", "");
                if("index.html".equals(filename)){
                    return directory(parts[2], parts[3], path);
                }
                if(filename.endsWith(".gem")){
                    // keep it backward compatible
                    filename = filename.replace("-java.gem", ".gem");
                    File local = new File(localStorage, filename.replace(".gem", ".pom"));
                    if(!local.exists()){
                        try {
                            if (!createFiles(parts[2], parts[3])){
                                return new FileLocation(filename + " is being generated", Type.TEMP_UNAVAILABLE);
                            }
                        } catch (FileNotFoundException e) {
                            return notFound("not found");
                        }
                    }
                    String url = null;
		    for( String platform : PLATFORMS )
		    {
			url = RUBYGEMS_S3_URL + "/" + filename.replace(".gem", platform + ".gem");
			if ( exists( url ) ) {
			    break;
			}
                    }
                    return new FileLocation( new URL( url ) );
                }
                if(filename.endsWith(SHA1) || filename.endsWith(".pom")){
                    File local = new File(localStorage, filename);
                    if(!local.exists()){
                        try {
                            if (!createFiles(parts[2], parts[3])){
                                return new FileLocation(filename + " is being generated", Type.TEMP_UNAVAILABLE);
                            }
                        } catch (FileNotFoundException e) {
                            return notFound("not found");
                        }
                    }
                    return new FileLocation(local, filename.endsWith(SHA1)? Type.ASCII_FILE: Type.XML_FILE);
                }
                return notFound("not found");
            default:
                return notFound("Completely unhandleable request!");
        }
        }
    }
     
    public boolean exists(String url){
        try {
           HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
           con.setRequestMethod("HEAD");
           return con.getResponseCode() == HttpURLConnection.HTTP_OK;
        }
        catch (FileNotFoundException e) {
            //e.printStackTrace();
            return false;
        }
        catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }
    
    private FileLocation directory(String gemname, String version, String path) throws IOException {
        HtmlDirectoryBuilder builder = new HtmlDirectoryBuilder();
        
        builder.buildHeader(path);
        
        String basename = gemname + "-" + version;
        String pomfile = basename + ".pom";
        String gemfile = basename + ".gem";
        builder.buildFileLink(pomfile);
        builder.buildFileLink(pomfile + SHA1);
        builder.buildFileLink(gemfile);
        builder.buildFileLink(gemfile + SHA1);
        
        builder.buildFooter();
        
        return new FileLocation(builder.toHTML(), Type.HTML_CONTENT);
    }
    
    private boolean createFiles(String name, String version) throws IOException {    
        String gemname = name + "-" + version.replace( "-SNAPSHOT", "" );
        try {
            synchronized (fileLocks) {
                if (fileLocks.contains(gemname)) {
                    return false;
                }
                else {
                    fileLocks.add(gemname);
                }
            }

            File gemfile = new File(this.localStorage, gemname + ".gem");
            File gemfileSha = new File(this.localStorage, gemname + ".gem" + SHA1);

            File pomfile = new File(this.localStorage, gemname + ".pom");
            File pomfileSha = new File(this.localStorage, gemname + ".pom" + SHA1);

            if (!(gemfileSha.exists() && pomfile.exists() && pomfileSha.exists())) {
		String url = null;
		for( String platform : PLATFORMS )
		{
		    url = RUBYGEMS_URL + "/" + gemname + platform + ".gem";
		    try {
			downloadGemfile(gemfile, new URL(url));
			break;
		    }
		    catch (FileNotFoundException ignore) {
		    }
		}

                String pom = createPom(gemfile);

                writeUTF8(pomfile, pom);
                writeUTF8(pomfileSha, sha1(pom));
            }
            
            // we do not keep the gemfile on disc
            gemfile.delete();
            return true;
        }
        finally {
            synchronized (fileLocks) {
                fileLocks.remove(gemname);
            }
        }
    }

    private String createPom(File gemfile) {
        // protect the script container
        synchronized (script) {
            return script.callMethod(createPom, "create", gemfile.getAbsolutePath(), String.class)
                         .replaceAll("&", "&amp;").replaceAll("&amp;amp;", "&amp;");            
        }
    }

    private void downloadGemfile(File gemfile, URL url) throws IOException {
        InputStream input = null;
        OutputStream output = null;
        MessageDigest sha = newSha1Digest();
        try {
            input = new BufferedInputStream(url.openStream());
            output = new BufferedOutputStream(new FileOutputStream(gemfile));
            
            int b = input.read();
            while(b != -1){
                output.write(b);
                sha.update((byte) b);
                b = input.read();
            }
        }
        finally {
            if( input != null){
                input.close();
            }
            if( output != null){
                output.close();
                writeSha(new File(gemfile.getAbsolutePath() + SHA1), sha);
            }
        }
    }

    private void writeSha(File file, MessageDigest sha) throws IOException {
        writeUTF8(file, toHex(sha.digest()));
    }

    private void writeUTF8(File file, String content) throws IOException {
        PrintWriter writer = null;
        try {
           writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file), 
                                                           Charset.forName("UTF-8")));
           writer.print(content);
        }
        finally {
            if(writer != null){
                writer.close();
            }
        }
    }

    private FileLocation versionListDirectory(String name, String path, boolean prereleases) throws IOException {
        HtmlDirectoryBuilder html = new HtmlDirectoryBuilder();
        
        html.buildHeader(path);

        VersionDirectoryBuilder builder = new VersionDirectoryBuilder(name, prereleases, html, BROKEN_GEMS.get(name));
        builder.build();
        
        html.buildFileLink("maven-metadata.xml");
        html.buildFileLink("maven-metadata.xml" + SHA1);
        
        html.buildFooter();
        
        return new FileLocation(html.toHTML(), Type.HTML_CONTENT);
    }

    private FileLocation notFound(String message) {
        return new FileLocation(message);
    }

    private FileLocation metadata(String name, boolean prereleases) throws IOException {
        MavenMetadataBuilder builder = new MavenMetadataBuilder(name, prereleases, BROKEN_GEMS.get(name));
        builder.build();
        return new FileLocation(builder.toXML(), Type.XML_CONTENT);
    }

    private FileLocation metadataSha1(String name, boolean prereleases) throws IOException {
        MavenMetadataBuilder builder = new MavenMetadataBuilder(name, prereleases, BROKEN_GEMS.get(name));
        builder.build();
        return new FileLocation(sha1(builder.toXML()), Type.ASCII_CONTENT);
    }
    
    private String sha1(String text) {
        MessageDigest md = newSha1Digest();
        try {
            md.update(text.getBytes("UTF-8"));
        }
        catch (UnsupportedEncodingException e) {
            throw new RuntimeException("should not happen", e);
        }
        return toHex(md.digest());
    }

    private MessageDigest newSha1Digest() {
        MessageDigest md;
        try {
            md = MessageDigest.getInstance("SHA-1");
        }
        catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("error getting sha1 instance", e); 
        }
        return md;
    }

    private String toHex(byte[] data) {
        StringBuilder buf = new StringBuilder();//data.length * 2);
        for (byte b: data) {
            if(b < 0){
                buf.append(Integer.toHexString(256 + b));
            }
            else if(b < 16) {
                buf.append('0').append(Integer.toHexString(b));
            }
            else {
                buf.append(Integer.toHexString(b));
            }
        }
        return buf.toString();
    }
}
