/*
 * Copyright 2000-2011 JetBrains s.r.o.
 *
 * 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 jetbrains.buildServer.tools;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jetbrains.buildServer.core.runtime.IProgressMonitor;
import jetbrains.buildServer.core.runtime.RuntimeUtil;
import jetbrains.buildServer.util.FileUtil;

import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.io.output.NullWriter;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.w3c.tidy.Tidy;

import com.intellij.openapi.util.JDOMUtil;

public class JDKTools {

  public static class JDKUrlResolver {

    private static String JAVA6_DOWNLOADS_PAGE_URL = "http://download.java.net/jdk6/source/";
    private static Pattern JAVA6_VERSION_PATTERN = Pattern.compile("(.*)<title>Java Platform, Standard Edition (.*?) Source Snapshot Releases</title>(.*)", Pattern.MULTILINE | Pattern.DOTALL);
    private static String JAVA6_VERSION_CHECKER_URL_PATTERN = "https://cds.sun.com/is-bin/INTERSHOP.enfinity/WFS/CDS-CDS_Developer-Site/en_US/-/USD/ViewProductDetail-Start?ProductRef=jdk-%s-oth-JPR@CDS-CDS_Developer";

    private static Logger LOG = Logger.getLogger(JDKUrlResolver.class);

    public JDKUrlResolver() {
    }

    public static URL getLatestJDK() throws IOException {
      final HttpClient client = new HttpClient();
      final GetMethod method = new GetMethod(JAVA6_DOWNLOADS_PAGE_URL);
      client.executeMethod(method);
      try {
        return getLatestProduct(client, asString(method.getResponseBodyAsStream()));
      } finally {
        method.releaseConnection();
      }

    }

    private static URL getLatestProduct(final @NotNull HttpClient client, final @NotNull String body) throws IOException {
      final Matcher matcher = JAVA6_VERSION_PATTERN.matcher(body);
      if (matcher.matches()) {
        final String checkVersion = matcher.group(2);
        final String checkURL = String.format(JAVA6_VERSION_CHECKER_URL_PATTERN, checkVersion);
        LOG.debug(String.format("Checking URL: %s", checkURL));
        final GetMethod checkMethod = new GetMethod(checkURL);
        try {
          int response = client.executeMethod(checkMethod);
          if (200 == response) {
            final Header cookie = checkMethod.getResponseHeader("Set-Cookie");
            if (cookie != null && cookie.getValue().contains("CDS_DETECT")) {
              return new URL(checkURL);
            } else {
              LOG.debug(String.format("Wrong 'Set-Cookie' header: '%s'", cookie));
            }
          } else {
            LOG.debug(String.format("Server returned wrong code '%d'", response));
          }
        } finally {
          checkMethod.releaseConnection();
        }
      }
      return null;
    }

    public static boolean accept(final @NotNull URL url) {
      if ("cds.sun.com".equals(url.getHost()) && url.getPath().startsWith("/is-bin/INTERSHOP.enfinity/WFS/CDS-CDS_Developer-Site")) {
        return true;
      }
      return false;
    }

    public static String asString(final @NotNull InputStream in) throws IOException {
      final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
      BufferedOutputStream bufferedStream = new BufferedOutputStream(byteArrayOutputStream);
      FileUtil.copyStreams(new BufferedInputStream(in), bufferedStream);
      final String out = new String(byteArrayOutputStream.toByteArray());
      bufferedStream.close();
      return out;
    }

    public static URL resolve(URL url, IProgressMonitor monitor) throws Exception {
      final HttpURLConnection stageOne = traverseDownloadPage(url, Platform2.current(), CPU.current());
      final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
      FileUtil.copyStreams(new BufferedInputStream(stageOne.getInputStream()), new BufferedOutputStream(byteArrayOutputStream));
      final String page = new String(byteArrayOutputStream.toByteArray());
      final URL target = parseDownloadResponse(page);
      LOG.debug(String.format("Resolved: %s", target));
      return target;
    }

    private static HttpURLConnection traverseDownloadPage(URL url, Platform2 platform, CPU cpu) throws Exception {
      String cookie;
      org.jdom.Element jForm;
      try {
        HttpURLConnection con = (HttpURLConnection) url.openConnection();
        cookie = con.getHeaderField("Set-Cookie");
        LOG.debug("Cookie=" + cookie);
        Tidy tidy = new Tidy();
        tidy.setXmlOut(true);
        tidy.setErrout(new PrintWriter(new NullWriter()));
        final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        tidy.parse(con.getInputStream(), buffer);
        String content = buffer.toString();
        final org.jdom.Document jdoc = JDOMUtil.loadDocument(content);
        final List<org.jdom.Element> jnodes = (List<org.jdom.Element>) org.jdom.xpath.XPath.selectNodes(jdoc, "//form");
        jForm = null;
        for (org.jdom.Element jElement : jnodes) {
          String action = jElement.getAttributeValue("action");
          LOG.debug("Found form:" + action);
          if (action.contains("ViewFilteredProducts")) {
            jForm = jElement;
            break;
          }
        }

      } catch (IOException e) {
        throw new Exception("Failed to access " + url, e);
      }

      url = new URL(jForm.getAttributeValue("action"));
      try {
        HttpURLConnection con = (HttpURLConnection) url.openConnection();//(HttpURLConnection) ProxyConfiguration.open(url);
        con.setRequestMethod("POST");
        con.setDoOutput(true);
        con.setRequestProperty("Cookie", cookie);
        con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
        PrintStream os = new PrintStream(con.getOutputStream());

        // select platform
        String primary = null, secondary = null;
        final org.jdom.Element jp = (org.jdom.Element) org.jdom.xpath.XPath.selectSingleNode(jForm, ".//select[@id='dnld_platform']");
        for (org.jdom.Element jopt : (List<org.jdom.Element>) jp.getChildren("option")) {
          String value = jopt.getAttributeValue("value");
          String vcap = value.toUpperCase(Locale.ENGLISH);
          if (!platform.is(vcap))
            continue;
          switch (cpu.accept(vcap)) {
          case PRIMARY:
            primary = value;
            break;
          case SECONDARY:
            secondary = value;
            break;
          case UNACCEPTABLE:
            break;
          }
        }
        if (primary == null)
          primary = secondary;
        if (primary == null)
          throw new IllegalArgumentException("Couldn't find the right download for " + platform + " and " + cpu + " combination");
        os.print(/*p.attributeValue*/jp.getAttributeValue("name") + '=' + primary);
        LOG.debug("Platform choice:" + primary);

        // select language
        org.jdom.Element jl = (org.jdom.Element) org.jdom.xpath.XPath.selectSingleNode(jForm, ".//select[@id='dnld_language']");
        if (jl != null) {
          os.print("&" + jl.getAttributeValue("name") + "=" + jl.getChild("option").getAttributeValue("value"));
        }
        for (org.jdom.Element je : (List<org.jdom.Element>) org.jdom.xpath.XPath.selectNodes(jForm, ".//input")) {
          os.print('&');
          os.print(je.getAttributeValue("name"));
          os.print('=');
          String value = je.getAttributeValue("value");
          if (value == null)
            os.print("on"); // assume this is a checkbox
          else
            os.print(URLEncoder.encode(value, "UTF-8"));
        }
        os.close();
        return con;
      } catch (IOException e) {
        throw new Exception("Failed to access " + url, e);
      }

    }

    private static URL parseDownloadResponse(/*TaskListener log, */String page) throws IOException {
      Pattern HREF = Pattern.compile("<a href=\"(http://cds.sun.com/[^\"]+/VerifyItem-Start[^\"]+)\"");
      Matcher m = HREF.matcher(page);
      // this page contains a missing --> that confuses dom4j/jtidy

      LOG.debug("Choosing the download bundle");
      List<String> urls = new ArrayList<String>();

      while (m.find()) {
        String url = m.group(1);
        LOG.debug("Considering a download link:" + url);

        // still more options to choose from.
        // avoid rpm bundles, and avoid tar.Z bundle
        if (url.contains("rpm"))
          continue;
        if (url.contains("tar.Z"))
          continue;
        // sparcv9 bundle is add-on to the sparc bundle, so just download 32bit sparc bundle, even on 64bit system
        if (url.contains("sparcv9"))
          continue;

        urls.add(url);
        LOG.debug("Found a download candidate: " + url);
      }

      if (urls.isEmpty()) {
        throw new IOException("found no matches in: " + page);
      }

      // prefer the first match because sometimes "optional downloads" follow the main bundle
      return new URL(urls.get(0));
    }

    private static enum Preference {
      PRIMARY, SECONDARY, UNACCEPTABLE
    }

    /**
     * Supported platform.
     */
    private static enum Platform2 {
      LINUX("jdk.sh"), SOLARIS("jdk.sh"), WINDOWS("jdk.exe");

      /**
       * Choose the file name suitable for the downloaded JDK bundle.
       */
      public final String bundleFileName;

      Platform2(String bundleFileName) {
        this.bundleFileName = bundleFileName;
      }

      public boolean is(String line) {
        return line.contains(name());
      }

      public static Platform2 current() throws Exception {
        String arch = System.getProperty("os.name").toLowerCase(Locale.ENGLISH);
        if (arch.contains("linux"))
          return LINUX;
        if (arch.contains("windows"))
          return WINDOWS;
        if (arch.contains("sun") || arch.contains("solaris"))
          return SOLARIS;
        throw new IllegalArgumentException/*DetectionFailedException*/("Unknown CPU name: " + arch);
      }
    }

    /**
     * CPU type.
     */
    private static enum CPU {
      i386, amd64, Sparc, Itanium;

      /**
       * In JDK5u3, I see platform like "Linux AMD64", while JDK6u3 refers to
       * "Linux x64", so just use "64" for locating bits.
       */
      public Preference accept(String line) {
        switch (this) {
        // these two guys are totally incompatible with everything else, so no fallback
        case Sparc:
          return must(line.contains("SPARC"));
        case Itanium:
          return must(line.contains("ITANIUM"));

          // 64bit Solaris, Linux, and Windows can all run 32bit executable, so fall back to 32bit if 64bit bundle is not found
        case amd64:
          if (line.contains("64"))
            return Preference.PRIMARY;
          if (line.contains("SPARC") || line.contains("ITANIUM"))
            return Preference.UNACCEPTABLE;
          return Preference.SECONDARY;
        case i386:
          if (line.contains("64") || line.contains("SPARC") || line.contains("ITANIUM"))
            return Preference.UNACCEPTABLE;
          return Preference.PRIMARY;
        }
        return Preference.UNACCEPTABLE;
      }

      private static Preference must(boolean b) {
        return b ? Preference.PRIMARY : Preference.UNACCEPTABLE;
      }

      public static CPU current() throws Exception {
        String arch = System.getProperty("os.arch").toLowerCase(Locale.ENGLISH);
        if (arch.contains("sparc"))
          return Sparc;
        if (arch.contains("ia64"))
          return Itanium;
        if (arch.contains("amd64") || arch.contains("86_64"))
          return amd64;
        if (arch.contains("86"))
          return i386;
        throw new Exception("Unknown CPU architecture: " + arch);
      }
    }

  }

  public static class LastJDKVersionProvider implements jetbrains.buildServer.core.runtime.osgx.ToolRepository.IToolVersionProvider {

    private static Logger LOG = Logger.getLogger(LastJDKVersionProvider.class);

    private static final Pattern URL_VERSION_PATTERN = Pattern.compile("(.*)cds.sun.com/is-bin/INTERSHOP.enfinity/WFS/CDS-CDS_Developer-Site/(.*)/ViewProductDetail-Start\\?ProductRef=jdk-(.*?)-(.*)@CDS-CDS_Developer");
    private static final Pattern VERSION_FORMAT_PATTERN = Pattern.compile("(\\d*)(.*?)(\\d*?)");

    private String myVersion;
    private URI myLocation;

    private void load() {
      try {
        myLocation = JDKUrlResolver.getLatestJDK().toURI();
        final String latestJdk = myLocation.toString();
        final Matcher matcher = URL_VERSION_PATTERN.matcher(latestJdk);
        if (matcher.matches()) {
          myVersion = buildVersion(matcher.group(3));
        }
      } catch (Exception e) {
        LOG.error(e.getMessage(), e);
      }
    }

    String buildVersion(String rawVersion) {
      final Matcher matcher = VERSION_FORMAT_PATTERN.matcher(rawVersion);
      if (matcher.matches()) {
        String minor = matcher.group(1);
        String micro = matcher.group(2);
        String update = matcher.group(3);
        return String.format("1.%s.%s_%s", minor, "u".equals(micro) ? 0 : micro, update);
      }
      return rawVersion;
    }

    @Override
    public String toString() {
      return getLocation().toString();
    }

    public String getVersion() {
      if (myVersion == null) {
        load();
      }
      return myVersion;
    }

    public URI getLocation() {
      if (myLocation == null) {
        load();
      }
      return myLocation;
    }

  }

  public static class JDKUrlInterceptor {

    private final URL myURL;

    public JDKUrlInterceptor(final URL url) {
      myURL = url;
    }

    @Override
    public String toString() {
      if (JDKUrlResolver.accept(myURL)) {
        try {
          return JDKUrlResolver.resolve(myURL, RuntimeUtil.NULL_MONITOR).toString();
        } catch (Exception e) {
          throw new RuntimeException(e);
        }
      }
      return myURL.toString();
    }

  }

}
