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

import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;

import org.lyra.Ledger;
import org.lyra.Host.Status;
import org.lyra.Host.Terminus;
import org.lyra.Host.UpdateEvent;
import org.lyra.Host.UpdateListener;

/**
 * Attaches an auditing stream to the connection with the Music Player Daemon
 * enabling us to track songs played, skipped, added to the playlist whilst in
 * operation.   This can be tied later on to list generation and player
 * habits.
 * <p>
 * @author Martin Foster
 * @version 1.0.4, %Revision, 134%, %LastChangedDate, 24/09/11 1:47AM%
 */
public class Audit implements UpdateListener{

	
	/*
	 * PACAKGE INSTANCES
	 */
	private Comptroller Manager;
	private Status Info;
	private Ledger Logger;
	private Map<String,String> Lang;
	
	
	/*
	 * CLASS ELEMENTS
	 */
	private String CurrentFilename;
	private Set<String> CurrentListing;
	
	// CONSTANTS
	private static final String CLASS = "org.lyra.Database.Audit";
	
	
	/**
	 * Constructor requires an element from the database and daemon side in
	 * order to function properly.   Only then can it create a link between
	 * the two elements.
	 * <p>
	 * @param manager Database interface for insertion into the auditing table.
	 * @param info Information handle on the daemon.
	 * @param logger Ledger instance for logging of information.
	 * @param lang Independent language instance.
	 */
	public Audit(Comptroller manager, Status info, Ledger logger, Map<String,String> lang) {
		
		// Pass up
		this.Manager = manager;
		this.Info    = info;
		this.Logger  = logger;
		this.Lang    = lang;
		
		// Extract the state information
		Map<String,String> state = Info.getState();
		
		// Initial establishment
		CurrentFilename = updateFilename(state);
		CurrentListing = updateCurrent();
	}
	// End of Constructor Method
	
	
	/* (non-Javadoc)
	 * @see org.lyra.Host.UpdateListener#hostUpdated(org.lyra.Host.UpdateEvent)
	 */
	public void hostUpdated(UpdateEvent update) {

		// Extract the state information
		Map<String,String> state = Info.getState();
		
		/*
		 * We need to capture the new state information elements and then from
		 * that point compare it against existing elements.   The big elements
		 * we need are of course the state of the player and the filename as
		 * it applies.
		 */
		String new_state    = state.get("state");
		String new_filename = updateFilename(state);
		
		
		/*
		 * Player updates will provide the greatest amount of opportunity for
		 * us to record the element.
		 */
		if (update.isPlayer()) {

			/*
			 * This circumstance will typically only take place when the
			 * playlist has reached its end during a consume state or when
			 * the playlist has been cleared.  So the song has been ended.
			 */
			if ((CurrentFilename != null)
			 && (new_filename == null)) {
				 
				 // Insert the element and list it as a stop
				 Manager.insertAudit(Terminus.STATE_STOP, CurrentFilename);
				 
				 // Log Entry
				 logReplace(
					Lang.get("Audit.Player"), 
					CurrentFilename, 
					Terminus.STATE_STOP
				  );
			 }
			
			/*
			 * The assumption is made that when the legacy filename is not
			 * defined than anything new should be followed.  We also use the
			 * same behaviour when the primary element is defined.
			 */
			else if (new_filename != null) {
				
				// Insert the new element into the database
				Manager.insertAudit(new_state, new_filename);
				
				// Log Message
				logReplace(
					Lang.get("Audit.Player"), 
					new_filename,
					new_state
				  );
			}
		}
		
		/*
		 * Playlist updates.   This will enable us to keep track of what has
		 * been happening to the list elements.
		 */
		if (update.isCurrent()) {
						
			// Extract for use
			Set<String> add = updateCurrent();
		

			// Track all removals
			Set<String> remove = new TreeSet<String>();
			
			// Loop through and working
			for (String file: CurrentListing) {
				
				/*
				 * If the entry exists in both then we will assume that they
				 * are of no consequence.
				 */
				if (add.contains(file)) {
					
					// Remove entry leaving only those 
					add.remove(file);
				}
				
				/*
				 * The entries here indicate that the entry does in fact not
				 * exist in the current listing, so it has been removed.
				 */
				else {
					
					// Append to the removal list
					remove.add(file);
				}
			}
			
			/*
			 * Entries that have been explicitly added to the listing for removal
			 * are dealt with here.
			 */
			Manager.insertAudit(
					Terminus.AUDIT_REMOVED, 
					remove.toArray(new String[0])
				);

			// Log Entry
			if (remove.size() > 0)
				logReplace(
					Lang.get("Audit.Current"), 
					remove.toString(),
					Terminus.AUDIT_REMOVED
				  );
			

			/*
			 * Additions are done automatically when we start running through
			 * the list of current entries.  Whatever is left in the new array
			 * has to be new.
			 */
			Manager.insertAudit(
					Terminus.AUDIT_ADDED, 
					add.toArray(new String[0])
				);
			
			// Log Entry
			if (add.size() > 0)
				logReplace(
					Lang.get("Audit.Current"), 
					add.toString(),
					Terminus.AUDIT_ADDED
				  );

			
			/*
			 * Pass on the entry for next use
			 */
			CurrentListing = updateCurrent();
		}
		
		/*
		 * Transfer over these elements for another historical use when we 
		 * do another pass.
		 */
		CurrentFilename = new_filename;
	}
	// End of hostUpdated Method
	
	
	/**
	 * Extracts a list of filenames within the the current playlist connection
	 * in order to more easily compare.
	 * <p>
	 * @return Set containing all songs in the Current playlist.
	 */
	private Set<String> updateCurrent() {
		
		// Containing element
		Set<String> store = new TreeSet<String>();
		
		/*
		 * Cycle through the entire list and extract the filename exclusively
		 * that way we can compare.
		 */
		for (Map<String,String> record: Info.getCurrent()) {

			// Extract filename
			store.add(record.get("file"));
		}
		
		// Return element
		return store;
	}
	// End of updateCurrent Method
	
	
	/**
	 * Extracts the information from the org.lyra.Host.Status class and extracts
	 * the currently playing song.   Based on state and currently playing 
	 * elements.
	 * <p>
	 * @return String containing the filename currently playing.
	 */
	private String updateFilename(Map<String,String> state) {
		
		// Confirm this element exists
		if (state.get("song") != null) {
			
			// Extract and parse song
			int num = Integer.parseInt(state.get("song"));
			
			// Extract the specific song
			if (Info.getCurrent().get(num) != null) {
				
				// Extract song detail
				Map<String,String> song = 
					Info.getCurrent().get(num);
				
				/*
				 * While we take the time to confirm every single element
				 * up to this point the fact that the String is not defined
				 * will not alter our decision.
				 */
				return song.get("file");
			}
		}
		
		// Return nothing if any of the above fails
		return null;
	}
	// End of updateFilename Method
	
	
	/**
	 * Since filenames can carry with them certain undesired elements, we
	 * make ensure they will have no effect in the logging.
	 * <p>
	 * @param msg Error message to pass up.
	 * @param file Filename that is affected.
	 * @param state State to change to.
	 */
	private void logReplace(String msg, String file, String state) {
		
		// Make filename safe
		file = Matcher.quoteReplacement(file);
		
		// Log accordingly
		Logger.Fine(
			CLASS,
			"hostUpdated",
			msg
				.replaceAll("MFILE", file)
				.replaceAll("MSTATE", state)
			);
	}
	// End of logReplace Method
}
