package com.dataart.android.devicehive.network;

import java.io.IOException;
import java.util.Map;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.util.EntityUtils;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.ResultReceiver;
import android.util.Log;

import com.dataart.android.devicehive.DeviceHive;

/**
 * Common base class for all service commands.
 */
public abstract class NetworkCommand implements Parcelable {
	
	protected final static ClassLoader CLASS_LOADER = NetworkCommand.class
			.getClassLoader();

	private final static String NAMESPACE = NetworkCommand.class.getName();

	private final static String KEY_TAG = NAMESPACE.concat(".KEY_TAG");
	
	private final static String KEY_STATUS_CODE = NAMESPACE
			.concat(".KEY_STATUS_CODE");

	private final static String KEY_EXCEPTION = NAMESPACE
			.concat(".KEY_EXCEPTION");

	private final static String KEY_COMMAND = NAMESPACE.concat(".KEY_COMMAND");

	private NetworkCommandConfig config;

	private static final HttpClient client = getClient();

	// timeouts
	private static final int DEFAULT_CONNECTION_TIMEOUT_MILLIS = 40000;
	private static final int DEFAULT_SOCKET_TIMEOUT_MILLIS = 40000;

	/**
	 * Http request type.
	 */
	protected static enum RequestType {
		GET, POST, PUT, DELETE
	}
	
	/**
	 * Start this command using given {@link Context} and {@link ResultReceiver}
	 * .
	 * 
	 * @param context
	 *            {@link Context} instance which is used to start command.
	 * @param config
	 *            {@link NetworkCommandConfig} instance which contains configuration data for given command.
	 */
	public void start(final Context context, final NetworkCommandConfig config) {
		final Intent intent = new Intent(context, DeviceHiveApiService.class);
		intent.putExtra(DeviceHiveApiService.EXTRA_COMMAND, this);
		intent.putExtra(DeviceHiveApiService.EXTRA_COMMAND_CONFIG, config);
		intent.putExtra(DeviceHiveApiService.EXTRA_COMMAND_SERIAL, isSerial());
		context.startService(intent);
	}
	
	/* package */ void setConfig(NetworkCommandConfig config) {
		this.config = config;
	}

	/**
	 * Override this method in order to force commands to be executed in a
	 * "serial" mode, i.e. in order they submitted (in single executor). Default
	 * implementation returns <code>false</code>.
	 * 
	 * @return true, if command should be executed in "serial" mode, otherwise
	 *         return false.
	 */
	protected boolean isSerial() {
		return false;
	}

	/**
	 * This convenient method is used to ensure the bundle has correct
	 * command-tag.
	 * 
	 * @return Tagged {@link Bundle} instance.
	 */
	protected final Bundle getTaggedBundle() {
		return getTaggedBundle(0);
	}

	/**
	 * Create a {@link Bundle} instance and ensure the bundle has correct
	 * command-tag.
	 * 
	 * @param capacity
	 *            {@link Bundle} initial capacity.
	 * @return Tagged {@link Bundle} instance.
	 */
	protected final Bundle getTaggedBundle(final int capacity) {
		final Bundle bundle = capacity > 0 ? new Bundle(capacity + 1)
				: new Bundle();
		bundle.putString(KEY_TAG, getClass().getName());
		return bundle;
	}

	/**
	 * Get Http request type.
	 * 
	 * @returnm {@link RequestType}
	 */
	protected abstract RequestType getRequestType();

	/**
	 * Get request path component.
	 * 
	 * @return Request path component.
	 */
	protected abstract String getRequestPath();

	/**
	 * Get request headers.
	 * 
	 * @return Request headers.
	 */
	protected abstract Map<String, String> getHeaders();

	/**
	 * Get {@link HttpEntity} for the request. Override this method to return
	 * appropriate entity for POST and PUT requests.
	 * 
	 * @return {@link HttpEntity} implementer instance.
	 */
	protected HttpEntity getRequestEntity() {
		return null;
	}

	/**
	 * Check if given status code is considered as successful for this request.
	 * Can be overridden in descendants.
	 * 
	 * @param statusCode
	 *            Response status code.
	 */
	protected boolean isSuccessStatusCode(int statusCode) {
		return statusCode == 200 || statusCode == 201;
	}

	/**
	 * Handle response.
	 * @param response String representation of the response body.
	 * @param resultData {@link Bundle} which is used to put and pass the results of command execution to the sender.
	 * @param context Current {@link Context} object.
	 * @return Status code.
	 * @see {@link DeviceHiveResultReceiver}.
	 */
	protected int handleResponse(final String response,
			final Bundle resultData, final Context context) {
		return -1;
	}

	/**
	 * Execute command. Called by the service on the dedicated thread.
	 * @param context Current {@link Context} object.
	 */
	protected void execute(Context context) {
		logD("Request started");
		final long start = System.currentTimeMillis();
		final Bundle resultData = getTaggedBundle();
		resultData.putParcelable(KEY_COMMAND, this);
		send(DeviceHiveResultReceiver.MSG_EXECUTE_REQUEST, resultData);

		HttpUriRequest httpRequest = toHttpRequest();
		addHeaders(httpRequest);
		final UsernamePasswordCredentials creds = config.getBasicAuthorisation();
		if (creds != null) {
			addBasicAuthenticationHeader(httpRequest, creds);
		}

		try {
			HttpResponse responce = client.execute(httpRequest);
			final int statusCode = responce.getStatusLine().getStatusCode();
			String responceString = EntityUtils.toString(responce.getEntity());
			logD("Responce status code: " + statusCode);
			logD("Responce body: " + responceString);
			resultData.putInt(KEY_STATUS_CODE, statusCode);
			if (!isSuccessStatusCode(statusCode)) {
				logD("Responce code is not OK. Responce body: "
						+ responceString);
				send(DeviceHiveResultReceiver.MSG_STATUS_FAILURE, resultData);
			} else {
				final int resultCode = handleResponse(responceString,
						resultData, context);
				if (resultCode > 0) {
					send(resultCode, resultData);
				}
			}
		} catch (ClientProtocolException e) {
			Log.e(DeviceHive.TAG, "Failed to execute request: ", e);
			resultData.putSerializable(KEY_EXCEPTION, e);
			send(DeviceHiveResultReceiver.MSG_EXCEPTION, resultData);
		} catch (IOException e) {
			Log.e(DeviceHive.TAG, "Failed to execute request: ", e);
			resultData.putSerializable(KEY_EXCEPTION, e);
			send(DeviceHiveResultReceiver.MSG_EXCEPTION, resultData);
		} finally {
			send(DeviceHiveResultReceiver.MSG_COMPLETE_REQUEST, resultData);
			long end = System.currentTimeMillis();
			logD(String.format("Request to %s completed: ", getTargetUrl())
					+ (end - start) + "ms");
		}
	}

	private HttpUriRequest addBasicAuthenticationHeader(HttpUriRequest request,
			UsernamePasswordCredentials creds) {
		Header hdr = BasicScheme.authenticate(creds, "utf-8", false);
		request.addHeader(hdr);
		return request;
	}

	private void send(final int resultCode, Bundle resultData) {
		if (config != null && config.resultReceiver != null) {
			if (resultData == null) {
				resultData = getTaggedBundle();
			} else if (!resultData.containsKey(KEY_TAG)) {
				resultData.putString(KEY_TAG, getClass().getName());
			}
			config.resultReceiver.send(resultCode, resultData);
		}
	}

	private String getTargetUrl() {
		return config.baseUrl + "/" + getRequestPath();
	}

	private void addHeaders(HttpUriRequest request) {
		Map<String, String> headers = getHeaders();
		for (Map.Entry<String, String> header : headers.entrySet()) {
			request.addHeader(header.getKey(), header.getValue());
		}
	}

	private HttpUriRequest toHttpRequest() {
		String requestUrl = getTargetUrl();
		switch (getRequestType()) {
		case POST: {
			HttpPost request = new HttpPost(requestUrl);
			HttpEntity entity = getRequestEntity();
			if (entity != null) {
				request.setEntity(entity);
			}
			return request;
		}
		case GET: {
			return new HttpGet(requestUrl);
		}
		case PUT: {
			HttpPut request = new HttpPut(requestUrl);
			HttpEntity entity = getRequestEntity();
			if (entity != null) {
				request.setEntity(entity);
			}
			return request;
		}
		case DELETE: {
			return new HttpDelete(requestUrl);
		}
		default:
			logD("Unrecognized request type: " + getRequestType());
			return null;
		}
	}

	private static DefaultHttpClient getClient() {
		HttpParams params = getDefaultHttpParams();
		SchemeRegistry registry = getDefaultSchemeRegistry();

		ThreadSafeClientConnManager manager = new ThreadSafeClientConnManager(
				params, registry);

		return new DefaultHttpClient(manager, params);
	}

	private static SchemeRegistry getDefaultSchemeRegistry() {
		SchemeRegistry registry = new SchemeRegistry();
		registry.register(new Scheme("http", PlainSocketFactory
				.getSocketFactory(), 80));
		final SSLSocketFactory sslSocketFactory = SSLSocketFactory
				.getSocketFactory();
		sslSocketFactory
				.setHostnameVerifier(SSLSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
		registry.register(new Scheme("https", sslSocketFactory, 443));
		return registry;
	}

	private static HttpParams getDefaultHttpParams() {
		HttpParams params = new BasicHttpParams();
		HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
		HttpProtocolParams.setContentCharset(params, "utf-8");
		params.setBooleanParameter("http.protocol.expect-continue", false);
		HttpConnectionParams.setConnectionTimeout(params,
				DEFAULT_CONNECTION_TIMEOUT_MILLIS);
		HttpConnectionParams
				.setSoTimeout(params, DEFAULT_SOCKET_TIMEOUT_MILLIS);
		return params;
	}
	
	private void logD(final String message) {
		if (config.isDebugLoggingEnabled) {
			Log.d(DeviceHive.TAG, message);
		}
	}

	/**
	 * Get command tag from tagged {@link Bundle}.
	 * @param resultData Tagged {@link Bundle}.
	 * @return Command tag.
	 */
	public final static String getCommandTag(final Bundle resultData) {
		return resultData.getString(KEY_TAG);
	}

	/**
	 * Get {@link Throwable} from tagged {@link Bundle}.
	 * @param resultData Tagged {@link Bundle}.
	 * @return {@link Throwable} if any exceptions occur during command execution, otherwise return null.
	 */
	public final static Throwable getThrowable(final Bundle resultData) {
		return (Throwable) resultData.getSerializable(KEY_EXCEPTION);
	}

	/**
	 * Get {@link NetworkCommand} from tagged {@link Bundle}.
	 * @param resultData Tagged {@link Bundle}.
	 * @return Current {@link NetworkCommand} instance.
	 */
	public final static NetworkCommand getCommand(final Bundle resultData) {
		return (NetworkCommand) resultData.getParcelable(KEY_COMMAND);
	}

	/**
	 * Get status code from tagged {@link Bundle}.
	 * @param resultData Tagged {@link Bundle}.
	 * @return Status code.
	 */
	public final static int getStatusCode(final Bundle resultData) {
		return resultData.getInt(KEY_STATUS_CODE);
	}

}
