//   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.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.logging.Level;

import de.ugoe.cs.autoquest.eventcore.Event;
import de.ugoe.cs.autoquest.keyboardmaps.VirtualKey;
import de.ugoe.cs.util.console.Console;

/**
 * <p>
 * This class provides the functionality to sort and clean up all key interactions in a log. In
 * particular:
 * </p>
 * <ol>
 * <li>In case a combination key (e.g., shift, alt, control) is held down, multiple
 * {@link KeyPressed} events are logged, even though only the first one is of importance. This class
 * removes all {@link KeyPressed} events for combination keys except the first.</li>
 * <li>In case a normal key is held down, multiple {@link KeyPressed} events are logged, but there
 * is only one {@link KeyReleased} event. This class adds a {@link KeyReleased} event for all such
 * {@link KeyPressed} events.</li>
 * <li>Due to message filtering of applications, it is possible that a {@link KeyReleased} event
 * without a preceding {@link KeyPressed} event is logged. This class either adds the missing
 * {@link KeyPressed} right in front of the {@link KeyReleased} or removes the {@link KeyReleased}
 * depending on the {@link #mode}.
 * <li>As a result of steps 2-3, we have always a matching {@link KeyPressed}/{@link KeyReleased}
 * pairs for all normal keys. This class replaces these pairs with a {@link KeyTyped} event at the
 * position of the {@link KeyPressed} event.</li>
 * <li>Sometimes combination keys are not released in the same order they have been pressed. This
 * class ensures that the {@link KeyReleased} are in the opposite order of the {@link KeyPressed}
 * events for all combination keys.</li>
 * </ol>
 * 
 * @version 1.0
 * @author Steffen Herbold
 */
public class KeyInteractionCorrector {

    /**
     * <p>
     * Describes the clean-up mode.
     * </p>
     * 
     * @version 1.0
     * @author Steffen Herbold
     */
    public static enum CleanupMode {
        /**
         * <p>
         * Single {@link KeyReleased} are removed from the sequence.
         * </p>
         */
        REMOVAL,

        /**
         * <p>
         * {@link KeyPressed} events are added to single {@link KeyReleased} events
         * </p>
         */
        ADDITION
    };

    /**
     * <p>
     * 
     * </p>
     */
    private final CleanupMode mode;

    /**
     * <p>
     * Constructor. Creates a new {@link KeyInteractionCorrector} with {@link #mode}=
     * {@link CleanupMode#ADDITION}.
     * </p>
     */
    public KeyInteractionCorrector() {
        this(CleanupMode.ADDITION);
    }

    /**
     * <p>
     * Constructor. Creates a new {@link KeyInteractionCorrector}.
     * </p>
     * 
     * @param mode
     *            {@link #mode} of the instance
     */
    public KeyInteractionCorrector(CleanupMode mode) {
        this.mode = mode;
    }

    /**
     * <p>
     * Sorts and cleans up key interactions according to the class specification (@see
     * {@link KeyInteractionCorrector} class comment).
     * </p>
     * <p>
     * This method returns a sorted copy of a sequence, the sequence itself is not changed.
     * </p>
     * 
     * @param sequence
     *            sequence which is sorted
     * @return sorted copy of sequence
     */
    public List<Event> sortKeyInteractions(final List<Event> sequence) {
        List<Event> sortedSequence = new LinkedList<Event>(sequence);

        handleIncompleteKeyPairs(sortedSequence);
        sortCombinationKeyPairs(sortedSequence);

        return sortedSequence;
    }

    /**
     * <p>
     * Performs tasks 1-4 defined in the class description. Operations are performed in-place on the
     * passed sequence.
     * </p>
     * 
     * @param sequence
     *            sequence which is sorted
     */
    private void sortCombinationKeyPairs(List<Event> sequence) {
        LinkedList<VirtualKey> pressedCombinationKeys = new LinkedList<VirtualKey>();

        for (int i = 0; i < sequence.size(); i++) {
            Event event = sequence.get(i);
            if (event.getType() instanceof KeyPressed) {
                final VirtualKey key = ((KeyPressed) event.getType()).getKey();
                if (key.isCombinationKey()) {
                    pressedCombinationKeys.add(key);
                }
            }
            if (event.getType() instanceof KeyReleased) {
                final VirtualKey key = ((KeyReleased) event.getType()).getKey();
                if (key.isCombinationKey()) {
                    /*
                     * if( pressedCombinationKeys.isEmpty() ) { Console.traceln(Level.INFO, "" + i);
                     * for( int j=i-30 ; j<=i ; j++ ) { Console.traceln(Level.INFO,
                     * sequence.get(i).toString()); } }
                     */
                    if (key.equals(pressedCombinationKeys.getLast())) {
                        pressedCombinationKeys.removeLast();
                    }
                    else {
                        // look-ahead to find new position
                        int offset;
                        for (offset = 1; offset + i < sequence.size(); offset++) {
                            Event lookaheadEvent = sequence.get(i + offset);
                            if (lookaheadEvent.getType() instanceof KeyReleased) {
                                if (((KeyReleased) lookaheadEvent.getType()).getKey()
                                    .equals(pressedCombinationKeys.getLast()))
                                {
                                    break;
                                }
                            }
                        }
                        sequence.add(i + offset + 1, event);
                        sequence.remove(i);
                        i--;
                    }
                }
            }
        }
    }

    /**
     * <p>
     * Performs task 5 defined in the class description. Operations are performed in-place on the
     * passed sequence.
     * </p>
     * 
     * @param sequence
     *            sequence which is sorted
     */
    private void handleIncompleteKeyPairs(List<Event> sequence) {
        Set<VirtualKey> pressedKeys = new HashSet<VirtualKey>();
        int firstPressedIndex = -1;

        Set<VirtualKey> pressedCombinationKeysSession = new HashSet<VirtualKey>();

        for (int i = 0; i < sequence.size(); i++) {
            Event event = sequence.get(i);
            if (event.getType() instanceof KeyboardFocusChange) {
                if (firstPressedIndex != -1) {
                    sequence.remove(i);
                    sequence.add(firstPressedIndex, event);
                }
            }

            if (event.getType() instanceof KeyPressed) {
                if (pressedKeys.isEmpty()) {
                    firstPressedIndex = i;
                }
                VirtualKey key = ((KeyPressed) event.getType()).getKey();
                if (!key.isCombinationKey()) {
                    ListIterator<Event> iter = sequence.listIterator(i);
                    iter.next();
                    iter.set(new Event(new KeyTyped(key), event.getTarget()));
                }
                else {
                    pressedCombinationKeysSession.add(key);
                    if (pressedKeys.contains(key)) {
                        sequence.remove(i);
                        i--;
                    }
                }
                pressedKeys.add(key);
            }
            if (event.getType() instanceof KeyReleased) {
                VirtualKey key = ((KeyReleased) event.getType()).getKey();
                if (!key.isCombinationKey()) {
                    if (pressedKeys.contains(key)) {
                        sequence.remove(i);
                        i--;
                    }
                    else {
                        // found KeyReleased event without KeyPressed
                        switch (mode)
                        {
                            case REMOVAL:
                                sequence.remove(i);
                                i--;
                                break;
                            case ADDITION:
                                ListIterator<Event> iter = sequence.listIterator(i);
                                iter.next();
                                iter.set(new Event(new KeyTyped(key), event.getTarget()));
                                break;
                            default:
                                throw new AssertionError(
                                                         "reached source code that should be unreachable");
                        }
                    }
                }
                else {
                    if (!pressedKeys.contains(key)) {
                        if (pressedCombinationKeysSession.contains(key)) {
                            Console.traceln(Level.SEVERE, "Found a " + key +
                                " KeyReleased event without a KeyPressed event." +
                                "The event will be dropped and the session is possibly faulty.");
                            sequence.remove(i);
                            i--;
                        }
                        else {
                            Console
                                .traceln(Level.SEVERE,
                                         "Found a " +
                                             key +
                                             " KeyReleased event without a KeyPressed event." +
                                             "Since no KeyPressed of key " +
                                             key +
                                             " has been part of the session " +
                                             "till now, we assume that the key has been pressed since the beginning " +
                                             "of the session and add a KeyPressed event for " +
                                             key + " to the start " + "of the session.");
                            sequence.add(0, new Event(new KeyPressed(key), event.getTarget()));
                            i++;
                        }
                    }
                }
                pressedKeys.remove(key);
                if (pressedKeys.isEmpty()) {
                    firstPressedIndex = -1;
                }
            }
        }
        if (!pressedKeys.isEmpty()) {
            Console
                .traceln(Level.WARNING,
                         "There was probably a failure during the handling of incomplete key event pairs.");
        }
    }
}
