/**
 * 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.StringReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.WordUtils;
import org.lyra.Ledger;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;


/**
 * Wraps the Sender class with XML parsing in order to hide much of the 
 * complexity associated with integration of the Last.fm service with Lyra
 * itself.   All of the details are hidden in the individual methods.
 * <p>
 * For example:
 * <p>
 * <code>
 *      Parser query = Parser.getInstance(Logger, Global, Lang);
 *      string token = query.getToken();
 * </code>
 * <p>
 * @author Martin Foster
 * @version 1.0.4, %Revision, 94%, %LastChangedDate, 09/03/11 10:00PM%
 * @since 1.0.1
 * @see org.lyra.LastDotFm.Sender
 */
public class Demarcation {

	
	/*
	 * PACKAGE INSTANCES
	 */
	
	// Logger Element
	private Ledger Logger;
	
	// Language and Settings Map
	private Map<String,String> Global;
	private Map<String,String> Lang;
	
	
	/*
	 * LOCAL INSTANCES
	 */
	
	// Request Maker
	private Sender Request;
	
	// XML Search Parser
	private XPath Search;
	
	// Reference to Self
	private static Demarcation OnesSelf;
	
	
	/*
	 * CONSTANTS
	 */
	private final static String CLASS = "org.lyra.LastDotFm.Demarcation";
	
	/**
	 * Base constructor for class.
	 * <p>
	 * @param logger Ledger instance for logging.
	 * @param global Global settings map.
	 * @param lang Language dependent map.
	 */
	private Demarcation(Ledger logger, Map<String,String> global, Map<String,String> lang) {
		
		// Pass up
		this.Logger = logger;
		this.Global = global;
		this.Lang   = lang;
		
		// Create instance for instances
		this.Request = new Sender(Logger, Global, Lang);
		
		// Create XML Parser
		this.Search = XPathFactory.newInstance().newXPath();
	}
	// End of Constructor Method
	
	
	/**
	 * This integration is a bit different then many of the others.  Completely
	 * optional it may never be used, or it may be employed at multiple locations
	 * this will permit us to only have one instance instead of many.
	 * <p>
	 * @param logger Ledger instance for logging.
	 * @param global Global settings map.
	 * @param lang Language dependent map.
	 * @return org.lyra.LastDotFm.Parser
	 */
	public static Demarcation getInstance(Ledger logger, Map<String,String> global, Map<String,String> lang) {
		
		// Check for Existence
		if (OnesSelf == null)
			OnesSelf = new Demarcation(logger, global, lang);
		
		// Return element
		return OnesSelf;
	}
	// End of getInstance Method
	
	
	/**
	 * XPath has an aversion to dealing with a string based document.  This can
	 * be a problem when that is the way we get information from the service.
	 * <p>
	 * @param reply String containing XML reply.
	 * @return Instance of InputSource.
	 */
	private InputSource getSource(String reply) {
		
		// Straight reply
		return new InputSource(new StringReader(reply));
	}
	// End of getSource Method
	
	
	/**
	 * Some of the strings will contain elements which are encoded in order
	 * to avoid conflicts with the XML document.   This will make the string
	 * usable.
	 * <p>
	 * @param encoded String encoded in XML document.
	 * @return String decoded from XML document.
	 */
	private String getString(String encoded) {
		
		/*
		 * Here we un-escape the XML document and then remove any HTML tags
		 * that may be hiding within.  Since the elements we use to display
		 * these elements do not support HTML directly.
		 */
		return StringEscapeUtils.unescapeXml(encoded)
		                        .replaceAll("\\<.*?>","");
	}
	// End of getString Method
	
	
	/**
	 * Hides the recurring code that tends to exist when making single element
	 * searches.  All we do is pass on the Map in order to proceed.
	 * <p>
	 * @param search String containing XPath search.
	 * @param params Map containing parameter string to pass on.
	 * @return String containing reply.
	 */
	private String getSingle(String search, Map<String,String> params) {
		
		// Extract the reply
		String reply = Request.executeParameters(params);
		
		// Parse if necessary
		if (reply != null) {
			
			// Assume something will go wrong
			try {
				
				/*
				 * XPath does not deal with strings.  This can be bothersome
				 * when that's how we get the associated reply.
				 */
				InputSource src = new InputSource(new StringReader(reply)); 
				
				// Attempt to return token
				return Search.evaluate(search, src);
			} 
			
			// Already dealing with failure
			catch (XPathExpressionException e) {
				
				// Log the error
				Logger.Severe(
					CLASS, 
					"getSingle",
					e
				  );
			}
		}
		
		// If worse comes to worse
		return null;
	}
	// End of getSingle Method
	
	
	/**
	 * Extracts all of the elements from a node and transfers it into a map
	 * in order to pass multi-value elements over.
	 * <p>
	 * @param search String containing XPath search.
	 * @param params Map containing parameter string to pass on.
	 * @return Map containing response.
	 */
	private Map<String,String> getMultiple(String search, Map<String,String> params) {
		
		// Response container
		Map<String,String> store = new HashMap<String,String>();
		
		// Extract the reply
		String reply = Request.executeParameters(params);
		
		// Parse if necessary
		if (reply != null) {
			
			// Assume something will go wrong
			try {
				
				/*
				 * XPath does not deal with strings.  This can be bothersome
				 * when that's how we get the associated reply.
				 */
				InputSource src = new InputSource(new StringReader(reply)); 
				
				// Attempt to return token
				Node group = (Node)Search.evaluate(
						search, 
						src,
						XPathConstants.NODE
					  );
				
				// Extract all the children
				NodeList list = group.getChildNodes();
				
				// Loop through
				for (int count=0; count < list.getLength(); count++) {
					
					// Extract first item
					Node current = list.item(count);
					
					// Store elements
					store.put(
						current.getNodeName(), 
						current.getTextContent()
					  );
				}
				
				// Return Answers
				return store;
			} 
			
			// Already dealing with failure
			catch (XPathExpressionException e) {
				
				// Log the error
				Logger.Severe(
					CLASS, 
					"getSingle",
					e
				  );
			}
		}
		
		// If worse comes to worse
		return store;
	}
	// End of getSingle Method
	
	
	/**
	 * Captures the album covert art used based on an album name and artist
	 * name combination.  
	 * <p>
	 * @param album String containing album cover.
	 * @param artist String containing artist name.
	 * @return String containing link to the desired cover.
	 */
	@SuppressWarnings("serial")
	public String extractAlbumInfo_Art(final String album, final String artist) {
		
		// Extract the reply
		return getSingle(
			"/lfm/album/image[@size='large']",
		    new HashMap<String,String>(){
			  {
	        	// Label Elements
				put("method", "album.getInfo");
				put("album", album);
				put("artist", artist);
				put("autocorrect", "1");
			  }
		  });
	}
	// End of extractAlbumInfo_Art Method
	
	
	/**
	 * Captures the album covert art used based on an album name and artist
	 * name combination.  
	 * <p>
	 * @param album String containing album cover.
	 * @param artist String containing artist name.
	 * @param lcode Two letter language code.
	 * @return Complex map containing consolidated results.
	 */
	@SuppressWarnings("serial")
	public Map<String,Object> extractAlbumInfo(final String album, final String artist, final String lcode) {
		
		// Result Document
		Map<String,Object> results = new HashMap<String,Object>();
		
		// Extract the reply
		Map<String,String> params  = new HashMap<String,String>(){
			  {
	        	// Label Elements
				put("method", "album.getInfo");
				put("album", album);
				put("artist", artist);
				put("autocorrect", "1");
				put("lang", lcode.toLowerCase());
			  }
		  };
		
		// Extract the reply
		String reply = Request.executeParameters(params);
		
		
		// Parse if necessary
		if (reply != null) {
			
			// Assume something will go wrong
			try {
				
				/*
				 * SINGLE ELEMENTS
				 * 
				 * Single Elements are searched out from the document and
				 * appended as necessary.
				 */
				
				// Artist Name
				results.put(
					"Artist",
					getString(Search.evaluate(
						"/lfm/album/artist",
						getSource(reply))
					  )
				  );
				
				// Album Cover Art
				results.put(
					"Image",
					Search.evaluate("/lfm/album/image[@size='large']", getSource(reply))
				  );
				
				// Release Date
				results.put(
					"Release",
					Search.evaluate("/lfm/album/releasedate", getSource(reply)).trim()
				 );
				
				// Address
				results.put(
					"Address",
					Search.evaluate("/lfm/album/url", getSource(reply))
				  );
				
				// Album Name
				results.put(
					"Title",
					getString(Search.evaluate(
						"/lfm/album/name", 
						getSource(reply))
					  )
				  );
				
				// Statistics - Listeners
				results.put(
					"Listeners",
					Search.evaluate("/lfm/album/listeners", getSource(reply))
				  );
				
				// Statistics - Play Count
				results.put(
					"Plays",
					Search.evaluate("/lfm/album/playcount", getSource(reply))
				  );
				
				// Wiki - Summary
				results.put(
					"Summary",
					getString(Search.evaluate(
						"/lfm/album/wiki/summary", 
						getSource(reply))
					  )
				  );
				
				// Wiki - Content
				results.put(
					"Content",
					getString(Search.evaluate(
						"/lfm/album/wiki/content",
						getSource(reply))
					  )
				  );
				
				/*
				 * TOP TAGS
				 * 
				 * Elements which are more complicated and will return an
				 * Embedded Map.  These particular elements will extract
				 * all of the top tags and necessary information.  
				 */
				
				// Extract Node List
				NodeList tags = (NodeList)Search.evaluate(
					"/lfm/album/toptags/*",
					getSource(reply),
					XPathConstants.NODESET
				  );

				// Storage Element
				Map<String,String> tstore = new HashMap<String,String>(); 
				
				
				// Loop Through
				for (int ctags=0; ctags < tags.getLength(); ctags++) {
					
					// Extract Tag
					Node tag = tags.item(ctags);
					
					// All Sub Elements
					NodeList elem = tag.getChildNodes();
					
					// Capture elements
					String key   = null;
					String value = null;
					
					// Loop Through Elements
					for (int celem=0; celem < elem.getLength(); celem++) {
						
						// Extract Current Node
						Node current = elem.item(celem);
								
						// Extract Name
						if (current.getNodeName().toLowerCase().equals("name"))
							key = current.getTextContent();
						
						// Extract Link
						else if (current.getNodeName().toLowerCase().equals("url"))
							value = current.getTextContent();
					}
					
					// Everything is good for inclusion
					if ((key   != null)
					 && (value != null)) {
						
						// Drop in Elements
						tstore.put(
							WordUtils.capitalizeFully(getString(key)), 
							value
						  );
					}
				}
				
				// Add in store
				results.put(
					"Tags",
					tstore
				  );
			} 
			
			// Already dealing with failure
			catch (XPathExpressionException e) {
				
				// Log the error
				Logger.Severe(
					CLASS, 
					"extractAlbumInfo",
					e
				  );
			}
		}	
		
		
		/*
		 * We still require purchase information, which is not included in
		 * the main search element.  So if we have a Music Brainz ID then
		 * we start again.
		 */
		if (results.size() > 0) {
			
			// Extract the reply
			params  = new HashMap<String,String>(){
				  {
		        	// Label Elements
					put("method", "album.getBuylinks");
					put("album", album);
					put("artist", artist);
					put("autocorrect", "1");
					put("country",Global.get("LastDotFm.Country"));
				  }
			  };
			
			// Extract the reply
			reply = Request.executeParameters(params);
			
			// Parse if necessary
			if (reply != null) {
				
				// Assume something will go wrong
				try {
					
					/*
					 * BUY LINKS
					 * 
					 * Elements which are more complicated and will return an
					 * Embedded Map.  These particular elements will extract
					 * all of the physical media suppliers.  
					 */
					
					// Extract Node List
					NodeList stores = (NodeList)Search.evaluate(
						"/lfm/affiliations/physicals/*",
						getSource(reply),
						XPathConstants.NODESET
					  );

					// Storage Element
					Map<String,String> bstore = new HashMap<String,String>(); 
					
					
					// Loop Through
					for (int cstores=0; cstores < stores.getLength(); cstores++) {
						
						// Extract Tag
						Node store = stores.item(cstores);
						
						// All Sub Elements
						NodeList elem = store.getChildNodes();
						
						// Capture elements
						String key   = null;
						String value = null;
						
						// Loop Through Elements
						for (int celem=0; celem < elem.getLength(); celem++) {
							
							// Extract Current Node
							Node current = elem.item(celem);
							
							// Extract Name
							if (current.getNodeName().toLowerCase().equals("suppliername"))
								key = current.getTextContent();
							
							// Extract Link
							else if (current.getNodeName().toLowerCase().equals("buylink"))
								value = current.getTextContent();
						}
						
						// Everything is good for inclusion
						if ((key   != null)
						 && (value != null)) {
							
							// Drop in Elements
							bstore.put(getString(key), value);
						}
					}
					
					// Add in store
					results.put(
						"Purchase",
						bstore
					  );
				} 
				
				// Already dealing with failure
				catch (XPathExpressionException e) {
					
					// Log the error
					Logger.Severe(
						CLASS, 
						"extractAlbumInfo",
						e
					  );
				}
			}
		}
		
		// Return Results
		return results;
	}
	// End of extractAlbumInfo Method
	
	
	/**
	 * Captures the artist photograph used as an alternate to album art covers
	 * when they cannot be found.   
	 * <p>
	 * @param artist String containing artist name.
	 * @return String containing link to the desired image.
	 */
	@SuppressWarnings("serial")
	public String extractArtistInfo_Art(final String artist) {
		
		// Extract the reply
		return getSingle(
			"/lfm/artist/image[@size='large']",
		    new HashMap<String,String>(){
			  {
	        	// Label Elements
				put("method", "artist.getInfo");
				put("artist", artist);
				put("autocorrect", "1");
			  }
		  });
	}
	// End of extractArtistInfo_Art Method
	
	
	/**
	 * Captures the album covert art used based on an album name and artist
	 * name combination.  
	 * <p>
	 * @param artist String containing artist name.
	 * @param lcode Two letter language code.
	 * @return Complex map containing consolidated results.
	 */
	@SuppressWarnings("serial")
	public Map<String,Object> extractArtistInfo(final String artist, final String lcode) {
		
		// Result Document
		Map<String,Object> results = new HashMap<String,Object>();
		
		// Extract the reply
		Map<String,String> params  = new HashMap<String,String>(){
			  {
	        	// Label Elements
				put("method", "artist.getInfo");
				put("artist", artist);
				put("autocorrect", "1");
				put("lang", lcode.toLowerCase());
			  }
		  };
		
		// Extract the reply
		String reply = Request.executeParameters(params);
		
		
		// Parse if necessary
		if (reply != null) {
			
			// Assume something will go wrong
			try {
				
				/*
				 * SINGLE ELEMENTS
				 * 
				 * Single Elements are searched out from the document and
				 * appended as necessary.
				 */
				
				// Artist Picture
				results.put(
					"Image",
					Search.evaluate("/lfm/artist/image[@size='large']", getSource(reply))
				  );
				
				// Address
				results.put(
					"Address",
					Search.evaluate("/lfm/artist/url", getSource(reply))
				  );
				
				// Artist Name
				results.put(
					"Title",
					getString(Search.evaluate(
						"/lfm/artist/name", 
						getSource(reply))
					  )
				  );
				
				// Statistics - Listeners
				results.put(
					"Listeners",
					Search.evaluate("/lfm/artist/stats/listeners", getSource(reply))
				  );
				
				// Statistics - Play Count
				results.put(
					"Plays",
					Search.evaluate("/lfm/artist/stats/playcount", getSource(reply))
				  );
				
				// Wiki - Summary
				results.put(
					"Summary",
					getString(Search.evaluate(
						"/lfm/artist/bio/summary", 
						getSource(reply))
					  )
				  );
				
				// Wiki - Content
				results.put(
					"Content",
					getString(Search.evaluate(
						"/lfm/artist/bio/content",
						getSource(reply))
					  )
				  );
				
				/*
				 * TOP TAGS
				 * 
				 * Elements which are more complicated and will return an
				 * Embedded Map.  These particular elements will extract
				 * all of the top tags and necessary information.  
				 */
				
				// Extract Node List
				NodeList tags = (NodeList)Search.evaluate(
					"/lfm/artist/tags/*",
					getSource(reply),
					XPathConstants.NODESET
				  );

				// Storage Element
				Map<String,String> tstore = new HashMap<String,String>(); 
				
				
				// Loop Through
				for (int ctags=0; ctags < tags.getLength(); ctags++) {
					
					// Extract Tag
					Node tag = tags.item(ctags);
					
					// All Sub Elements
					NodeList elem = tag.getChildNodes();
					
					// Capture elements
					String key   = null;
					String value = null;
					
					// Loop Through Elements
					for (int celem=0; celem < elem.getLength(); celem++) {
						
						// Extract Current Node
						Node current = elem.item(celem);
								
						// Extract Name
						if (current.getNodeName().toLowerCase().equals("name"))
							key = current.getTextContent();
						
						// Extract Link
						else if (current.getNodeName().toLowerCase().equals("url"))
							value = current.getTextContent();
					}
					
					// Everything is good for inclusion
					if ((key   != null)
					 && (value != null)) {
						
						// Drop in Elements
						tstore.put(
							WordUtils.capitalizeFully(getString(key)),
							value
						  );
					}
				}
				
				// Add in store
				results.put(
					"Tags",
					tstore
				  );
				
				
				/*
				 * BUY LINKS
				 * 
				 * Elements which are more complicated and will return an
				 * Embedded Map.  These particular elements will extract
				 * all of the physical media suppliers.  
				 */
				
				// Extract Node List
				NodeList stores = (NodeList)Search.evaluate(
					"/lfm/artist/similar/*",
					getSource(reply),
					XPathConstants.NODESET
				  );

				// Storage Element
				Map<String,String> bstore = new HashMap<String,String>(); 
				
				
				// Loop Through
				for (int cstores=0; cstores < stores.getLength(); cstores++) {
					
					// Extract Tag
					Node store = stores.item(cstores);
					
					// All Sub Elements
					NodeList elem = store.getChildNodes();
					
					// Capture elements
					String key   = null;
					String value = null;
					
					// Loop Through Elements
					for (int celem=0; celem < elem.getLength(); celem++) {
						
						// Extract Current Node
						Node current = elem.item(celem);
						
						// Extract Name
						if (current.getNodeName().toLowerCase().equals("name"))
							key = current.getTextContent();
						
						// Extract Link
						else if (current.getNodeName().toLowerCase().equals("url"))
							value = current.getTextContent();
					}
					
					// Everything is good for inclusion
					if ((key   != null)
					 && (value != null)) {
						
						// Drop in Elements
						bstore.put(getString(key), value);
					}
				}
				
				// Add in store
				results.put(
					"Similar",
					bstore
				  );
			} 
			
			// Already dealing with failure
			catch (XPathExpressionException e) {
				
				// Log the error
				Logger.Severe(
					CLASS, 
					"extractAlbumInfo",
					e
				  );
			}
		}	
		
		// Return Results
		return results;
	}
	// End of extractArtistInfo Method
	
	
	/**
	 * Fetch a session key for a user. The third step in the authentication 
	 * process. See the authentication how-to for more information. 
	 * <p>
	 * @param token Generated token used to link session.
	 * @return Map containing session information.
	 */
	@SuppressWarnings("serial")
	public Map<String,String> extracAuthSession(final String token) {
		
		// Extract the reply
		return getMultiple(
			"/lfm/session",
		    new HashMap<String,String>(){
			  {
	        	// Label Elements
				put("method", "auth.getSession");
				put("token", token);
			  }
		 });
	}
	// End of extractAuthSession Method
	
	/**
	 * Fetch an unauthorised request token for an API account. This is step 2 
	 * of the authentication process for desktop applications. Web 
	 * applications do not need to use this service. 
	 * <p>
	 * @return String containing token.
	 */
	@SuppressWarnings("serial")
	public String extracAuthToken() {
		
		// Extract the reply
		return getSingle(
			"/lfm/token",
		    new HashMap<String,String>(){
			  {
	        	// Label Elements
				put("method", "auth.getToken");
			  }
		  });
	}
	// End of extractAuthToken Method

	
	/*
	 * TRANSMIT
	 * 
	 * Transmit entries do not have a requirement to act based on a reply from
	 * the host.  Hence these are sent off regardless of requirements.
	 */
	
	
	/**
	 * Sends off a notice that a certain track is being played on our Last.fm
	 * integrated client.
	 * <p>
	 * @param track String with track title.
	 * @param artist String with artist name.
	 * @param album String with album name (optional).
	 * @param trackno String with track number (optional).
	 * @param duration String with duration in seconds (optional).
	 */
	@SuppressWarnings("serial")
	public void transmitUpdateNowPlaying(
			final String track, 
			final String artist, 
			String album, 
			String trackno, 
			String duration) {
		
		// Main record for parameters
		Map<String,String> params = new HashMap<String,String>(){
			  {
		        	// Label Elements
					put("method", "track.updateNowPlaying");
					put("track", track);
					put("artist", artist);
					put("sk", Global.get("LastDotFm.Session"));
			  }
		  };
		
		  /*
		   * Optional additions.  We add those in separately in order to avoid
		   * causing errors with the host.
		   */
		  
		  // Album Title
		  if (album != null)
			params.put("album", album);
		  
		  // Track Number
		  if (trackno != null)
			params.put("trackNumber", trackno);
		  
		  // Duration Times
		  if (duration != null)
			params.put("duration", duration);
		  
		// Pass on the request and forget  
		Request.addParameters(params);
	}
	// End of transmitUpdateNowPlaying Method
	
	
	/**
	 * This is the second component that is to be sent when a track has
	 * reached a certain point.  So we throw this in at the end of a song
	 * play in order to meet most of their terms.
	 * <p>
	 * @param start Long value containing start time.
	 * @param track String with track title.
	 * @param artist String with artist name.
	 * @param album String with album name (optional).
	 * @param trackno String with track number (optional).
	 * @param duration String with duration in seconds (optional).
	 */
	@SuppressWarnings("serial")
	public void transmitTrackScrobble(
			final Long start,
			final String track, 
			final String artist, 
			final String album, 
			final String trackno, 
			final String duration) {
		
		// Main record for parameters
		Map<String,String> params = new HashMap<String,String>(){
			  {
		        	// Label Elements
					put("method", "track.scrobble");
					put("timestamp", Long.toString(start));
					put("track", track);
					put("artist", artist);
					put("sk", Global.get("LastDotFm.Session"));
			  }
		  };
		
		  /*
		   * Optional additions.  We add those in separately in order to avoid
		   * causing errors with the host.
		   */
		  
		  // Album Title
		  if (album != null)
			params.put("album", album);
		  
		  // Track Number
		  if (trackno != null)
			params.put("trackNumber", trackno);
		  
		  // Duration Times
		  if (duration != null)
			params.put("duration", duration);
		  
		// Pass on the request and forget  
		Request.addParameters(params);
	}
	// End of transmitTrackScrobble Method
	
	
	/**
	 * This is the second component that is to be sent when a track has
	 * reached a certain point.  So we throw this in at the end of a song
	 * play in order to meet most of their terms.
	 * <p>
	 * @param track String with track title.
	 * @param artist String with artist name.
	 * @param album String with album name (optional).
	 * @param trackno String with track number (optional).
	 * @param duration String with duration in seconds (optional).
	 */
	public void transmitTrackScrobble(
			String track, 
			String artist, 
			String album, 
			String trackno, 
			String duration) {
		
		// Calculate start time
		final Long start = 
			(System.currentTimeMillis() / 1000) - Integer.parseInt(duration);
		
		// Pass on to main method with generated start time
		transmitTrackScrobble(
				start, 
				track, 
				artist, 
				album, 
				trackno, 
				duration
			);
	}
	// End of transmitTrackScrobble Method
	
	
	/**
	 * Creates a playlist and passes on all of the associated songs to the
	 * Last.fm service.   Since there is no inherent way to remove and modify
	 * lists, this is mostly considered a 'copy' command and not heavily
	 * integrated into Lyra.
	 * <p>
	 * @param playlist A list of all songs in the playlist.
	 * @param identifier A string containing the identifier for the list.
	 */
	@SuppressWarnings("serial")
	public void transmitPlaylistCreate(final List<Map<String,String>> playlist, final String identifier) {
		
		/*
		 * The first step is to create the playlist itself.  This will permit
		 * us to extract the Playlist identifier and use that to append all of
		 * the necessary songs.
		 */
		final String id = getSingle(
			"/lfm/playlists/playlist/id",
		    new HashMap<String,String>(){
			  {
		        // Label Elements
				put("method", "playlist.create");
				put("title", identifier);
				put("sk", Global.get("LastDotFm.Session"));
			  }
		  });
		
		
		/*
		 * We got an identifier successfully.  So now we can begin the
		 * process of getting all songs transferred over to the Last.fm
		 * service.
		 */
		if ((id != null)
		 && (!id.trim().isEmpty())) {
		
			// Cycle through elements
			for (final Map<String,String> current: playlist) {
			
				// Insert every element and forget
				Request.addParameters(new HashMap<String,String>(){
					  {
				        // Label Elements
						put("method", "playlist.addTrack");
						put("playlistID", id);
						put("track", current.get("Title"));
						put("artist", current.get("Artist"));
						put("sk", Global.get("LastDotFm.Session"));
					  }
				  });
				
			}
		}
	}
	// End of transmitPlaylistCreate method
	
}
