/**
 * 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.LastDotFm;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Map;

import org.lyra.Ledger;
import org.lyra.Widgets.HTTPClient;


/**
 * The Sender class provides the ability to send requests to the Last.fm
 * service and retrieve contents.    All that is required to make a request
 * is a Map<String, String> element that contains all of the parameters and
 * values necessary to make the request.
 * <p>
 * Requests can be made in one of two variants:
 * <p>
 * <b>Interactive</b>.   Permits the user to make a request and then retrieve
 * the contents returned from the Last.fm service.   These will be the most
 * common elements used in integration.
 * <p>
 * <code>
 * 		Map<String,String> param = new HashMap<String,String>(){
 *         {
 *	        	put("method","album.getInfo");
 *	        	put("album" ,"The Baroness");
 *         	    put("artist" ,"Sarah Slean");
 *  	        put("autocorrect" ,"1");
 *   	        put("api_key" ,"b25b959554ed76058ac220b7b2e0a026");
 *	        }
 *		  };
 * 
 *     Sender requests = new Sender(logger, global, lang);
 *     String xmlresponse = requests.exsecuteParameters(param);
 *     
 *     ...
 * </code>
 * <p>
 * <b>Queued.</b>  Permits a developer to queue requests and have them 
 * committed within a period of time.    Since we limit the speed at 
 * which the requests are made this should prevent going beyond their terms
 * of service while reducing code.
 * <p>
 * <code>
 * 		Map<String,String> param = new HashMap<String,String>(){
 *         {
 *	        	put("method","album.getInfo");
 *	        	put("album" ,"The Baroness");
 *         	    put("artist" ,"Sarah Slean");
 *  	        put("autocorrect" ,"1");
 *   	        put("api_key" ,"b25b959554ed76058ac220b7b2e0a026");
 *	        }
 *		  };
 * 
 *     Sender requests = new Sender(logger, global, lang);
 *     requests.addParameters(param);
 *     ...
 *     ...
 *     requests.addParameters(param);
 * </code>
 * <p>
 * <b>NOTE:</b> The above examples never return an output.  Useful for playlist
 * elements such as playlist generation.
 * <p>
 * @author Martin Foster
 * @version 1.0.4, %Revision, 92%, %LastChangedDate, 06/03/11 10:46PM%
 * @since 1.0.1
 */
public class Sender implements Runnable{

	/*
	 * PACKAGE INSTANCES
	 */
	private Ledger Logger;
	
	private Map<String,String> Global;
	private Map<String,String> Lang;
	
	/*
	 * CLASS INSTANCES
	 */
	
	// Collection queue for requests
	private LinkedList<Map<String,String>> Queue =
		new LinkedList<Map<String,String>>();
	
	// Collection of start times to average
	private LinkedList<Long> Span =
		new LinkedList<Long>();
	
	// Queue handler
	private Thread myThread = null;
	
	// Are we done processing
	private boolean processDone = true;
	
	// Request Class
	private HTTPClient Caller;
	
	/*
	 * CONSTANTS
	 */
	private static final String CLASS    = "org.lyra.LastDotFm.Sender";	
	private static final String DIGEST   = "MD5";


	/**
	 * Maximum amount of requests that can be made within a second according
	 * to the terms of service for Last.fm.
	 */
	private static final int THROTTLE = 5;
	private static final long PERIOD  = 1000;
	
	
	/**
	 * Base constructor for the class.
	 * <p>
	 * @param logger Ledger instance for record processing.
	 * @param global Global settings map.
	 * @param lang Language map for multiple language support.
	 */
	public Sender(Ledger logger, Map<String,String> global, Map<String,String> lang) {
	
		// Pass up
		this.Logger = logger;
		this.Lang   = lang;
		this.Global = global;
		
		// Create instance
		this.Caller = new HTTPClient(Logger, Lang);
	}
	// End of Constructor Method
	
	
	/**
	 * Inserts an element into the Span queue.  This will permit us to
	 * calculate the distance between the first and fist element in order to
	 * throttle the request queue as necessary.
	 */
	private void addSpan() {
		
		// Simple pass through for defaults
		addSpan(System.currentTimeMillis());
	}
	// End of addSpan Method
	
	
	/**
	 * Inserts an element into the Span queue.  This will permit us to
	 * calculate the distance between the first and fist element in order to
	 * throttle the request queue as necessary.
	 * <p>
	 * @param process Long unit-time element.
	 */
	private synchronized void addSpan(Long process) {
		
		// Add in the element
		Span.add(process);
		
		// Drop off the first element if necessary
		if (Span.size() > THROTTLE)
			Span.removeFirst();
	}
	// End of addSpan Method
	
	
	/**
	 * Performs the necessary calculations to make sure that our queued
	 * parameter handling is within the limits of the terms of service. This
	 * will also consider interactive requests which are not queued nor are
	 * they paused.
	 * <p>
	 * @return Long containing wait delay.
	 */
	private synchronized Long getSpan() {
		
		/*
		 * We need at least two elements for the elements above to actually
		 * work.  So lets make sure that happens.
		 */
		if (Span.size() < 2)
			return (long)0;
		
		/*
		 * This simple calculation will tell us exactly how many operations
		 * that have taken place between the first and last request and will
		 * permit us to delay the next queued request until it meets the Last.fm
		 * terms of service.
		 */
		long span = Span.getLast() - Span.getFirst();
		
		// No need to wait
		if (span > PERIOD)
			return (long)0;
		
		// Delay until we meet requirements
		return PERIOD - span;
	}
	// End of getSpan Method
	
	
	/**
	 * Permits us to add in parameters that will be processed in a Queue form
	 * and within the limitations as set by usage terms of Last.fm
	 * integration.   Requests made here are not considered time critical or
	 * even necessarily guaranteed to be completed if the user closes the
	 * application.
	 * <p>
	 * @param param Map containing parameters to send off to Last.fm.
	 */
	public synchronized void addParameters(Map<String,String> param) {
		
		// Insert the parameters at the end of the queue
		Queue.addLast(param);
		
		// Start up the thread process if not already running
		if (processDone) {
			
			// Reset boolean
			processDone = false;
			
			// Start processing
			myThread = new Thread(this);
			myThread.start();
		}
	}
	// End of addParameters Method
	
	
	/**
	 * Extracts the oldest parameters from the queue in order to process them
	 * against the Last.fm site.
	 * <p>
	 * @return Oldest parameter Map for request in queue.
	 */
	private synchronized Map<String,String> getParameters() {
		
		// Direct extract
		return Queue.removeFirst();
	}
	// End of addParameters Method
	

	/**
	 * Will return a boolean value on if there are other parameters to be
	 * process as necessary.
	 * <p>
	 * @return Boolean on requirement.
	 */
	private synchronized boolean moreParameters() {
		
		// Process for another go
		if (Queue.size() > 0)
			return true;
		
		// Return false by default
		return false;
	}
	// End of moreParameters Method
	
	
	/**
	 * Every request made to the Last.fm requires a generated signature and
	 * included along with the parameters passed.   This is used as a 
	 * verification as to the generated source.
	 * <p>
	 * @param params Map containing parameter list for passage.
	 */
	private void signParameters(Map<String,String> params) {
		
		/*
		 * We always append the API key.  Which is application specific as is
		 * used to identify Lyra with Last.fm.
		 */
		params.put("api_key", Global.get("LastDotFm.Key"));
		
		
		/*
		 * The first step is to take the keys and sort them alphabetically.
		 * Once sorted, we combine all of the elements together in order to
		 * proceed with the signature addition.
		 */
		
		// Convert to an array so we can sort
		String[] keys = params.keySet().toArray(new String[0]);
		
		// Sort the array
		Arrays.sort(keys);
		
		// Append all elements for signature
		StringBuffer sb = new StringBuffer();
		
		// Loop and append
		for (String key: keys) {
			
			sb.append(key);
			sb.append(params.get(key));
		}
		
		// Append the secret key to complete the string
		sb.append(Global.get("LastDotFm.Secret"));

		
		/*
		 * Now that we have the generated string there is a requirement to 
		 * create an MD5 hash.  This will be used by Last.fm to confirm 
		 * that Lyra generated the application.
		 */
		try {
			
			// Create instance
			MessageDigest md5 = MessageDigest.getInstance(DIGEST);
			
			/* 
			 * This is where we have to comply with the desired standard.  As
			 * we have to take a string, make sure that it is encoded as UTF-8
			 * and then create a digest from that.
			 */
			byte[] bytes = md5.digest(sb.toString().getBytes("UTF-8"));
			
			// Create a new string 32 characters in length
			StringBuilder b = new StringBuilder(32);

			// Cycle through
			for (byte aByte : bytes) {
				
				// Conversion of Integer to Hex
				String hex = Integer.toHexString((int) aByte & 0xFF);

				// Format the string appropriately
				if (hex.length() == 1)
					b.append('0');
				
				// Add in the new string to the builder
				b.append(hex);
			}
				
			/*
			 * Now we have all of the steps necessary to append the signature
			 * to our parameter list.
			 */
			params.put("api_sig", b.toString());
		}
		
		// This should never happen
		catch (NoSuchAlgorithmException e) {
			
			// Log the error
			Logger.Warn(
				CLASS, 
				"signParameters",
				Lang.get("LastDotFm.Error.Digest")
			  );
		}
		
		// This should never happen
		catch (UnsupportedEncodingException e) {
			
			// Log the error
			Logger.Warn(
				CLASS, 
				"signParameters",
				Lang.get("LastDotFm.Error.Encoding")
			  );
		}
	}
	// End of signParameters Method
	
	
	/**
	 * Makes the request to the Last.fm service.  Making signatures, recording
	 * the time and returning the reply document.
	 * <p>
	 * @param current Map containing parameters to pass on. 
	 * @return String document containing the full reply.
	 */
	public String executeParameters(Map<String,String> current) {
		
		// Process signatures
		signParameters(current);
		
		// Record time for future processing
		addSpan();
		
		// Make the request in the queue
		return Caller.fetchPost(
			Global.get("LastDotFm.Site"), 
			current
		  );
	}
	// End of executeParameters Method
	
	
	/* (non-Javadoc)
	 * @see java.lang.Runnable#run()
	 */
	public void run() {

		/*
		 * We keep processing until we are out of parameters to process
		 * entirely.  The thread will remain active until then.
		 */
		while (moreParameters()) {
			
			try {
				
				/*
				 * Extract the delay and act out on it, if necessary.  We do so
				 * here as it will essentially add in an additional buffer with
				 * all the additional steps left.
				 */
				Long delay = getSpan();
				
				// Delay the necessary period
				if (delay > 0) {
					
					// Log the error
					Logger.Fine(
						CLASS, 
						"run",
						Lang.get("LastDotFm.Queue.Delay")
						    .replaceAll("MDELAY", Long.toString(delay))
					  );
					
					// Perform delay
					Thread.sleep(delay);
				}
				
				// Perform delay
				Thread.sleep(delay);

				/*
				 * Here we extract the parameters, sign them and make a request
				 * to the Last.fm service.
				 */
				Map<String,String> current = getParameters();
				
				// Make the request and discard return
				executeParameters(current);
			} 
			
			// Stub - Loop will ensure recover
			catch (InterruptedException e) {}
		}
		
		// We are done thread will end naturally
		processDone = true;
	}
	// End of run Method

}
