/*
jukebox - http://raf.org/jukebox/

Copyright (C) 2002 raf <raf@raf.org>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
or visit http://www.gnu.org/copyleft/gpl.html
*/

/*
jukeboxc.java - graphical jukebox network client

20021208 raf <raf@raf.org>
*/

import java.io.*;
import java.util.*;
import java.net.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;
import javax.swing.table.*;
import javax.swing.filechooser.*;

public class jukeboxc extends JFrame implements ActionListener
{
	public static void main(String[] arg)
	{
		if (arg.length >= 1 && (arg[0].equals("-h") || arg[0].equals("--help")))
		{
			System.out.println(help());
			System.exit(0);
		}

		if (arg.length >= 1 && (arg[0].equals("-V") || arg[0].equals("--version")))
		{
			System.out.println("jukeboxc.jar-0.1");
			System.exit(0);
		}

		String host = (arg.length >= 1) ? arg[0] : "jukebox";
		String port = (arg.length >= 2) ? arg[1] : "1221";
		new jukeboxc("Jukebox", host, port).show();
	}

	static String help()
	{
		return
			"NAME\n" +
			"\n" +
			"jukeboxc.jar - graphical jukebox network client\n" +
			"\n" +
			"SYNOPSIS\n" +
			"\n" +
			"  java -jar jukeboxc.jar [options] [host [port]]\n" +
			"  options:\n" +
			"    -h, --help    - Show the help message then exit\n" +
			"    -V, --version - Show the version message then exit\n" +
			"\n" +
			"DESCRIPTION\n" +
			"\n" +
			"jukeboxc.jar is a graphical client for jukeboxd, the jukebox network server.\n" +
			"The first argument, if any, specifies the hostname of the jukebox server.\n" +
			"The default host name is \"jukebox\". The second argument, if any, specifies\n" +
			"the TCP port to connect to. The default port is 1221.\n" +
			"\n" +
			"This client lets the user select music by artist, title and category. By\n" +
			"default (i.e. if no selection is entered), it plays the user's favourite\n" +
			"tracks (as specified in Jukebox tags in the table of contents files).\n" +
			"\n" +
			"The specification of selection criteria is reasonably good. Plus (+) and\n" +
			"minus (-) characters can precede search terms to require or ban their\n" +
			"presence in matching tracks. Single or double quotes can be used to group\n" +
			"terms together into a phrase (Apostrophes must be quoted with double quotes).\n" +
			"\n" +
			"There are several check boxes that affect things. The \"All\" checkbox selects\n" +
			"all tracks. The \"Strict\" checkbox causes the selection criteria to be searched\n" +
			"for in jukebox tags only. Artist names and album and track titles are not\n" +
			"considered. This means that tracks are selected by category rather than name.\n" +
			"The \"And\" checkbox makes every search term mandatory. This has the same effect\n" +
			"as adding a \"+\" character to the start of every search term that does not\n" +
			"already start with a \"+\" or \"-\"). The \"Sequential\" checkbox causes the\n" +
			"selected tracks to be played sequentially rather than in random order. When\n" +
			"tracks are played sequentially, the jukebox stops when it reaches the end of\n" +
			"the sequence. This is useful for playing a single album. The \"Continuous\"\n" +
			"checkbox causes jukebox to continue at the top of the sequence instead.\n" +
			"\n" +
			"The user can start or stop the jukebox, view selected tracks (i.e. playlists)\n" +
			"and reduce the selection manually before playing them. Playlists can also be\n" +
			"saved and loaded again to be played later. If you want to reorder the tracks\n" +
			"in a playlist, select them with the mouse and then click the up arrow button\n" +
			"or type Control-Up to move the the selection up. Click the down arrow button\n" +
			"or type Control-Down to move the selection down.\n" +
			"\n" +
			"Pressing return/enter while the search term field has focus is the same as\n" +
			"clicking on the Play button. Pressing Control-P is the same as clicking the\n" +
			"Playlist button.\n" +
			"\n" +
			"BUGS\n" +
			"\n" +
			"Does not defend against a broken/malicious jukebox server. If the server does\n" +
			"not respond when expected, this client will wait forever. Since the server\n" +
			"should always be on a locally controlled host, this shouldn't be an issue.\n" +
			"Accepting this flaw means that this program can run under java-1.2. Fixing it\n" +
			"would require at least java-1.4.\n" +
			"\n" +
			"SEE ALSO\n" +
			"\n" +
			"  rip(1), riptrack(1), mktoc(1), toc2names(1), toc2tags(1),\n" +
			"  cdr(1), cdrw(1), burn(1), burnw(1), cdbackup(1), mp3backup(1),\n" +
			"  jukebox(1), jukeboxc(1), jukeboxc.jar(1), jukeboxd(8),\n" +
			"  jukeboxd-init.d(8), jukebox.conf(5),\n" +
			"  http://raf.org/jukebox/Jukebox-HOWTO\n" +
			"\n" +
			"AUTHOR\n" +
			"\n" +
			"raf <raf@raf.org>\n";
	}

	String user = System.getProperty("user.name");
	String hostnl = System.getProperty("line.separator");
	String inetnl = "\015\012";
	JTextField search_text = new JTextField(20);
	JCheckBox all_checkbox = new JCheckBox("All");
	JCheckBox strict_checkbox = new JCheckBox("Strict");
	JCheckBox and_checkbox = new JCheckBox("And");
	JCheckBox sequential_checkbox = new JCheckBox("Sequential");
	JCheckBox continuous_checkbox = new JCheckBox("Continuous");
	JButton play_button = new JButton("Play");
	JButton stop_button = new JButton("Stop");
	JButton list_button = new JButton("Playlist");
	JButton help_button = new JButton("Help");
	JButton quit_button = new JButton("Quit");
	Playlist playlist_frame = null;
	String host;
	int port;

	public jukeboxc(String title, String host, String port)
	{
		super(title);
		this.host = host;

		try
		{
			this.port = Integer.parseInt(port);
		}
		catch (NumberFormatException e)
		{
			error("Invalid port number: '" + port + "'", e);
			System.exit(1);
		}

		setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
		addWindowListener(new WindowAdapter()
			{
				public void windowClosing(WindowEvent event)
				{
					System.exit(0);
				}
			}
		);

		setContentPane(createMainPanel());
		pack();
		Util.centreFrame(this);
	}

	JPanel createMainPanel()
	{
		JPanel panel = Util.verticalBoxPanel(5, 5, 5, 5);
		JPanel criteria_panel = Util.verticalBoxPanel("Jukebox Selection Criteria");
		JPanel checkbox_panel = Util.horizontalBoxPanel(0, 0, 0, 0);
		JPanel button_panel = Util.horizontalBoxPanel(5, 5, 5, 5);

		Dimension space = new Dimension(5, 5);
		checkbox_panel.add(Box.createHorizontalGlue());
		checkbox_panel.add(all_checkbox);
		checkbox_panel.add(Box.createRigidArea(space));
		checkbox_panel.add(strict_checkbox);
		checkbox_panel.add(Box.createRigidArea(space));
		checkbox_panel.add(and_checkbox);
		checkbox_panel.add(Box.createRigidArea(space));
		checkbox_panel.add(sequential_checkbox);
		checkbox_panel.add(Box.createRigidArea(space));
		checkbox_panel.add(continuous_checkbox);
		checkbox_panel.add(Box.createHorizontalGlue());

		sequential_checkbox.setSelected(false);
		continuous_checkbox.setSelected(true);
		continuous_checkbox.setEnabled(false);

		button_panel.add(Box.createHorizontalGlue());
		button_panel.add(play_button);
		button_panel.add(Box.createRigidArea(space));
		button_panel.add(stop_button);
		button_panel.add(Box.createRigidArea(space));
		button_panel.add(list_button);
		button_panel.add(Box.createRigidArea(space));
		button_panel.add(help_button);
		button_panel.add(Box.createRigidArea(space));
		button_panel.add(quit_button);
		button_panel.add(Box.createHorizontalGlue());

		criteria_panel.add(search_text);
		criteria_panel.add(checkbox_panel);

		panel.add(criteria_panel);
		panel.add(button_panel);

		all_checkbox.addActionListener(this);
		sequential_checkbox.addActionListener(this);
		continuous_checkbox.addActionListener(this);
		play_button.addActionListener(this);
		stop_button.addActionListener(this);
		list_button.addActionListener(this);
		help_button.addActionListener(this);
		quit_button.addActionListener(this);

		Util.redirect_enter_to_button(search_text, play_button);
		Util.redirect_key_to_button(search_text, KeyEvent.VK_P, KeyEvent.CTRL_MASK, list_button);

		play_button.setToolTipText("Play default/selected music");
		stop_button.setToolTipText("Stop playing music");
		list_button.setToolTipText("Show default/selected music (Ctrl-P)");
		help_button.setToolTipText("Show the documentation");
		quit_button.setToolTipText("Quit this application");

		all_checkbox.setToolTipText("Don't select tracks, just play anything");
		strict_checkbox.setToolTipText("Search in jukebox tags only, not in artists and titles as well");
		and_checkbox.setToolTipText("Search for tracks that match all search terms");
		sequential_checkbox.setToolTipText("Play tracks in order, not randomly");
		continuous_checkbox.setToolTipText("Play continuously, even when sequential");

		search_text.requestFocus();

		return panel;
	}

	public void actionPerformed(ActionEvent event)
	{
		Object source = event.getSource();

		if (source == play_button)
		{
			doPlay();
		}
		else if (source == stop_button)
		{
			doStop();
		}
		else if (source == list_button)
		{
			doList();
		}
		else if (source == help_button)
		{
			doHelp();
		}
		else if (source == quit_button)
		{
			System.exit(0);
		}
		else if (source == all_checkbox)
		{
			boolean all = all_checkbox.isSelected();
			search_text.setEnabled(!all);
			strict_checkbox.setEnabled(!all);
			and_checkbox.setEnabled(!all);
			if (all)
				search_text.setText("");
		}
		else if (source == sequential_checkbox)
		{
			setSequential(sequential_checkbox.isSelected(), false, true);
		}
		else if (source == continuous_checkbox)
		{
			setContinuous(continuous_checkbox.isSelected(), false, true);
		}
	}

	void setSequential(boolean sequential, boolean do_set, boolean propagate)
	{
		if (do_set)
			sequential_checkbox.setSelected(sequential);

		continuous_checkbox.setEnabled(sequential);
		setContinuous(!sequential, true, false);

		if (playlist_frame != null && propagate)
			playlist_frame.setSequential(sequential, true, false);
	}

	void setContinuous(boolean continuous, boolean do_set, boolean propagate)
	{
		if (do_set)
			continuous_checkbox.setSelected(continuous);

		if (playlist_frame != null && propagate)
			playlist_frame.setContinuous(continuous, true, false);
	}

	void doPlay()
	{
		if (!checkRequest())
			return;

		error(action(play_request()));
	}

	void doStop()
	{
		error(action(stop_request()));
	}

	void doList()
	{
		if (!checkRequest())
			return;

		String response = action(list_request());

		if (response != null)
		{
			if (response.indexOf(inetnl) == -1 && response.indexOf("\t") == -1)
			{
				error(response);
			}
			else
			{
				if (playlist_frame == null)
					playlist_frame = new Playlist(this, response);
				else
				{
					playlist_frame.set(response);
					playlist_frame.setCheckboxes();
				}

				playlist_frame.show();
			}
		}
	}

	boolean checkRequest()
	{
		String criteria = search_text.getText();
		boolean valid = true;
		String errors = "";

		for (int i = 0; i < criteria.length(); ++i)
		{
			char c = criteria.charAt(i);

			if (!Character.isDigit(c) && !Character.isLetter(c) && !Character.isWhitespace(c) && c != ',' && c != '.' && c != '?' && c != '!' && c != '+' && c != '-' && c != '"' && c != '\'')
			{
				if (errors.indexOf(c) == -1)
					errors += c;

				valid = false;
			}
		}

		if (!valid)
			error("Invalid selection criteria (The character" + (errors.length() == 1 ? "" : "s") + " \"" + errors + "\" " + (errors.length() == 1 ? "is" : "are") + " not allowed)");

		return valid;
	}

	String play_request()
	{
		String request = "user:" + user + " command:start options:";

		if (all_checkbox.isSelected())
			request += "a";
		if (strict_checkbox.isSelected())
			request += "j";
		if (and_checkbox.isSelected())
			request += "m";
		if (sequential_checkbox.isSelected())
			request += "s";
		if (continuous_checkbox.isEnabled() && continuous_checkbox.isSelected())
			request += "c";

		return request + " criteria:" + search_text.getText();
	}

	String stop_request()
	{
		return "user:" + user + " command:stop";
	}

	String list_request()
	{
		String request = "user:" + user + " command:list options:e";

		if (all_checkbox.isSelected())
			request += "a";
		if (strict_checkbox.isSelected())
			request += "j";
		if (and_checkbox.isSelected())
			request += "m";
		if (sequential_checkbox.isSelected())
			request += "s";
		if (continuous_checkbox.isEnabled() && continuous_checkbox.isSelected())
			request += "c";

		return request + " criteria:" + search_text.getText();
	}

	String action(String request)
	{
		if (!request.endsWith(inetnl))
			request += inetnl;

		try
		{
			Socket socket = new Socket(host, port); // no timeout in java :(
			InputStream in = socket.getInputStream();
			OutputStream out = socket.getOutputStream();
			out.write(request.getBytes());
			byte[] buf = new byte[1024];
			StringBuffer sb = new StringBuffer(1024);
			int bytes, length;
			while ((bytes = in.read(buf)) > 0) // no timeout in java-1.2 :(
				sb.append(new String(buf, 0, bytes));
			while ((length = sb.length()) > 0 && (sb.charAt(length - 1) == '\015' || sb.charAt(length - 1) == '\012'))
				sb.deleteCharAt(length - 1);
			in.close();
			out.close();
			socket.close();
			return sb.toString();
		}
		catch (UnknownHostException e1)
		{
			error("Unknown Host Error (" + host + ")", e1);
		}
		catch (IOException e2)
		{
			error("I/O Error (host " + host + ", port " + port + ")", e2);
		}

		return null;
	}

	void error(String msg)
	{
		error(msg, null);
	}

	void error(String msg, Throwable e)
	{
		if (msg != null || e != null)
			JOptionPane.showMessageDialog(this, Util.exceptionToDisplay(msg, e), "Error", JOptionPane.ERROR_MESSAGE);
	}

	void doHelp()
	{
		JOptionPane.showMessageDialog(this, Util.exceptionToDisplay(help(), null, 600, 500), "Help", JOptionPane.PLAIN_MESSAGE);
	}
}

class Playlist extends JFrame implements ActionListener, ListSelectionListener
{
	TrackModel track_model = new TrackModel();
	JTable track_table = new JTable(track_model);
	ListSelectionModel track_select = new DefaultListSelectionModel();
	JButton play_button = new JButton("Play");
	JButton stop_button = new JButton("Stop");
	JButton save_button = new JButton("Save");
	JButton load_button = new JButton("Load");
	JButton close_button = new JButton("Close");
	JCheckBox sequential_checkbox = new JCheckBox("Sequential");
	JCheckBox continuous_checkbox = new JCheckBox("Continuous");
	JButton up_button = new JButton(getImageIcon("up-arrow.gif"));
	JButton down_button = new JButton(getImageIcon("down-arrow.gif"));
	String playlist = null;
	jukeboxc parent;

	Playlist(jukeboxc parent, String playlist)
	{
		super("Playlist");
		this.parent = parent;
		this.playlist = playlist;
		setDefaultCloseOperation(JDialog.HIDE_ON_CLOSE);
		setContentPane(createPanel());
		pack();
		Util.centreFrame(this);
	}

	Icon getImageIcon(String name)
	{
		return new ImageIcon(this.getClass().getResource(name));
	}

	JPanel createPanel()
	{
		JPanel panel = Util.verticalBoxPanel(5, 5, 5, 5);

		panel.add(createTablePanel());
		panel.add(Box.createRigidArea(new Dimension(5, 5)));
		panel.add(createButtonPanel());

		return panel;
	}

	JPanel createTablePanel()
	{
		JPanel panel = new JPanel();

		track_select.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
		track_select.addListSelectionListener(this);
		track_table.setSelectionModel(track_select);
		track_model.allowSortByHeader(track_table);

		track_table.getColumnModel().getColumn(0).setPreferredWidth(150);
		track_table.getColumnModel().getColumn(1).setPreferredWidth(150);
		track_table.getColumnModel().getColumn(2).setPreferredWidth(300);

		JScrollPane scroll_pane = new JScrollPane(track_table);
		scroll_pane.setPreferredSize(new Dimension(600, 500));

		panel.setLayout(new BorderLayout());
		panel.setBorder(Util.createEtchedTitledBorder("Selected Tracks"));
		panel.add(scroll_pane, BorderLayout.CENTER);

		set(playlist);

		track_table.addKeyListener
		(
			new KeyAdapter()
			{
				public void keyPressed(KeyEvent event)
				{
					int keycode = event.getKeyCode();
					int modifiers = event.getModifiers();

					if (modifiers != KeyEvent.CTRL_MASK)
						return;

					switch (keycode)
					{
						case KeyEvent.VK_UP:
						case KeyEvent.VK_LEFT:
							doUp();
							break;

						case KeyEvent.VK_DOWN:
						case KeyEvent.VK_RIGHT:
							doDown();
							break;
					}
				}
			}
		);

		return panel;
	}

	JPanel createButtonPanel()
	{
		JPanel panel = Util.horizontalBoxPanel(5, 5, 5, 5);

		JPanel panel1 = Util.verticalBoxPanel(0, 0, 0, 0);
		Dimension size = new Dimension(25, 13);
		up_button.setPreferredSize(size);
		down_button.setPreferredSize(size);
		panel1.add(up_button);
		panel1.add(down_button);

		Dimension space = new Dimension(5, 0);
		panel.add(panel1);
		panel.add(Box.createHorizontalGlue());
		panel.add(play_button);
		panel.add(Box.createRigidArea(space));
		panel.add(stop_button);
		panel.add(Box.createRigidArea(space));
		panel.add(save_button);
		panel.add(Box.createRigidArea(space));
		panel.add(load_button);
		panel.add(Box.createRigidArea(space));
		panel.add(close_button);
		panel.add(Box.createRigidArea(space));
		panel.add(sequential_checkbox);
		panel.add(Box.createRigidArea(space));
		panel.add(continuous_checkbox);
		panel.add(Box.createHorizontalGlue());

		play_button.addActionListener(this);
		stop_button.addActionListener(this);
		save_button.addActionListener(this);
		load_button.addActionListener(this);
		close_button.addActionListener(this);
		up_button.addActionListener(this);
		down_button.addActionListener(this);
		sequential_checkbox.addActionListener(this);
		continuous_checkbox.addActionListener(this);

		play_button.setToolTipText("Play the selected tracks");
		stop_button.setToolTipText("Stop playing music");
		save_button.setToolTipText("Save the selected tracks to a file");
		load_button.setToolTipText("Load a playlist from a file");
		close_button.setToolTipText("Close the playlist window");
		up_button.setToolTipText("Move the selected tracks up (Ctrl-Up)");
		down_button.setToolTipText("Move the selected tracks down (Ctrl-Down)");
		sequential_checkbox.setToolTipText("Play tracks in order, not randomly");
		continuous_checkbox.setToolTipText("Play continuously, even when sequential");

		setCheckboxes();

		return panel;
	}

	public void actionPerformed(ActionEvent event)
	{
		Object source = event.getSource();

		if (source == play_button)
		{
			doPlay();
		}
		else if (source == stop_button)
		{
			parent.doStop();
		}
		else if (source == save_button)
		{
			doSave();
		}
		else if (source == load_button)
		{
			doLoad();
		}
		else if (source == close_button)
		{
			setVisible(false);
		}
		else if (source == up_button)
		{
			doUp();
		}
		else if (source == down_button)
		{
			doDown();
		}
		else if (source == sequential_checkbox)
		{
			setSequential(sequential_checkbox.isSelected(), false, true);
		}
		else if (source == continuous_checkbox)
		{
			setContinuous(continuous_checkbox.isSelected(), false, true);
		}
	}

	void setSequential(boolean sequential, boolean do_set, boolean propagate)
	{
		if (do_set)
			sequential_checkbox.setSelected(sequential);

		continuous_checkbox.setEnabled(sequential);
		setContinuous(!sequential, true, false);

		if (propagate)
			parent.setSequential(sequential, true, false);
	}

	void setContinuous(boolean continuous, boolean do_set, boolean propagate)
	{
		if (do_set)
			continuous_checkbox.setSelected(continuous);

		if (propagate)
			parent.setContinuous(continuous, true, false);
	}

	void setCheckboxes()
	{
		sequential_checkbox.setSelected(parent.sequential_checkbox.isSelected());
		continuous_checkbox.setSelected(parent.continuous_checkbox.isSelected());
		continuous_checkbox.setEnabled(parent.continuous_checkbox.isEnabled());
	}

	void set(String playlist)
	{
		this.playlist = playlist;

		Vector tracks = Util.split(this.playlist, parent.inetnl);

		track_model.removeAllRows();
		String previous_artist = "";
		String previous_album = "";

		for (Iterator ti = tracks.iterator(); ti.hasNext(); )
		{
			String track = (String)ti.next();
			Vector items = Util.split(track, "\t");

			if (items.size() != 4)
				continue;

			String artist = (String)items.get(0);
			String album = (String)items.get(1);
			String title = (String)items.get(2);
			String file = (String)items.get(3);

			if (artist.equals(""))
				artist = previous_artist;
			if (album.equals(""))
				album = previous_album;

			previous_artist = artist;
			previous_album = album;

			track_model.addRow(new TrackData(artist, album, title, file));
		}

		if (track_model.getRowCount() == 0)
			track_select.clearSelection();
		else
			track_select.setSelectionInterval(0, track_model.getRowCount() - 1);

		track_table.revalidate();
		track_table.repaint();
	}

	void doPlay()
	{
		int min = track_select.getMinSelectionIndex();
		int max = track_select.getMaxSelectionIndex();

		if (min == -1 || max == -1)
		{
			error("No tracks selected");
			return;
		}

		StringBuffer playlist = new StringBuffer(64 * 1024);

		for (int i = min; i <= max; ++i)
		{
			if (track_select.isSelectedIndex(i))
			{
				TrackData track_data = (TrackData)track_model.getRow(i);
				playlist.append(track_data.file);
				playlist.append(parent.inetnl);
			}
		}

		playlist.append(parent.inetnl);

		error(parent.action(playlist_request(playlist)));
	}

	String playlist_request(StringBuffer playlist)
	{
		String request = "user:" + parent.user;

		request += " command:file options:f";

		if (sequential_checkbox.isSelected())
			request += "s";
		if (continuous_checkbox.isEnabled() && continuous_checkbox.isSelected())
			request += "c";

		request += " criteria:";
		request += parent.inetnl;
		request += playlist.toString();

		return request;
	}

	void doSave()
	{
		int min = track_select.getMinSelectionIndex();
		int max = track_select.getMaxSelectionIndex();

		if (min == -1 || max == -1)
		{
			error("No tracks selected");
			return;
		}

		JFileChooser fc = new JFileChooser();
		fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
		int rc = fc.showSaveDialog(this);
		if (rc != JFileChooser.APPROVE_OPTION)
			return;

		File file = fc.getSelectedFile();
		if (file == null)
			return;

		StringBuffer playlist = new StringBuffer(64 * 1024);

		for (int i = min; i <= max; ++i)
		{
			if (track_select.isSelectedIndex(i))
			{
				TrackData track_data = (TrackData)track_model.getRow(i);
				playlist.append(track_data.artist);
				playlist.append('\t');
				playlist.append(track_data.album);
				playlist.append('\t');
				playlist.append(track_data.title);
				playlist.append('\t');
				playlist.append(track_data.file);
				playlist.append(parent.hostnl);
			}
		}

		try
		{
			PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(file)));
			writer.print(playlist.toString());
			writer.flush();
			writer.close();
		}
		catch (IOException exp)
		{
			JOptionPane.showMessageDialog(null,
				"File: " + file.getName() + "\n" + exp.toString(),
				"Error", JOptionPane.ERROR_MESSAGE);
		}
	}

	void doLoad()
	{
		JFileChooser fc = new JFileChooser();
		fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
		int rc = fc.showOpenDialog(this);
		if (rc != JFileChooser.APPROVE_OPTION)
			return;

		File file = fc.getSelectedFile();
		if (file == null)
			return;

		StringBuffer playlist = new StringBuffer(64 * 1024);

		try
		{
			BufferedReader bfr = new BufferedReader(new FileReader(file));

			for (;;)
			{
				String line = bfr.readLine();
				if (line == null)
					break;

				playlist.append(line);
				playlist.append(parent.inetnl);
			}

			bfr.close();
		}
		catch (IOException exp)
		{
			JOptionPane.showMessageDialog(null,
				"File: " + file.getName() + "\n" + exp.toString(),
				"Error", JOptionPane.ERROR_MESSAGE);
			return;
		}

		set(playlist.toString());
	}

	void doUp()
	{
		if (track_select.isSelectionEmpty())
			return;

		int min = track_select.getMinSelectionIndex();
		int max = track_select.getMaxSelectionIndex();
		int rows = track_model.getRowCount();
		int j;

		for (int i = min; i <= max; ++i)
		{
			if (track_select.isSelectedIndex(i))
			{
				for (j = i + 1; j < rows && track_select.isSelectedIndex(j); ++j)
				{}

				if (i != 0)
				{
					Object row = track_model.getRow(i - 1);
					track_model.insertRow(row, j);
					track_model.removeRow(i - 1);
					track_select.addSelectionInterval(i - 1, i - 1);
					track_select.removeSelectionInterval(j - 1, j - 1);
				}

				i = j;
			}
		}
	}

	void doDown()
	{
		if (track_select.isSelectionEmpty())
			return;

		int min = track_select.getMinSelectionIndex();
		int max = track_select.getMaxSelectionIndex();
		int rows = track_model.getRowCount();
		int j;

		for (int i = max; i >= min; --i)
		{
			if (track_select.isSelectedIndex(i))
			{
				for (j = i - 1; j >= 0 && track_select.isSelectedIndex(j); --j)
				{}

				if (i != rows - 1)
				{
					Object row = track_model.getRow(i + 1);
					track_model.removeRow(i + 1);
					track_model.insertRow(row, j + 1);
					track_select.addSelectionInterval(i + 1, i + 1);
					track_select.removeSelectionInterval(j + 1, j + 1);
				}

				i = j;
			}
		}
	}

	public void valueChanged(ListSelectionEvent event)
	{}

	void error(String msg)
	{
		error(msg, null);
	}

	void error(String msg, Throwable e)
	{
		if (msg != null || e != null)
			JOptionPane.showMessageDialog(this, Util.exceptionToDisplay(msg, e), "Error", JOptionPane.ERROR_MESSAGE);
	}
}

class TrackModel extends TableDataModel
{
	public int getColumnCount()
	{
		return 3;
	}

	public String getColumnName(int col)
	{
		switch (col)
		{
			case 0: return "Artist";
			case 1: return "Album";
			case 2: return "Title";
		}

		return null;
	}

	public Object getValueAt(int row, int col)
	{
		TrackData track = (TrackData)getRow(row);

		switch (col)
		{
			case 0: return track.artist;
			case 1: return track.album;
			case 2: return track.title;
		}

		return null;
	}

	public int compareRows(Object a, Object b, int col)
	{
		TrackData data1 = (TrackData)a;
		TrackData data2 = (TrackData)b;

		switch (col)
		{
			case 0: return compareString(data1.artist, data2.artist);
			case 1: return compareString(data1.album, data2.album);
			case 2: return compareString(data1.title, data2.title);
		}

		return 0;
	}
}

class TrackData
{
	String artist;
	String album;
	String title;
	String file;

	TrackData(String artist, String album, String title, String file)
	{
		this.artist = artist;
		this.album = album;
		this.title = title;
		this.file = file;
	}
}

class Util
{
	static Vector split(String string, String delimiter)
	{
		Vector tokens = new Vector();
		int last = 0;

		for (int pos = string.indexOf(delimiter); pos != -1; pos = string.indexOf(delimiter, pos))
		{
			tokens.add(string.substring(last, pos));
			last = pos += delimiter.length();
		}

		tokens.add(string.substring(last));

		return tokens;
	}

	static JPanel horizontalBoxPanel(int top, int left, int bottom, int right)
	{
		JPanel p = new JPanel();
		p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
		p.setBorder(BorderFactory.createEmptyBorder(top, left, bottom, right));
		return p;
	}

	static JPanel verticalBoxPanel(String title)
	{
		JPanel p = new JPanel();
		p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
		p.setBorder(createEtchedTitledBorder(title));
		return p;
	}

	static JPanel verticalBoxPanel(int top, int left, int bottom, int right)
	{
		JPanel p = new JPanel();

		p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
		p.setBorder(BorderFactory.createEmptyBorder(top, left, bottom, right));

		return p;
	}

	private static final Border etched_border = BorderFactory.createCompoundBorder(
		BorderFactory.createEtchedBorder(),
		BorderFactory.createEmptyBorder(5, 5, 5, 5));

	static Border createEtchedTitledBorder(String title)
	{
		return BorderFactory.createTitledBorder(etched_border, title);
	}

	static void redirect_key_to_button(JComponent component, final int key, final int modifiers, final JButton button)
	{
		component.addKeyListener(
			new KeyAdapter()
			{
				public void keyPressed(KeyEvent event)
				{
					if (event.getKeyCode() == key && event.getModifiers() == modifiers)
						button.doClick();
				}
			}
		);
	}

	static void redirect_enter_to_button(JComponent component, JButton button)
	{
		redirect_key_to_button(component, KeyEvent.VK_ENTER, 0, button);
	}

	static void centreFrame(Component frame)
	{
		Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
		if (screen == null)
			return;

		int height = (frame.getHeight() > screen.height) ? screen.height : frame.getHeight();
		int width = (frame.getWidth() > screen.width) ? screen.width : frame.getWidth();
		frame.setSize(width, height);

		int x = screen.width / 2 -  frame.getWidth() / 2;
		int y = screen.height / 2 - frame.getHeight() / 2;
		frame.setLocation(x, y);
	}

	static String exceptionToString(String message, Throwable exp)
	{
		StringWriter sw = new StringWriter();
		PrintWriter pw = new PrintWriter(sw);

		pw.println(message);

		if (exp != null)
		{
			pw.println();
			pw.println("Message: " + exp.getMessage());
			exp.printStackTrace(pw);
		}

		return sw.toString();
	}

	static Object exceptionToDisplay(String message, Throwable exp)
	{
		return exceptionToDisplay(message, exp, 300, 150);
	}

	static Object exceptionToDisplay(String message, Throwable exp, int width, int height)
	{
		if (exp == null && message.indexOf("\n") == -1)
			return message;

		JTextArea ta = new JTextArea();
		ta.setEditable(false);
		ta.append(exceptionToString(message, exp));
		ta.getCaret().setDot(0);
		JScrollPane sp = new JScrollPane(ta);
		sp.setPreferredSize(new Dimension(width, height));

		return sp;
	}
}

abstract class TableDataModel extends AbstractTableModel
{
	private Vector vec_data = new Vector();
	private JTable sort_table = null;
	private int sort_column = 0;
	private boolean sort_ascending;
	private Comparator sort_comparator = null;

	public int compareRows(Object data1, Object data2, int column)
	{
		return 0;
	}

	public int getRowCount()
	{
		return vec_data.size();
	}

	public Object getRow(int row)
	{
		return vec_data.elementAt(row);
	}

	public synchronized void addRow(Object data)
	{
		vec_data.addElement(data);
	}

	public synchronized void insertRow(Object data, int row)
	{
		vec_data.insertElementAt(data, row);
	}

	public synchronized void removeRow(Object data)
	{
		vec_data.removeElement(data);
	}

	public synchronized void removeRow(int i)
	{
		vec_data.removeElementAt(i);
	}

	public synchronized void removeAllRows()
	{
		vec_data.removeAllElements();
	}

	public synchronized int indexOf(Object data)
	{
		return vec_data.indexOf(data);
	}

	public synchronized void setRowAt(Object data, int row)
	{
		vec_data.setElementAt(data, row);
	}

	public boolean isCellEditable(int row, int col)
	{
		return false;
	}

	public void sortByColumn(int column, boolean ascending)
	{
		if (sort_table != null)
			sort_table.clearSelection();

		if (column >= 0)
			sort_column = column;
		else if (sort_column < 0 || sort_column > getColumnCount())
			sort_column = 0;

		sort_ascending = ascending;
		int rows = getRowCount();
		if (rows > 0)
		{
			Object[] sort_data = vec_data.toArray();

			if (sort_comparator == null)
			{
				sort_comparator = new Comparator()
				{
					public int compare(Object a, Object b)
					{
						return compareRows(a, b, sort_column);
					}

					public boolean equals(Object b)
					{
						return false;
					}
				};
			}

			Arrays.sort(sort_data, sort_comparator);
			vec_data.removeAllElements();

			if (sort_ascending)
			{
				for (int i = 0; i < rows; i++)
					vec_data.addElement(sort_data[i]);
			}
			else
			{
				for (int i = 0; i < rows; i++)
					vec_data.addElement(sort_data[rows - i - 1]);
			}

			if (sort_table != null)
			{
				sort_table.revalidate();
				sort_table.repaint();
			}
		}
	}

	void allowSortByHeader(JTable table)
	{
		sort_table = table;

		MouseAdapter ma = new MouseAdapter()
		{
			public void mouseClicked(MouseEvent e)
			{
				TableColumnModel cm = sort_table.getColumnModel();
				int viewColumn = cm.getColumnIndexAtX(e.getX());
				int column = sort_table.convertColumnIndexToModel(viewColumn);
				if (e.getClickCount() == 1 && column != -1)
				{
					int shiftPressed = e.getModifiers() & InputEvent.SHIFT_MASK;
					boolean ascending = (shiftPressed == 0);
					sortByColumn(column, ascending);
				}
			}
		};

		table.setColumnSelectionAllowed(false);
		table.getTableHeader().addMouseListener(ma);
	}

	int compareString(String s1, String s2)
	{
		return (s1 != null ? s1 : "").compareTo(s2 != null ? s2 : "");
	}
}

// vi:set ts=4 sw=4
