/*
 * Copyright 2011 JBoss, by Red Hat, Inc
 *
 * 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 org.jboss.errai.tools.proxy;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.Security;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Logger;

/**
 * @author Yutaka Yoshida, Greg Murray, Heiko Braun
 *         <p/>
 *         Minimum set of HTTPclient supporting both http and https.
 *         It's aslo capable of POST, but it doesn't provide doGet because
 *         the caller can just read the inputstream.
 */
public class HttpClient {

  private static Logger logger;
  private String proxyHost = null;
  private int proxyPort = -1;
  private boolean isHttps = false;
  private boolean isProxy = false;
  private HttpURLConnection urlConnection = null;
  private Map headers;

  private String setCookieHeader;

  private XmlHttpProxy.CookieCallback callback;

  /**
   * @param phost   PROXY host name
   * @param pport   PROXY port string
   * @param url     URL string
   * @param headers Map
   */
  public HttpClient(
      String phost,
      int pport,
      String url,
      Map headers,
      String method,
      XmlHttpProxy.CookieCallback callback)
      throws MalformedURLException {
    this.callback = callback;

    if (phost != null && pport != -1) {
      this.isProxy = true;
    }

    this.proxyHost = phost;
    this.proxyPort = pport;

    if (url.trim().startsWith("https:")) {
      isHttps = true;
    }

    this.urlConnection = getURLConnection(url);
    try {
      this.urlConnection.setRequestMethod(method);
    }
    catch (java.net.ProtocolException pe) {
      HttpClient.getLogger().severe("Unable protocol method to " + method + " : " + pe);
    }
    this.headers = headers;
    writeHeaders(headers);

  }

  private void writeHeaders(Map headers) {
    if (this.callback != null) {
      Map<String, XmlHttpProxy.Cookie> cookies = callback.getCookies();
      Iterator it = cookies.keySet().iterator();
      while (it.hasNext()) {
        XmlHttpProxy.Cookie c = cookies.get(it.next());
        if (headers == null) headers = new HashMap();
        headers.put(
            "Cookie", c.name + "=" + c.value // + "; Path=" + c.path
        );
      }

    }
    // set headers
    if (headers != null) {
      Iterator it = headers.keySet().iterator();
      if (it != null) {
        while (it.hasNext()) {
          String key = (String) it.next();
          String value = (String) headers.get(key);
          System.out.println("Set Request Header: " + key + "->" + value);
          this.urlConnection.setRequestProperty(key, value);
        }
      }
    }
  }

  /**
   * @param phost    PROXY host name
   * @param pport    PROXY port string
   * @param url      URL string
   * @param headers  Map
   * @param userName string
   * @param password string
   */
  public HttpClient(String phost,
                    int pport,
                    String url,
                    Map headers,
                    String method,
                    String userName,
                    String password,
                    XmlHttpProxy.CookieCallback callback)
      throws MalformedURLException {

    this.callback = callback;

    try {
      if (phost != null && pport != -1) {
        this.isProxy = true;
      }

      this.proxyHost = phost;
      this.proxyPort = pport;
      if (url.trim().startsWith("https:")) {
        isHttps = true;
      }
      this.urlConnection = getURLConnection(url);
      try {
        this.urlConnection.setRequestMethod(method);
      }
      catch (java.net.ProtocolException pe) {
        HttpClient.getLogger().severe("Unable protocol method to " + method + " : " + pe);
      }
      // set basic authentication information
      String auth = userName + ":" + password;
      String encoded = new sun.misc.BASE64Encoder().encode(auth.getBytes());
      // set basic authorization
      this.urlConnection.setRequestProperty("Authorization", "Basic " + encoded);
      this.headers = headers;
      writeHeaders(headers);
    }
    catch (Exception ex) {
      HttpClient.getLogger().severe("Unable to set basic authorization for " + userName + " : " + ex);
    }
  }

  /**
   * private method to get the URLConnection
   *
   * @param str URL string
   */
  private HttpURLConnection getURLConnection(String str)
      throws MalformedURLException {
    try {

      if (isHttps) {
        /* when communicating with the server which has unsigned or invalid
        * certificate (https), SSLException or IOException is thrown.
        * the following line is a hack to avoid that
        */
        Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
        System.setProperty("java.protocol.handler.pkgs", "com.sun.net.ssl.internal.www.protocol");
        if (isProxy) {
          System.setProperty("https.proxyHost", proxyHost);
          System.setProperty("https.proxyPort", proxyPort + "");
        }
      }
      else {
        if (isProxy) {
          System.setProperty("http.proxyHost", proxyHost);
          System.setProperty("http.proxyPort", proxyPort + "");
        }
      }

      URL url = new URL(str);
      HttpURLConnection uc = (HttpURLConnection) url.openConnection();

      // if this header has not been set by a request set the user agent.
      if (headers == null ||
          (headers != null && headers.get("user-agent") == null)) {
        // set user agent to mimic a common browser
        String ua = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322)";
        uc.setRequestProperty("user-agent", ua);
      }

      uc.setInstanceFollowRedirects(false);

      return uc;
    }
    catch (MalformedURLException me) {
      throw new MalformedURLException(str + " is not a valid URL");
    }
    catch (Exception e) {
      throw new RuntimeException("Unknown error creating UrlConnection: " + e);
    }
  }

  public String getSetCookieHeader() {
    return setCookieHeader;
  }

  /**
   * returns the inputstream from URLConnection
   *
   * @return InputStream
   */
  public InputStream getInputStream() {
    try {
      // logger doesnt work, because it writes to stderr,
      // which causes GwtTest to interpret it as failure
      System.out.println(
          this.urlConnection.getRequestMethod() + " " +
              this.urlConnection.getURL() + ": " +
              this.urlConnection.getResponseCode()
      );

      try {
        // HACK: manually follow redirects, for the login to work
        // HTTPUrlConnection auto redirect doesn't respect the provided headers
        if (this.urlConnection.getResponseCode() == 302) {
          HttpClient redirectClient =
              new HttpClient(proxyHost, proxyPort, urlConnection.getHeaderField("Location"),
                  headers, urlConnection.getRequestMethod(), callback);
          redirectClient.getInputStream().close();
        }
      }
      catch (Throwable e) {
        System.out.println("Following redirect failed");
      }

      setCookieHeader = this.urlConnection.getHeaderField("Set-Cookie");

      return (this.urlConnection.getInputStream());
    }
    catch (Exception e) {
      System.out.println("Failed to open " + this.urlConnection.getURL());
      e.printStackTrace();
      return null;
    }
  }

  /**
   * return the OutputStream from URLConnection
   *
   * @return OutputStream
   */
  public OutputStream getOutputStream() {

    try {
      return (this.urlConnection.getOutputStream());
    }
    catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }

  /**
   * posts data to the inputstream and returns the InputStream.
   *
   * @param postData    data to be posted. must be url-encoded already.
   * @param contentType allows you to set the contentType of the request.
   * @return InputStream input stream from URLConnection
   */
  public InputStream doPost(String postData, String contentType) {
    this.urlConnection.setDoOutput(true);
    if (contentType != null) this.urlConnection.setRequestProperty("Content-type", contentType);

    OutputStream os = this.getOutputStream();
    PrintStream ps = new PrintStream(os);
    ps.print(postData);
    ps.close();
    return (this.getInputStream());
  }

  public String getContentEncoding() {
    if (this.urlConnection == null) return null;
    return (this.urlConnection.getContentEncoding());
  }

  public int getContentLength() {
    if (this.urlConnection == null) return -1;
    return (this.urlConnection.getContentLength());
  }

  public String getContentType() {
    if (this.urlConnection == null) return null;
    return (this.urlConnection.getContentType());
  }

  public long getDate() {
    if (this.urlConnection == null) return -1;
    return (this.urlConnection.getDate());
  }

  public String getHeader(String name) {
    if (this.urlConnection == null) return null;
    return (this.urlConnection.getHeaderField(name));
  }

  public long getIfModifiedSince() {
    if (this.urlConnection == null) return -1;
    return (this.urlConnection.getIfModifiedSince());
  }

  public static Logger getLogger() {
    if (logger == null) {
      logger = Logger.getLogger("jmaki.xhp.Log");
    }
    return logger;
  }
}