package net.sf.fmj.gui.controlpanel;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.logging.Logger;

import javax.media.Controller;
import javax.media.ControllerClosedEvent;
import javax.media.ControllerErrorEvent;
import javax.media.ControllerEvent;
import javax.media.ControllerListener;
import javax.media.Duration;
import javax.media.MediaTimeSetEvent;
import javax.media.Player;
import javax.media.RealizeCompleteEvent;
import javax.media.ResourceUnavailableEvent;
import javax.media.RestartingEvent;
import javax.media.StartEvent;
import javax.media.StopEvent;
import javax.media.Time;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JToggleButton;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import net.sf.fmj.ejmf.toolkit.util.SourcedTimer;
import net.sf.fmj.ejmf.toolkit.util.SourcedTimerEvent;
import net.sf.fmj.ejmf.toolkit.util.SourcedTimerListener;
import net.sf.fmj.ejmf.toolkit.util.TimeSource;
import net.sf.fmj.gui.customslider.CustomSlider;
import net.sf.fmj.utility.LoggerSingleton;

/**
 * 
 * @author Warren Bloomer
 *
 */
public class SwingLookControlPanel extends JPanel implements TimeSource, SourcedTimerListener {

	// TODO: the buttons need to enable/disable based on the controller state
	private static final Logger logger = LoggerSingleton.logger;

    public static final int USE_PLAY_CONTROL 		= 0x0001;
    public static final int USE_STOP_CONTROL 		= 0x0002;
    public static final int USE_BACK_CONTROL 		= 0x0004;
    public static final int USE_FORWARD_CONTROL 	= 0x0008;
    public static final int USE_NEXT_CONTROL 		= 0x0010;
    public static final int USE_PREVIOUS_CONTROL 	= 0x0020;
    public static final int USE_POSITION_CONTROL 	= 0x0040;
    public static final int USE_POSITION_TEXT 		= 0x0080;
    public static final int USE_LENGTH_TEXT 		= 0x0100;
    public static final int USE_VOLUME_CONTROL 		= 0x0200;
    public static final int USE_MUTE_CONTROL 		= 0x0400;
    public static final int SINGLE_LINE				= 0x0800;
	
    public static final int DEFAULT_FLAGS = USE_PLAY_CONTROL | /*USE_STOP_CONTROL | USE_BACK_CONTROL | USE_FORWARD_CONTROL |*/ USE_NEXT_CONTROL | USE_PREVIOUS_CONTROL | USE_POSITION_CONTROL | USE_LENGTH_TEXT | USE_POSITION_TEXT | USE_VOLUME_CONTROL | USE_MUTE_CONTROL;
    
	private static final boolean USE_STANDARD_SLIDER = true;	// true to use a JSlider for the position slider, false to use FmjSlider
	
	private final int flags;
	private final Skin skin;
	
	public static final Skin DEFAULT_SKIN = new DefaultSkin();
	
	private Player player; 
	
	private SourcedTimer        controlTimer;
	   // Timer will fire every TIMER_TICK milliseconds
    final private static int TIMER_TICK = 250;

	private JButton playButton = null;
	private JButton stopButton = null;
	private JButton backButton = null;
	private JButton forwardButton = null;
	private JButton nextButton = null;
	private JButton previousButton = null;
	private JPanel positionPanel = null;
	private JSlider positionSlider = null;
	private JPanel buttonPanel = null;
	private JLabel positionLabel = null;
	private JLabel lengthLabel = null;
	private JPanel audioPanel = null;
	private JSlider volumeSlider = null;
	private JToggleButton muteButton = null;

	public SwingLookControlPanel(int flags, Skin skin) {
		super();
		this.flags = flags;
		this.skin = skin;
		initialize();
	}
	
	public SwingLookControlPanel(int flags, Skin skin, Player player) {
		super();
		this.flags = flags;
		this.skin = skin;
		initialize();
		setPlayer(player);
	}
	
	public SwingLookControlPanel() {
		this(DEFAULT_FLAGS, DEFAULT_SKIN);
	}
	
	public SwingLookControlPanel(Player player) {
		this(DEFAULT_FLAGS, DEFAULT_SKIN, player);
	}

	public void setPlayer(Player player) {
		this.player = player;
		
		
		final TransportControlState transportControlState = new TransportControlState();
		if (player.getState() == Controller.Started)
		{
			transportControlState.setAllowPlay(false);
			transportControlState.setAllowStop(true);
			transportControlState.setAllowVolume(player.getGainControl() != null);
		}
		else
		{
			transportControlState.setAllowPlay(true);
			transportControlState.setAllowStop(false);
			transportControlState.setAllowVolume(false); // can't get gain control on unrealized player
		}
		
		if (getPreviousButton() != null)
			getPreviousButton().setEnabled(true);
		if (getNextButton() != null)
			getNextButton().setEnabled(true);
		if (getForwardButton() != null)
			getForwardButton().setEnabled(true);
		if (getBackButton() != null)
			getBackButton().setEnabled(true);
		
		onStateChange(transportControlState);
		
	    // Setup timer
	    controlTimer = new SourcedTimer(this, TIMER_TICK);
  	    controlTimer.addSourcedTimerListener(this);
	    
  	    if (player.getState() == Controller.Started)
  	    {	
  			onDurationChange(player.getDuration().getNanoseconds());
  	    	
  	    	controlTimer.start();	// this handles the case where it is already started before we get here, in which case controllerUpdate will never get called with the initial state
  	    }
  	    
		player.addControllerListener(controllerListener);
	}
	
	/** controller listener to listen to controller events from the player */
	private ControllerListener controllerListener = new ControllerListener() {
		public void controllerUpdate(ControllerEvent event) {

			logger.fine("Got controller event: " + event);
			
			final Player player = (Player) event.getSourceController();

			if (player != SwingLookControlPanel.this.player)
				return;		// ignore messages from old players.
			
			// TODO: handle RestartingEvent
			if (event instanceof RealizeCompleteEvent) {
				// controller realized
			}
			else if (event instanceof ResourceUnavailableEvent) {
			}
			else if (event instanceof StopEvent) {

				final TransportControlState transportControlState = new TransportControlState();
				transportControlState.setAllowPlay(true);
				transportControlState.setAllowStop(false);
				transportControlState.setAllowVolume(player.getGainControl() != null);

				SwingLookControlPanel.this.onStateChange(transportControlState);

			}
			else if (event instanceof StartEvent)
			{

				final TransportControlState transportControlState = new TransportControlState();
				transportControlState.setAllowPlay(false);
				transportControlState.setAllowStop(true);
				transportControlState.setAllowVolume(player.getGainControl() != null);
				
				SwingLookControlPanel.this.onStateChange(transportControlState);
			}
			else if (event instanceof ControllerErrorEvent)
			{
			}
			else if (event instanceof ControllerClosedEvent) 
			{
			}
			
			// Slider-related: start/stop timer, etc:
			// if (isOperational())
			{
				if (event instanceof StartEvent || event instanceof RestartingEvent)
				{
		  			SwingLookControlPanel.this.onDurationChange(player.getDuration().getNanoseconds());
					SwingLookControlPanel.this.onProgressChange(getTime());
					controlTimer.start();
				} else if (event instanceof StopEvent || event instanceof ControllerErrorEvent)
				{
					controlTimer.stop();
				} else if (event instanceof MediaTimeSetEvent)
				{
		  			SwingLookControlPanel.this.onDurationChange(player.getDuration().getNanoseconds());	// just in case
		  			
					// This catches any direct setting of media time
					// by application. Additionally, it catches
					// setMediaTime(0) by StandardStopControl.
					SwingLookControlPanel.this.onProgressChange(getTime());
				}
			}
			
		}
	};

	/**
	 * This method initializes this
	 * 
	 */
	private void initialize() {
        
		if ((flags & SINGLE_LINE) != 0)
		{
			this.setLayout(new BorderLayout());
	        this.setSize(new Dimension(553, 58));
	
	        if (getButtonPanel() != null)
	        	this.add(getButtonPanel(), BorderLayout.WEST);
	        if (getPositionPanel() != null)
	        	this.add(getPositionPanel(), BorderLayout.CENTER);
	        if (getAudioPanel() != null)
	        	this.add(getAudioPanel(), BorderLayout.EAST);
		}
		else
		{
			this.setLayout(new BorderLayout());
	        this.setSize(new Dimension(553, 58));
	
	        if (getPositionPanel() != null)
	        	this.add(getPositionPanel(), BorderLayout.NORTH);
	        if (getButtonPanel() != null)
	        	this.add(getButtonPanel(), BorderLayout.WEST);
	        if (getAudioPanel() != null)
	        	this.add(getAudioPanel(), BorderLayout.EAST);
		}
        setAudioControlEnabled(false);
	}
	
	private void start() {
		if (player != null) {
			player.start();
		}
	}

	private void stop() {
		if (player != null) {
			player.stop();
			player.setMediaTime(new Time(0));
		}
	}
	private void pause() {
		if (player != null) {
			player.stop();
			//player.setPosition(0);
		}
	}
	
	private void setRate(float rate) {
		if (player != null) {
			player.setRate(rate);
		}
	}
	
	private void setGain(float gain) {
    	if (player != null && player.getGainControl() != null)
    	{
    		player.getGainControl().setLevel(gain);
    	}
	}

	private void setMute(boolean mute) {
    	if (player != null && player.getGainControl() != null)
    	{
    		player.getGainControl().setMute(mute);
    	}
	}
	
	/**
	 * This method initializes playButton	
	 * 	
	 * @return javax.swing.JButton	
	 */
	private JButton getPlayButton() {
		if ((flags & USE_PLAY_CONTROL) == 0)
			return null;
		if (playButton == null) {
			playButton = new JButton();
			//playButton.setText("play");
			playButton.setOpaque(false);
			playButton.setIcon(skin.getPlayIcon());
			playButton.addActionListener(new java.awt.event.ActionListener() {
				public void actionPerformed(java.awt.event.ActionEvent e) {
					if (playButtonIsPause)
						pause();
					else
					{
						setRate(1.0f);	// TODO: necessary?
						start();
					}
				}
			});
			playButton.setEnabled(false);
		}
		return playButton;
	}

	/**
	 * This method initializes stopButton	
	 * 	
	 * @return javax.swing.JButton	
	 */
	private JButton getStopButton() {
		if ((flags & USE_STOP_CONTROL) == 0)
			return null;

		if (stopButton == null) {
			stopButton = new JButton();
			//stopButton.setText("stop");
			stopButton.setOpaque(false);
			stopButton.setIcon(skin.getStopIcon());
			stopButton.addActionListener(new java.awt.event.ActionListener() {
				public void actionPerformed(java.awt.event.ActionEvent e) {
					stop();
				}
			});
			stopButton.setEnabled(false);
		}
		return stopButton;
	}

	/**
	 * This method initializes backButton	
	 * 	
	 * @return javax.swing.JButton	
	 */
	private JButton getBackButton() {
		if ((flags & USE_BACK_CONTROL) == 0)
			return null;

		if (backButton == null) {
			backButton = new JButton();
			//backButton.setText("back");
			backButton.setOpaque(false);
			backButton.setIcon(skin.getRewindIcon());
			backButton.addActionListener(new java.awt.event.ActionListener() {
				public void actionPerformed(java.awt.event.ActionEvent e) {
					setRate(-2.0f);
				}
			});
			backButton.setEnabled(false);
		}
		return backButton;
	}

	/**
	 * This method initializes forwardButton	
	 * 	
	 * @return javax.swing.JButton	
	 */
	private JButton getForwardButton() {
		if ((flags & USE_FORWARD_CONTROL) == 0)
			return null;

		if (forwardButton == null) {
			forwardButton = new JButton();
			//forwardButton.setText("forward");
			forwardButton.setOpaque(false);
			forwardButton.setIcon(skin.getFastForwardIcon());
			forwardButton.addActionListener(new java.awt.event.ActionListener() {
				public void actionPerformed(java.awt.event.ActionEvent e) {
					setRate(2.0f);
				}
			});
			forwardButton.setEnabled(false);
		}
		return forwardButton;
	}

	/**
	 * This method initializes nextButton	
	 * 	
	 * @return javax.swing.JButton	
	 */
	private JButton getNextButton() {
		if ((flags & USE_NEXT_CONTROL) == 0)
			return null;

		if (nextButton == null) {
			nextButton = new JButton();
			//nextButton.setText("next");
			nextButton.setIcon(skin.getStepForwardIcon());
			nextButton.setOpaque(false);
			nextButton.setEnabled(false);
		}
		return nextButton;
	}

	/**
	 * This method initializes previousButton	
	 * 	
	 * @return javax.swing.JButton	
	 */
	private JButton getPreviousButton() {
		if ((flags & USE_PREVIOUS_CONTROL) == 0)
			return null;

		if (previousButton == null) {
			previousButton = new JButton();
			previousButton.setOpaque(false);
			//previousButton.setText("previous");
			previousButton.setIcon(skin.getStepBackwardIcon());
			previousButton.setEnabled(false);
		}
		return previousButton;
	}

	/**
	 * This method initializes positionPanel	
	 * 	
	 * @return javax.swing.JPanel	
	 */
	private JPanel getPositionPanel() {
		if ((flags & USE_POSITION_CONTROL) == 0)
			return null;

		if (positionPanel == null) {
			if ((flags & USE_LENGTH_TEXT) != 0)
			{
				lengthLabel = new JLabel();
				lengthLabel.setText(nanosToString(0));
				lengthLabel.setOpaque(false);
			}
			
			if ((flags & USE_POSITION_TEXT) != 0)
			{
				positionLabel = new JLabel();
				positionLabel.setText(nanosToString(0));
				positionLabel.setOpaque(false);
			}
			positionPanel = new JPanel();
			positionPanel.setOpaque(false);
			positionPanel.setLayout(new BorderLayout());
			positionPanel.add(getPositionSlider(), BorderLayout.CENTER);
			if (positionPanel != null)
				positionPanel.add(positionLabel, BorderLayout.WEST);
			if (lengthLabel != null)
				positionPanel.add(lengthLabel, BorderLayout.EAST);
		}
		return positionPanel;
	}

	/**
	 * This method initializes positionSlider	
	 * 	
	 * @return javax.swing.JSlider	
	 */
	private JSlider getPositionSlider() {
		if ((flags & USE_POSITION_CONTROL) == 0)
			return null;

		if (positionSlider == null) {
			positionSlider = USE_STANDARD_SLIDER ? new JSlider() : new CustomSlider();
			positionSlider.setOpaque(false);
			positionSlider.setValue(0);
			positionSlider.setMinimum(0);
			positionSlider.setMaximum(0);
			positionSlider.setEnabled(false);
			positionSlider.addChangeListener(new ChangeListener() {

				public void stateChanged(ChangeEvent e)
				{	// note: this gets called whether the user updates it, or whether it is updated automatically.
					
					if (suppressProgressChangeNotification)
						return;
					
					final JSlider source = (JSlider) e.getSource();
					sliderDragInProgress = source.getValueIsAdjusting();
					if (source.getValueIsAdjusting())
						return;	// we only care about "final" positions the user has adjusted to.
					final int valueMillis = source.getValue();
					logger.fine("User adjusted position slider to (millis): " + valueMillis + " from " + source.getMinimum() + "-" + source.getMaximum());
					
					player.setMediaTime(new Time(((double) valueMillis) / 1000.0)); // TODO: use nanos
				}
				
			});
		}
		return positionSlider;
	}

	/**
	 * This method initializes buttonPanel	
	 * 	
	 * @return javax.swing.JPanel	
	 */
	private JPanel getButtonPanel() {
		if (buttonPanel == null) {
			buttonPanel = new JPanel();
			buttonPanel.setLayout(new GridBagLayout());
			buttonPanel.setOpaque(false);
			if (getPreviousButton() != null)
				buttonPanel.add(getPreviousButton(), new GridBagConstraints());
			if (getBackButton() != null)
				buttonPanel.add(getBackButton(), new GridBagConstraints());
			if (getStopButton() != null)
				buttonPanel.add(getStopButton(), new GridBagConstraints());
			if (getPlayButton() != null)
				buttonPanel.add(getPlayButton(), new GridBagConstraints());
			if (getForwardButton() != null)
				buttonPanel.add(getForwardButton(), new GridBagConstraints());
			if (getNextButton() != null)
				buttonPanel.add(getNextButton(), new GridBagConstraints());
		}
		return buttonPanel;
	}



	/**
	 * This method initializes audioPanel	
	 * 	
	 * @return javax.swing.JPanel	
	 */
	private JPanel getAudioPanel() {
		if ((flags & USE_VOLUME_CONTROL) == 0)
			return null;

		if (audioPanel == null) {
			GridBagConstraints gridBagConstraints = new GridBagConstraints();
			gridBagConstraints.fill = GridBagConstraints.VERTICAL;
			gridBagConstraints.weightx = 1.0;
			audioPanel = new JPanel();
			audioPanel.setLayout(new GridBagLayout());
			audioPanel.setOpaque(false);
			audioPanel.add(getVolumeSlider(), gridBagConstraints);
			if (getMuteButton() != null)
				audioPanel.add(getMuteButton(), new GridBagConstraints());
		}
		return audioPanel;
	}
	
	public void setAudioControlEnabled(boolean enabled) {
		if (getVolumeSlider() != null)
			getVolumeSlider().setEnabled(enabled);
		if (getMuteButton() != null)
			getMuteButton().setEnabled(enabled);
	}

	/**
	 * This method initializes volumeSlider	
	 * 	
	 * @return javax.swing.JSlider	
	 */
	private JSlider getVolumeSlider() {
		if ((flags & USE_VOLUME_CONTROL) == 0)
			return null;

		if (volumeSlider == null) {
			volumeSlider = new JSlider();
			volumeSlider.setMinimum(0);
			volumeSlider.setMaximum(100);
			volumeSlider.setValue(70);
			volumeSlider.setPreferredSize(new Dimension(100, 29));
			volumeSlider.setOpaque(false);
			volumeSlider.addChangeListener(
					new ChangeListener() {
						public void stateChanged(ChangeEvent e) {
							if (!volumeSlider.getValueIsAdjusting()) {
								float newValue = (float) volumeSlider.getValue() / 100.0f;
								setGain(newValue);
							}
						}
					}
					);
		}
		return volumeSlider;
	}

	/**
	 * This method initializes muteButton	
	 * 	
	 * @return javax.swing.JToggleButton	
	 */
	private JToggleButton getMuteButton() {
		if ((flags & USE_VOLUME_CONTROL) == 0)
			return null;

		if ((flags & USE_MUTE_CONTROL) == 0)
			return null;

		if (muteButton == null) {
			muteButton = new JToggleButton();
			muteButton.setIcon(skin.getMuteOffIcon());
			muteButton.setOpaque(false);
			muteButton.addActionListener(new ActionListener() {

				public void actionPerformed(ActionEvent e)
				{
					setMute(muteButton.isSelected());
					muteButton.setIcon(muteButton.isSelected() ? skin.getMuteOnIcon() : skin.getMuteOffIcon());
				}
				
			});
		}
		return muteButton;
	}

	boolean playButtonIsPause;
	
	public void onStateChange(TransportControlState state)
	{
		if (getStopButton() != null)
			getStopButton().setEnabled(state.isAllowStop());
		if (getPlayButton() != null)
		{	
			if (state.isAllowPlay() || state.isAllowStop())
			{
				if (state.isAllowStop())
				{	getPlayButton().setIcon(skin.getPauseIcon());
					playButtonIsPause = true;
				}
				else
				{	getPlayButton().setIcon(skin.getPlayIcon());
					playButtonIsPause = false;
				}
				getPlayButton().setEnabled(state.isAllowPlay() || state.isAllowStop());
			}
		}
		setAudioControlEnabled(state.isAllowVolume());
	}

	private static int nanosToMillis(long nanos)
	{	return (int) (nanos / 1000000L);
	}
	public void onDurationChange(long nanos)
	{
		if (nanos == Duration.DURATION_UNKNOWN.getNanoseconds() ||
			nanos == Duration.DURATION_UNBOUNDED.getNanoseconds())
		{
			if (positionSlider != null)
			{
				suppressProgressChangeNotification = true;
				positionSlider.setEnabled(false);
				suppressProgressChangeNotification = false;
			}
			if (lengthLabel != null)
				lengthLabel.setText("");
		}
		else
		{
			if (positionSlider != null)
			{
				suppressProgressChangeNotification = true;
				positionSlider.setEnabled(true);
				positionSlider.setMinimum(0);
				positionSlider.setMaximum(nanosToMillis(nanos));	// millis is good enough.
				suppressProgressChangeNotification = false;
			}
			if (lengthLabel != null)
				lengthLabel.setText(nanosToString(nanos));
		}
	}

	// TODO: make sure there are no race conditions with suppressProgressChangeNotification and sliderDragInProgress
	// TODO: make sure that after a drag is done we update to the correct position
	// TODO: should positionLabel get updated during a slider drag?
	private volatile boolean suppressProgressChangeNotification = false; // to prevent us from treating a progress change to the slider generated internally like a user one
	private volatile boolean sliderDragInProgress = false; // prevent us from moving the slider while the user is dragging it
	public void onProgressChange(long nanos)
	{
		if (positionSlider != null)
		{
			if (positionSlider.isEnabled())
			{	if (!sliderDragInProgress)
				{
					suppressProgressChangeNotification = true;
					positionSlider.setValue(nanosToMillis(nanos));
					suppressProgressChangeNotification = false;
				}
			}
		}
		if (positionLabel != null)
			positionLabel.setText(nanosToString(nanos));

	}
	
	private static String zeroPad(int i, int len)
	{
		String result = Integer.toString(i);
		while (result.length() < len)
			result = "0" + result;
		return result;
	}
	private static String nanosToString(long nanos)
	{
		final long seconds = nanos / 1000000000L;
		final long minutes = seconds / 60;
		return "" + zeroPad((int) minutes, 2) + ":" + zeroPad((int) (seconds % 60), 2);
	}

 	/**
  	* This method implements the SourcedTimerListener interface.
  	* Each timer tick causes slider thumbnail to move if a ProgressBar
  	* was built for this control panel.
  	*
  	* @see            net.sf.fmj.ejmf.toolkit.util.SourcedTimer
  	*/
    public void timerUpdate(SourcedTimerEvent e) {
     	// Since we are also the TimeSource, we can get
     	// directly from StandardControls instance.
     	// Normally, one would call e.getTime().

    	onProgressChange(getTime());

    }
    
	  // For TimeSource interface
	/**
	* As part of TimeSource interface, getTime returns
	* the current media time in nanoseconds.	
	*/
    public long getTime() {
    	if (player == null)
    		return 0L;
    	return player.getMediaNanoseconds();
    }

	/**
	* This method is used as a divisor to  convert
	* getTime to seconds.
	*/
    public long getConversionDivisor() {
         return TimeSource.NANOS_PER_SEC;
    }
} 

class TransportControlState
{
	private boolean allowStop;
	private boolean allowPlay;
	private boolean allowVolume;
	
	public boolean isAllowPlay()
	{
		return allowPlay;
	}
	public void setAllowPlay(boolean allowPlay)
	{
		this.allowPlay = allowPlay;
	}
	public boolean isAllowStop()
	{
		return allowStop;
	}
	public void setAllowStop(boolean allowStop)
	{
		this.allowStop = allowStop;
	}
	public boolean isAllowVolume()
	{
		return allowVolume;
	}
	public void setAllowVolume(boolean allowVolume)
	{
		this.allowVolume = allowVolume;
	}
	
}

