/**
 * Copyright 2009 Google 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.openid4java.appengine;

import com.google.appengine.api.urlfetch.FetchOptions;
import com.google.appengine.api.urlfetch.HTTPHeader;
import com.google.appengine.api.urlfetch.HTTPMethod;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.ResponseTooLargeException;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.common.base.Joiner;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.Singleton;

import org.apache.http.Header;
import org.apache.http.message.BasicHeader;
import org.openid4java.util.AbstractHttpFetcher;
import org.openid4java.util.HttpRequestOptions;
import org.openid4java.util.HttpResponse;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.servlet.http.HttpServletResponse;

/**
 * Implementation of HttpFetcher for AppEngine.
 */
@Singleton
public class AppEngineHttpFetcher extends AbstractHttpFetcher {

  private final URLFetchService fetchService;

  @Inject
  public AppEngineHttpFetcher(URLFetchService fetchService) {
    this.fetchService = fetchService;
  }

  @Override
  public HttpResponse get(String url, HttpRequestOptions requestOptions)
      throws IOException {
    return fetch(url, requestOptions, HTTPMethod.GET, null);
  }

  @Override
  public HttpResponse head(String url, HttpRequestOptions requestOptions)
      throws IOException {
    return fetch(url, requestOptions, HTTPMethod.HEAD, null);
  }

  @Override
  public HttpResponse post(String url, Map<String, String> parameters,
      HttpRequestOptions requestOptions) throws IOException {
    return fetch(url, requestOptions, HTTPMethod.POST, encodeParameters(parameters ));
  }

  private String encodeParameters(Map<String, String> params) {
    Map<String, String> escapedParams = Maps.newHashMap();
    for (Entry<String, String> entry : params.entrySet()) {
      try {
        escapedParams.put(URLEncoder.encode(entry.getKey(), "UTF-8"),
                          URLEncoder.encode(entry.getValue(), "UTF-8"));
      } catch (UnsupportedEncodingException e) {
        // this should not happen
        throw new RuntimeException("platform does not support UTF-8", e);
      }
    }
    return Joiner.on("&").withKeyValueSeparator("=").join(escapedParams);
  }

  private HttpResponse fetch(String url, HttpRequestOptions requestOptions,
      HTTPMethod method, String content) throws IOException {

    final FetchOptions options = getFetchOptions(requestOptions);

    String currentUrl = url;

    for (int i = 0; i <= requestOptions.getMaxRedirects(); i++) {

      HTTPRequest httpRequest = new HTTPRequest(new URL(currentUrl),
          method, options);

      addHeaders(httpRequest, requestOptions);

      if (method == HTTPMethod.POST && content != null) {
        httpRequest.setPayload(content.getBytes());
      }

      HTTPResponse httpResponse;
      try {
        httpResponse = fetchService.fetch(httpRequest);
      } catch (ResponseTooLargeException e) {
        return new TooLargeResponse(currentUrl);
      }

      if (!isRedirect(httpResponse.getResponseCode())) {
        boolean isResponseTooLarge =
          (getContentLength(httpResponse) > requestOptions.getMaxBodySize());
        return new AppEngineFetchResponse(httpResponse,
            isResponseTooLarge, currentUrl);
      } else {
        currentUrl = getResponseHeader(httpResponse, "Location").getValue();
      }
    }
    throw new IOException("exceeded maximum number of redirects");
  }

  private static int getContentLength(HTTPResponse httpResponse) {
    byte[] content = httpResponse.getContent();
    if (content == null) {
      return 0;
    } else {
      return content.length;
    }
  }

  private static void addHeaders(HTTPRequest httpRequest,
      HttpRequestOptions requestOptions) {

    String contentType = requestOptions.getContentType();

    if (contentType != null) {
      httpRequest.addHeader(new HTTPHeader("Content-Type", contentType));
    }

    Map<String, String> headers = getRequestHeaders(requestOptions);

    if (headers != null) {
      for (Map.Entry<String, String> header : headers.entrySet()) {
        httpRequest.addHeader(new HTTPHeader(header.getKey(), header.getValue()));
      }
    }
  }

  @SuppressWarnings("unchecked")
  private static Map<String, String> getRequestHeaders(
      HttpRequestOptions requestOptions) {
    return requestOptions.getRequestHeaders();
  }

  private static Header getResponseHeader(HTTPResponse httpResponse, String headerName) {
    Header[] allHeaders = getResponseHeaders(httpResponse, headerName);
    if (allHeaders.length == 0) {
      return null;
    } else {
      return allHeaders[0];
    }
  }

  private static Header[] getResponseHeaders(HTTPResponse httpResponse, String headerName) {
    List<HTTPHeader> allHeaders = httpResponse.getHeaders();
    List<Header> matchingHeaders = new ArrayList<Header>();
    for (HTTPHeader header : allHeaders) {
      if (header.getName().equalsIgnoreCase(headerName)) {
        matchingHeaders.add(new BasicHeader(header.getName(), header.getValue()));
      }
    }
    return matchingHeaders.toArray(new Header[matchingHeaders.size()]);
  }

  private static boolean isRedirect(int responseCode) {
    switch (responseCode) {
      case HttpServletResponse.SC_MOVED_PERMANENTLY:
      case HttpServletResponse.SC_MOVED_TEMPORARILY:
      case HttpServletResponse.SC_SEE_OTHER:
      case HttpServletResponse.SC_TEMPORARY_REDIRECT:
        return true;
      default:
        return false;
    }
  }

  private FetchOptions getFetchOptions(HttpRequestOptions requestOptions) {
    return FetchOptions.Builder.disallowTruncate()
        .doNotFollowRedirects()
        .setDeadline(requestOptions.getConnTimeout() / 1000.0);
  }

  private static class AppEngineFetchResponse implements HttpResponse {

    private final com.google.appengine.api.urlfetch.HTTPResponse httpResponse;
    private final boolean bodySizeExceeded;
    private String finalUri;

    public AppEngineFetchResponse(
        com.google.appengine.api.urlfetch.HTTPResponse httpResponse,
        boolean bodySizeExceeded,
        String finalUri) {
      this.httpResponse = httpResponse;
      this.bodySizeExceeded = bodySizeExceeded;
      this.finalUri = finalUri;
    }

    public String getBody() {
      byte[] content = httpResponse.getContent();
      return (content == null || content.length == 0) ? null : new String(content);
    }

    public String getFinalUri() {
      return finalUri;
    }

    public Header getResponseHeader(String headerName) {
      return AppEngineHttpFetcher.getResponseHeader(httpResponse, headerName);
    }

    public Header[] getResponseHeaders(String headerName) {
      return AppEngineHttpFetcher.getResponseHeaders(httpResponse, headerName);
    }

    public boolean isBodySizeExceeded() {
      return bodySizeExceeded;
    }

    public int getStatusCode() {
      return httpResponse.getResponseCode();
    }
  }

  private static class TooLargeResponse implements HttpResponse {

    private String finalUri;

    public TooLargeResponse(String finalUri) {
      this.finalUri = finalUri;
    }

    public String getBody() {
      throw new ResponseTooLargeException(finalUri);
    }

    public String getFinalUri() {
      return finalUri;
    }

    public Header getResponseHeader(String headerName) {
      throw new ResponseTooLargeException(finalUri);
    }

    public Header[] getResponseHeaders(String headerName) {
      throw new ResponseTooLargeException(finalUri);
    }

    public boolean isBodySizeExceeded() {
      return true;
    }

    public int getStatusCode() {
      throw new ResponseTooLargeException(finalUri);
    }
  }
}
