/*
 * DefaultInputHandler.java - Default implementation of an input handler
 * Copyright (C) 1999 Slava Pestov
 *
 * You may use and modify this package for any purpose. Redistribution is
 * permitted, in both source and binary form, provided that this notice
 * remains intact in all source distributions of this package.
 */

package processing.app.syntax;

import javax.swing.KeyStroke;
import java.awt.event.*;
import java.awt.Toolkit;
import java.util.Map;
import java.util.HashMap;
import java.util.StringTokenizer;

/**
 * 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 HashMap();
        }

        /**
         * 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("HOME",HOME);
                addKeyBinding("END",END);
                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);
        }

        /**
         * 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)
        {
                Map 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 Map)
                                        current = (Map)o;
                                else
                                {
                                        o = new HashMap();
                                        current.put(keyStroke,o);
                                        current = (Map)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();

          // moved this earlier so it doesn't get random meta clicks
          if (keyCode == KeyEvent.VK_CONTROL ||
              keyCode == KeyEvent.VK_SHIFT ||
              keyCode == KeyEvent.VK_ALT ||
              keyCode == KeyEvent.VK_META) {
            return;
          }

          // don't get command-s or other menu key equivs on mac
          // unless it's something that's specifically bound (cmd-left or right)
          //if ((modifiers & KeyEvent.META_MASK) != 0) return;
          if ((modifiers & InputEvent.META_MASK) != 0) {
            KeyStroke keyStroke = KeyStroke.getKeyStroke(keyCode, modifiers);
            if (currentBindings.get(keyStroke) == null) {
              return;
            }
          }

                /*
                char keyChar = evt.getKeyChar();
                System.out.println("code=" + keyCode + " char=" + keyChar +
                                   " charint=" + ((int)keyChar));
                System.out.println("other codes " + KeyEvent.VK_ALT + " " +
                                   KeyEvent.VK_META);
                */

                if((modifiers & ~InputEvent.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 Map)
                        {
                                currentBindings = (Map)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();

                // this is the apple/cmd key on macosx.. so menu commands
                // were being passed through as legit keys.. added this line
                // in an attempt to prevent.
                if ((modifiers & InputEvent.META_MASK) != 0) return;

                // Prevent CTRL-/ from going through as a typed '/' character
                // http://code.google.com/p/processing/issues/detail?id=596
                if ((modifiers & InputEvent.CTRL_MASK) != 0 && c == '/') return;

                if (c != KeyEvent.CHAR_UNDEFINED) // &&
                  //                (modifiers & KeyEvent.ALT_MASK) == 0)
                {
                  if(c >= 0x20 && c != 0x7f)
                        {
                                KeyStroke keyStroke = KeyStroke.getKeyStroke(
                                        Character.toUpperCase(c));
                                Object o = currentBindings.get(keyStroke);

                                if(o instanceof Map)
                                {
                                        currentBindings = (Map)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 |= InputEvent.CTRL_MASK;
                                        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 Map bindings;
        private Map currentBindings;

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