/*
 *   JSampler - a java front-end for LinuxSampler
 *
 *   Copyright (C) 2005-2009 Grigor Iliev <grigor@grigoriliev.com>
 *
 *   This file is part of JSampler.
 *
 *   JSampler is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License version 2
 *   as published by the Free Software Foundation.
 *
 *   JSampler 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 JSampler; if not, write to the Free Software
 *   Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 *   MA  02111-1307  USA
 */

package org.jsampler.view.std;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Window;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;

import java.util.logging.Level;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.JWindow;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.TransferHandler;

import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;

import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;
import javax.swing.text.StyledDocument;

import org.jsampler.CC;
import org.jsampler.DefaultLSConsoleModel;
import org.jsampler.HF;
import org.jsampler.OrchestraInstrument;
import org.jsampler.JSPrefs;
import org.jsampler.LSConsoleModel;
import org.jsampler.LscpUtils;

import org.jsampler.event.LSConsoleEvent;
import org.jsampler.event.LSConsoleListener;

import static javax.swing.KeyStroke.*;
import static org.jsampler.view.std.JSLSConsolePane.*;
import static org.jsampler.view.std.StdPrefs.*;


/**
 *
 * @author Grigor Iliev
 */
public class JSLSConsolePane extends JPanel {
	private enum AutocompleteMode { AUTOCOMPLETE, HISTORY_SEARCH, CMD_LIST_SEARCH }
	private AutocompleteMode autocompleteMode = AutocompleteMode.AUTOCOMPLETE;
	
	private Window owner;
	
	private final LSConsoleTextPane console = new LSConsoleTextPane();
	
	private final JPanel inputPane = new JPanel();
	private final JLabel lInput = new JLabel();
	private final JTextField tfSearch = new JTextField();
	private final CmdLineTextField tfInput = new CmdLineTextField();
	
	private AutoCompleteWindow autoCompleteWindow;
	
	private final LSConsoleModel model = new DefaultLSConsoleModel();
	
	private boolean processingSearch = false;
	
	protected JPanel mainPane = new JPanel();
	
	
	/**
	 * Creates a new instance of <code>JSLSConsolePane</code>.
	 */
	public
	JSLSConsolePane(Window owner) {
		setOwner(owner);
		setLayout(new java.awt.BorderLayout());
		
		int i = preferences().getIntProperty(LS_CONSOLE_HISTSIZE);
		model.setCommandHistorySize(i);
		
		loadConsoleHistory();
		
		mainPane.setOpaque(false);
		mainPane.setLayout(new BoxLayout(mainPane, BoxLayout.Y_AXIS));
		
		i = preferences().getIntProperty(LS_CONSOLE_BACKGROUND_COLOR);
		setBackgroundColor(new Color(i));
		
		i = preferences().getIntProperty(LS_CONSOLE_TEXT_COLOR);
		setTextColor(new Color(i));
		
		mainPane.add(new JScrollPane(console));
		
		inputPane.setLayout(new BoxLayout(inputPane, BoxLayout.X_AXIS));
		inputPane.setBorder(tfInput.getBorder());
		tfInput.setBorder(BorderFactory.createEmptyBorder());
		tfSearch.setBorder(BorderFactory.createEmptyBorder());
		
		lInput.setOpaque(false);
		lInput.setVisible(false);
		inputPane.add(lInput);
		
		Dimension d = new Dimension(Short.MAX_VALUE, tfSearch.getPreferredSize().height);
		tfSearch.setMaximumSize(d);
		
		tfSearch.setVisible(false);
		tfSearch.setFocusTraversalKeysEnabled(false);
		inputPane.add(tfSearch);
		
		d = new Dimension(Short.MAX_VALUE, tfInput.getPreferredSize().height);
		tfInput.setMaximumSize(d);
		
		tfInput.setFocusTraversalKeysEnabled(false);
		inputPane.add(tfInput);
		
		mainPane.add(inputPane);
		
		add(mainPane);
		
		tfInput.addActionListener(getHandler());
		tfInput.getDocument().addDocumentListener(getHandler());
		getModel().addLSConsoleListener(getHandler());
		
		tfSearch.addActionListener(new Actions(Actions.APPLY_SEARCH));
		
		installKeyboardListeners();
	}
	
	private JSPrefs
	preferences() { return CC.getViewConfig().preferences(); }
	
	private void
	installKeyboardListeners() {
		KeyStroke k = getKeyStroke(KeyEvent.VK_TAB, 0);
		tfInput.getInputMap(WHEN_FOCUSED).put(k, Actions.AUTOCOMPLETE);
		tfInput.getActionMap().put(Actions.AUTOCOMPLETE, new Actions(Actions.AUTOCOMPLETE));
		
		k = getKeyStroke(KeyEvent.VK_R, InputEvent.CTRL_MASK);
		tfInput.getInputMap(WHEN_FOCUSED).put(k, Actions.HISTORY_SEARCH);
		tfInput.getActionMap().put (
			Actions.HISTORY_SEARCH, new Actions(Actions.HISTORY_SEARCH)
		);
		
		k = getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_MASK);
		tfInput.getInputMap(WHEN_FOCUSED).put(k, Actions.CMD_LIST_SEARCH);
		tfInput.getActionMap().put (
			Actions.CMD_LIST_SEARCH, new Actions(Actions.CMD_LIST_SEARCH)
		);
		
		k = getKeyStroke(KeyEvent.VK_UP, 0);
		tfInput.getInputMap(JComponent.WHEN_FOCUSED).put(k, Actions.MOVE_UP);
		tfInput.getActionMap().put(Actions.MOVE_UP, new Actions(Actions.MOVE_UP));
		
		k = getKeyStroke(KeyEvent.VK_DOWN, 0);
		tfInput.getInputMap(WHEN_FOCUSED).put(k, Actions.MOVE_DOWN);
		tfInput.getActionMap().put(Actions.MOVE_DOWN, new Actions(Actions.MOVE_DOWN));
		
		k = getKeyStroke(KeyEvent.VK_HOME, 0);
		tfInput.getInputMap(WHEN_FOCUSED).put(k, Actions.MOVE_HOME);
		tfInput.getActionMap().put(Actions.MOVE_HOME, new Actions(Actions.MOVE_HOME));
		
		k = getKeyStroke(KeyEvent.VK_END, 0);
		tfInput.getInputMap(WHEN_FOCUSED).put(k, Actions.MOVE_END);
		tfInput.getActionMap().put(Actions.MOVE_END, new Actions(Actions.MOVE_END));
		
		k = getKeyStroke(KeyEvent.VK_L, InputEvent.CTRL_MASK);
		tfInput.getInputMap(WHEN_FOCUSED).put(k, Actions.CLEAR_CONSOLE);
		tfInput.getActionMap().put (
			Actions.CLEAR_CONSOLE, new Actions(Actions.CLEAR_CONSOLE)
		);
		
		k = getKeyStroke(KeyEvent.VK_ESCAPE, 0);
		tfInput.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put (
			k, Actions.CANCEL_SELECTION
		);
		tfInput.getActionMap().put (
			Actions.CANCEL_SELECTION, new Actions(Actions.CANCEL_SELECTION)
		);
		
		k = getKeyStroke(KeyEvent.VK_D, InputEvent.CTRL_MASK);
		tfInput.getInputMap(WHEN_FOCUSED).put(k, Actions.QUIT_SESSION);
		tfInput.getActionMap().put(Actions.QUIT_SESSION, new Actions(Actions.QUIT_SESSION));
		
		
		k = getKeyStroke(KeyEvent.VK_UP, 0);
		tfSearch.getInputMap(WHEN_FOCUSED).put(k, Actions.MOVE_UP);
		tfSearch.getActionMap().put(Actions.MOVE_UP, new Actions(Actions.MOVE_UP));
		
		k = getKeyStroke(KeyEvent.VK_DOWN, 0);
		tfSearch.getInputMap(WHEN_FOCUSED).put(k, Actions.MOVE_DOWN);
		tfSearch.getActionMap().put(Actions.MOVE_DOWN, new Actions(Actions.MOVE_DOWN));
		
		k = getKeyStroke(KeyEvent.VK_HOME, 0);
		tfSearch.getInputMap(WHEN_FOCUSED).put(k, Actions.MOVE_HOME);
		tfSearch.getActionMap().put(Actions.MOVE_HOME, new Actions(Actions.MOVE_HOME));
		
		k = getKeyStroke(KeyEvent.VK_END, 0);
		tfSearch.getInputMap(WHEN_FOCUSED).put(k, Actions.MOVE_END);
		tfSearch.getActionMap().put(Actions.MOVE_END, new Actions(Actions.MOVE_END));
		
		k = getKeyStroke(KeyEvent.VK_ESCAPE, 0);
		tfSearch.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put (
			k, Actions.CANCEL_SEARCH
		);
		tfSearch.getActionMap().put (
			Actions.CANCEL_SEARCH, new Actions(Actions.CANCEL_SEARCH)
		);
	}
	
	public void
	disconnect() { if(model != null) ((DefaultLSConsoleModel)model).disconnect(); }
	
	private void
	loadConsoleHistory() {
		String s = CC.getJSamplerHome();
		if(s == null) return;
		
		File f = new File(s + File.separator + "console_history");
		if(!f.isFile()) return;
		
		try {
			BufferedReader br = new BufferedReader(new FileReader(f));
			s = br.readLine();
			while(s != null) {
				getModel().addToCommandHistory(s);
				s = br.readLine();
			}
			br.close();
		} catch(Exception x) {
			CC.getLogger().log(Level.INFO, HF.getErrorMessage(x), x);
		}
	}
	
	protected JTextPane
	getLSConsoleTextPane() { return console; }
	
	/**
	 * Saves the console history into the <code>console_history</code>
	 * file, located in the JSampler's home directory.
	 */
	public void
	saveConsoleHistory() {
		if(CC.getJSamplerHome() == null) return;
		
		try {
			String s = CC.getJSamplerHome() + File.separator + "console_history";
			FileOutputStream fos = new FileOutputStream(s, false);
			
			for(String line : getModel().getCommandHistory()) {
				fos.write(line.getBytes("US-ASCII"));
				fos.write('\n');
			}
			
			fos.close();
		} catch(Exception x) {
			CC.getLogger().log(Level.INFO, HF.getErrorMessage(x), x);
		}
	}
	
	/**
	 * Delete the <code>console_history</code> file, located in the
	 * JSampler's home directory.
	 */
	public static void
	clearConsoleHistory() {
		String s = CC.getJSamplerHome();
		if(s == null) return;
		
		File f = new File(s + File.separator + "console_history");
		if(f.isFile()) f.delete();
	}
	
	/**
	 * Gets the LS Console data model.
	 * @return The LS Console data model.
	 */
	public LSConsoleModel
	getModel() { return model; }
	
	/**
	 * Sets the text color of the LS Console.
	 * @param c The text color of the LS Console.
	 */
	public void
	setTextColor(Color c) {
		console.setTextColor(c);
		
		lInput.setForeground(c);
		tfInput.setForeground(c);
		tfSearch.setForeground(c);
		
		autoCompleteWindow.setTextColor(c);
	}
	
	/**
	 * Gets the text color of the LS Console.
	 * @return The text color of the LS Console.
	 */
	public Color
	getTextColor() { return console.getTextColor(); }
	
	/**
	 * Sets the background color of the LS Console.
	 * @param c The background color of the LS Console.
	 */
	public void
	setBackgroundColor(Color c) {
		console.setBackground(c);
		
		lInput.setBackground(c);
		inputPane.setBackground(c);
		tfInput.setBackground(c);
		tfSearch.setBackground(c);
		
		autoCompleteWindow.setBackgroundColor(c);
	}
	
	/**
	 * Gets the background color of the LS Console.
	 * @return The background color of the LS Console.
	 */
	public Color
	getBackgroundColor() { return console.getBackground(); }
	
	/**
	 * Sets the notification messages' color.
	 * @param c The notification messages' color.
	 */
	public void
	setNotifyColor(Color c) { console.setNotifyColor(c); }
	
	/**
	 * Sets the warning messages' color.
	 * @param c The warning messages' color.
	 */
	public void
	setWarningColor(Color c) { console.setWarningColor(c); }
	
	/**
	 * Sets the error messages' color.
	 * @param c The error messages' color.
	 */
	public void
	setErrorColor(Color c) { console.setErrorColor(c); }
	
	public class CmdLineTextField extends JTextField {
		CmdLineTextField() {
			setTransferHandler(new TransferHandler("instrumentLoad"));
		}
		
		public String
		getInstrumentLoad() { return getSelectedText(); }
		
		public void setInstrumentLoad(String instr) {
			if(instr == null) return;
			
			if(!OrchestraInstrument.isDnDString(instr)) {
				replaceSelection(instr);
				return;
			}
			
			String[] args = instr.split("\n");
			if(args.length < 6) return;
			
			String s = "LOAD INSTRUMENT NON_MODAL '" + args[4] + "' " + args[5] + " ";
			getModel().setCommandLineText(s);
			requestFocus();
		}
	}
	
	public void
	setOwner(Window owner) {
		if(autoCompleteWindow != null && autoCompleteWindow.isVisible())
			autoCompleteWindow.setVisible(false);
		
		autoCompleteWindow = new AutoCompleteWindow(owner);
		
		if(getOwner() != null) getOwner().removeWindowListener(getHandler());
		owner.addWindowListener(getHandler());
		
		this.owner = owner;
	}
	
	public Window
	getOwner() { return owner; }
	
	/** Hides the autocomplete window. */
	public void
	hideAutoCompleteWindow() { autoCompleteWindow.setVisible(false); }
	
	protected void
	quitSession() {
		getModel().clearSessionHistory();
		console.setText("");
	}
	
	
	private final SearchHandler searchHandler = new SearchHandler();
	
	private SearchHandler
	getSearchHandler() { return searchHandler; }
	
	private class SearchHandler implements DocumentListener {
		// DocumentListener
		public void
		insertUpdate(DocumentEvent e) { processSearch(); }
		
		public void
		removeUpdate(DocumentEvent e) { processSearch(); }
		
		public void
		changedUpdate(DocumentEvent e) { processSearch(); }
	}
	
		
	private final Handler eventHandler = new Handler();
	
	private Handler
	getHandler() { return eventHandler; }
	
	private class Handler extends WindowAdapter
				implements ActionListener, DocumentListener, LSConsoleListener {
		
		// ActionListener
		public void
		actionPerformed(ActionEvent e) {
			if(autoCompleteWindow.isVisible()) autoCompleteWindow.applySelection();
			else getModel().execCommand();
		}
		
		// DocumentListener
		public void
		insertUpdate(DocumentEvent e) { getModel().setCommandLineText(tfInput.getText()); }
		
		public void
		removeUpdate(DocumentEvent e) { getModel().setCommandLineText(tfInput.getText()); }
		
		public void
		changedUpdate(DocumentEvent e) { getModel().setCommandLineText(tfInput.getText()); }
		
		// WindowListener
		@Override
		public void
		windowActivated(WindowEvent e) {
			if(autocompleteMode == AutocompleteMode.AUTOCOMPLETE) {
				tfInput.requestFocusInWindow();
			}  else tfSearch.requestFocusInWindow();
		}
		
		@Override
		public void
		windowDeactivated(WindowEvent e) { autoCompleteWindow.setVisible(false); }
			
		@Override
		public void
		windowIconified(WindowEvent e) { autoCompleteWindow.setVisible(false); }
		
		// LSConsoleListener
		
		/** Invoked when the text in the command line is changed. */
		public void
		commandLineTextChanged(LSConsoleEvent e) {
			commandChanged(e.getPreviousCommandLineText());
		}
		
		/** Invoked when the command in the command line has been executed. */
		public void 
		commandExecuted(LSConsoleEvent e) {
			console.addCommand(getModel().getLastExecutedCommand());
		}
		
		/** Invoked when response is received from LinuxSampler. */
		public void
		responseReceived(LSConsoleEvent e) {
			console.addCommandResponse(e.getResponse());
		}
	}
	
	private void
	commandChanged(String oldCmdLine) {
		if(!tfInput.getText().equals(getModel().getCommandLineText()))
			tfInput.setText(getModel().getCommandLineText());
		
		if(LscpUtils.spellCheck(tfInput.getText())) {
			tfInput.setForeground(console.getTextColor());
		} else {
			tfInput.setForeground(console.getErrorColor());
			if(autoCompleteWindow.isVisible()) autoCompleteWindow.setVisible(false);
		}
		
		if(autoCompleteWindow.isVisible()) processAutoComplete(oldCmdLine);
	}
	
	private boolean
	isProcessingSearch() { return processingSearch; }
	
	private void
	setProcessingSearch(boolean b) { processingSearch = b; }
	
	/** Invoked when the tab key is pressed. */
	private void
	processAutoComplete() {
		processAutoComplete(null);
	}
	
	private void
	processAutoComplete(String oldCmdLine) {
		String cmdLine = getModel().getCommandLineText();
		switch(autocompleteMode) {
		case AUTOCOMPLETE:
			
			break;
		case HISTORY_SEARCH:
		case CMD_LIST_SEARCH:
			if(autoCompleteWindow.isVisible()) return;
		}
		
		autocompleteMode = AutocompleteMode.AUTOCOMPLETE;
		
		final String[] cmdS;
		
		try {
			cmdS = LscpUtils.getCompletionPossibilities(cmdLine);
		} catch(IllegalStateException e) {
			autoCompleteWindow.setVisible(false);
			java.awt.Toolkit.getDefaultToolkit().beep();
			return;
		}
		
		if(cmdS.length == 0) {
			autoCompleteWindow.setVisible(false);
			return;
		}
		if(cmdS.length == 1) {
			if(oldCmdLine != null && oldCmdLine.startsWith(cmdLine)) return;
			
			// To prevent IllegalStateException exception.
			SwingUtilities.invokeLater(new Runnable() {
				public void
				run() { tfInput.setText(cmdS[0]); }
			});
			
			return;
		}
		
		autoCompleteWindow.setCommandList(cmdS);
		autoCompleteWindow.setVisible(true);
	}
	
	private void
	startHistorySearch() {
		autocompleteMode = AutocompleteMode.HISTORY_SEARCH;
		lInput.setText("History Search: ");
		
		startSearch();
	}
	
	private void
	startCmdListSearch() {
		autocompleteMode = AutocompleteMode.CMD_LIST_SEARCH;
		lInput.setText("Command List Search: ");
		
		startSearch();
	}
	
	private void
	startSearch() {
		if(!isProcessingSearch())
			tfSearch.getDocument().addDocumentListener(getSearchHandler());
		
		setProcessingSearch(true);
		
		lInput.setVisible(true);
		tfInput.setVisible(false);
		tfSearch.setText(getModel().getCommandLineText());
		tfSearch.setVisible(true);
		tfSearch.requestFocusInWindow();
		
		revalidate();
		repaint();
		
		processSearch();
	}
	
	private void
	processSearch() {
		String[] cmdS = null;
		
		switch(autocompleteMode) {
		case HISTORY_SEARCH:
			cmdS = getModel().searchCommandHistory(tfSearch.getText());
			break;
		case CMD_LIST_SEARCH:
			cmdS = getModel().searchCommandList(tfSearch.getText());
			break;
		}
		
		autoCompleteWindow.setCommandList(cmdS);
		autoCompleteWindow.setVisible(cmdS.length > 0);
	}
	
	private void
	stopSearch(boolean cancelSearch) {
		setProcessingSearch(false);
		
		tfSearch.getDocument().removeDocumentListener(getSearchHandler());
		
		lInput.setVisible(false);
		lInput.setText("");
		tfSearch.setVisible(false);
		tfInput.setVisible(true);
		tfInput.requestFocusInWindow();
		
		revalidate();
		repaint();
		
		if(cancelSearch) {
			autoCompleteWindow.setVisible(false);
			return;
		}
		
		if(autoCompleteWindow.isVisible()) autoCompleteWindow.applySelection();
		else getModel().setCommandLineText(tfSearch.getText());
	}
	
	
	protected class Actions extends AbstractAction {
		public static final String AUTOCOMPLETE = "autocomplete";
		public static final String HISTORY_SEARCH = "historySearch";
		public static final String CMD_LIST_SEARCH = "cmdListSearch";
		public static final String CANCEL_SEARCH = "cancelSearch";
		public static final String APPLY_SEARCH = "applySearch";
		public static final String MOVE_UP = "moveUp";
		public static final String MOVE_DOWN = "moveDown";
		public static final String MOVE_HOME = "moveHome";
		public static final String MOVE_END = "moveEnd";
		public static final String CANCEL_SELECTION = "cancelSelection";
		public static final String CLEAR_CONSOLE = "clearConsole";
		public static final String CLEAR_SESSION_HISTORY = "clearSessionHistory";
		public static final String CLEAR_COMMAND_HISTORY = "clearCommandHistory";
		public static final String QUIT_SESSION = "quitSession";
		
		public
		Actions(String name) { super(name); }
		
		public void
		actionPerformed(ActionEvent e) {
			String key = getValue(Action.NAME).toString();
			
			if(key == AUTOCOMPLETE) {
				processAutoComplete();
			} else if(key == HISTORY_SEARCH) {
				startHistorySearch();
			} else if(key == CMD_LIST_SEARCH) {
				startCmdListSearch();
			} else if(key == CANCEL_SEARCH) {
				stopSearch(true);
			} else if(key == APPLY_SEARCH) {
				stopSearch(false);
			} else if(key == MOVE_UP) {
				if(autoCompleteWindow.isVisible()) {
					autoCompleteWindow.selectPreviousItem();
					return;
				}
				
				getModel().browseCommandHistoryUp();
			} else if(key == MOVE_DOWN) {
				if(autoCompleteWindow.isVisible()) {
					autoCompleteWindow.selectNextItem();
					return;
				}
				
				getModel().browseCommandHistoryDown();
			} else if(key == MOVE_HOME) {
				if(autoCompleteWindow.isVisible()) {
					autoCompleteWindow.selectFirstItem();
					return;
				}
				
				JTextField tf = tfInput.isVisible() ? tfInput : tfSearch;
				tf.setCaretPosition(0);
			} else if(key == MOVE_END) {
				if(autoCompleteWindow.isVisible()) {
					autoCompleteWindow.selectLastItem();
					return;
				}
				
				JTextField tf = tfInput.isVisible() ? tfInput : tfSearch;
				tf.setCaretPosition(tf.getText().length());
			} else if(key == CANCEL_SELECTION) {
				autoCompleteWindow.setVisible(false);
			} else if(key == CLEAR_CONSOLE) {
				console.setText("");
			} else if(key == CLEAR_SESSION_HISTORY) {
				getModel().clearSessionHistory();
			} else if(key == CLEAR_COMMAND_HISTORY) {
				getModel().clearCommandHistory();
			} else if(key == QUIT_SESSION) {
				quitSession();
			}
		}
	}
	
	private class AutoCompleteWindow extends JWindow {
		private int MAX_HEIGHT = 140;
		
		private JList list = new JList();
		private JScrollPane scrollPane;
		
		AutoCompleteWindow(Window owner) {
			super(owner);
			
			owner.addComponentListener(getHandler());
			
			list.addMouseListener(new MouseAdapter() {
				public void
				mouseClicked(MouseEvent e) {
					if(list.getSelectedIndex() != -1) applySelection();
				}
			});
			
			list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
						
			scrollPane = new JScrollPane(list);
			//sp.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.RAISED));
			scrollPane.setPreferredSize (
				new Dimension(scrollPane.getPreferredSize().width, MAX_HEIGHT)
			);
			add(scrollPane);
			
			int i = JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT;
				getRootPane().getInputMap(i).put (
				KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
				"applySelection"
			);
		
			getRootPane().getActionMap().put ("applySelection", new AbstractAction() {
				public void
				actionPerformed(ActionEvent e) { applySelection(); }
			});
		
		}
		
		public void
		setCommandList(String[] cmdS) {
			list.setListData(cmdS);
			if(cmdS.length > 0) list.setSelectedIndex(0);
			
			int h = list.getPreferredSize().height + 6;
			if(h > MAX_HEIGHT) h = MAX_HEIGHT;
			if(h == scrollPane.getSize().height) return;
			
			scrollPane.setPreferredSize (
				new Dimension(scrollPane.getPreferredSize().width, h)
			);
			
			scrollPane.setMaximumSize (
				new Dimension(scrollPane.getPreferredSize().width, h)
			);
			
			setVisible(false);
			setSize(getSize().width, h);
			setVisible(true);
		}
		
		public void
		setVisible(boolean b) {
			if(b) updateLocation0();
			super.setVisible(b);
		}
		
		public void
		selectNextItem() {
			int size = list.getModel().getSize();
			if(size == 0) return;
			
			int i = list.getSelectedIndex();
			if(i == -1 || i == size - 1) list.setSelectedIndex(0);
			else list.setSelectedIndex(i + 1);
			
			list.ensureIndexIsVisible(list.getSelectedIndex());
		}
		
		public void
		selectPreviousItem() {
			int size = list.getModel().getSize();
			if(size == 0) return;
			
			int i = list.getSelectedIndex();
			if(i == -1 || i == 0) list.setSelectedIndex(size - 1);
			else list.setSelectedIndex(i - 1);
			
			list.ensureIndexIsVisible(list.getSelectedIndex());
		}
		
		public void
		selectFirstItem() {
			int size = list.getModel().getSize();
			if(size == 0) return;
			
			list.setSelectedIndex(0);
			
			list.ensureIndexIsVisible(list.getSelectedIndex());
		}
		
		public void
		selectLastItem() {
			int size = list.getModel().getSize();
			if(size == 0) return;
			
			list.setSelectedIndex(size - 1);
			
			list.ensureIndexIsVisible(list.getSelectedIndex());
		}
		
		/**
		 * Sets the text color of the autocompletion list.
		 * @param c The text color of autocompletion list.
		 */
		public void
		setTextColor(Color c) {
			//Object o = list.getCellRenderer();
			//if(o instanceof JComponent) ((JComponent)o).setForeground(c);
			list.setForeground(c);
		}
		
		/**
		 * Sets the background color of the autocompletion list.
		 * @param c The background color of the autocompletion list.
		 */
		public void
		setBackgroundColor(Color c) { list.setBackground(c); }
	
		private void
		updateLocation() {
			if(!isVisible()) return;
			updateLocation0();
		}
		
		private void
		updateLocation0() {
			Dimension d;
			d = new Dimension(inputPane.getSize().width - 6, getSize().height);
			setPreferredSize(d);
			
			Point p = inputPane.getLocationOnScreen();
			pack();
			setLocation(p.x + 3, p.y - getSize().height);
		}
		
		private void
		applySelection() {
			Object o = list.getSelectedValue();
			if(o != null) tfInput.setText(o.toString());
			setVisible(false);
		}
		
		private final Handler eventHandler = new Handler();
	
		private Handler
		getHandler() { return eventHandler; }
	
		private class Handler extends ComponentAdapter {
			
			// ComponentListener
			public void
			componentMoved(ComponentEvent e) { updateLocation(); }
				
			public void
			componentResized(ComponentEvent e) { updateLocation(); }
		}
	}
}

class LSConsoleTextPane extends JTextPane {
	private final String STYLE_ROOT = "root";
	private final String STYLE_REGULAR = "regular";
	private final String STYLE_BOLD = "bold";
	private final String STYLE_NOTIFY_0 = "notificationMessage0";
	private final String STYLE_NOTIFY = "notificationMessage";
	private final String STYLE_WARN_0 = "warningMessage0";
	private final String STYLE_WARN = "warningMessage";
	private final String STYLE_ERROR_0 = "errorMessage0";
	private final String STYLE_ERROR = "errorMessage";
	
	private Color cmdColor;
	private Color notifyColor;
	private Color warnColor;
	private Color errorColor;
	
	LSConsoleTextPane() {
		int i = preferences().getIntProperty(LS_CONSOLE_TEXT_COLOR);
		cmdColor = new Color(i);
		
		i = preferences().getIntProperty(LS_CONSOLE_NOTIFY_COLOR);
		notifyColor = new Color(i);
		
		i = preferences().getIntProperty(LS_CONSOLE_ERROR_COLOR);
		errorColor = new Color(i);
		
		i = preferences().getIntProperty(LS_CONSOLE_WARNING_COLOR);
		warnColor = new Color(i);
		
		Style def;
		def = StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE);
		StyledDocument doc = getStyledDocument();
		Style root = doc.addStyle(STYLE_ROOT, def);
		Style regular = doc.addStyle(STYLE_REGULAR, root);
		
		Style style = doc.addStyle(STYLE_BOLD, regular);
		StyleConstants.setBold(style, true);
		
		style = doc.addStyle(STYLE_NOTIFY_0, regular);
		StyleConstants.setForeground(style, notifyColor);
		doc.addStyle(STYLE_NOTIFY, style);
		
		style = doc.addStyle(STYLE_WARN_0, regular);
		StyleConstants.setForeground(style, warnColor);
		doc.addStyle(STYLE_WARN, style);
			
		style = doc.addStyle(STYLE_ERROR_0, regular);
		StyleConstants.setForeground(style, errorColor);
		doc.addStyle(STYLE_ERROR, style);
			
		setEditable(false);
		setBorder(BorderFactory.createEmptyBorder());
		
	}
	
	private JSPrefs
	preferences() { return CC.getViewConfig().preferences(); }
	
	/**
	 * Sets the text color.
	 * @param c The text color.
	 */
	public void
	setTextColor(Color c) {
		cmdColor = c;
		
		StyledDocument doc = getStyledDocument();
		Style root = doc.getStyle(STYLE_ROOT);
		StyleConstants.setForeground(root, cmdColor);
	}
	
	/**
	 * Gets the text color of the LS Console.
	 * @return The text color of the LS Console.
	 */
	public Color
	getTextColor() { return cmdColor; }
	
	/**
	 * Sets the notification messages' color.
	 * @param c The notification messages' color.
	 */
	public void
	setNotifyColor(Color c) {
		notifyColor = c;
		
		Style notify = getStyledDocument().getStyle(STYLE_NOTIFY_0);
		StyleConstants.setForeground(notify, notifyColor);
	}
	
	/**
	 * Gets the notification messages' color.
	 * @return The notification messages' color.
	 */
	public Color
	getNotifyColor() { return notifyColor; }
	
	/**
	 * Gets the warning messages' color.
	 * @return The warning messages' color.
	 */
	public Color
	getWarningColor() { return warnColor; }
	
	/**
	 * Sets the warning messages' color.
	 * @param c The warning messages' color.
	 */
	public void
	setWarningColor(Color c) {
		warnColor = c;
		
		Style warn = getStyledDocument().getStyle(STYLE_WARN_0);
		StyleConstants.setForeground(warn, warnColor);
	}
	
	/**
	 * Gets the error messages' color.
	 * @return The error messages' color.
	 */
	public Color
	getErrorColor() { return errorColor; }
	
	/**
	 * Sets the error messages' color.
	 * @param c The error messages' color.
	 */
	public void
	setErrorColor(Color c) {
		errorColor = c;
		
		Style error = getStyledDocument().getStyle(STYLE_ERROR_0);
		StyleConstants.setForeground(error, errorColor);
	}
	
	/**
	 * Adds the specified command to this text pane.
	 * @param cmd The command to be added.
	 */
	public void
	addCommand(String cmd) {
		StyledDocument doc = getStyledDocument();
		try {
			String s = "lscp> ";
			doc.insertString(doc.getLength(), s, doc.getStyle(STYLE_BOLD));
			s = cmd + "\n";
			doc.insertString(doc.getLength(), s, doc.getStyle(STYLE_REGULAR));
		} catch(Exception x) {
			CC.getLogger().log(Level.INFO, HF.getErrorMessage(x), x);
		}
	}
	
	/**
	 * Adds the specified command response to this text pane.
	 * @param cmd The command response to be added.
	 */
	public void
	addCommandResponse(String cmdResponse) {
		StyledDocument doc = getStyledDocument();
		try {
			String s = cmdResponse + "\n";
			Style style = doc.getStyle(STYLE_REGULAR);
			if(s.startsWith("ERR:")) style = doc.getStyle(STYLE_ERROR);
			else if(s.startsWith("WRN:")) style = doc.getStyle(STYLE_WARN);
			else if(s.startsWith("NOTIFY:")) style = doc.getStyle(STYLE_NOTIFY);
			doc.insertString(doc.getLength(), s, style);
		} catch(Exception x) {
			CC.getLogger().log(Level.INFO, HF.getErrorMessage(x), x);
		}
	}
}
