/*******************************************************************************
 * Copyright (c) 2010 Steffen Pingel and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Steffen Pingel - initial API and implementation
 *******************************************************************************/

package org.eclipse.mylyn.internal.commons.xmlrpc;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import java.util.regex.Pattern;

import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.auth.AuthScheme;
import org.apache.commons.httpclient.auth.NTLMScheme;
import org.apache.xmlrpc.XmlRpcException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.mylyn.commons.core.CoreUtil;
import org.eclipse.mylyn.commons.net.AuthenticationCredentials;
import org.eclipse.mylyn.commons.net.AuthenticationType;
import org.eclipse.mylyn.commons.net.Policy;
import org.eclipse.mylyn.commons.net.UnsupportedRequestException;

/**
 * @author Steffen Pingel
 */
public abstract class XmlRpcOperation<T> {

	private static final Pattern RPC_METHOD_NOT_FOUND_PATTERN = Pattern.compile("No such handler: "); //$NON-NLS-1$

	protected static final int XML_FAULT_GENERAL_ERROR = 1;

	protected static final int XML_FAULT_PERMISSION_DENIED = 403;

	private final CommonXmlRpcClient client;

	public XmlRpcOperation(CommonXmlRpcClient client) {
		this.client = client;
	}

	protected Object call(IProgressMonitor monitor, String method, Object... parameters) throws XmlRpcException {
		monitor = Policy.monitorFor(monitor);
		XmlRpcException lastException = null;
		for (int attempt = 0; attempt < 3; attempt++) {
//			if (!client.isProbed()) {
//				try {
//					probeAuthenticationScheme(monitor);
//				} finally {
//					client.setProbed(true);
//				}
//			}

			try {
				return executeCall(monitor, method, parameters);
			} catch (XmlRpcLoginException e) {
				try {
					client.getLocation().requestCredentials(AuthenticationType.REPOSITORY, null, monitor);
				} catch (UnsupportedRequestException ignored) {
					throw e;
				}
				lastException = e;
			} catch (XmlRpcPermissionDeniedException e) {
				try {
					client.getLocation().requestCredentials(AuthenticationType.REPOSITORY, null, monitor);
				} catch (UnsupportedRequestException ignored) {
					throw e;
				}
				lastException = e;
			} catch (XmlRpcProxyAuthenticationException e) {
				try {
					client.getLocation().requestCredentials(AuthenticationType.PROXY, null, monitor);
				} catch (UnsupportedRequestException ignored) {
					throw e;
				}
				lastException = e;
			} catch (XmlRpcSslCertificateException e) {
				try {
					client.getLocation().requestCredentials(AuthenticationType.CERTIFICATE, null, monitor);
				} catch (UnsupportedRequestException ignored) {
					throw e;
				}
				lastException = e;
			}
		}
		if (lastException != null) {
			throw lastException;
		} else {
			// this path should never be reached
			throw new IllegalStateException();
		}
	}

	private void checkForException(Object result) throws NumberFormatException, XmlRpcException {
		if (result instanceof Map<?, ?>) {
			Map<?, ?> exceptionData = (Map<?, ?>) result;
			if (exceptionData.containsKey("faultCode") && exceptionData.containsKey("faultString")) { //$NON-NLS-1$ //$NON-NLS-2$ 
				throw new XmlRpcException(Integer.parseInt(exceptionData.get("faultCode").toString()), //$NON-NLS-1$
						(String) exceptionData.get("faultString")); //$NON-NLS-1$
			} else if (exceptionData.containsKey("title")) { //$NON-NLS-1$
				String message = (String) exceptionData.get("title"); //$NON-NLS-1$
				String detail = (String) exceptionData.get("_message"); //$NON-NLS-1$
				if (detail != null) {
					message += ": " + detail; //$NON-NLS-1$
				}
				throw new XmlRpcException(XML_FAULT_GENERAL_ERROR, message);
			}
		}
	}

	protected boolean credentialsValid(AuthenticationCredentials credentials) {
		return credentials != null;
	}

	public abstract T execute() throws XmlRpcException;

	protected Object executeCall(IProgressMonitor monitor, String method, Object... parameters) throws XmlRpcException {
		try {
			if (CommonXmlRpcClient.DEBUG_XMLRPC) {
				System.err.println("Calling " + client.getLocation().getUrl() + ": " + method + " " + CoreUtil.toString(parameters)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			}

			AuthenticationCredentials credentials = client.updateCredentials();
			XmlRpcClientRequest request = new XmlRpcClientRequest(client.getClient().getClientConfig(),
					getXmlRpcUrl(credentials), method, parameters, monitor);
			return client.getClient().execute(request);
		} catch (XmlRpcHttpException e) {
			handleAuthenticationException(e.code, e.getAuthScheme());
			// if not handled, re-throw exception
			throw e;
		} catch (XmlRpcIllegalContentTypeException e) {
			throw e;
		} catch (XmlRpcException e) {
			// XXX work-around for http://trac-hacks.org/ticket/5848 
			if ("XML_RPC privileges are required to perform this operation".equals(e.getMessage()) //$NON-NLS-1$
					|| e.code == XML_FAULT_PERMISSION_DENIED) {
				handleAuthenticationException(HttpStatus.SC_FORBIDDEN, null);
				// should never happen as call above should always throw an exception
				throw new XmlRpcRemoteException(e);
			} else if (isNoSuchMethodException(e)) {
				throw new XmlRpcNoSuchMethodException(e);
			} else {
				throw new XmlRpcRemoteException(e);
			}
		} catch (OperationCanceledException e) {
			throw e;
		} catch (Exception e) {
			throw new XmlRpcException("Unexpected exception", e); //$NON-NLS-1$
		}
	}

	protected final CommonXmlRpcClient getClient() {
		return client;
	}

	protected Object getMultiCallResult(Object item) {
		return ((Object[]) item)[0];
	}

	protected URL getXmlRpcUrl(AuthenticationCredentials credentials) {
		try {
			return new URL(client.getLocation().getUrl());
		} catch (MalformedURLException e) {
			throw new RuntimeException("Encoding of URL failed", e); //$NON-NLS-1$
		}
	}

	protected boolean handleAuthenticationException(int code, AuthScheme authScheme) throws XmlRpcException {
		if (code == HttpStatus.SC_UNAUTHORIZED) {
			if (CommonXmlRpcClient.DEBUG_AUTH) {
				System.err.println(client.getLocation().getUrl() + ": Unauthorized (" + code + ")"); //$NON-NLS-1$ //$NON-NLS-2$ 
			}
			client.digestScheme = null;
			XmlRpcLoginException exception = new XmlRpcLoginException();
			exception.setNtlmAuthRequested(authScheme instanceof NTLMScheme);
			throw exception;
		} else if (code == HttpStatus.SC_FORBIDDEN) {
			if (CommonXmlRpcClient.DEBUG_AUTH) {
				System.err.println(client.getLocation().getUrl() + ": Forbidden (" + code + ")"); //$NON-NLS-1$ //$NON-NLS-2$ 
			}
			client.digestScheme = null;
			throw new XmlRpcPermissionDeniedException();
		} else if (code == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) {
			if (CommonXmlRpcClient.DEBUG_AUTH) {
				System.err.println(client.getLocation().getUrl() + ": Proxy authentication required (" + code + ")"); //$NON-NLS-1$ //$NON-NLS-2$ 
			}
			throw new XmlRpcProxyAuthenticationException();
		}
		return false;
	}

	protected boolean isNoSuchMethodException(XmlRpcException e) {
		if (RPC_METHOD_NOT_FOUND_PATTERN.matcher(e.getMessage()).find()) {
			return true;
		}
		return false;
	}

//	protected Object[] multicall(IProgressMonitor monitor, Map<String, Object>... calls) throws XmlRpcException {
//		Object[] result = (Object[]) call(monitor, "system.multicall", new Object[] { calls }); //$NON-NLS-1$
//		for (Object item : result) {
//			checkForException(item);
//		}
//		return result;
//	}

	protected MulticallResult call(IProgressMonitor monitor, Multicall call) throws XmlRpcException {
		Object[] response = (Object[]) call(monitor, "system.multicall", new Object[] { call.getCalls() }); //$NON-NLS-1$
		for (Object item : response) {
			checkForException(item);
		}
		return new MulticallResult(response);
	}

}