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

import javax.swing.AbstractButton;
import javax.swing.Box;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.ImageIcon;
import javax.swing.BorderFactory;
import javax.swing.JPopupMenu;
import javax.swing.JToggleButton;
import javax.swing.SwingUtilities;
import javax.swing.border.TitledBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import java.awt.Font;
import java.awt.Color;
import java.awt.Point;

import javax.swing.JSlider;

import org.lyra.Ledger;
import org.lyra.Lyra;
import org.lyra.Database.Comptroller;
import org.lyra.Host.Options;
import org.lyra.Host.Pendulum;
import org.lyra.Host.PendulumListener;
import org.lyra.Host.Playback;
import org.lyra.Host.Status;
import org.lyra.Host.Terminus;
import org.lyra.Host.UpdateEvent;
import org.lyra.Host.UpdateListener;
import org.lyra.Host.Updater;
import org.lyra.Interface.Menu.CrossfadeMenu;
import org.lyra.Interface.Menu.VolumeMenu;
import org.lyra.Widgets.TickerBufferedLabel;
import org.lyra.Widgets.TickerSimpleLabel;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;


/**
 * This class serves as the primary interface for the <i>PlayerOrganiserFrame</i>
 * class.   While that class deals with finding and selecting songs for play this
 * panel provides the missing functionality which will permit it to control
 * play back.
 * <p>
 * @author Martin Foster
 * @version 1.0.1, %Revision, 91%, %LastChangedDate, 05/03/11 2:04AM%
 * @see org.lyra.Interface.PlayerOrganiserFrame
 */
public class PlayerPanel extends JPanel {

	// Serial Requirements
	private static final long serialVersionUID = 1L;

	// Information and Album Art Display
	private JPanel panelDisplay = null;
	
	// Informational section of the player
	private JPanel panelInformation = null;
	private JLabel labelTitle = null;
	private JPanel panelTitle = null;
	private JLabel labelTime = null;
	private JPanel panelTime = null;
	private JSlider sliderProgress = null;
		
	// Player Control Group
	private JPanel panelControl = null;
	private JButton buttonBack = null;
	private JButton buttonPlay = null;
	private JButton buttonNext = null;
	private JButton buttonStop = null;
	
	// Player Options Group
	private JButton buttonVolume = null;
	private JButton buttonCrossfade = null;

	private JToggleButton toggleRandom = null;
	private JToggleButton toggleRepeat = null;
	private JToggleButton toggleSingle = null;

	// Icon Changes
	private ImageIcon iconPlay   = null;
	private ImageIcon iconPause  = null;
	private ImageIcon iconVolume = null;
	private ImageIcon iconMute   = null;
	
	
	/*
	 * CLASS INSTANCES
	 */
	private Interact Interface;
	

	/*
	 * PACKAGE INSTANCES
	 */
	private Comptroller Manager;
	private Ledger Logger;
	private Status Information;
	private Playback Control;
	private Options Modes;

	// Volume Control
	private VolumeMenu VolumeControl;
	private JPopupMenu VolumePopup;
	
	// Crossfade Control
	private CrossfadeMenu CrossfadeControl;
	private JPopupMenu CrossfadePopup;
	
	// Language Elements
	private Map<String,String> Lang;
	private Map<String,String> Global;
	
	// Enabled Elements
	private Set<String> Enabled;
	
	// Watcher Handler
	private Updater Watcher;
	
	/*
	 * CONSTANTS
	 */
	
	// Assigned width for the ticker tape
	private static final int TICKER_WIDTH = 700;
	
	/**
	 * This is the default constructor.   Typically used for the visual editors
	 * only as the application will typically need to pass on logging and 
	 * language elements.
	 */
	@SuppressWarnings("serial")
	public PlayerPanel() {
		
		// Have the super class work things out
		super();
		
		/*
		 * Under normal circumstances this section will be passed on from the
		 * database.   However Visual Editors do not have this ability to we
		 * create a HashMap that can be used either way.
		 */
		Lang = new HashMap<String,String>(){
			{
	        	// Current Playlist Control
	        	put("Player.Control.Back","Shuffle current playlist...");
	        	put("Player.Control.Stop","Remove selected entry...");
	        	put("Player.Control.Play","Clear contents of current playlist...");
	        	put("Player.Control.Next","Shift selected song up...");
	        	
	        	// Options Group
	        	put("Player.Option.Consume", "");
	        	put("Player.Option.Volume",  "");
	        	put("Player.Option.Mode",    "");
	        	
	        	// Default Information Element
	        	put ("Player.Information.Artist", "");
	        	put ("Player.Information.Title",  "");
	        	put ("Player.Information.Time",   "");
			}
		};
		
		// Blanked list for successful generation
		Enabled = new TreeSet<String>();
		
		// Initialise the contents
		initFrame();
	}
	// End of Constructor Method

	
	/**
	 * Primary constructor class for this component.  Will pass on all of the
	 * necessary control elements to make it interactive.
	 * <p>
	 * @param logger Ledger instance for logging of events.
	 * @param manager Database comptroller instance.
	 * @param information State and status information for playback and modes.
	 * @param control Playback control instance.
	 * @param modes Options handler for control host Options.
	 * @param watcher Updater instance to receive changes in player state.
	 * @param clock Timer to keep players up to date.
	 * @param global Global settings.
	 * @param lang Language elements.
	 */
	public PlayerPanel(
			Ledger logger, 
			Comptroller manager, 
			Status information, 
			Playback control, 
			Options modes, 
			Updater watcher, 
			Pendulum clock, 
			Map<String,String> global, 
			Map<String,String> lang) {
		
		// Have the super class do its thing
		super();
		
		// Pass up
		this.Manager     = manager;
		this.Logger      = logger;
		this.Information = information;
		this.Control     = control;
		this.Modes       = modes;
		this.Global      = global;
		this.Lang        = lang;
		this.Watcher     = watcher;
		
		// Enabled Component
		this.Enabled     = Information.getEnabledAsSet();
		
		// Create Instance of Listener
		Interface = new Interact();
		
		// Initialise the contents
		initFrame();
		
		// Update Elements
		Interface.updateState();
		Interface.updateTime();
		
		// Add this interface as a watcher
		watcher.addUpdateListener(Interface);
		clock.addTimerListener(Interface);
	}
	// End of Constructor Class
	
	
	/**
	 * This method initialises the baseline structure.   Enabling the panel
	 * to be initialised and passed on.
	 */
	private void initFrame() {

		// Establish layout and border
		this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));

		/*
		 * Here we have a choice.  We can either display just the information
		 * with no integration based on the existence of the integration.  Or
		 * we can display a complete panel with both elements.
		 */
		
		// Display Information Only
		if (Global.get("LastDotFm.Session").equals("0"))
			this.add(getPanelInformation(), null);
		
		// Fully Integrated Panel
		else this.add(getPanelDisplay(), null);
		
		
		// Spacer Instance
		this.add(Box.createVerticalStrut(Constants.getSmallGap()));
		
		// Grouping element
		this.add(getPanelControl(), null);
		
		
		/*
		 * Core to the implementation of the panel is the use of PopupMenu 
		 * structures for both the volume and crossfade control.
		 */
		
		// Volume Control
		VolumeControl = new VolumeMenu(Lang);
		VolumePopup = VolumeControl.getPopupVolume();

		// Crossfade Control
		CrossfadeControl = new CrossfadeMenu(Lang);
		CrossfadePopup = CrossfadeControl.getPopupCrossfade();
		
		/*
		 * Update listeners will allow us to control certain features that
		 * Volume and Crossfade pass on.
		 */
		VolumeControl.addChangeListener(Interface);
		CrossfadeControl.addChangeListener(Interface);
	}
	// End of initFrame Method
	

	/**
	 * This method initialises buttonBack.	
	 * <p>
	 * @return javax.swing.JButton	
	 */
	private JButton getButtonBack() {
		
		// Check for Instance
		if (buttonBack == null) {
			
			// Create Instance
			buttonBack = new JButton();
			
			// Icon and Language
			buttonBack.setIcon(new ImageIcon(
					Lyra.getIconByMap("Playback-Previous")));
			buttonBack.setToolTipText(Lang.get("Tooltip.Playback.Previous"));
			
			// Identifier and Listener
			buttonBack.setName("ACTION.BACK");
			buttonBack.addActionListener(Interface);
			
			// Establish Enabled State
			buttonBack.setEnabled(Enabled.contains(
				Terminus.PROTOCOL.get("Player.Previous")));
		}
		
		// Return Instance
		return buttonBack;
	}
	// End of getButtonBack Method

	
	/**
	 * This method initialises buttonPlay.	This button also includes the
	 * pause button, icon changes based on state.
	 * <p>
	 * @return javax.swing.JButton	
	 */
	private JButton getButtonPlay() {
		
		// Check for Existence
		if (buttonPlay == null) {
			
			// Create Instance
			buttonPlay = new JButton();
			
			// Text
			buttonPlay.setToolTipText(Lang.get("Tooltip.Playback.Play"));
			
			// Identifier and Listener
			buttonPlay.setName("CONTROL.PLAY");
			buttonPlay.addActionListener(Interface);
			
			// Establish Enabled State
			buttonPlay.setEnabled(Enabled.contains(
				Terminus.PROTOCOL.get("Player.Play")));
		}
		
		// Return Instance
		return buttonPlay;
	}
	// End of getButtonPlay Method

	
	/**
	 * This method initialises playerNext.
	 * <p>
	 * @return javax.swing.JButton	
	 */
	private JButton getButtonNext() {
		
		// Check for Existence
		if (buttonNext == null) {
			
			// Create Instance
			buttonNext = new JButton();
			
			// Icon and Text Elements
			buttonNext.setIcon(new ImageIcon(
					Lyra.getIconByMap("Playback-Next")));
			buttonNext.setToolTipText(Lang.get("Tooltip.Playback.Next"));
			
			// Identifier and Listener
			buttonNext.setName("CONTROL.NEXT");
			buttonNext.addActionListener(Interface);
			
			// Establish Enabled State
			buttonNext.setEnabled(Enabled.contains(
				Terminus.PROTOCOL.get("Player.Next")));
		}
		
		// Return Instance
		return buttonNext;
	}
	// End of getPlayerNext Method
	

	/**
	 * This method initialises buttonStop.	
	 * <p>
	 * @return javax.swing.JButton	
	 */
	private JButton getButtonStop() {
		
		// Check for Existence
		if (buttonStop == null) {
			
			// Create Instance
			buttonStop = new JButton();
			
			// Icon and Text Elements
			buttonStop.setIcon(new ImageIcon(
					Lyra.getIconByMap("Playback-Stop")));
			buttonStop.setToolTipText(Lang.get("Tooltip.Playback.Stop"));
			
			// Identifier and Listener
			buttonStop.setName("CONTROL.STOP");
			buttonStop.addActionListener(Interface);
			
			// Establish Enabled State
			buttonStop.setEnabled(Enabled.contains(
				Terminus.PROTOCOL.get("Player.Stop")));
		}
		
		// Return Instance
		return buttonStop;
	}
	// End of getButtonStop Method


	/**
	 * This method initialises panelInformation.	
	 * <p> 	
	 * @return javax.swing.JPanel	
	 */
	private JPanel getPanelInformation() {
		
		// Check for Existence
		if (panelInformation == null) {

			// Create Instance
			panelInformation = new JPanel();
			
			// Setup Layout and Border
			panelInformation.setLayout(new BoxLayout(getPanelInformation(), BoxLayout.Y_AXIS));

			// This is only necessary if we are not integrating
			if (Global.get("LastDotFm.Session").equals("0"))
				panelInformation.setBorder(
					BorderFactory.createTitledBorder(
						null, 
						"", 
						TitledBorder.DEFAULT_JUSTIFICATION, 
						TitledBorder.DEFAULT_POSITION, 
						new Font(Font.DIALOG, Font.BOLD, 12), 
						new Color(51, 51, 51))
				  );
			

			
			// Add in Elements
			panelInformation.add(getPanelTitle(), null);
			panelInformation.add(getPanelTime(), null);
			
			// Spacer
			panelInformation.add(Box.createVerticalStrut(5));
			
			// Progress Bar
			panelInformation.add(getSliderProgress(), null);
			
			// Identifier and listener
			panelInformation.setName("HOLDER.INFORMATION");
		}
		
		// Return Instance
		return panelInformation;
	}
	// End of getPanelInformation Method

	
	/**
	 * Group container which will carry both the informational display and
	 * the album art display from the Last.fm service.
	 * <p>
	 * @return javax.swing.JPanel
	 */
	private JPanel getPanelDisplay() {
		
		// Check for Existence
		if (panelDisplay == null) {
			
			// Create Instance
			panelDisplay = new JPanel();
			
			// Setup Layout and Border
			panelDisplay.setLayout(new BoxLayout(getPanelDisplay(), BoxLayout.X_AXIS));
			panelDisplay.setBorder(
				BorderFactory.createTitledBorder(
					null, 
					"", 
					TitledBorder.DEFAULT_JUSTIFICATION, 
					TitledBorder.DEFAULT_POSITION, 
					new Font(Font.DIALOG, Font.BOLD, 12), 
					new Color(51, 51, 51))
			  );

			// Create Instance
			AlbumArtPanel panelArt = new AlbumArtPanel(
					Manager, 
					Information, 
					Logger, 
					Watcher, 
					Lang, 
					Global
				);
			
			// Insert Elements
			panelDisplay.add(getPanelInformation());
			
			// Spacer Instance
			this.add(Box.createHorizontalStrut(Constants.getNormalGap()));

			// Album Art Display
			panelDisplay.add(panelArt);
		}
		
		// Return Instance
		return panelDisplay;
	}
	// End of getPanelDisplay Method
	
	
	/**
	 * This method initialises labelTime.	
	 * <p> 	
	 * @return javax.swing.JLabel	
	 */
	private JLabel getLabelTime() {
		
		// Check for Existence
		if (labelTime == null) {
			
			// Instance and Text
			labelTime = new JLabel(
				Lang.get("Player.Information.Time")
			  );
		}
		
		// Return Instance
		return labelTime;
	}
	// End of getLabelTime Method
	

	/**
	 * This method initialises labelTitle.	
	 * <p> 	
	 * @return javax.swing.JLabel	
	 */
	private JLabel getLabelTitle() {
		
		// Check for Existence
		if (labelTitle == null) {
			
			// Create new font
			Font font_pass = new Font(Font.DIALOG, Font.BOLD, 13);
			
			/*
			 * We check to see if display is double buffered or not.  If that
			 * is the case, we then chose to make use of a ticker which moves
			 * a pixel at a time and provides a smoother look overall.
			 */
			if (Boolean.parseBoolean(Global.get("Setting.Interface.Smooth"))) {
				
				// Create Instance
				TickerBufferedLabel doubled = new TickerBufferedLabel(
						Lang.get("Player.Information.Title"),
						font_pass,
						TICKER_WIDTH
					);
				
				// Assign instance
				labelTitle = doubled;
			}
			
			/*
			 * When all else fails we fall back to a truer implementation
			 * of a ticker.   As in all of the events are swing native without
			 * custom drawing.   However the ticker moves a character at a time
			 * instead of pixel based.
			 */
			else {
				
				// Create Instance
				TickerSimpleLabel doubled = new TickerSimpleLabel(
						Lang.get("Player.Information.Title"),
						font_pass,
						TICKER_WIDTH
					);
				
				// Adjust Separator
				doubled.setSeparator(
					Lang.get("Player.Information.Separator"));
				
				// Pass up the instance
				labelTitle = doubled;
			}
		}
		
		// Return Instance
		return labelTitle;
	}
	// End of getLabelTitle Method

	
	/**
	 * This method initialises labelTitle.	
	 * <p> 	
	 * @return javax.swing.JTextField	
	 */
	private JPanel getPanelTitle() {
		
		// Check for Existence
		if (panelTitle == null) {
			
			// Create Instance
			panelTitle = new JPanel();
			
			// Establish Layout
			panelTitle.setLayout(new BoxLayout(panelTitle, BoxLayout.X_AXIS));

			// Element and glue
			panelTitle.add(getLabelTitle(), null);
			panelTitle.add(Box.createHorizontalGlue());
		}
		
		// Return Instance
		return panelTitle;
	}
	// End of getPanelTitle Method
	
	
	/**
	 * This method initialises labelTime.	
	 * <p> 	
	 * @return javax.swing.JTextField	
	 */
	private JPanel getPanelTime() {
		
		// Check for Existence
		if (panelTime == null) {
			
			// Create Instance
			panelTime = new JPanel();
			
			// Establish Layout
			panelTime.setLayout(new BoxLayout(panelTime, BoxLayout.X_AXIS));

			// Element and glue
			panelTime.add(getLabelTime(), null);
			panelTime.add(Box.createHorizontalGlue());
		}
		
		// Return Instance
		return panelTime;
	}
	// End of getPanelTime Method
	
	
	/**
	 * This method initialises sliderProgress	
	 * <p>
	 * @return javax.swing.JSlider	
	 */
	private JSlider getSliderProgress() {
		
		// Check for Existence
		if (sliderProgress == null) {
			
			// Create Instance
			sliderProgress = new JSlider();
			
			// Initial Value
			sliderProgress.setValue(0);
			
			// Identifier and Listener
			sliderProgress.setName("CONTROL.PROGRESS");
			sliderProgress.addChangeListener(Interface);
			
			// Establish Enabled State
			sliderProgress.setEnabled(Enabled.contains(
				Terminus.PROTOCOL.get("Player.Seek.ID")));
		}
		
		// Return Instance
		return sliderProgress;
	}
	// End of getSliderProgress Method
	

	/**
	 * This method initialises buttonVolume.
	 * <p>
	 * @return javax.swing.JButton	
	 */
	private JButton getButtonVolume() {
		
		// Check for Existence
		if (buttonVolume == null) {
			
			// Create Instance
			buttonVolume = new JButton();
			
			// Icon and Text Information
			buttonVolume.setIcon(new ImageIcon(
					Lyra.getIconByMap("Options-Volume")));
			buttonVolume.setToolTipText(Lang.get("Tooltip.Options.Volume"));
			
			// Identifier and Listener
			buttonVolume.setName("OPTIONS.VOLUME");
			buttonVolume.addActionListener(Interface);
			
			// Establish Enabled State
			buttonVolume.setEnabled(Enabled.contains(
				Terminus.PROTOCOL.get("Mode.Volume")));
		}
		
		// Return Instance
		return buttonVolume;
	}
	// End of getButtonVolume Method

	
	/**
	 * This method initialises buttonCrossfade.	
	 * <p>	
	 * @return javax.swing.JButton	
	 */
	private JButton getButtonCrossfade() {
		
		// Check for Existence
		if (buttonCrossfade == null) {
			
			// Create Instance
			buttonCrossfade = new JButton();
			
			// Icon and Language
			buttonCrossfade.setIcon(new ImageIcon(
					Lyra.getIconByMap("Options-Crossfade")));
			buttonCrossfade.setToolTipText(Lang.get("Tooltip.Options.Crossfade"));
			
			// Identifier and Listener
			buttonCrossfade.setName("OPTIONS.CROSSFADE");
			buttonCrossfade.addActionListener(Interface);
			
			// Establish Enabled State
			buttonCrossfade.setEnabled(Enabled.contains(
				Terminus.PROTOCOL.get("Mode.Crossfade")));
		}
		
		// Return Instance
		return buttonCrossfade;
	}
	// End of getButtonMode Method

	
	/**
	 * This method initialises toggleRandom.	
	 * <p> 	
	 * @return javax.swing.JToggleButton	
	 */
	private JToggleButton getToggleRandom() {
		
		// Check For Existence
		if (toggleRandom == null) {
			
			// Create Existence
			toggleRandom = new JToggleButton();
			
			// Icons
			toggleRandom.setIcon(new ImageIcon(
					Lyra.getIconByMap("Options-Random-False")));
			toggleRandom.setRolloverIcon(new ImageIcon(
					Lyra.getIconByMap("Options-Random-False")));
			toggleRandom.setSelectedIcon(new ImageIcon(
					Lyra.getIconByMap("Options-Random-True")));
			
			// Language Element
			toggleRandom.setToolTipText(Lang.get("Tooltip.Options.Random"));
			
			// Identifier and Action Listener
			toggleRandom.setName("OPTIONS.RANDOM");
			toggleRandom.addActionListener(Interface);
			
			// Establish Enabled State
			toggleRandom.setEnabled(Enabled.contains(
				Terminus.PROTOCOL.get("Mode.Random")));
		}
		
		// Return Instance
		return toggleRandom;
	}
	// End of getToggleRandom Method
	

	/**
	 * This method initialises toggleRepeat.	
	 * <p> 	
	 * @return javax.swing.JToggleButton	
	 */
	private JToggleButton getToggleRepeat() {
		
		// Check For Existence
		if (toggleRepeat == null) {
			
			// Create Existence
			toggleRepeat = new JToggleButton();
			
			// Icon Establishment
			toggleRepeat.setIcon(new ImageIcon(
					Lyra.getIconByMap("Options-Repeat-False")));
			toggleRepeat.setRolloverIcon(new ImageIcon(
					Lyra.getIconByMap("Options-Repeat-False")));
			toggleRepeat.setSelectedIcon(new ImageIcon(
					Lyra.getIconByMap("Options-Repeat-True")));

			// Language Elements
			toggleRepeat.setToolTipText(Lang.get("Tooltip.Options.Repeat"));
			
			// Identifier and Action Listener
			toggleRepeat.setName("OPTIONS.REPEAT");
			toggleRepeat.addActionListener(Interface);
			
			// Establish Enabled State
			toggleRepeat.setEnabled(Enabled.contains(
				Terminus.PROTOCOL.get("Mode.Repeat")));
		}
		
		// Return Instance
		return toggleRepeat;
	}
	// End of getToggleRepeat Method
	
	
	/**
	 * This method initialises toggleSingle.	
	 * <p> 	
	 * @return javax.swing.JToggleButton	
	 */
	private JToggleButton getToggleSingle() {
		
		// Check For Existence
		if (toggleSingle == null) {
			
			// Create Existence
			toggleSingle = new JToggleButton();
			
			// Icon
			toggleSingle.setIcon(new ImageIcon(
					Lyra.getIconByMap("Options-Single-False")));
			toggleSingle.setRolloverIcon(new ImageIcon(
					Lyra.getIconByMap("Options-Single-False")));
			toggleSingle.setSelectedIcon(new ImageIcon(
					Lyra.getIconByMap("Options-Single-True")));
			
			// Language
			toggleSingle.setToolTipText(Lang.get("Tooltip.Options.Single"));
			
			// Identifier and Action Listener
			toggleSingle.setName("OPTIONS.SINGLE");
			toggleSingle.addActionListener(Interface);
			
			// Establish Enabled State
			toggleSingle.setEnabled(Enabled.contains(
				Terminus.PROTOCOL.get("Mode.Single")));
		}
		
		// Return Instance
		return toggleSingle;
	}
	// End of getToggleSingle Method
	
	
	/**
	 * This method initialises panelControl.  This panel defines all of the 
	 * control buttons for play back.
	 * <p>
	 * @return javax.swing.JPanel	
	 */
	private JPanel getPanelControl() {
		
		// Check for Existence
		if (panelControl == null) {
			
			// Create Instance
			panelControl = new JPanel();
			
			// Establish Layout
			panelControl.setLayout(new BoxLayout(getPanelControl(), BoxLayout.X_AXIS));

			
			/*
			 * Primary playback control buttons used to start, stop, pause
			 * playback and navigate the current playlist.
			 */
			
			panelControl.add(getButtonBack(), null);
			panelControl.add(getButtonPlay(), null);
			panelControl.add(getButtonStop(), null);
			panelControl.add(getButtonNext(), null);
			
			// Divisional Spacer
			panelControl.add(Box.createHorizontalStrut(Constants.getNormalGap()), null);
			
			
			/*
			 * Options for the host and control. Offers the user additional
			 * capabilities and fine tuning of the host.
			 */
			
			panelControl.add(getButtonVolume(), null);
			panelControl.add(getButtonCrossfade(), null);
			
			// Divisional Spacer
			panelControl.add(Box.createHorizontalStrut(Constants.getNormalGap()), null);
			
			// State carriers
			panelControl.add(getToggleRandom(), null);
			panelControl.add(getToggleRepeat(), null);
			panelControl.add(getToggleSingle(), null);
			
			// Take up the rest of the space
			panelControl.add(Box.createHorizontalGlue(), null);
		}
		
		// Return Instance
		return panelControl;
	}
	// End of getPanelControl Method

	
	/**
	 * This method initialises iconPlay.
	 * <p>
	 * @return javax.swing.ImageIcon
	 */
	private ImageIcon getIconPlay() {
		
		// Check for Existence
		if (iconPlay == null) {
			
			// Create Instance
			iconPlay = new ImageIcon(
					Lyra.getIconByMap("Playback-Play"));
		}
		
		// Return instance
		return iconPlay;
	}
	// End of getIconPlay Method
	
	
	/**
	 * This method initialises iconPause.
	 * <p>
	 * @return javax.swing.ImageIcon
	 */
	private ImageIcon getIconPause() {
		
		// Check for Existence
		if (iconPause == null) {
			
			// Create Instance
			iconPause = new ImageIcon(
					Lyra.getIconByMap("Playback-Pause"));
		}
		
		// Return instance
		return iconPause;
	}
	// End of getIconPause Method
	
	
	/**
	 * This method initialises iconVolume.
	 * <p>
	 * @return javax.swing.ImageIcon
	 */
	private ImageIcon getIconVolume() {
		
		// Check for Existence
		if (iconVolume == null) {
			
			// Create Instance
			iconVolume = new ImageIcon(
					Lyra.getIconByMap("Options-Volume"));
		}
		
		// Return instance
		return iconVolume;
	}
	// End of getIconVolume Method
	
	
	/**
	 * This method initialises iconMute.
	 * <p>
	 * @return javax.swing.ImageIcon
	 */
	private ImageIcon getIconMute() {
		
		// Check for Existence
		if (iconMute == null) {
			
			// Create Instance
			iconMute = new ImageIcon(
					Lyra.getIconByMap("Options-Mute"));
		}
		
		// Return instance
		return iconMute;
	}
	// End of getIconMute Method
	
	/*
	 * SETTER ELEMENTS
	 * 
	 * Setter elements exist to act on behalf of the logic elements.  Logic
	 * elements should not deal with the elements directly unless it is to
	 * read certain status information.
	 */
	
	/**
	 * Adjusts the icon for the play button setting it to a pause icon.
	 */
	public void setButtonPlayAsPause() {
		
		// Adjust the icon setting it to pause
		getButtonPlay().setIcon(getIconPause());
	}
	// End of setButtonPlayAsPause Method
	
	
	/**
	 * Adjusts the icon for the play button setting it to a play icon.
	 */
	public void setButtonPlayAsPlay() {
		
		// Adjust the icon setting it to pause
		getButtonPlay().setIcon(getIconPlay());
	}
	// End of setButtonPlayAsPlay Method
	
	
	/**
	 * Adjusts the icon for the volume button setting it to a mute icon.
	 */
	public void setButtonVolumeAsMute() {
		
		// Adjust the icon setting it to pause
		getButtonVolume().setIcon(getIconMute());
	}
	// End of setButtonVolumeAsMute Method
	
	
	/**
	 * Adjusts the icon for the play button setting it to a play icon.
	 */
	public void setButtonVolumeAsVolume() {
		
		// Adjust the icon setting it to pause
		getButtonVolume().setIcon(getIconVolume());
	}
	// End of setButtonVolumeAsVolume Method
	
	
	/**
	 * Adjusts the state information for the toggle button.  
	 * <p>
	 * @param state Boolean containing state information.
	 */
	public void setToggleRandom(boolean state) {
		
		// Direct Adjustment
		getToggleRandom().setSelected(state);
	}
	// End of setToggleRandom Method
	
	
	/**
	 * Adjusts the state information for the toggle button.  
	 * <p>
	 * @param state Boolean containing state information.
	 */
	public void setToggleRepeat(boolean state) {
		
		// Direct Adjustment
		getToggleRepeat().setSelected(state);
	}
	// End of setToggleRepeat Method
	
	
	/**
	 * Adjusts the state information for the toggle button.  
	 * <p>
	 * @param state Boolean containing state information.
	 */
	public void setToggleSingle(boolean state) {
		
		// Direct Adjustment
		getToggleSingle().setSelected(state);
	}
	// End of setToggleSingle Method
	

	/**
	 * Sets the display label for the title field.
	 * <p>
	 * @param title String carrying title name.
	 */
	public void setLabelTitle(String title) {
		
		// Confirm difference and set
		if (!getLabelTitle().getText().equals(title))
			getLabelTitle().setText(title);
	}
	// End of setLabelTitle Method
	
	
	/**
	 * Sets the slider of the position and capacity.
	 * <p>
	 * @param elapsed Integer carrying current runtime.
	 * @param total Integer representing total play time.
	 */
	public void setSliderProgress(int elapsed, int total) {
		
		// Block processing
		Interface.SliderProcess = false;
		
		// We only need to adapt the maximum if its different
		if (getSliderProgress().getMaximum() != total)
			getSliderProgress().setMaximum(total);
		
		// Establish position
		getSliderProgress().setValue(elapsed);
		
		// Block processing
		Interface.SliderProcess = true;
	}
	// End of setSliderProgress Method
	
	
	/**
	 * Sets the display label for the time field.
	 * <p>
	 * @param elapsed Integer carrying current runtime.
	 * @param total Integer representing total play time.
	 */
	public void setLabelTime(int elapsed, int total) {
		
		// Parse the total elapsed time
		long time_min  = TimeUnit.SECONDS.toMinutes(elapsed);
		long time_sec = TimeUnit.SECONDS.toSeconds(elapsed - (time_min * 60));
		
		// Parse the total minutes for the song
		long total_min  = TimeUnit.SECONDS.toMinutes(total);
		long total_sec = TimeUnit.SECONDS.toSeconds(total - (total_min * 60));
		
		// Confirm difference and set 
		getLabelTime().setText(
			Lang.get("Player.Information.Template")
				.replaceAll("MEMIN", Interface.getNumber(time_min))
				.replaceAll("MESEC", Interface.getNumber(time_sec))
				.replaceAll("MTMIN", Interface.getNumber(total_min))
				.replaceAll("MTSEC", Interface.getNumber(total_sec))
		    );
	}
	// End of setLabelTime Method
	
	
	/**
	 * The code separation from the user interface.   This Class handles all
	 * of the complexities of making the interface interactive.
	 * <p>
	 * @author Martin Foster
	 */
	private class Interact implements 
		ActionListener, 
		ChangeListener,
		UpdateListener, 
		PendulumListener {

		
		/*
		 * Storage elements which contain all the state information we
		 * require.
		 */
		private Map<String,String> State;
		private List<Map<String,String>> Playlist;
		
		
		/*
		 * We need a mechanism to block updates in regards to the slider
		 * since it will trigger obscene updates and obstruct play.
		 */
		public boolean SliderProcess = true;
		
		
		/**
		 * Base constructor for the class.
		 */
		public Interact() {}
		
		
		/**
		 * Simple method which will take a number and convert it to a string
		 * maintaining elements such as an additional zero for formatting.
		 * <p>
		 * @param value Number to convert.
		 * @return String of converted and formatted number.
		 */
		public String getNumber(Long value) {
			
			// Do a quick check
			String num = (value < 10)
				? "0" + Long.toString(value)
				: Long.toString(value);
			
			// Return the number
			return num;
		}
		// End of getNumber Method
		
		
		/**
		 * Method which is used after the base is in to ensure that all of the
		 * time elements are updated to match initial state. 
		 */
		private void updateTime() {
			/*
			 * Now that we have the song and state tells us what is playing
			 * we can now discern the time related elements and pass it on
			 * for processing.
			 */
			if (!State.get("state").equals(Terminus.STATE_STOP)) {
				
				// Extract currently playing song
				Map<String,String> song 
				  = Playlist.get(Integer.parseInt(State.get("song")));
				
				// Calculate elements
				int elapsed = Integer.parseInt(State.get("time").split(":")[0]);
				int total   = Integer.parseInt(song.get("Time"));

				// Time element
				setLabelTime(elapsed, total);
			
				// Slider element
				setSliderProgress(elapsed, total);
			}
		}
		// End of updateTime Method
		
		
		/**
		 * This method will update various elements to reflect current state
		 * information from the Host itself. 
		 */
		private void updateState () {
			
			// Change over the state information
			State    = Information.getState();
			Playlist = Information.getCurrent();
			
			// Update all Elements
			statePlayer();
			stateOptions();
			stateMixer();
		}
		// End of updateState Method
		

		/**
		 * Provides an updated set of coordinates to the popup menu in order to
		 * appear as though it is attached to a widget directly.
		 * <p>
		 * @param caller Caller component to place against.
		 * @param popup Popup menu to spawn.
		 */
		private void updatePosition(JComponent caller, JPopupMenu popup) {
			
			// Extract the point
			Point pos = caller.getLocationOnScreen();
			
			// Adjust the X coordinate position
			int xpos = (int)(caller.getWidth() + pos.getX());
			int ypos = (int)pos.getY();
			
			// Adjust the position
			popup.setLocation(xpos, ypos);
		}
		// End of updatePosition Method
		
		
		/**
		 * Updates Player controls in the panel.
		 */
		private void statePlayer() {
			
			/*
			 * The Play button and how it looks changes based on the state.
			 * When playing it will display itself as a pause button while in
			 * all other states will make use of the play icon as would 
			 * normally be expected.
			 */
			
			// Pause
			if (State.get("state").equals(Terminus.STATE_PLAY))
				setButtonPlayAsPause();
			
			// Play as Default
			else setButtonPlayAsPlay();

			
			/*
			 * Update the label elements in the song display component.  Artist
			 * Time and Label are all components that we will require to update.
			 */
			if (State.get("song") != null) {
				
				// Extract currently playing song
				Map<String,String> song 
				  = Playlist.get(Integer.parseInt(State.get("song")));
				
				// Establish texts
				setLabelTitle(
					song.get("Artist") + 
					Lang.get("Player.Information.Separator") + 
					song.get("Title")
				  );
			}
			
			/*
			 * We reset to nothing once we loose track of what song is
			 * currently playing.
			 */
			else {
				
				// Adjust Title
				setLabelTitle(Lang.get("Player.Information.Title"));
				
				// Adjust time
				getLabelTime().setText(Lang.get("Player.Information.Time"));
			}
		}
		// End of statePlayer Method
		
		
		/**
		 * Updates Host Options in the panel.
		 */
		private void stateOptions() {
			
			/*
			 * Stateful buttons exist within the interface and provides the
			 * capability to control certain of the host options such as single
			 * play and random play.
			 */
			
			// Single Play
			if (State.containsKey("single"))
				setToggleSingle(
					Status.fetchState(State.get("single")));
			
			// Repeat Mode
			if (State.containsKey("repeat"))
				setToggleRepeat(
					Status.fetchState(State.get("repeat")));
			
			// Random Play
			if (State.containsKey("random"))
				setToggleRandom(
					Status.fetchState(State.get("random")));
			
			// Crossfade Control
			if (State.containsKey("xfade"))
				CrossfadeControl.setState(
						Integer.parseInt(State.get("xfade")));
		}
		// End of stateOptions Method
		
		
		/**
		 * Updates Mixer elements in the panel.
		 */
		private void stateMixer() {
			
			// Safety reset
			if (!State.containsKey("volume"))
				return;
			
			/*
			 * Volume control requires an icon change based on the volume
			 * level.  If its the case we will make sure that elements are
			 * properly in place.
			 */
			
			// Volume Control
			VolumeControl.setState(
				Integer.parseInt(State.get("volume"))
			  );
			
			// Adjustment to icon look and feel
			if (VolumeControl.getState() > 0)
				setButtonVolumeAsVolume();
			
			// Mute icon
			else setButtonVolumeAsMute();
		}
		// End of stateVolume Method
		
		
		/*
		 * LISTENERS
		 * 
		 * Listeners are how we get updates and event notifications.  This is
		 * where logic will start to play a part.
		 */

		/* (non-Javadoc)
		 * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
		 */
		public void actionPerformed(ActionEvent evt) {
			
			/*
			 * In this case we only care about buttons and will use this as a
			 * way proceed.  We use abstract button since it will catch toggles
			 * and normal buttons.
			 */
			if (evt.getSource() instanceof AbstractButton) {
				
				// Extract source name
				String name = ((AbstractButton)evt.getSource()).getName();
				
				
				/*
				 * Time to compare and see which element was actually put into
				 * use.  Very useful for us to know ahead of time.
				 * 
				 * This specific section deals with elements that are really
				 * one-to-one with the commands they would call.  No inherent
				 * complexity.
				 */
				
				// Push back an element
				if (name.equals(getButtonBack().getName()))
					Control.playPrevious();
				
				// Advance an Element
				else if (name.equals(getButtonNext().getName()))
					Control.playNext();
				
				// Stop playback altogether
				else if (name.equals(getButtonStop().getName()))
					Control.playStop();
				
				/*
				 * The Play/Pause button requires a lot more thought in the
				 * matter because the same button will change based on state
				 * and as a result needs to be tracked.
				 * 
				 * Also the play button does not normally start without knowing
				 * a position.  So we typically track what we are doing.
				 */
				else if (name.equals(getButtonPlay().getName())) {
					
					/*
					 * Stopped states makes things fairly straight forward 
					 * over all since it will simply start playback from the
					 * top of the list.
					 */
					if (State.get("state").equals(Terminus.STATE_STOP)) {
						
						// Discover position
						int pos = (State.get("song") != null)
							? Integer.parseInt(State.get("song"))
							: 0;
							
						// Start at designated position
						Control.playStart(pos);
					}
					
					/*
					 * Pause can be fairly straight forward as well since it
					 * will restart the song currently playing.  Hence we
					 * just make use of state information to recover.
					 */
					else if (State.get("state").equals(Terminus.STATE_PAUSE))
						Control.playResume();

					/*
					 * When playing the only option we have left is to pause
					 * the elements in play.
					 */
					else if (State.get("state").equals(Terminus.STATE_PLAY))
						Control.playPause();
				}
				
				/*
				 * Popup windows provide certain capabilities to the display
				 * that would be cumbersome to implement directly.   Hence we
				 * have them in hidden window.
				 */
				
				// Volume Control
				else if (name.equals(getButtonVolume().getName())) {
					
					// Inverse visibility in case
					VolumePopup.setVisible(!VolumePopup.isVisible());

					// Adjust Position
					if (VolumePopup.isVisible())
						updatePosition(
							getButtonVolume(), 
							VolumePopup
						  );
				}
					
				// Crossfade Control
				else if (name.equals(getButtonCrossfade().getName())) {
					
					// Inverse visibility in case
					CrossfadePopup.setVisible(!CrossfadePopup.isVisible());

					// Adjust Position
					if (CrossfadePopup.isVisible())
						updatePosition(
							getButtonCrossfade(), 
							CrossfadePopup
						  );
				}
				
				/*
				 * The rest of the options are stateful options only and do
				 * not carry any additional information.  
				 */
				
				// Random Play
				else if (name.equals(getToggleRandom().getName())) 
					Modes.setRandom(getToggleRandom().isSelected());
					
				// Repeat Play
				else if (name.equals(getToggleRepeat().getName())) 
					Modes.setRepeat(getToggleRepeat().isSelected());
						
				// Single Play
				else if (name.equals(getToggleSingle().getName())) 
					Modes.setSingle(getToggleSingle().isSelected());
			}
		}
		// End of actionPerformed Method


		/* (non-Javadoc)
		 * @see org.lyra.Host.UpdateListener#hostUpdated(org.lyra.Host.UpdateEvent)
		 */
		public void hostUpdated(UpdateEvent update) {
			
			// Change over the state information
			State    = Information.getState();
			Playlist = Information.getCurrent();
			
			
			/*
			 * Since this update can come from another thread we have no choice
			 * but to ensure that the update happens within the Swing thread.
			 * 
			 * We are more discerning when it comes to what updates are
			 * triggered at one time.  This should make updates a little less
			 * wasteful of resources.
			 */
			
			// Player Updates
			if ((update.isPlayer())
			 || (update.isCurrent()))

				SwingUtilities.invokeLater(new Runnable() {
					
					/* (non-Javadoc)
					 * @see java.lang.Runnable#run()
					 */
					public void run() {
						statePlayer();
					}
				});
					
			// Mixer Elements	
			if (update.isMixer())
				SwingUtilities.invokeLater(new Runnable() {
							
					/* (non-Javadoc)
					 * @see java.lang.Runnable#run()
					 */
					public void run() {
						stateMixer();
					}
				}); 
			
			// Host Options
			if (update.isOptions())
				SwingUtilities.invokeLater(new Runnable() {
							
					/* (non-Javadoc)
					 * @see java.lang.Runnable#run()
					 */
					public void run() {
						stateOptions();
					}
				});
		}
		// End of hostUpdated Method

		
		/* (non-Javadoc)
		 * @see org.lyra.Host.PendulumListener#pendulumUpdated(int, int)
		 */
		public void pendulumUpdated(int elapsed, int total) {
			
			/*
			 * If the JSlider instance is currently adjusting then we are
			 * to assume the user is fiddling with it and wants to adjust
			 * the current location it is at.   Hence none of this processing
			 * will take place.
			 */
			if (getSliderProgress().getValueIsAdjusting())
				return;

			/*
			 * Since the pendulum runs in a separate thread we can't have it
			 * make changes to Swing elements without going through the 
			 * Swing worker thread.
			 */
			
			// The thread requires finals when we pass on variables
			final int pelapsed = elapsed;
			final int ptotal = total;
			
			// Execute when worker thread has a chance
			SwingUtilities.invokeLater(new Runnable() {
				
				
				/* (non-Javadoc)
				 * @see java.lang.Runnable#run()
				 */
				public void run() {
					
					// Call both elements for update
					setLabelTime(pelapsed, ptotal);
					setSliderProgress(pelapsed, ptotal);
				}
			});
		}
		// End of pendulumUpdated Method
		

		/* (non-Javadoc)
		 * @see java.awt.event.ChangeListener#stateChanged(java.awt.event.ChangeEvent)
		 */
		public void stateChanged(ChangeEvent evt) {

			// We only deal with JSlider events
			if (evt.getSource() instanceof JSlider) {
				
				// Extract Source
				JSlider source = (JSlider)evt.getSource();
				
				// We only deal once settled
				if (!source.getValueIsAdjusting()) {
					
					// Crossfade Control
					if (source.getName().equals(CrossfadeControl.getName()))
						Modes.setCrossfade(CrossfadeControl.getState());
					
					// Volume Control
					else if (source.getName().equals(VolumeControl.getName()))
						Modes.setVolume(VolumeControl.getState());
					
					// Progress Position
					else if (source.getName().equals(getSliderProgress().getName())) {
						
						/*
						 * We only process when the event is triggered through
						 * a natural event.  A user interface update will block
						 * this event in order to prevent massive waste of
						 * resources.
						 */
						if (!SliderProcess) return;
						
						// Safety measure
						if (State.get("songid") == null) return;
						
						// Pass on command
						Control.playSeek(
							Integer.parseInt(State.get("songid")), 
							getSliderProgress().getValue()
						  );
					}
				}
			}
		}
		// End of stateChanged Method

	}
	// End of Interact Class
}
