package ca.uhn.fhir.rest.server.exceptions;

import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import ca.uhn.fhir.model.base.resource.BaseOperationOutcome;

/*
 * #%L
 * HAPI FHIR - Core Library
 * %%
 * Copyright (C) 2014 University Health Network
 * %%
 * 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.
 * #L%
 */

/**
 * Base class for RESTful client and server exceptions. RESTful client methods will only throw exceptions which are subclasses of this exception type, and RESTful server methods should also only call
 * subclasses of this exception type.
 */
public abstract class BaseServerResponseException extends RuntimeException {

	private static final Map<Integer, Class<? extends BaseServerResponseException>> ourStatusCodeToExceptionType = new HashMap<Integer, Class<? extends BaseServerResponseException>>();
	private static final long serialVersionUID = 1L;

	static {
		registerExceptionType(AuthenticationException.STATUS_CODE, AuthenticationException.class);
		registerExceptionType(InternalErrorException.STATUS_CODE, InternalErrorException.class);
		registerExceptionType(InvalidRequestException.STATUS_CODE, InvalidRequestException.class);
		registerExceptionType(MethodNotAllowedException.STATUS_CODE, MethodNotAllowedException.class);
		registerExceptionType(NotImplementedOperationException.STATUS_CODE, NotImplementedOperationException.class);
		registerExceptionType(NotModifiedException.STATUS_CODE, NotModifiedException.class);
		registerExceptionType(ResourceNotFoundException.STATUS_CODE, ResourceNotFoundException.class);
		registerExceptionType(ResourceGoneException.STATUS_CODE, ResourceGoneException.class);
		registerExceptionType(PreconditionFailedException.STATUS_CODE, PreconditionFailedException.class);
		registerExceptionType(ResourceVersionConflictException.STATUS_CODE, ResourceVersionConflictException.class);
		registerExceptionType(UnprocessableEntityException.STATUS_CODE, UnprocessableEntityException.class);
	}

	private List<String> myAdditionalMessages = null;
	private BaseOperationOutcome myBaseOperationOutcome;
	private String myResponseBody;
	private String myResponseMimeType;
	private int myStatusCode;

	/**
	 * Constructor
	 * 
	 * @param theStatusCode
	 *            The HTTP status code corresponding to this problem
	 * @param theMessage
	 *            The message
	 */
	public BaseServerResponseException(int theStatusCode, String theMessage) {
		super(theMessage);
		myStatusCode = theStatusCode;
		myBaseOperationOutcome = null;
	}

	/**
	 * Constructor
	 * 
	 * @param theStatusCode
	 *            The HTTP status code corresponding to this problem
	 * @param theMessages
	 *            The messages
	 */
	public BaseServerResponseException(int theStatusCode, String... theMessages) {
		super(theMessages != null && theMessages.length > 0 ? theMessages[0] : null);
		myStatusCode = theStatusCode;
		myBaseOperationOutcome = null;
		if (theMessages != null && theMessages.length > 1) {
			myAdditionalMessages = Arrays.asList(Arrays.copyOfRange(theMessages, 1, theMessages.length, String[].class));
		}
	}

	/**
	 * Constructor
	 * 
	 * @param theStatusCode
	 *            The HTTP status code corresponding to this problem
	 * @param theMessage
	 *            The message
	 * @param theBaseOperationOutcome
	 *            An BaseOperationOutcome resource to return to the calling client (in a server) or the BaseOperationOutcome
	 *            that was returned from the server (in a client)
	 */
	public BaseServerResponseException(int theStatusCode, String theMessage, BaseOperationOutcome theBaseOperationOutcome) {
		super(theMessage);
		myStatusCode = theStatusCode;
		myBaseOperationOutcome = theBaseOperationOutcome;
	}

	/**
	 * Constructor
	 * 
	 * @param theStatusCode
	 *            The HTTP status code corresponding to this problem
	 * @param theMessage
	 *            The message
	 * @param theCause
	 *            The cause
	 */
	public BaseServerResponseException(int theStatusCode, String theMessage, Throwable theCause) {
		super(theMessage, theCause);
		myStatusCode = theStatusCode;
		myBaseOperationOutcome = null;
	}

	/**
	 * Constructor
	 * 
	 * @param theStatusCode
	 *            The HTTP status code corresponding to this problem
	 * @param theMessage
	 *            The message
	 * @param theCause
	 *            The underlying cause exception
	 * @param theBaseOperationOutcome
	 *            An BaseOperationOutcome resource to return to the calling client (in a server) or the BaseOperationOutcome
	 *            that was returned from the server (in a client)
	 */
	public BaseServerResponseException(int theStatusCode, String theMessage, Throwable theCause, BaseOperationOutcome theBaseOperationOutcome) {
		super(theMessage, theCause);
		myStatusCode = theStatusCode;
		myBaseOperationOutcome = theBaseOperationOutcome;
	}

	/**
	 * Constructor
	 * 
	 * @param theStatusCode
	 *            The HTTP status code corresponding to this problem
	 * @param theCause
	 *            The underlying cause exception
	 */
	public BaseServerResponseException(int theStatusCode, Throwable theCause) {
		super(theCause.toString(), theCause);
		myStatusCode = theStatusCode;
		myBaseOperationOutcome = null;
	}

	/**
	 * Constructor
	 * 
	 * @param theStatusCode
	 *            The HTTP status code corresponding to this problem
	 * @param theCause
	 *            The underlying cause exception
	 * @param theBaseOperationOutcome
	 *            An BaseOperationOutcome resource to return to the calling client (in a server) or the BaseOperationOutcome
	 *            that was returned from the server (in a client)
	 */
	public BaseServerResponseException(int theStatusCode, Throwable theCause, BaseOperationOutcome theBaseOperationOutcome) {
		super(theCause.toString(), theCause);
		myStatusCode = theStatusCode;
		myBaseOperationOutcome = theBaseOperationOutcome;
	}

	public List<String> getAdditionalMessages() {
		return myAdditionalMessages;
	}

	/**
	 * Returns the {@link BaseOperationOutcome} resource if any which was supplied in the response, or <code>null</code>
	 */
	public BaseOperationOutcome getOperationOutcome() {
		return myBaseOperationOutcome;
	}

	/**
	 * In a RESTful client, this method will be populated with the body of the HTTP respone if one was provided by the server, or <code>null</code> otherwise.
	 * <p>
	 * In a restful server, this method is currently ignored.
	 * </p>
	 */
	public String getResponseBody() {
		return myResponseBody;
	}

	/**
	 * In a RESTful client, this method will be populated with the HTTP status code that was returned with the HTTP response.
	 * <p>
	 * In a restful server, this method is currently ignored.
	 * </p>
	 */
	public String getResponseMimeType() {
		return myResponseMimeType;
	}

	/**
	 * Returns the HTTP status code corresponding to this problem
	 */
	public int getStatusCode() {
		return myStatusCode;
	}

	/**
	 * Sets the BaseOperationOutcome resource associated with this exception. In server
	 * implementations, this is the OperartionOutcome resource to include with the HTTP response. In 
	 * client implementations you should not call this method.
	 * 
	 * @param theBaseOperationOutcome The BaseOperationOutcome resource
	 * Sets the BaseOperationOutcome resource associated with this exception. In server implementations, this is the OperartionOutcome resource to include with the HTTP response. In client
	 * implementations you should not call this method.
	 */
	public void setOperationOutcome(BaseOperationOutcome theBaseOperationOutcome) {
		myBaseOperationOutcome = theBaseOperationOutcome;
	}

	/**
	 * This method is currently only called internally by HAPI, it should not be called by user code.
	 */
	public void setResponseBody(String theResponseBody) {
		myResponseBody = theResponseBody;
	}

	/**
	 * This method is currently only called internally by HAPI, it should not be called by user code.
	 */
	public void setResponseMimeType(String theResponseMimeType) {
		myResponseMimeType = theResponseMimeType;
	}

	/**
	 * For unit tests only
	 */
	static boolean isExceptionTypeRegistered(Class<?> theType) {
		return ourStatusCodeToExceptionType.values().contains(theType);
	}

	public static BaseServerResponseException newInstance(int theStatusCode, String theMessage) {
		if (ourStatusCodeToExceptionType.containsKey(theStatusCode)) {
			try {
				return ourStatusCodeToExceptionType.get(theStatusCode).getConstructor(new Class[] { String.class }).newInstance(theMessage);
			} catch (InstantiationException e) {
				throw new InternalErrorException(e);
			} catch (IllegalAccessException e) {
				throw new InternalErrorException(e);
			} catch (IllegalArgumentException e) {
				throw new InternalErrorException(e);
			} catch (InvocationTargetException e) {
				throw new InternalErrorException(e);
			} catch (NoSuchMethodException e) {
				throw new InternalErrorException(e);
			} catch (SecurityException e) {
				throw new InternalErrorException(e);
			}
		} else {
			return new UnclassifiedServerFailureException(theStatusCode, theMessage);
		}
	}

	static void registerExceptionType(int theStatusCode, Class<? extends BaseServerResponseException> theType) {
		if (ourStatusCodeToExceptionType.containsKey(theStatusCode)) {
			throw new Error("Can not register " + theType + " to status code " + theStatusCode + " because " + ourStatusCodeToExceptionType.get(theStatusCode) + " already registers that code");
		}
		ourStatusCodeToExceptionType.put(theStatusCode, theType);
	}

}
