/*
 * Copyright 2013 Fae Hutter
 *
 * 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 com.vaguehope.onosendai.util;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Locale;
import java.util.concurrent.TimeUnit;

import org.apache.http.client.HttpResponseException;

public final class HttpHelper {

	private static final int HTTP_CONNECT_TIMEOUT_SECONDS = 20;
	private static final int HTTP_READ_TIMEOUT_SECONDS = 60;
	private static final int MAX_REDIRECTS = 5;
	private static final int MAX_ERR_BODY_LENGTH_CHAR = 100;

	private static final int HTTP_NOT_FOUND = 404;

	private static final LogWrapper LOG = new LogWrapper("HH");

	public enum Method {
		GET,
		HEAD;
	}

	public interface HttpStreamHandler<R> {

		/**
		 * Does not need to do anything, will still be thrown.
		 */
		void onError (Exception e);

		R handleStream (URLConnection connection, InputStream is, int contentLength) throws IOException;

	}

	public static class FinalUrlHandler<R> implements HttpStreamHandler<R> {

		private final HttpStreamHandler<R> delagate;
		private URL url;
		private int contentLength;

		public FinalUrlHandler (final HttpStreamHandler<R> delagate) {
			this.delagate = delagate;
		}

		public URL getUrl () {
			return this.url;
		}

		public int getContentLength () {
			return this.contentLength;
		}

		@Override
		public void onError (final Exception e) {
			this.delagate.onError(e);
		}

		@Override
		public R handleStream (final URLConnection connection, final InputStream is, final int contentLength) throws IOException {
			this.url = connection.getURL();
			this.contentLength = contentLength;
			return this.delagate.handleStream(connection, is, contentLength);
		}

	}

	private HttpHelper () {
		throw new AssertionError();
	}

	public static <R> R fetch (final Method method, final String sUrl, final HttpStreamHandler<R> streamHandler) throws IOException {
		try {
			return fetchWithFollowRedirects(method, new URL(sUrl), streamHandler, 0);
		}
		catch (final Exception e) { // NOSONAR need to report failures to onError().
			streamHandler.onError(e);
			if (e instanceof RuntimeException) throw (RuntimeException) e;
			if (e instanceof IOException) throw (IOException) e;
			throw new IllegalStateException(e);
		}
	}

	private static <R> R fetchWithFollowRedirects (final Method method, final URL url, final HttpStreamHandler<R> streamHandler, final int redirectCount) throws IOException, URISyntaxException {
		final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
		try {
			connection.setRequestMethod(method.toString());
			connection.setInstanceFollowRedirects(false);
			connection.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(HTTP_CONNECT_TIMEOUT_SECONDS));
			connection.setReadTimeout((int) TimeUnit.SECONDS.toMillis(HTTP_READ_TIMEOUT_SECONDS));
			connection.setRequestProperty("User-Agent", "curl/1"); // Make it really clear this is not a browser.
			//connection.setRequestProperty("Accept-Encoding", "identity"); This fixes missing Content-Length headers but feels wrong.
			connection.connect();

			InputStream is = null;
			try {
				final int responseCode = connection.getResponseCode();

				// For some reason some devices do not follow redirects. :(
				if (responseCode == 301 || responseCode == 302 || responseCode == 303 || responseCode == 307) { // NOSONAR not magic numbers.  Its HTTP spec.
					if (redirectCount >= MAX_REDIRECTS) throw new TooManyRedirectsException(responseCode, url, MAX_REDIRECTS);
					final String locationHeader = connection.getHeaderField("Location");
					if (locationHeader == null) throw new HttpResponseException(responseCode, "Location header missing.  Headers present: "
							+ connection.getHeaderFields() + ".");
					connection.disconnect();

					final URL locationUrl;
					if (locationHeader.toLowerCase(Locale.ENGLISH).startsWith("http")) {
						locationUrl = new URL(locationHeader);
					}
					else {
						locationUrl = url.toURI().resolve(locationHeader).toURL();
					}
					return fetchWithFollowRedirects(method, locationUrl, streamHandler, redirectCount + 1);
				}

				if (responseCode < 200 || responseCode >= 300) { // NOSONAR not magic numbers.  Its HTTP spec.
					throw new NotOkResponseException(responseCode, connection, url);
				}

				is = connection.getInputStream();
				final int contentLength = connection.getContentLength();
				if (contentLength < 1) LOG.w("Content-Length=%s for %s.", contentLength, url);
				return streamHandler.handleStream(connection, is, contentLength);
			}
			finally {
				IoHelper.closeQuietly(is);
			}
		}
		finally {
			connection.disconnect();
		}
	}

	private static String summariseHttpErrorResponse (final HttpURLConnection connection) throws IOException {
		final int responseCode = connection.getResponseCode();
		if (responseCode == HTTP_NOT_FOUND) return String.format("HTTP %s %s", responseCode, connection.getResponseMessage());
		return String.format("HTTP %s %s: %s",
				responseCode, connection.getResponseMessage(),
				IoHelper.toString(connection.getErrorStream(), MAX_ERR_BODY_LENGTH_CHAR));
	}

	public abstract static class FinalUrlException extends HttpResponseException {

		private static final long serialVersionUID = -5533900604169920207L;

		private final URL lastUrl;

		public FinalUrlException (final String message, final int responseCode, final URL lastUrl) {
			super(responseCode, message);
			this.lastUrl = lastUrl;
		}

		public URL getLastUrl () {
			return this.lastUrl;
		}

	}

	public static class TooManyRedirectsException extends FinalUrlException {

		private static final long serialVersionUID = -7981953690474511370L;

		public TooManyRedirectsException (final int responseCode, final URL lastUrl, final int redirectCount) {
			super("Max redirects of " + redirectCount + " exceeded.",
					responseCode, lastUrl);
		}

	}

	public static class NotOkResponseException extends FinalUrlException {

		private static final long serialVersionUID = 2450727724111193765L;

		public NotOkResponseException (final int responseCode, final HttpURLConnection connection, final URL lastUrl) throws IOException {
			super(summariseHttpErrorResponse(connection),
					responseCode, lastUrl);
		}

	}

}