/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.jclouds.http;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Predicates.and;
import static com.google.common.base.Predicates.equalTo;
import static com.google.common.base.Predicates.in;
import static com.google.common.base.Predicates.not;
import static com.google.common.base.Predicates.notNull;
import static com.google.common.base.Throwables.getCausalChain;
import static com.google.common.base.Throwables.propagate;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.get;
import static com.google.common.collect.Iterables.size;
import static com.google.common.collect.Multimaps.filterKeys;
import static com.google.common.io.BaseEncoding.base64;
import static com.google.common.io.ByteStreams.toByteArray;
import static com.google.common.net.HttpHeaders.CACHE_CONTROL;
import static com.google.common.net.HttpHeaders.CONTENT_DISPOSITION;
import static com.google.common.net.HttpHeaders.CONTENT_ENCODING;
import static com.google.common.net.HttpHeaders.CONTENT_LANGUAGE;
import static com.google.common.net.HttpHeaders.CONTENT_LENGTH;
import static com.google.common.net.HttpHeaders.CONTENT_MD5;
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static com.google.common.net.HttpHeaders.EXPIRES;
import static org.jclouds.util.Closeables2.closeQuietly;

import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Map.Entry;

import javax.inject.Named;
import javax.inject.Singleton;
import javax.ws.rs.HttpMethod;

import org.jclouds.Constants;
import org.jclouds.io.ContentMetadata;
import org.jclouds.io.MutableContentMetadata;
import org.jclouds.io.Payload;
import org.jclouds.io.PayloadEnclosing;
import org.jclouds.io.Payloads;
import org.jclouds.logging.Logger;
import org.jclouds.logging.internal.Wire;

import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.common.collect.ImmutableSet.Builder;
import com.google.common.reflect.Invokable;
import com.google.inject.Inject;

@Singleton
public class HttpUtils {

   @Inject(optional = true)
   @Named(Constants.PROPERTY_RELAX_HOSTNAME)
   private boolean relaxHostname = false;

   private final int globalMaxConnections;
   private final int globalMaxConnectionsPerHost;
   private final int connectionTimeout;
   private final int soTimeout;
   
   @Inject(optional = true)
   @Named(Constants.PROPERTY_TRUST_ALL_CERTS)
   private boolean trustAllCerts;

   @Inject
   public HttpUtils(@Named(Constants.PROPERTY_CONNECTION_TIMEOUT) int connectionTimeout,
         @Named(Constants.PROPERTY_SO_TIMEOUT) int soTimeout,
         @Named(Constants.PROPERTY_MAX_CONNECTIONS_PER_CONTEXT) int globalMaxConnections,
         @Named(Constants.PROPERTY_MAX_CONNECTIONS_PER_HOST) int globalMaxConnectionsPerHost) {
      this.soTimeout = soTimeout;
      this.connectionTimeout = connectionTimeout;
      this.globalMaxConnections = globalMaxConnections;
      this.globalMaxConnectionsPerHost = globalMaxConnectionsPerHost;
   }

   public int getSocketOpenTimeout() {
      return soTimeout;
   }

   public int getConnectionTimeout() {
      return connectionTimeout;
   }

   public boolean relaxHostname() {
      return relaxHostname;
   }

   public boolean trustAllCerts() {
      return trustAllCerts;
   }

   public int getMaxConnections() {
      return globalMaxConnections;
   }

   public int getMaxConnectionsPerHost() {
      return globalMaxConnectionsPerHost;
   }

   public static byte[] toByteArrayOrNull(PayloadEnclosing response) {
      if (response.getPayload() != null) {
         InputStream input = null;
         try {
            input = response.getPayload().openStream();
            return toByteArray(input);
         } catch (IOException e) {
            propagate(e);
         } finally {
            closeQuietly(input);
         }
      }
      return null;
   }

   public static Optional<String> tryFindHttpMethod(Invokable<?, ?> method) {
      Builder<String> methodsBuilder = ImmutableSet.builder();
      for (Annotation annotation : method.getAnnotations()) {
         HttpMethod http = annotation.annotationType().getAnnotation(HttpMethod.class);
         if (http != null)
            methodsBuilder.add(http.value());
      }
      Collection<String> methods = methodsBuilder.build();
      switch (methods.size()) {
      case 0:
         return Optional.absent();
      case 1:
         return Optional.of(get(methods, 0));
      default:
         throw new IllegalStateException("You must specify at most one HttpMethod annotation on: " + method);
      }
   }
   
   /**
    * Content stream may need to be read. However, we should always close the http stream.
    * 
    * @throws IOException
    */
   public static byte[] closeClientButKeepContentStream(PayloadEnclosing response) {
      byte[] returnVal = toByteArrayOrNull(response);
      if (returnVal != null && !response.getPayload().isRepeatable()) {
         Payload newPayload = Payloads.newByteArrayPayload(returnVal);
         MutableContentMetadata fromMd = response.getPayload().getContentMetadata();
         MutableContentMetadata toMd = newPayload.getContentMetadata();
         copy(fromMd, toMd);
         response.setPayload(newPayload);
      }
      return returnVal;
   }

   public static void copy(ContentMetadata fromMd, MutableContentMetadata toMd) {
      toMd.setCacheControl(fromMd.getCacheControl());
      toMd.setContentLength(fromMd.getContentLength());
      toMd.setContentMD5(fromMd.getContentMD5());
      toMd.setContentType(fromMd.getContentType());
      toMd.setContentDisposition(fromMd.getContentDisposition());
      toMd.setContentEncoding(fromMd.getContentEncoding());
      toMd.setContentLanguage(fromMd.getContentLanguage());
      toMd.setExpires(fromMd.getExpires());
   }

   public void logRequest(Logger logger, HttpRequest request, String prefix) {
      if (logger.isDebugEnabled()) {
         logger.debug("%s %s", prefix, request.getRequestLine().toString());
         logMessage(logger, request, prefix);
      }
   }

   private void logMessage(Logger logger, HttpMessage message, String prefix) {
      for (Entry<String, String> header : message.getHeaders().entries()) {
         if (header.getKey() != null)
            logger.debug("%s %s: %s", prefix, header.getKey(), header.getValue());
      }
      if (message.getPayload() != null) {
         if (message.getPayload().getContentMetadata().getCacheControl() != null)
            logger.debug("%s %s: %s", prefix, CACHE_CONTROL, message.getPayload().getContentMetadata().getCacheControl());
         if (message.getPayload().getContentMetadata().getContentType() != null)
            logger.debug("%s %s: %s", prefix, CONTENT_TYPE, message.getPayload().getContentMetadata().getContentType());
         if (message.getPayload().getContentMetadata().getContentLength() != null)
            logger.debug("%s %s: %s", prefix, CONTENT_LENGTH, message.getPayload().getContentMetadata()
                  .getContentLength());
         byte[] md5 = message.getPayload().getContentMetadata().getContentMD5();
         if (md5 != null)
            logger.debug("%s %s: %s", prefix, CONTENT_MD5, base64().encode(md5));
         if (message.getPayload().getContentMetadata().getContentDisposition() != null)
            logger.debug("%s %s: %s", prefix, CONTENT_DISPOSITION, message.getPayload().getContentMetadata()
                  .getContentDisposition());
         if (message.getPayload().getContentMetadata().getContentEncoding() != null)
            logger.debug("%s %s: %s", prefix, CONTENT_ENCODING, message.getPayload().getContentMetadata()
                  .getContentEncoding());
         if (message.getPayload().getContentMetadata().getContentLanguage() != null)
            logger.debug("%s %s: %s", prefix, CONTENT_LANGUAGE, message.getPayload().getContentMetadata()
                  .getContentLanguage());
         if (message.getPayload().getContentMetadata().getExpires() != null)
            logger.debug("%s %s: %s", prefix, EXPIRES, message.getPayload().getContentMetadata().getExpires());
      }
   }

   public void logResponse(Logger logger, HttpResponse response, String prefix) {
      if (logger.isDebugEnabled()) {
         logger.debug("%s %s", prefix, response.getStatusLine().toString());
         logMessage(logger, response, prefix);
      }
   }

   public void checkRequestHasRequiredProperties(HttpRequest message) {
      checkArgument(
            message.getPayload() == null || message.getFirstHeaderOrNull(CACHE_CONTROL) == null,
            "configuration error please use request.getPayload().getContentMetadata().setCacheControl(value) as opposed to adding a cache control header: %s",
                  message);
      checkArgument(
            message.getPayload() == null || message.getFirstHeaderOrNull(CONTENT_TYPE) == null,
            "configuration error please use request.getPayload().getContentMetadata().setContentType(value) as opposed to adding a content type header: %s",
                  message);
      checkArgument(
            message.getPayload() == null || message.getFirstHeaderOrNull(CONTENT_LENGTH) == null,
            "configuration error please use request.getPayload().getContentMetadata().setContentLength(value) as opposed to adding a content length header: %s",
                  message);
      checkArgument(
            message.getPayload() == null || message.getPayload().getContentMetadata().getContentLength() != null
                  || "chunked".equalsIgnoreCase(message.getFirstHeaderOrNull("Transfer-Encoding")),
            "either chunked encoding must be set on the http request or contentlength set on the payload: %s", message);
      checkArgument(
            message.getPayload() == null || message.getFirstHeaderOrNull("Content-MD5") == null,
            "configuration error please use request.getPayload().getContentMetadata().setContentMD5(value) as opposed to adding a content md5 header: %s",
                  message);
      checkArgument(
            message.getPayload() == null || message.getFirstHeaderOrNull("Content-Disposition") == null,
            "configuration error please use request.getPayload().getContentMetadata().setContentDisposition(value) as opposed to adding a content disposition header: %s",
                  message);
      checkArgument(
            message.getPayload() == null || message.getFirstHeaderOrNull(CONTENT_ENCODING) == null,
            "configuration error please use request.getPayload().getContentMetadata().setContentEncoding(value) as opposed to adding a content encoding header: %s",
                  message);
      checkArgument(
            message.getPayload() == null || message.getFirstHeaderOrNull(CONTENT_LANGUAGE) == null,
            "configuration error please use request.getPayload().getContentMetadata().setContentLanguage(value) as opposed to adding a content language header: %s",
                  message);
      checkArgument(
            message.getPayload() == null || message.getFirstHeaderOrNull(EXPIRES) == null,
            "configuration error please use request.getPayload().getContentMetadata().setExpires(value) as opposed to adding an expires header: %s",
                  message);
   }

   public static void releasePayload(HttpMessage from) {
      if (from.getPayload() != null)
         from.getPayload().release();
   }

   public static String nullToEmpty(byte[] md5) {
      return md5 != null ? base64().encode(md5) : "";
   }

   public static String nullToEmpty(Collection<String> collection) {
      return (collection == null || collection.isEmpty()) ? "" : collection.iterator().next();
   }

   public static Long attemptToParseSizeAndRangeFromHeaders(HttpMessage from) throws HttpException {
      String contentRange = from.getFirstHeaderOrNull("Content-Range");
      if (contentRange == null && from.getPayload() != null) {
         return from.getPayload().getContentMetadata().getContentLength();
      } else if (contentRange != null) {
         return Long.parseLong(contentRange.substring(contentRange.lastIndexOf('/') + 1));
      }
      return null;
   }

   public static void checkRequestHasContentLengthOrChunkedEncoding(HttpMessage request, String message) {
      boolean chunked = "chunked".equals(request.getFirstHeaderOrNull("Transfer-Encoding"));
      checkArgument(request.getPayload() == null || chunked
            || request.getPayload().getContentMetadata().getContentLength() != null, message);
   }

   public static void wirePayloadIfEnabled(Wire wire, HttpMessage request) {
      if (request.getPayload() != null && wire.enabled()) {
         wire.output(request);
         checkRequestHasContentLengthOrChunkedEncoding(request,
               "After wiring, the request has neither chunked encoding nor content length: " + request);
      }
   }

   public static <T> T returnValueOnCodeOrNull(Throwable from, T value, Predicate<Integer> codePredicate) {
      Iterable<HttpResponseException> throwables = filter(getCausalChain(from), HttpResponseException.class);
      if (size(throwables) >= 1 && get(throwables, 0).getResponse() != null
            && codePredicate.apply(get(throwables, 0).getResponse().getStatusCode())) {
         return value;
      }
      return null;
   }

   public static Multimap<String, String> filterOutContentHeaders(Multimap<String, String> headers) {
      // http message usually comes in as a null key header, let's filter it out.
      return ImmutableMultimap.copyOf(filterKeys(headers, and(notNull(), not(in(ContentMetadata.HTTP_HEADERS)))));
   }

   public static boolean contains404(Throwable t) {
      return returnValueOnCodeOrNull(t, true, equalTo(404)) != null;
   }
}
