//   Copyright 2012 Georg-August-Universität Göttingen, Germany
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.

package de.ugoe.cs.autoquest.eventcore.gui;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;

import de.ugoe.cs.autoquest.eventcore.Event;
import de.ugoe.cs.autoquest.eventcore.IEventTarget;
import de.ugoe.cs.autoquest.eventcore.gui.KeyInteraction;
import de.ugoe.cs.autoquest.eventcore.gui.KeyPressed;
import de.ugoe.cs.autoquest.eventcore.gui.KeyReleased;
import de.ugoe.cs.autoquest.eventcore.gui.TextInput.TextEquality;
import de.ugoe.cs.autoquest.eventcore.guimodel.ITextArea;
import de.ugoe.cs.autoquest.eventcore.guimodel.ITextField;
import de.ugoe.cs.autoquest.keyboardmaps.KeyboardMap;
import de.ugoe.cs.autoquest.keyboardmaps.KeyboardMapFactory;
import de.ugoe.cs.autoquest.keyboardmaps.VirtualKey;

/**
 * <p>
 * The text input detector iterates a list of events and searches for subsequent key events. Those
 * are replaced by a single text input event representing the text entered through the key events.
 * The replacement is only done, if the key events have a text field or text area as target
 * </p>
 * 
 * @version $Revision: $ $Date: 18.03.2012$
 * @author 2012, last modified by $Author: patrick$
 */
public class TextInputDetector {

    /** the keyboard map to use for character recognition */
    private KeyboardMap keyboardMap = KeyboardMapFactory.createKeyboardMap(Locale.GERMAN);

    /** the keys pressed in parallel */
    private List<VirtualKey> pressedKeys = new ArrayList<VirtualKey>();

    /** the chosen text equality type */
    private final TextEquality textEqualityType;

    /**
     * <p>
     * Constructor. Creates a new TextInputDectector that generates {@link TextInput} with
     * {@link TextEquality#LEXICAL} equality.
     * </p>
     * 
     */
    public TextInputDetector() {
        this(TextEquality.LEXICAL);
    }

    /**
     * <p>
     * Constructor. Creates a new TextInputDectector that generates {@link TextInput} with a given
     * {@link TextEquality} type.
     * </p>
     * 
     * @param textEqualityType
     *            equality type of the generated events
     */
    public TextInputDetector(TextEquality textEqualityType) {
        this.textEqualityType = textEqualityType;
    }

    /**
     * <p>
     * in the provided list of events, this method detects any event sequences that consists of key
     * interactions and replaces them with a single text input interaction. This contains the
     * entered text as well as the replaced key interaction events
     * </p>
     * 
     * @param sequence
     *            the event sequence to search for text input events
     * 
     * @return the resulting sequence, in which key interactions on text fields and areas are
     *         reduced to text input interactions
     */
    public List<Event> detectTextInputs(List<Event> sequence) {
        List<Event> resultingSequence = new LinkedList<Event>();

        int textEntryStartIndex = -1;
        IEventTarget lastEventTarget = null;

        int index = 0;
        Event currentEvent = null;
        Event textInputEvent = null;
        while (index < sequence.size()) {
            currentEvent = sequence.get(index);
            textInputEvent = null;

            if (isKeyInteraction(currentEvent) && isDataInputEventTarget(currentEvent.getTarget()))
            {
                if (textEntryStartIndex < 0) {
                    textEntryStartIndex = index;
                    lastEventTarget = currentEvent.getTarget();
                }
                else if (!lastEventTarget.equals(currentEvent.getTarget())) {
                    textInputEvent =
                        handleTextEntrySequence(sequence, textEntryStartIndex, index - 1,
                                                lastEventTarget);

                    textEntryStartIndex = index;
                    lastEventTarget = currentEvent.getTarget();
                }
                currentEvent = null;
            }
            else {
                if (textEntryStartIndex >= 0) {
                    textInputEvent =
                        handleTextEntrySequence(sequence, textEntryStartIndex, index - 1,
                                                lastEventTarget);

                    textEntryStartIndex = -1;
                    lastEventTarget = null;
                }

            }

            if (textInputEvent != null) {
                resultingSequence.add(textInputEvent);
            }

            if (currentEvent != null) {
                resultingSequence.add(currentEvent);
            }

            index++;
        }

        if (textEntryStartIndex >= 0) {
            textInputEvent =
                handleTextEntrySequence(sequence, textEntryStartIndex, sequence.size() - 1,
                                        lastEventTarget);

            if (textInputEvent != null) {
                resultingSequence.add(textInputEvent);
            }
        }

        return resultingSequence;
    }

    /**
     * <p>
     * returns true if the provide event is a key interaction; false else
     * </p>
     * 
     * @param event
     *            the even to check
     * 
     * @return as described
     */
    private boolean isKeyInteraction(Event event) {
        return (event.getType() instanceof KeyInteraction);
    }

    /**
     * <p>
     * creates a single text input event as replacement for key interactions being part of the
     * subsequence of events denoted by the start and end index in the provide event sequence. If no
     * text was entered, because the subsequence only contained key released events, then no text
     * input event is generated (the method returns null).
     * </p>
     * 
     * @param sequence
     *            the event sequence of which the subsequence is analyzed
     * @param startIndex
     *            the start index in the event sequence from which the analysis should start
     *            (inclusive)
     * @param endIndex
     *            the end index in the event sequence where the analysis should end (inclusive)
     * @param eventTarget
     *            the event target to be used for the new event
     * 
     * @return a text input event representing the text input resulting from the events of the
     *         provided subsequence
     * 
     * @throws IllegalArgumentException
     *             if the denoted subsequence contains other events than key interactions
     */
    private Event handleTextEntrySequence(List<Event> sequence,
                                          int startIndex,
                                          int endIndex,
                                          IEventTarget eventTarget)
    {
        List<Event> textInputEvents = new ArrayList<Event>();

        String enteredText = determineEnteredText(sequence, startIndex, endIndex, textInputEvents);

        if ((enteredText != null) && (!"".equals(enteredText))) {
            TextInput textInput = new TextInput(enteredText, textInputEvents, textEqualityType);
            return new Event(textInput, eventTarget);
        }
        else {
            return null;
        }
    }

    /**
     * <p>
     * check if an event target is a data input field, i.e. a text field or a text area
     * </p>
     * 
     * @param eventTarget
     *            the event target to check
     * 
     * @return true, if it is a text field or a text area ; false else
     */
    private boolean isDataInputEventTarget(IEventTarget eventTarget) {
        return ((eventTarget instanceof ITextField) || (eventTarget instanceof ITextArea));
    }

    /**
     * <p>
     * determines the text entered in the event subsequence denoted by the start and end index of
     * the provided event sequence. The method records any pressed and released key interaction as
     * well as combinations of pressed key interactions (such as shift + letter) and determines the
     * respective character. All identified characters are then combined to the entered text. The
     * method also identifies the usage of the back space. The enter key is ignored for text fields
     * in which pressing the enter key results in finishing the text entry. All analyzed key
     * interaction events are stored in the provided text input events result list. If the
     * subsequence only contains key released events, no text was entered and the returned text is
     * null.
     * </p>
     * 
     * @param sequence
     *            the event sequence of which the subsequence is analyzed
     * @param startIndex
     *            the start index in the event sequence from which the analysis should start
     *            (inclusive)
     * @param endIndex
     *            the end index in the event sequence where the analysis should end (inclusive)
     * @param textInputEvents
     *            a buffer to contain any key interaction event analyzed (in out)
     * 
     * @return the text entered through the interaction events of the denoted subsequence
     * 
     * @throws IllegalArgumentException
     *             if the denoted sequence contains other events than key interactions
     */
    private String determineEnteredText(List<Event> sequence,
                                        int startIndex,
                                        int endIndex,
                                        List<Event> textInputEvents)
        throws IllegalArgumentException
    {
        Event event;
        StringBuffer enteredText = new StringBuffer();

        for (int i = startIndex; i <= endIndex; i++) {
            event = sequence.get(i);

            if (event.getType() instanceof KeyPressed || event.getType() instanceof KeyTyped) {
                VirtualKey key = ((KeyInteraction) event.getType()).getKey();

                pressedKeys.add(key);

                if (key == VirtualKey.BACK_SPACE) {
                    if (enteredText.length() > 0) {
                        enteredText.deleteCharAt(enteredText.length() - 1);
                    }
                }
                else if (key == VirtualKey.ENTER) {
                    // text fields only contain one line of code. Therefore the return is ignored.
                    if (!(event.getTarget() instanceof ITextField)) {
                        enteredText.append(getCharacter(key, pressedKeys));
                    }
                }
                else {
                    char theChar = getCharacter(key, pressedKeys);
                    if (theChar != Character.UNASSIGNED) {
                        enteredText.append(theChar);
                    }
                }
            }
            else if (event.getType() instanceof KeyReleased || event.getType() instanceof KeyTyped)
            {
                pressedKeys.remove(((KeyInteraction) event.getType()).getKey());
            }
            else {
                throw new IllegalArgumentException(
                                                   "the subsequence denoted by the indexes contains other interactions than "
                                                       + "just key strokes");
            }

            textInputEvents.add(event);
        }

        if (enteredText.length() > 0) {
            return enteredText.toString();
        }
        else {
            return null;
        }
    }

    /**
     * <p>
     * determines the character matching the pressed key depending on other keys pressed in parallel
     * such as the shift key.
     * </p>
     * 
     * @param key
     *            the key for which the character shall be determined
     * @param pressedKeys
     *            the list of other keys pressed in parallel
     * 
     * @return the character resulting from the combination of pressed keys
     */
    private char getCharacter(VirtualKey key, List<VirtualKey> pressedKeys) {
        boolean numlock = false;
        boolean shift = false;
        boolean altgr = false;

        for (VirtualKey pressedKey : pressedKeys) {
            if (pressedKey.isShiftKey()) {
                shift = !shift;
            }
            else if (pressedKey == VirtualKey.ALT_GRAPH) {
                altgr = !altgr;
            }
            else if (pressedKey == VirtualKey.NUM_LOCK) {
                numlock = !numlock;
            }
        }

        return keyboardMap.getCharacterFor(key, numlock, shift, altgr, false);
    }

}
