/*
 *  soapUI, copyright (C) 2004-2012 smartbear.com 
 *
 *  soapUI is free software; you can redistribute it and/or modify it under the 
 *  terms of version 2.1 of the GNU Lesser General Public License as published by 
 *  the Free Software Foundation.
 *
 *  soapUI 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 Lesser General Public License for more details at gnu.org.
 */

package org.syntax.jedit;

import java.awt.Toolkit;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.Hashtable;
import java.util.StringTokenizer;

import javax.swing.KeyStroke;

/**
 * The default input handler. It maps sequences of keystrokes into actions and
 * inserts key typed events into the text area.
 * 
 * @author Slava Pestov
 * @version $Id$
 */
public class DefaultInputHandler extends InputHandler
{
	/**
	 * Creates a new input handler with no key bindings defined.
	 */
	public DefaultInputHandler()
	{
		bindings = currentBindings = new Hashtable();
	}

	/**
	 * Sets up the default key bindings.
	 */
	public void addDefaultKeyBindings()
	{
		addKeyBinding( "BACK_SPACE", BACKSPACE );
		addKeyBinding( "C+BACK_SPACE", BACKSPACE_WORD );
		addKeyBinding( "DELETE", DELETE );
		addKeyBinding( "C+DELETE", DELETE_WORD );

		addKeyBinding( "ENTER", INSERT_BREAK );
		addKeyBinding( "TAB", INSERT_TAB );

		addKeyBinding( "INSERT", OVERWRITE );
		addKeyBinding( "C+\\", TOGGLE_RECT );

		addKeyBinding( "HOME", HOME );
		addKeyBinding( "END", END );
		addKeyBinding( "C+A", SELECT_ALL );
		addKeyBinding( "S+HOME", SELECT_HOME );
		addKeyBinding( "S+END", SELECT_END );
		addKeyBinding( "C+HOME", DOCUMENT_HOME );
		addKeyBinding( "C+END", DOCUMENT_END );
		addKeyBinding( "CS+HOME", SELECT_DOC_HOME );
		addKeyBinding( "CS+END", SELECT_DOC_END );

		addKeyBinding( "PAGE_UP", PREV_PAGE );
		addKeyBinding( "PAGE_DOWN", NEXT_PAGE );
		addKeyBinding( "S+PAGE_UP", SELECT_PREV_PAGE );
		addKeyBinding( "S+PAGE_DOWN", SELECT_NEXT_PAGE );

		addKeyBinding( "LEFT", PREV_CHAR );
		addKeyBinding( "S+LEFT", SELECT_PREV_CHAR );
		addKeyBinding( "C+LEFT", PREV_WORD );
		addKeyBinding( "CS+LEFT", SELECT_PREV_WORD );
		addKeyBinding( "RIGHT", NEXT_CHAR );
		addKeyBinding( "S+RIGHT", SELECT_NEXT_CHAR );
		addKeyBinding( "C+RIGHT", NEXT_WORD );
		addKeyBinding( "CS+RIGHT", SELECT_NEXT_WORD );
		addKeyBinding( "UP", PREV_LINE );
		addKeyBinding( "S+UP", SELECT_PREV_LINE );
		addKeyBinding( "DOWN", NEXT_LINE );
		addKeyBinding( "S+DOWN", SELECT_NEXT_LINE );

		addKeyBinding( "C+ENTER", REPEAT );

		// Clipboard
		addKeyBinding( "C+C", CLIP_COPY );
		addKeyBinding( "C+V", CLIP_PASTE );
		addKeyBinding( "C+X", CLIP_CUT );
	}

	/**
	 * Adds a key binding to this input handler. The key binding is a list of
	 * white space separated key strokes of the form <i>[modifiers+]key</i> where
	 * modifier is C for Control, A for Alt, or S for Shift, and key is either a
	 * character (a-z) or a field name in the KeyEvent class prefixed with VK_
	 * (e.g., BACK_SPACE)
	 * 
	 * @param keyBinding
	 *           The key binding
	 * @param action
	 *           The action
	 */
	public void addKeyBinding( String keyBinding, ActionListener action )
	{
		Hashtable current = bindings;

		StringTokenizer st = new StringTokenizer( keyBinding );
		while( st.hasMoreTokens() )
		{
			KeyStroke keyStroke = parseKeyStroke( st.nextToken() );
			if( keyStroke == null )
				return;

			if( st.hasMoreTokens() )
			{
				Object o = current.get( keyStroke );
				if( o instanceof Hashtable )
					current = ( Hashtable )o;
				else
				{
					o = new Hashtable();
					current.put( keyStroke, o );
					current = ( Hashtable )o;
				}
			}
			else
				current.put( keyStroke, action );
		}
	}

	/**
	 * Removes a key binding from this input handler. This is not yet
	 * implemented.
	 * 
	 * @param keyBinding
	 *           The key binding
	 */
	public void removeKeyBinding( String keyBinding )
	{
		throw new InternalError( "Not yet implemented" );
	}

	/**
	 * Removes all key bindings from this input handler.
	 */
	public void removeAllKeyBindings()
	{
		bindings.clear();
	}

	/**
	 * Returns a copy of this input handler that shares the same key bindings.
	 * Setting key bindings in the copy will also set them in the original.
	 */
	public InputHandler copy()
	{
		return new DefaultInputHandler( this );
	}

	/**
	 * Handle a key pressed event. This will look up the binding for the key
	 * stroke and execute it.
	 */
	public void keyPressed( KeyEvent evt )
	{
		int keyCode = evt.getKeyCode();
		int modifiers = evt.getModifiers();

		if( keyCode == KeyEvent.VK_CONTROL || keyCode == KeyEvent.VK_SHIFT || keyCode == KeyEvent.VK_ALT
				|| keyCode == KeyEvent.VK_META )
			return;

		if( ( modifiers & ~KeyEvent.SHIFT_MASK ) != 0 || evt.isActionKey() || keyCode == KeyEvent.VK_BACK_SPACE
				|| keyCode == KeyEvent.VK_DELETE || keyCode == KeyEvent.VK_ENTER || keyCode == KeyEvent.VK_TAB
				|| keyCode == KeyEvent.VK_ESCAPE )
		{
			if( grabAction != null )
			{
				handleGrabAction( evt );
				return;
			}

			KeyStroke keyStroke = KeyStroke.getKeyStroke( keyCode, modifiers );
			Object o = currentBindings.get( keyStroke );
			if( o == null )
			{
				// Don't beep if the user presses some
				// key we don't know about unless a
				// prefix is active. Otherwise it will
				// beep when caps lock is pressed, etc.
				if( currentBindings != bindings )
				{
					Toolkit.getDefaultToolkit().beep();
					// F10 should be passed on, but C+e F10
					// shouldn't
					repeatCount = 0;
					repeat = false;
					evt.consume();
				}
				currentBindings = bindings;
				return;
			}
			else if( o instanceof ActionListener )
			{
				currentBindings = bindings;

				executeAction( ( ( ActionListener )o ), evt.getSource(), null );

				evt.consume();
				return;
			}
			else if( o instanceof Hashtable )
			{
				currentBindings = ( Hashtable )o;
				evt.consume();
				return;
			}
		}
	}

	/**
	 * Handle a key typed event. This inserts the key into the text area.
	 */
	public void keyTyped( KeyEvent evt )
	{
		int modifiers = evt.getModifiers();
		char c = evt.getKeyChar();
		if( c != KeyEvent.CHAR_UNDEFINED && ( modifiers & KeyEvent.ALT_MASK ) == 0
				&& ( modifiers & KeyEvent.CTRL_MASK ) == 0 && ( modifiers & KeyEvent.META_MASK ) == 0 )
		{
			if( c >= 0x20 && c != 0x7f )
			{
				KeyStroke keyStroke = KeyStroke.getKeyStroke( Character.toUpperCase( c ) );
				Object o = currentBindings.get( keyStroke );

				if( o instanceof Hashtable )
				{
					currentBindings = ( Hashtable )o;
					return;
				}
				else if( o instanceof ActionListener )
				{
					currentBindings = bindings;
					executeAction( ( ActionListener )o, evt.getSource(), String.valueOf( c ) );
					return;
				}

				currentBindings = bindings;

				if( grabAction != null )
				{
					handleGrabAction( evt );
					return;
				}

				// 0-9 adds another 'digit' to the repeat number
				if( repeat && Character.isDigit( c ) )
				{
					repeatCount *= 10;
					repeatCount += ( c - '0' );
					return;
				}

				executeAction( INSERT_CHAR, evt.getSource(), String.valueOf( evt.getKeyChar() ) );

				repeatCount = 0;
				repeat = false;
			}
		}
	}

	/**
	 * Converts a string to a keystroke. The string should be of the form
	 * <i>modifiers</i>+<i>shortcut</i> where <i>modifiers</i> is any combination
	 * of A for Alt, C for Control, S for Shift or M for Meta, and
	 * <i>shortcut</i> is either a single character, or a keycode name from the
	 * <code>KeyEvent</code> class, without the <code>VK_</code> prefix.
	 * 
	 * @param keyStroke
	 *           A string description of the key stroke
	 */
	public static KeyStroke parseKeyStroke( String keyStroke )
	{
		if( keyStroke == null )
			return null;
		int modifiers = 0;
		int index = keyStroke.indexOf( '+' );
		if( index != -1 )
		{
			for( int i = 0; i < index; i++ )
			{
				switch( Character.toUpperCase( keyStroke.charAt( i ) ) )
				{
				case 'A' :
					modifiers |= InputEvent.ALT_MASK;
					break;
				case 'C' :
					modifiers |= getMenuShortcutKeyMask();
					break;
				case 'M' :
					modifiers |= InputEvent.META_MASK;
					break;
				case 'S' :
					modifiers |= InputEvent.SHIFT_MASK;
					break;
				}
			}
		}
		String key = keyStroke.substring( index + 1 );
		if( key.length() == 1 )
		{
			char ch = Character.toUpperCase( key.charAt( 0 ) );
			if( modifiers == 0 )
				return KeyStroke.getKeyStroke( ch );
			else
				return KeyStroke.getKeyStroke( ch, modifiers );
		}
		else if( key.length() == 0 )
		{
			System.err.println( "Invalid key stroke: " + keyStroke );
			return null;
		}
		else
		{
			int ch;

			try
			{
				ch = KeyEvent.class.getField( "VK_".concat( key ) ).getInt( null );
			}
			catch( Exception e )
			{
				System.err.println( "Invalid key stroke: " + keyStroke );
				return null;
			}

			return KeyStroke.getKeyStroke( ch, modifiers );
		}
	}

	// private members
	private Hashtable bindings;
	private Hashtable currentBindings;

	private DefaultInputHandler( DefaultInputHandler copy )
	{
		bindings = currentBindings = copy.bindings;
	}
}
