/**
 * Copyright (c) 2011, Martin Foster <martin@ethereal-realms.org>
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 
 *     * Redistributions of source code must retain the above copyright notice, 
 *       this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright 
 *       notice, this list of conditions and the following disclaimer in the 
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the Ethereal-Realms.org nor the names of its
 *       contributors may be used to endorse or promote products derived
 *       from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.lyra.Widgets;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;

import org.lyra.Ledger;
import org.lyra.Operations;


/**
 * Wrapper method for built-in Java commands that enable a one-method use for
 * calling and retrieving documents over the HTTP protocol. Requires an 
 * instance of Ledger and a language map in order to track requests being made.
 * All errors are handled internally, as a result it becomes necessary to check
 * for non-null string returns in order to ensure that the request was proper.
 * <p>
 * @author Martin Foster
 * @version 1.0.4, %Revision, 147%, %LastChangedDate, 06/11/11 4:57PM%
 * @since 1.0.1
 */
public class HTTPClient {
	
	/*
	 * PACKAGE INSTANCES
	 */
	private Ledger Logger;
	
	private Map<String,String> Lang;
	
	/*
	 * CLASS INSTANCES
	 */
	
	// File operations
	private Operations Ops;
	
	/*
	 * CONSTANTS
	 */
	private static final String CLASS = "org.lyra.Widgets.HTTPClient";
	private static final String CLIENT = "Lyra";
	
	/**
	 * Encoding scheme used for HTTP sessions. 
	 */
	public static final String ENCODING = "UTF-8";
	
	
	
	/**
	 * Base constructor for class.
	 * <p>  
	 * @param logger Ledger instance for application logging.
	 * @param lang Language instance for error codes.
	 */
	public HTTPClient(Ledger logger, Map<String,String> lang) {
		
		// Pass up
		this.Logger = logger;
		this.Lang   = lang;
		
		// Create Instance
		this.Ops = new Operations(Logger);
		
	}
	// End of Constructor Method
	
	
	/**
	 * General method which handles both POST and GET requests based on a
	 * simple switch.   
	 * <p>
	 * @param address Formatted URL address.
	 * @param args Key/Pair map containing all the arguments to pass on.
	 * @param post Boolean indicating if we treat this as a POST or GET request.
	 * @param dest Destination file name.
	 * @return String document containing the reply.
	 */
	private String fetchCommon(String address, Map<String,String> args, boolean post, File dest) {
	
		/*
		 * There are a lot of areas where this code can just go wrong.  So we
		 * simply wrap everything and log as necessary.
		 */
		try {
			
			/*
			 * The first step in a post element is to format all of the arguments
			 * into a usable parameter line.   Then we pass it on.
			 */
			StringBuffer sb_post = new StringBuffer();
			
			// Loop through arguments
			for (String key: args.keySet()) {
				
				// Only append if this is a later entry
				if (sb_post.length() > 0)
					sb_post.append("&");
				
				// Encode the parameter sequence similar to what is seen in a get
				sb_post.append(URLEncoder.encode(key, ENCODING));
				sb_post.append("=");
				sb_post.append(URLEncoder.encode(args.get(key), ENCODING));
			}


			/*
			 * We now establish a connection and establish certain behavioural
			 * attributes that we come to expect.  One of the features will be a
			 * custom User-Agent string to identify our application when interfacing
			 * with another.
			 */
			
			// GET specific
			if ((!post) 
			 && (sb_post.length() > 0)) {
			
				// Append to address
				address += "?" + sb_post.toString();

				// Log event
				Logger.Info(
					CLASS, 
					"fetchCommon",
					Lang.get("Fetch.Request.GET")
						.replaceAll("MURL", address)
				  );
			}
			
			URL link = new URL(address);
		    HttpURLConnection conn = (HttpURLConnection)link.openConnection();
		    conn.setRequestProperty("User-Agent", 
		    		CLIENT + " (os:" +
		    		System.getProperty("os.arch") + " " +
		    		System.getProperty("os.name") + " " + 
		    		System.getProperty("os.version") + "; jre:" + 
		    		System.getProperty("java.vm.version") + ")"
		       );
		   

		    // Explicit acknowledgement of input
		    conn.setDoInput(true);
		   
		    // POST specific section
		    if ((post)
		     && (sb_post.length() > 0)){
		    	
			    /*
			     * We output the parameters in a post method.  These have already been
			     * generated, so its a simple case of opening the Output connection
			     * and passing on the completed string.
			     */
		    	
		    	// Explicit requirement of input
			    conn.setDoOutput(true);
			    
			    // Open handle and send out parameters
			    PrintWriter pout = new PrintWriter(new OutputStreamWriter(
			    		conn.getOutputStream(), ENCODING), true);
			    pout.print(sb_post.toString());
			    pout.flush();
			    
			    
				// Log event
				Logger.Info(
					CLASS, 
					"fetchCommon",
					Lang.get("Fetch.Request.POST")
						.replaceAll("MURL", address)
						.replaceAll("MPARAM", sb_post.toString())
				  );
		    }
		    
		    
		    /*
		     * Now that the we passed on our information, the host will have time
		     * to process and reply.  Now we need to extract the information and
		     * make sure we are good to go.
		     */
		    if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
		    	
		    	// Extract user input
			    InputStream theData = conn.getInputStream();


			    /*
			     * Standard reply, this will return a text element that will
			     * be simply collected and passed on to the user.  
			     */
			    if ((dest == null) 
			     && (conn.getContentType().toLowerCase().startsWith("text"))) {
			    	
				      // Collections
				      BufferedReader pin = new BufferedReader(new InputStreamReader(theData, ENCODING));
				      String line;
					  StringBuffer sb_input = new StringBuffer();
				      
					  // Loop through and collect
				      while (( line = pin.readLine()) != null) 
				    	  sb_input.append(line);
				      

				    	  
				      // Log event
				      Logger.Fine(
				    	   CLASS, 
				    	   "fetchCommon",
							Lang.get("Fetch.Request.Reply")
								.replaceAll(
									"MBODY", 
									Matcher.quoteReplacement(sb_input.toString())
								  )
					    ); 

				      // Return document
				      return sb_input.toString();
			    }
			    
			    /*
			     * These elements return information that will be saved
			     * directly to a designated filename.
			     */
			    else if ((dest != null) 
			      && (conn.getContentType().toLowerCase().startsWith("image"))) {

			    	// Safety
			    	if (dest.isDirectory()) return null;
			    	
			    	// The file either exists or can be created
			    	if ((dest.isFile())
			    	 || ((!dest.exists())
			    	  && (dest.createNewFile()))) {
			    		
			    		// Write out to file
			    		Ops.fileWrite(theData, dest);

			    		// Return proper filename
			    		return dest.getName();
			    	}
			    }
			    
			    /*
			     * We should warn the user about any instance that does not
			     * return a TEXT/* or an IMAGE/* data type. 
			     */
			    else {
			    		
					// Log event
					Logger.Warn(
						CLASS, 
						"fetchCommon",
						Lang.get("Fetch.Error.Content")
							.replaceAll("MTYPE", conn.getContentType())
					  );
			    }
		    }
		    
		    /*
		     * If we do not get the code we want, the user should be notified
		     * prior to finishing up.
		     */
		    else {
		    	
				// Log event
				Logger.Warn(
					CLASS, 
					"fetchCommon",
					Lang.get("Fetch.Error.Return")
						.replaceAll("MCODE", Integer.toString(conn.getResponseCode()))
				  );
		    }
		}
		
		// Catches errors in the URL
		catch (MalformedURLException e) {
			
			// Log event
			Logger.Warn(
				CLASS, 
				"fetchCommon",
				Lang.get("Fetch.Error.Address")
					.replaceAll("MERR", e.toString())
			  );
		}
		
		// Catches IO errors and Encoding errors
		catch (IOException e) {
			
			// Log event
			Logger.Warn(
				CLASS, 
				"fetchCommon",
				Lang.get("Fetch.Error.InputOutput")
					.replaceAll("MERR", e.toString())
			  );
		}

		// If all else fails return null
		return null;
		
	}
	// End of fetchCommon Method
	
	
	/**
	 * Makes a POST specific request to a host, using a Map<String,String>
	 * collection as a key/pair store.
	 * <p>
	 * @param address Formatted URL address.
	 * @param args Key/Pair map containing all the arguments to pass on.
	 * @return String document containing the reply.
	 */
	public String fetchPost(String address, Map<String,String> args) {
		
		// Straight pass through
		return fetchCommon(address, args, true, null);
	}
	// End of fetchPost Method

	
	/**
	 * Makes a GET specific request to a host, using a Map<String,String>
	 * collection as a key/pair store.
	 * <p>
	 * @param address Formatted URL address.
	 * @param args Key/Pair map containing all the arguments to pass on.
	 * @return String document containing the reply.
	 */
	public String fetchGet(String address, Map<String,String> args) {
		
		// Straight pass through
		return fetchCommon(address, args, false, null);
	}
	// End of fetchPost Method

	
	/**
	 * Makes a GET specific request to a host. Since the element returned is an
	 * image the information is saved directly to a file.
	 * <p>
	 * @param address Formatted URL address.
	 * @param dest Destination File instance.
	 * @return String document containing the reply.
	 */
	public String fetchImage(String address, File dest) {
		
		// Straight pass through
		return fetchCommon(
				address, 
				new HashMap<String,String>(), 
				false, 
				dest
			  );
	}
	// End of fetchPost Method
}
