/*
 * InputHandler.java - Manages key bindings and executes actions
 * :tabSize=4:indentSize=4:noTabs=false:
 * :folding=explicit:collapseFolds=1:
 *
 * Copyright (C) 1999, 2003 Slava Pestov
 *
 * 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 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.
 */

package org.gjt.sp.jedit.gui;

//{{{ Imports
import javax.swing.*;
import javax.swing.text.JTextComponent;

import org.gjt.sp.jedit.textarea.JEditTextArea;
import org.gjt.sp.jedit.*;
import org.gjt.sp.jedit.buffer.JEditBuffer;
import org.gjt.sp.jedit.input.AbstractInputHandler;
import org.gjt.sp.util.GenericGUIUtilities;
import org.gjt.sp.util.Log;
import org.gjt.sp.util.StandardUtilities;

import java.awt.event.KeyEvent;
import java.awt.*;
//}}}

/**
 * An input handler converts the user's key strokes into concrete actions.
 * It also takes care of macro recording and action repetition.<p>
 *
 * This class provides all the necessary support code for an input
 * handler, but doesn't actually do any key binding logic. It is up
 * to the implementations of this class to do so.
 *
 * @author Slava Pestov
 * @version $Id: InputHandler.java 24411 2016-06-19 11:02:53Z kerik-sf $
 * @see org.gjt.sp.jedit.gui.DefaultInputHandler
 */
public abstract class InputHandler extends AbstractInputHandler<EditAction>
{
	//{{{ InputHandler constructor
	/**
	 * Creates a new input handler.
	 * @param view The view
	 */
	protected InputHandler(View view)
	{
		this.view = view;
	} //}}}

	//{{{ handleKey() method
	/**
	 * Handles a keystroke.
	 * @param keyStroke The key stroke.
	 * @return true if the input could be handled.
	 * @since jEdit 4.2pre5
	 */
	public final boolean handleKey(KeyEventTranslator.Key keyStroke)
	{
		return handleKey(keyStroke, false);
	} //}}}

	//{{{ processKeyEvent() method
	/**
	 * Forwards key events directly to the input handler.
	 * This is slightly faster than using a KeyListener
	 * because some Swing overhead is avoided.
	 * @since 4.3pre7
	 */
	@Override
	public void processKeyEvent(KeyEvent evt, int from, boolean global)
	{
		if(Debug.DUMP_KEY_EVENTS)
		{
			Log.log(Log.DEBUG,this,"Key event                 : "
				+ AbstractInputHandler.toString(evt) + " from " + from);
			Log.log(Log.DEBUG,this,view+".isFocused()="+view.isFocused()+'.',new Exception());
		}

		if(view.getTextArea().hasFocus() && from == View.VIEW)
			return;

		evt = _preprocessKeyEvent(evt);
		if(evt == null)
			return;

		if(Debug.DUMP_KEY_EVENTS)
		{
			Log.log(Log.DEBUG,this,"Key event after workaround: "
				+ AbstractInputHandler.toString(evt) + " from " + from);
		}

		Component prefixFocusOwner = view.getPrefixFocusOwner();
		boolean focusOnTextArea = false;
		switch(evt.getID())
		{
		case KeyEvent.KEY_TYPED:
			// if the user pressed eg C+e n n in the
			// search bar we want focus to go back there
			// after the prefix is done
			if(prefixFocusOwner != null)
			{
				if(prefixFocusOwner.isShowing())
				{
					prefixFocusOwner.requestFocus();
					focusOnTextArea = true;
				}
			}

			if(keyEventInterceptor != null)
				keyEventInterceptor.keyTyped(evt);
			else if(from == View.ACTION_BAR
				|| isPrefixActive()
				|| view.getTextArea().hasFocus())
			{
				processKeyEventKeyStrokeHandling(evt,from,"type ",global);
			}


			processKeyEventSub(focusOnTextArea);

			break;
		case KeyEvent.KEY_PRESSED:
			if(keyEventInterceptor != null)
				keyEventInterceptor.keyPressed(evt);
			else if(KeyEventWorkaround.isBindable(evt.getKeyCode()))
			{
				if(prefixFocusOwner != null)
				{
					if(prefixFocusOwner.isShowing())
					{
						prefixFocusOwner.requestFocus();
						focusOnTextArea = true;
					}
					view.setPrefixFocusOwner(null);
				}

				processKeyEventKeyStrokeHandling(evt,from,"press",global);

				processKeyEventSub(focusOnTextArea);

			}
			break;
		case KeyEvent.KEY_RELEASED:
			if(keyEventInterceptor != null)
				keyEventInterceptor.keyReleased(evt);
			break;
		}
	} //}}}

	//{{{ _preprocessKeyEvent() method
	private KeyEvent _preprocessKeyEvent(KeyEvent evt)
	{
		if(view.isClosed())
			return null;
		Component focusOwner = view.getFocusOwner();
		if(focusOwner instanceof JComponent)
		{
			JComponent comp = (JComponent)focusOwner;
			InputMap map = comp.getInputMap();
			ActionMap am = comp.getActionMap();

			if(map != null && am != null && comp.isEnabled())
			{
				KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(evt);
				Object binding = map.get(keyStroke);
				if(binding != null && am.get(binding) != null)
				{
					return null;
				}
			}
		}
		
		if(focusOwner instanceof JTextComponent)
		{
			// fix for the bug where key events in JTextComponents
			// inside views are also handled by the input handler
			if(evt.getID() == KeyEvent.KEY_PRESSED)
			{
				switch(evt.getKeyCode())
				{
				case KeyEvent.VK_ENTER:
				case KeyEvent.VK_TAB:
				case KeyEvent.VK_BACK_SPACE:
				case KeyEvent.VK_SPACE:
					return null;
				}
			}
		}

		if(evt.isConsumed())
			return null;

		if(Debug.DUMP_KEY_EVENTS)
		{
			Log.log(Log.DEBUG,this,"Key event (preprocessing) : "
					+ AbstractInputHandler.toString(evt));
		}

		return KeyEventWorkaround.processKeyEvent(evt);
	} //}}}

	//{{{ processKeyEventSub() method
	private void processKeyEventSub(boolean focusOnTextArea)
	{
		// we might have been closed as a result of
		// the above
		if(view.isClosed())
			return;

		// this is a weird hack.
		// we don't want C+e a to insert 'a' in the
		// search bar if the search bar has focus...
		if(isPrefixActive())
		{
			Component focusOwner = view.getFocusOwner();
			if(focusOwner instanceof JTextComponent)
			{
				view.setPrefixFocusOwner(focusOwner);
				view.getTextArea().requestFocus();
			}
			else if(focusOnTextArea)
			{
				view.getTextArea().requestFocus();
			}
			else
			{
				view.setPrefixFocusOwner(null);
			}
		}
		else
		{
			view.setPrefixFocusOwner(null);
		}
	}
	//}}}

	//{{{ getRepeatCount() method
	/**
	 * Returns the number of times the next action will be repeated.
	 */
	public int getRepeatCount()
	{
		return repeatCount;
	} //}}}

	//{{{ setRepeatCount() method
	/**
	 * Sets the number of times the next action will be repeated.
	 * @param repeatCount The repeat count
	 */
	public void setRepeatCount(int repeatCount)
	{
		int oldRepeatCount = this.repeatCount;
		this.repeatCount = repeatCount;
		if(oldRepeatCount != repeatCount)
			view.getStatus().setMessage(null);
	} //}}}

	//{{{ getLastAction() method
	/**
	 * Returns the last executed action.
	 * @since jEdit 2.5pre5
	 */
	public EditAction getLastAction()
	{
		return lastAction;
	} //}}}

	//{{{ readNextChar() method
	/**
	 * Invokes the specified BeanShell code, replacing __char__ in the
	 * code with the next input character.
	 * @param msg The prompt to display in the status bar
	 * @param code The code
	 * @since jEdit 3.2pre2
	 */
	public void readNextChar(String msg, String code)
	{
		view.getStatus().setMessage(msg);
		readNextChar = code;
	} //}}}

	//{{{ invokeAction() method
	/**
	 * Invokes the specified action, repeating and recording it as
	 * necessary.
	 * @param action The action
	 * @since jEdit 4.2pre1
	 */
	@Override
	public void invokeAction(String action)
	{
		invokeAction(jEdit.getAction(action));
	} //}}}

	//{{{ invokeAction() method
	/**
	 * Invokes the specified action, repeating and recording it as
	 * necessary.
	 * @param action The action
	 */
	@Override
	public void invokeAction(EditAction action)
	{
		JEditBuffer buffer = view.getBuffer();

		/* if(buffer.insideCompoundEdit())
			buffer.endCompoundEdit(); */

		// remember the last executed action
		if(!action.noRememberLast())
		{
			HistoryModel.getModel("action").addItem(action.getName());
			if(lastAction == action)
				lastActionCount++;
			else
			{
				lastAction = action;
				lastActionCount = 1;
			}
		}

		// remember old values, in case action changes them
		int _repeatCount = repeatCount;

		// execute the action
		if(action.noRepeat() || _repeatCount == 1)
			action.invoke(view);
		else
		{
			// stop people doing dumb stuff like C+ENTER 100 C+n
			if(_repeatCount > REPEAT_COUNT_THRESHOLD)
			{
				String label = action.getLabel();
				if(label == null)
					label = action.getName();
				else
					label = GenericGUIUtilities.prettifyMenuLabel(label);

				Object[] pp = { label, _repeatCount };

				if(GUIUtilities.confirm(view,"large-repeat-count",pp,
					JOptionPane.WARNING_MESSAGE,
					JOptionPane.YES_NO_OPTION)
					!= JOptionPane.YES_OPTION)
				{
					repeatCount = 1;
					view.getStatus().setMessage(null);
					return;
				}
			}

			try
			{
				buffer.beginCompoundEdit();

				for(int i = 0; i < _repeatCount; i++)
					action.invoke(view);
			}
			finally
			{
				buffer.endCompoundEdit();
			}
		}

		Macros.Recorder recorder = view.getMacroRecorder();

		if(recorder != null && !action.noRecord())
			recorder.record(_repeatCount,action.getCode());

		// If repeat was true originally, clear it
		// Otherwise it might have been set by the action, etc
		if(_repeatCount != 1)
		{
			// first of all, if this action set a
			// readNextChar, do not clear the repeat
			if(readNextChar != null)
				return;

			repeatCount = 1;
			view.getStatus().setMessage(null);
		}
	} //}}}

	//{{{ invokeLastAction() method
	public void invokeLastAction()
	{
		if(lastAction == null)
			javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null); 
		else
			invokeAction(lastAction);
	} //}}}

	//{{{ Instance variables
	protected final View view;

	//}}}

	//{{{ userInput() method
	protected void userInput(char ch)
	{
		lastActionCount = 0;

		JEditTextArea textArea = view.getTextArea();

		/* Buffer buffer = view.getBuffer();
		if(!buffer.insideCompoundEdit())
			buffer.beginCompoundEdit(); */

		if(repeatCount == 1)
			textArea.userInput(ch);
		else
		{
			// stop people doing dumb stuff like C+ENTER 100 C+n
			if(repeatCount > REPEAT_COUNT_THRESHOLD)
			{
				Object[] pp = { String.valueOf(ch),
					repeatCount };

				if(GUIUtilities.confirm(view,
					"large-repeat-count.user-input",pp,
					JOptionPane.WARNING_MESSAGE,
					JOptionPane.YES_NO_OPTION)
					!= JOptionPane.YES_OPTION)
				{
					repeatCount = 1;
					view.getStatus().setMessage(null);
					return;
				}
			}

			JEditBuffer buffer = view.getBuffer();
			try
			{
				if(repeatCount != 1)
					buffer.beginCompoundEdit();
				for(int i = 0; i < repeatCount; i++)
					textArea.userInput(ch);
			}
			finally
			{
				if(repeatCount != 1)
					buffer.endCompoundEdit();
			}
		}

		Macros.Recorder recorder = view.getMacroRecorder();

		if(recorder != null)
		{
			recorder.recordInput(repeatCount,ch,
				textArea.isOverwriteEnabled());
		}

		repeatCount = 1;
	} //}}}

	//{{{ invokeReadNextChar() method
	protected void invokeReadNextChar(char ch)
	{
		JEditBuffer buffer = view.getBuffer();

		/* if(buffer.insideCompoundEdit())
			buffer.endCompoundEdit(); */

		String charStr = StandardUtilities.charsToEscapes(String.valueOf(ch));

		// this might be a bit slow if __char__ occurs a lot
		int index;
		while((index = readNextChar.indexOf("__char__")) != -1)
		{
			readNextChar = readNextChar.substring(0,index)
				+ '\'' + charStr + '\''
				+ readNextChar.substring(index + 8);
		}

		Macros.Recorder recorder = view.getMacroRecorder();
		if(recorder != null)
			recorder.record(getRepeatCount(),readNextChar);

		view.getStatus().setMessage(null);

		if(getRepeatCount() != 1)
		{
			try
			{
				buffer.beginCompoundEdit();

				BeanShell.eval(view,BeanShell.getNameSpace(),
					"for(int i = 1; i < "
					+ getRepeatCount() + "; i++)\n{\n"
					+ readNextChar + "\n}");
			}
			finally
			{
				buffer.endCompoundEdit();
			}
		}
		else
			BeanShell.eval(view,BeanShell.getNameSpace(),readNextChar);

		readNextChar = null;
	} //}}}
}
