package io.swagger.client;

import org.apache.http.*;
import org.apache.http.client.*;
import org.apache.http.client.methods.*;
import org.apache.http.conn.*;
import org.apache.http.conn.scheme.*;
import org.apache.http.conn.ssl.*;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.*;
import org.apache.http.impl.conn.*;
import org.apache.http.impl.conn.tsccm.*;
import org.apache.http.params.*;
import org.apache.http.util.EntityUtils;

import java.io.File;
import java.net.Socket;
import java.net.UnknownHostException;
import java.net.URLEncoder;

import java.util.Collection;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

import java.security.GeneralSecurityException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.*;

import java.text.DateFormat;
import java.text.SimpleDateFormat;

import java.util.Date;
import java.util.TimeZone;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import com.google.gson.JsonParseException;

public class ApiInvoker {
  private static ApiInvoker INSTANCE = new ApiInvoker();
  private Map<String, String> defaultHeaderMap = new HashMap<String, String>();

  private HttpClient client = null;

  private boolean ignoreSSLCertificates = false;

  private ClientConnectionManager ignoreSSLConnectionManager;

  /** Content type "text/plain" with UTF-8 encoding. */
  public static final ContentType TEXT_PLAIN_UTF8 = ContentType.create("text/plain", Consts.UTF_8);

  /**
   * ISO 8601 date time format.
   * @see https://en.wikipedia.org/wiki/ISO_8601
   */
  public static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");

  /**
   * ISO 8601 date format.
   * @see https://en.wikipedia.org/wiki/ISO_8601
   */
  public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");

  static {
    // Use UTC as the default time zone.
    DATE_TIME_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
    DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));

    // Set default User-Agent.
    setUserAgent("Android-Java-Swagger");
  }

  public static void setUserAgent(String userAgent) {
    INSTANCE.addDefaultHeader("User-Agent", userAgent);
  }

  public static Date parseDateTime(String str) {
    try {
      return DATE_TIME_FORMAT.parse(str);
    } catch (java.text.ParseException e) {
      throw new RuntimeException(e);
    }
  }

  public static Date parseDate(String str) {
    try {
      return DATE_FORMAT.parse(str);
    } catch (java.text.ParseException e) {
      throw new RuntimeException(e);
    }
  }

  public static String formatDateTime(Date datetime) {
    return DATE_TIME_FORMAT.format(datetime);
  }

  public static String formatDate(Date date) {
    return DATE_FORMAT.format(date);
  }

  public static String parameterToString(Object param) {
    if (param == null) {
      return "";
    } else if (param instanceof Date) {
      return formatDateTime((Date) param);
    } else if (param instanceof Collection) {
      StringBuilder b = new StringBuilder();
      for(Object o : (Collection)param) {
        if(b.length() > 0) {
          b.append(",");
        }
        b.append(String.valueOf(o));
      }
      return b.toString();
    } else {
      return String.valueOf(param);
    }
  }

  /*
    Format to {@code Pair} objects.
  */
  public static List<Pair> parameterToPairs(String collectionFormat, String name, Object value){
    List<Pair> params = new ArrayList<Pair>();

    // preconditions
    if (name == null || name.isEmpty() || value == null) return params;

    Collection valueCollection = null;
    if (value instanceof Collection) {
      valueCollection = (Collection) value;
    } else {
      params.add(new Pair(name, parameterToString(value)));
      return params;
    }

    if (valueCollection.isEmpty()){
      return params;
    }

    // get the collection format
    collectionFormat = (collectionFormat == null || collectionFormat.isEmpty() ? "csv" : collectionFormat); // default: csv

    // create the params based on the collection format
    if (collectionFormat.equals("multi")) {
      for (Object item : valueCollection) {
        params.add(new Pair(name, parameterToString(item)));
      }

      return params;
    }

    String delimiter = ",";

    if (collectionFormat.equals("csv")) {
      delimiter = ",";
    } else if (collectionFormat.equals("ssv")) {
      delimiter = " ";
    } else if (collectionFormat.equals("tsv")) {
      delimiter = "\t";
    } else if (collectionFormat.equals("pipes")) {
      delimiter = "|";
    }

    StringBuilder sb = new StringBuilder() ;
    for (Object item : valueCollection) {
      sb.append(delimiter);
      sb.append(parameterToString(item));
    }

    params.add(new Pair(name, sb.substring(1)));

    return params;
  }

  public ApiInvoker() {
    initConnectionManager();
  }

  public static ApiInvoker getInstance() {
    return INSTANCE;
  }

  public void ignoreSSLCertificates(boolean ignoreSSLCertificates) {
    this.ignoreSSLCertificates = ignoreSSLCertificates;
  }

  public void addDefaultHeader(String key, String value) {
    defaultHeaderMap.put(key, value);
  }

  public String escapeString(String str) {
    return str;
  }

  public static Object deserialize(String json, String containerType, Class cls) throws ApiException {
    try{
      if("list".equalsIgnoreCase(containerType) || "array".equalsIgnoreCase(containerType)) {
        return JsonUtil.deserializeToList(json, cls);
      }
      else if(String.class.equals(cls)) {
        if(json != null && json.startsWith("\"") && json.endsWith("\"") && json.length() > 1)
          return json.substring(1, json.length() - 1);
        else
          return json;
      }
      else {
        return JsonUtil.deserializeToObject(json, cls);
      }
    }
    catch (JsonParseException e) {
      throw new ApiException(500, e.getMessage());
    }
  }

  public static String serialize(Object obj) throws ApiException {
    try {
      if (obj != null)
        return JsonUtil.serialize(obj);
      else
        return null;
    }
    catch (Exception e) {
      throw new ApiException(500, e.getMessage());
    }
  }

  public String invokeAPI(String host, String path, String method, List<Pair> queryParams, Object body, Map<String, String> headerParams, Map<String, String> formParams, String contentType) throws ApiException {
    HttpClient client = getClient(host);

    StringBuilder b = new StringBuilder();
    b.append("?");
    if (queryParams != null){
      for (Pair queryParam : queryParams){
        if (!queryParam.getName().isEmpty()) {
          b.append(escapeString(queryParam.getName()));
          b.append("=");
          b.append(escapeString(queryParam.getValue()));
          b.append("&");
        }
      }
    }

    String querystring = b.substring(0, b.length() - 1);
    String url = host + path + querystring;

    HashMap<String, String> headers = new HashMap<String, String>();

    for(String key : headerParams.keySet()) {
      headers.put(key, headerParams.get(key));
    }

    for(String key : defaultHeaderMap.keySet()) {
      if(!headerParams.containsKey(key)) {
        headers.put(key, defaultHeaderMap.get(key));
      }
    }
    headers.put("Accept", "application/json");

    // URL encoded string from form parameters
    String formParamStr = null;

    // for form data
    if ("application/x-www-form-urlencoded".equals(contentType)) {
      StringBuilder formParamBuilder = new StringBuilder();

      // encode the form params
      for (String key : formParams.keySet()) {
        String value = formParams.get(key);
        if (value != null && !"".equals(value.trim())) {
          if (formParamBuilder.length() > 0) {
            formParamBuilder.append("&");
          }
          try {
            formParamBuilder.append(URLEncoder.encode(key, "utf8")).append("=").append(URLEncoder.encode(value, "utf8"));
          }
          catch (Exception e) {
            // move on to next
          }
        }
      }
      formParamStr = formParamBuilder.toString();
    }

    HttpResponse response = null;
    try {
      if ("GET".equals(method)) {
        HttpGet get = new HttpGet(url);
        get.addHeader("Accept", "application/json");
        for(String key : headers.keySet()) {
          get.setHeader(key, headers.get(key));
        }
        response = client.execute(get);
      }
      else if ("POST".equals(method)) {
        HttpPost post = new HttpPost(url);
        if (formParamStr != null) {
          post.setHeader("Content-Type", contentType);
          post.setEntity(new StringEntity(formParamStr, "UTF-8"));
        } else if (body != null) {
          if (body instanceof HttpEntity) {
            // this is for file uploading
            post.setEntity((HttpEntity) body);
          } else {
            post.setHeader("Content-Type", contentType);
            post.setEntity(new StringEntity(serialize(body), "UTF-8"));
          }
        }
        for(String key : headers.keySet()) {
          post.setHeader(key, headers.get(key));
        }
        response = client.execute(post);
      }
      else if ("PUT".equals(method)) {
        HttpPut put = new HttpPut(url);
        if (formParamStr != null) {
          put.setHeader("Content-Type", contentType);
          put.setEntity(new StringEntity(formParamStr, "UTF-8"));
        } else if (body != null) {
          put.setHeader("Content-Type", contentType);
          put.setEntity(new StringEntity(serialize(body), "UTF-8"));
        }
        for(String key : headers.keySet()) {
          put.setHeader(key, headers.get(key));
        }
        response = client.execute(put);
      }
      else if ("DELETE".equals(method)) {
        HttpDelete delete = new HttpDelete(url);
        for(String key : headers.keySet()) {
          delete.setHeader(key, headers.get(key));
        }
        response = client.execute(delete);
      }
      else if ("PATCH".equals(method)) {
        HttpPatch patch = new HttpPatch(url);
        if (formParamStr != null) {
          patch.setHeader("Content-Type", contentType);
          patch.setEntity(new StringEntity(formParamStr, "UTF-8"));
        } else if (body != null) {
          patch.setHeader("Content-Type", contentType);
          patch.setEntity(new StringEntity(serialize(body), "UTF-8"));
        }
        for(String key : headers.keySet()) {
          patch.setHeader(key, headers.get(key));
        }
        response = client.execute(patch);
      }

      int code = response.getStatusLine().getStatusCode();
      String responseString = null;
      if(code == 204) {
        responseString = "";
        return responseString;
      }
      else if(code >= 200 && code < 300) {
        if(response.getEntity() != null) {
          HttpEntity resEntity = response.getEntity();
          responseString = EntityUtils.toString(resEntity);
        }
        return responseString;
      }
      else {
        if(response.getEntity() != null) {
          HttpEntity resEntity = response.getEntity();
          responseString = EntityUtils.toString(resEntity);
        }
        else
          responseString = "no data";
      }
      throw new ApiException(code, responseString);
    }
    catch(IOException e) {
      throw new ApiException(500, e.getMessage());
    }
  }

  private HttpClient getClient(String host) {
    if (client == null) {
      if (ignoreSSLCertificates && ignoreSSLConnectionManager != null) {
        // Trust self signed certificates
        client = new DefaultHttpClient(ignoreSSLConnectionManager, new BasicHttpParams());
      } else {
        client = new DefaultHttpClient();
      }
    }
    return client;
  }

  private void initConnectionManager() {
    try {
      final SSLContext sslContext = SSLContext.getInstance("SSL");

      // set up a TrustManager that trusts everything
      TrustManager[] trustManagers = new TrustManager[] {
        new X509TrustManager() {
          public X509Certificate[] getAcceptedIssuers() {
            return null;
          }
          public void checkClientTrusted(X509Certificate[] certs, String authType) {}
          public void checkServerTrusted(X509Certificate[] certs, String authType) {}
      }};

      sslContext.init(null, trustManagers, new SecureRandom());

      SSLSocketFactory sf = new SSLSocketFactory((KeyStore)null) {
        private javax.net.ssl.SSLSocketFactory sslFactory = sslContext.getSocketFactory();

        public Socket createSocket(Socket socket, String host, int port, boolean autoClose)
          throws IOException, UnknownHostException {
          return sslFactory.createSocket(socket, host, port, autoClose);
        }

        public Socket createSocket() throws IOException {
          return sslFactory.createSocket();
        }
      };

      sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
      Scheme httpsScheme = new Scheme("https", sf, 443);
      SchemeRegistry schemeRegistry = new SchemeRegistry();
      schemeRegistry.register(httpsScheme);
      schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));

      ignoreSSLConnectionManager = new ThreadSafeClientConnManager(new BasicHttpParams(), schemeRegistry);
    } catch (NoSuchAlgorithmException e) {
      // This will only be thrown if SSL isn't available for some reason.
    } catch (KeyManagementException e) {
      // This might be thrown when passing a key into init(), but no key is being passed.
    } catch (GeneralSecurityException e) {
      // This catches anything else that might go wrong.
      // If anything goes wrong we default to the standard connection manager.
    }
  }
}
