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

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

import org.lyra.Ledger;

/**
 * The Multimedia Player Daemon does not send updates to track the timing of
 * the song.   As a result the user is left with either forcing an update every
 * half second to a second in order to ensure that it displays properly or to
 * create their own timer.
 * <p>
 * In order to save on bandwidth we have created a timer and associated
 * listener that will perform the function in between player updates.  This
 * class will also listen in on updates and always provide an update regardless
 * of state.   The difference however is that the thread will not be available
 * unless the playback mode is set to play.
 * <p>
 * @author Martin Foster
 * @version 1.0.1, %Revision, 99%, %LastChangedDate, 17/03/11 8:17PM%
 * @see org.lyra.Host.PendulumListener
 * @see org.lyra.Host.Updater
 */
public class Pendulum implements Runnable, UpdateListener{

	// Primary thread
	private Thread myThread;
	
	// List of listeners
	private List<PendulumListener> Clients;
	
	// Processing element
	private boolean Process = true;
	
	// End as planned during the last update
	long myEnd;

	// Total length to calculate elapsed
	int myTotal;
	
	// Class Identifier
	private static final String CLASS = "org.lyra.host.Timer";
	
	
	/*
	 * PACKAGE INSTANCES
	 */
	private Ledger Logger;
	private Status Information;
	
	private Map<String,String> Lang;
	
	
	/**
	 * Default constructor for the class.   Will ensure that logger and status
	 * elements are in place for the main code.
	 * <p>
	 * @param logger Ledger instance for logging events.
	 * @param information Status instance for keeping track of states.
	 * @param lang Language map.
	 */
	public Pendulum(Ledger logger, Status information, Map<String,String> lang) {
	
		// Pass up values
		this.Logger      = logger;
		this.Information = information;
		this.Lang        = lang;
		
		// Create instance of listeners
		this.Clients      = new LinkedList<PendulumListener>(); 
		
		
		/*
		 * If the song is already in play then it will be required to send out
		 * updates as soon as possible.   Without an explicit update however
		 * it will not be started naturally.  This will force the process.
		 */
		Update();
	}
	// End of Constructor Class
	
	
	/**
	 * This method will handle the actual update and decide what to so with it.
	 * Since we have the need to update at two points, one being the constructor
	 * and the other being the actual update handler.
	 */
	private synchronized void Update() {
		/*
		 * In all cases we will expire the thread and prevent it from
		 * updating as we start this process.
		 */
		if (myThread != null) {
			
			// Kill processing
			Process = false;
			
			// Interrupt the thread
			myThread.interrupt();
		}
		
		
		// Extract state information
		Map<String,String> state = Information.getState();
		
		
		/*
		 * In the case of a stopped state we really only need to revert
		 * back to default values.   There is no calculation required
		 * or thread launched.
		 */
		if (state.get("state").equals(Terminus.STATE_STOP)) {

			// Log the event
			Logger.Info(
				CLASS,
				"hostUpdated",
				Lang.get("Pendulum.Log.Stop")
			  );
			
			// Pass a straight forward update
			fireUpdateRequired(0,0);
		}
		
		
		/*
		 * Both pause and play require a certain amount of calculations
		 * prior to taking place.   The core difference is in the way that
		 * the player will pause an addition thread.
		 */
		else {
			
			// We always take a look at what is playing
			int song = Integer.parseInt(state.get("song"));
			
			// Extract the song details
			Map<String,String> current 
			  = Information.getCurrent().get(song);

			
			/*
			 * We need to parse out the times and then calculate the end
			 * based on system time.  The end time will prove to be a far
			 * more accurate element to base ourselves on since it will be
			 * based on system time.
			 */
			
			// Extract elapsed time
			int elapsed = 
				Integer.parseInt(state.get("time").split(":")[0]);

			// Extract total time
			int total   = 
				Integer.parseInt(current.get("Time"));
			
			
			/*
			 * For a pause state, this is the end of the line.  The play
			 * element requires addition processing as mentioned above.
			 */
			if (state.get("state").equals(Terminus.STATE_PAUSE)) {
				
				// Logger
				Logger.Info(
					CLASS,
					"hostUpdated",
					Lang.get("Pendulum.Log.Pause")
				  );
				
				// Fire event and done.
				fireUpdateRequired(elapsed, total);
			}
			
			/*
			 * Only the start of thread will pass by here.
			 */
			else {
				
				// Where will this end of the system time
				myEnd = (System.currentTimeMillis()/1000) + (total - elapsed);
				
				// Pass up
				myTotal = total;
				
				// Allow process to begin
				Process = true;
		
				// Log the Process
				Logger.Info(
					CLASS,
					"hostUpdated",
					Lang.get("Pendulum.Log.Play")
				  );
				
				// Start the thread
				myThread = new Thread(this);
				myThread.start();
			}
		}
	}
	// End of Update Method
	
	
	/*
	 * LISTENER ELEMENTS
	 * 
	 * Elements which when added together compose enable this class to behave
	 * well with listener elements.
	 */
	
	/**
	 * Adds an <i>TimerListener</i> to the updater.
	 * <p>
	 * @param l Class implementing the TimerListener interface.
	 */
	public void addTimerListener(PendulumListener l) {
		
		// Add in an listener
		Clients.add(l);
	}
	// End of addTimerListener Method
	
	
	/**
	 * Removes an <i>TimerListener</i> from the updater.
	 * <p>
	 * @param l Instance of class implementing the TimerListener interface.
	 */
	public void removeTimerListener(PendulumListener l) {
		
		// Remove from Collection
		Clients.remove(l);
	}
	// End of removeTimerListener Method
	
	
	/**
	 * Returns an array of all the <i>TimerListeners</i> added to this class
	 * with <i>addTimerListener()</i>.
	 * <p>
	 * @return All of the TimerListeners added or an empty array if no listeners have been added.
	 */
	public PendulumListener[] getTimerListeners() {
		
		// Have the collection do all the work
		return Clients.toArray(new PendulumListener[0]);
	}
	// End of getTimerListeners Method
	
	
	/**
	 * Triggers a notification to all of the elements in order to update
	 * the time and elapsed time elements for the song.
	 * <p>
	 * @param elapsed Integer carrying current runtime.
	 * @param total Integer representing total play time.
	 */
	private void fireUpdateRequired(int elapsed, int total) {

		// Log the event
		Logger.Finest(
			CLASS,
			"fireUpdateRequired",
			Lang.get("Pendulum.Log.Fire")
			  .replaceAll("MELAPSED", Integer.toString(elapsed))
			  .replaceAll("MTOTAL", Integer.toString(total))
		  );
		
		
		/*
		 * We loop through in reverse order in order to avoid situations where
		 * a Listener removes itself from this component and actually changes
		 * the structure of the collection.  In reverse order, positions will
		 * not be affected by a removal.
		 */
		for (int count = Clients.size()-1; count >= 0; count--)
			Clients.get(count).pendulumUpdated(elapsed, total);
	}
	// End of fireUpdateRequired Method
	
	
	/*
	 * INTERFACES
	 */
	
	/* (non-Javadoc)
	 * @see org.lyra.Host.UpdateListener#hostUpdated(org.lyra.Host.UpdateEvent)
	 */
	public void hostUpdated(UpdateEvent update) {
		
		/*
		 * We only deal with player updates, since there is no reason to watch
		 * for other elements considering they do not apply to the player
		 * directly.
		 */
		if (update.isPlayer()) {
			
			// We let a standard method deal with this 
			Update();
		}
	}
	// End of hostUpdated Method

	
	/* (non-Javadoc)
	 * @see java.lang.Runnable#run()
	 */
	public void run() {

		// Elapsed time as calculated
		int elapsed = 0;
		
		// Process as long as necessary
		while ((Process)
		    && (elapsed != myTotal)) {

			try {
				/*
				 * Look at the current time and then the time its supposed to end
				 * at.  This will be the elapsed period of time.
				 */
				elapsed = myTotal - (int)(myEnd -(System.currentTimeMillis()/1000));
				
				/*
				 * Fire an update to all of the clients notifying them that we have
				 * a new time for update.
				 */
				fireUpdateRequired(elapsed, myTotal);
				
				/*
				 * Now we wait out for a period of time.  That way when we proceed
				 * the update will reflect current time.
				 */
				Thread.sleep(1000);
			} 
			
			/*
			 * Continue processing as necessary.   Under normal circumstances
			 * an interruption of this thread will be preceded by changing of 
			 * state to prevent this thread from continuing.
			 */
			catch (InterruptedException e) {}
		}
	}
	// End of run Method
	
	
	/**
	 * Method used to stop any given thread running on this class.   Normally
	 * called once we are done with the application.
	 */
	public void stopRun() {
		if (myThread != null) {
			
			// Process is to stop
			Process = false;
			
			// Close the thread
			myThread.interrupt();
		}
	}
	// End of stopRun Method
}
