package com.lonepulse.zombielink.request;

/*
 * #%L
 * ZombieLink
 * %%
 * Copyright (C) 2013 - 2014 Lonepulse
 * %%
 * 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%
 */

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.http.HttpHeaders;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.ContentType;
import org.apache.http.message.BasicNameValuePair;

import com.lonepulse.zombielink.annotation.FormParam;
import com.lonepulse.zombielink.annotation.FormParams;
import com.lonepulse.zombielink.annotation.Param;
import com.lonepulse.zombielink.proxy.InvocationContext;
import com.lonepulse.zombielink.util.Metadata;

/**
 * <p>This is a concrete implementation of {@link AbstractRequestProcessor} which discovers <b>form 
 * parameters</b> in a request which are annotated with @{@link FormParam} or @{@link FormParams} and 
 * constructs a a list of <a href="http://en.wikipedia.org/wiki/POST_(HTTP)#Use_for_submitting_web_forms"> 
 * form-urlencoded</a> <b>name-value</b> pairs which will be sent in the request body.</p> 
 * 
 * <p>The @{@link FormParam} annotation should be used on an implementation of {@link CharSequence} which 
 * provides the <i>value</i> for each <i>name-value</i> pair; and the supplied {@link FormParam#value()} 
 * provides the <i>name</i>.</p>
 * 
 * <p>The @{@link FormParams} annotation should be used on a {@code Map<CharSequence, CharSequence>} of 
 * name and value pairs.</p>
 * 
 * @version 1.3.0
 * <br><br>
 * @since 1.3.0
 * <br><br>
 * @author <a href="http://sahan.me">Lahiru Sahan Jayasinghe</a>
 */
class FormParamProcessor extends AbstractRequestProcessor {

	
	/**
	 * <p>Accepts the {@link InvocationContext} with an {@link HttpEntityEnclosingRequestBase} and 
	 * creates a list of <a href="http://en.wikipedia.org/wiki/POST_(HTTP)#Use_for_submitting_web_forms">
	 * form-urlencoded</a> name-value pairs using arguments annotated with @{@link FormParam} and 
	 * @{@link FormParams}. It's then inserted to the body of the request being processed.</p>
	 * 
	 * <p><b>Note</b> that any {@link HttpRequestBase}s which aren't {@link HttpEntityEnclosingRequestBase}s 
	 * will be ignored.</p>
	 * 
	 * <p>See {@link AbstractRequestProcessor#process(InvocationContext, HttpRequestBase)}.</p>
	 * 
	 * @param context
	 * 			the {@link InvocationContext} which is used to discover any annotated form parameters 
	 * <br><br>
	 * @param request
	 * 			prefers an instance of {@link HttpPost} so as to conform with HTTP 1.1; however, other  
	 * 			{@link HttpEntityEnclosingRequestBase}s will be entertained to allow compliance with 
	 * 			unusual endpoint definitions (as long as they are {@link HttpEntityEnclosingRequestBase}s) 
	 * <br><br>
 	 * @return the same instance of {@link HttpRequestBase} which was given for processing form parameters 
	 * <br><br>
	 * @throws RequestProcessorException
	 * 			if a form parameters failed to be created and inserted into the request body
	 * <br><br>
	 * @since 1.3.0
	 */
	@Override
	protected HttpRequestBase process(InvocationContext context, HttpRequestBase request) {

		try {
			
			if(request instanceof HttpEntityEnclosingRequestBase) {
				
				List<NameValuePair> nameValuePairs = new LinkedList<NameValuePair>();
				
				//add static name and value pairs
				List<Param> constantFormParams = RequestUtils.findStaticFormParams(context);
				
				for (Param param : constantFormParams) {
					
					nameValuePairs.add(new BasicNameValuePair(param.name(), param.value()));
				}
				
				//add individual name and value pairs
				List<Entry<FormParam, Object>> formParams = Metadata.onParams(FormParam.class, context);
				
				for (Entry<FormParam, Object> entry : formParams) {
					
					String name = entry.getKey().value();
					Object value = entry.getValue();
					
					if(!(value instanceof CharSequence)) {
						
						StringBuilder errorContext = new StringBuilder()
						.append("Form (url-encoded) parameters can only be of type ")
						.append(CharSequence.class.getName())
						.append(". Please consider implementing CharSequence ")
						.append("and providing a meaningful toString() representation for the ")
						.append("<name> of the form parameter. ");
						
						throw new RequestProcessorException(new IllegalArgumentException(errorContext.toString()));
					}
					
					nameValuePairs.add(new BasicNameValuePair(name, String.valueOf(value)));
				}
				
				//add batch name and value pairs (along with any static params)
				List<Entry<FormParams, Object>> queryParamMaps = Metadata.onParams(FormParams.class, context);
				
				for (Entry<FormParams, Object> entry : queryParamMaps) {
					
					Param[] constantParams = entry.getKey().value();
					
					if(constantParams != null && constantParams.length > 0) {
					
						for (Param param : constantParams) {
							
							nameValuePairs.add(new BasicNameValuePair(param.name(), param.value()));
						}
					}
					
					Object map = entry.getValue();
					
					if(!(map instanceof Map)) {
					
						StringBuilder errorContext = new StringBuilder()
						.append("@FormParams can only be applied on <java.util.Map>s. ")
						.append("Please refactor the method to provide a Map of name and value pairs. ");
						
						throw new RequestProcessorException(new IllegalArgumentException(errorContext.toString()));
					}
					
					Map<?, ?> nameAndValues = (Map<?, ?>)map;
					
					for (Entry<?, ?> nameAndValue : nameAndValues.entrySet()) {
						
						Object name = nameAndValue.getKey();
						Object value = nameAndValue.getValue();
						
						if(!(name instanceof CharSequence && 
							(value instanceof CharSequence || value instanceof Collection))) {
							
							StringBuilder errorContext = new StringBuilder()
							.append("The <java.util.Map> identified by @FormParams can only contain mappings of type ")
							.append("<java.lang.CharSequence, java.lang.CharSequence> or ")
							.append("<java.lang.CharSequence, java.util.Collection<? extends CharSequence>>");
							
							throw new RequestProcessorException(new IllegalArgumentException(errorContext.toString()));
						}
						
						if(value instanceof CharSequence) {
						
							nameValuePairs.add(new BasicNameValuePair(
								((CharSequence)name).toString(), ((CharSequence)value).toString()));
						}
						else { //add multi-valued form params 
							
							Collection<?> multivalues = (Collection<?>) value;
							
							for (Object multivalue : multivalues) {
								
								if(!(multivalue instanceof CharSequence)) {
									
									StringBuilder errorContext = new StringBuilder()
									.append("Values for the <java.util.Map> identified by @FormParams can only contain collections ")
									.append("of type java.util.Collection<? extends CharSequence>");
									
									throw new RequestProcessorException(new IllegalArgumentException(errorContext.toString()));
								}
								
								nameValuePairs.add(new BasicNameValuePair(
										((CharSequence)name).toString(), ((CharSequence)multivalue).toString()));
							}
						}
					}
				}
				
				UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(nameValuePairs);
				urlEncodedFormEntity.setContentType(ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
	
				request.setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
				((HttpEntityEnclosingRequestBase)request).setEntity(new UrlEncodedFormEntity(nameValuePairs));
			}
			
			return request;
		}
		catch(Exception e) {
			
			throw (e instanceof RequestProcessorException)? 
					(RequestProcessorException)e :new RequestProcessorException(context, getClass(), e);
		}
	}
}
