//   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.usability;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import de.ugoe.cs.autoquest.eventcore.IEventTarget;
import de.ugoe.cs.autoquest.eventcore.IEventType;
import de.ugoe.cs.autoquest.eventcore.gui.TextInput;
import de.ugoe.cs.autoquest.eventcore.guimodel.ITextArea;
import de.ugoe.cs.autoquest.eventcore.guimodel.ITextField;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IEventTaskInstance;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IIterationInstance;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IOptionalInstance;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ISelectionInstance;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ISequenceInstance;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskInstance;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskModel;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IUserSession;

/**
 * TODO comment
 * 
 * @version $Revision: $ $Date: 16.07.2012$
 * @author 2012, last modified by $Author: pharms$
 */
public class TextInputStatisticsRule implements UsabilityEvaluationRule {

    /*
     * (non-Javadoc)
     * 
     * @see de.ugoe.cs.usability.UsabilityEvaluationRule#evaluate(TaskTree)
     */
    @Override
    public UsabilityEvaluationResult evaluate(ITaskModel taskModel) {
        TextInputStatistics statistics = new TextInputStatistics();
        calculateStatistics(taskModel.getUserSessions(), statistics);

        UsabilityEvaluationResult results = new UsabilityEvaluationResult();
        analyzeStatistics(statistics, results);

        return results;
    }

    /**
     *
     */
    private void analyzeStatistics(TextInputStatistics       statistics,
                                   UsabilityEvaluationResult results)
    {
        checkTextInputRatio(statistics, results);
        checkTextFieldEntryRepetitions(statistics, results);
        checkTextFieldNoLetterOrDigitInputs(statistics, results);
    }

    /**
     *
     */
    private void checkTextInputRatio(TextInputStatistics       statistics,
                                     UsabilityEvaluationResult results)
    {
        float allTextFieldInputs =
            statistics.getNoOfTextFieldInputs() + statistics.getNoOfTextAreaInputs();

        float ratio = allTextFieldInputs / (float) statistics.getNoOfAllEvents();

        UsabilityDefectSeverity severity = null;
        if (ratio > 0.9) {
            severity = UsabilityDefectSeverity.HIGH;
        }
        else if (ratio > 0.7) {
            severity = UsabilityDefectSeverity.MEDIUM;
        }
        else if (ratio > 0.5) {
            severity = UsabilityDefectSeverity.LOW;
        }
        else if (ratio > 0.3) {
            severity = UsabilityDefectSeverity.INFO;
        }

        if (severity != null) {
            Map<String, String> parameters = new HashMap<String, String>();
            parameters.put("textInputRatio", DecimalFormat.getInstance().format(ratio * 100));

            results.addDefect
                (severity, UsabilityDefectDescription.TEXT_FIELD_INPUT_RATIO, parameters);
        }
    }

    /**
     *
     */
    private void checkTextFieldEntryRepetitions(TextInputStatistics       statistics,
                                                UsabilityEvaluationResult results)
    {
        List<TextFieldCorrelation> textFieldCorrelations =
            statistics.determineTextFieldCorrelations();
        
        for (TextFieldCorrelation entry : textFieldCorrelations) {
            int noOfUsagesOfTextField1 = statistics.getUsageCount(entry.textField1);
            int noOfUsagesOfTextField2 = statistics.getUsageCount(entry.textField2);
            int noOfUsagesOfTextField1WithSameTextInTextField2 = entry.enteredTexts.size();
            
            float ratioTextField1 =
                noOfUsagesOfTextField1WithSameTextInTextField2 / (float) noOfUsagesOfTextField1;
            
            float ratioTextField2 =
                noOfUsagesOfTextField1WithSameTextInTextField2 / (float) noOfUsagesOfTextField2;
            
            createTextFieldEntryRepetitionDefect
                (ratioTextField1, entry.textField1, entry.textField2, results);
            
            createTextFieldEntryRepetitionDefect
                (ratioTextField2, entry.textField2, entry.textField1, results);
            
        }
    }

    /**
     *
     */
    private void createTextFieldEntryRepetitionDefect(float                     ratioOfEqualEntries,
                                                      ITextField                textField1,
                                                      ITextField                textField2,
                                                      UsabilityEvaluationResult results)
    {
        UsabilityDefectSeverity severity = null;
        if (ratioOfEqualEntries > 0.9) {
            severity = UsabilityDefectSeverity.HIGH;
        }
        else if (ratioOfEqualEntries > 0.5) {
            severity = UsabilityDefectSeverity.MEDIUM;
        }
        else if (ratioOfEqualEntries > 0.2) {
            severity = UsabilityDefectSeverity.LOW;
        }
        else if (ratioOfEqualEntries > 0.1) {
            severity = UsabilityDefectSeverity.INFO;
        }
        
        if (severity != null) {
            Map<String, String> parameters = new HashMap<String, String>();
            parameters.put("textRepetitionRatio",
                           DecimalFormat.getInstance().format(ratioOfEqualEntries * 100));
            parameters.put("textField1", textField1.toString());
            parameters.put("textField2", textField2.toString());

            results.addDefect
                (severity, UsabilityDefectDescription.TEXT_FIELD_INPUT_REPETITIONS, parameters);
        }
    }

    /**
     *
     */
    private void checkTextFieldNoLetterOrDigitInputs(TextInputStatistics       statistics,
                                                     UsabilityEvaluationResult results)
    {
        for (ITextField textField : statistics.getAllTextFields()) {
            int allCharactersCount = 0;
            int noLetterOrDigitCount = 0;

            for (String textInput : statistics.getAllInputsInto(textField)) {
                for (int j = 0; j < textInput.length(); j++) {
                    if (!Character.isWhitespace(textInput.charAt(j))) {
                        if (!Character.isLetterOrDigit(textInput.charAt(j))) {
                            noLetterOrDigitCount++;
                        }
                        allCharactersCount++;
                    }
                }
            }

            float ratio = (float) noLetterOrDigitCount / (float) allCharactersCount;

            UsabilityDefectSeverity severity = null;
            if (ratio > 0.1) { // every 10th sign
                severity = UsabilityDefectSeverity.HIGH;
            }
            else if (ratio > 0.05) { // every 20th sign
                severity = UsabilityDefectSeverity.MEDIUM;
            }
            else if (ratio > 0.02) { // every 50th sign
                severity = UsabilityDefectSeverity.LOW;
            }
            else if (ratio > 0.01) { // every 100th sign
                severity = UsabilityDefectSeverity.INFO;
            }

            if (severity != null) {
                Map<String, String> parameters = new HashMap<String, String>();
                parameters.put("textField", textField.toString());
                parameters.put("noLetterOrDigitRatio",
                               DecimalFormat.getInstance().format(ratio * 100));

                results.addDefect
                    (severity, UsabilityDefectDescription.TEXT_FIELD_NO_LETTER_OR_DIGIT_RATIO, parameters);
            }
        }
    }

    /**
     * 
     */
    private void calculateStatistics(Collection<IUserSession> sessions,
                                     TextInputStatistics      statistics)
    {
        System.out.print("calculating statistics ... ");
        for (IUserSession session : sessions) {
            for (ITaskInstance taskInstance : session) {
                calculateStatistics(taskInstance, session, statistics);
            }
        }
        System.out.println("done");
    }

    /**
     *
     */
    private void calculateStatistics(ITaskInstance       taskInstance,
                                     IUserSession        session,
                                     TextInputStatistics statistics)
    {
        if (isTextInput(taskInstance)) {
            calculateStatistics((IEventTaskInstance) taskInstance, session, statistics);
        }
        else {
            if (taskInstance instanceof ISequenceInstance) {
                for (ITaskInstance child : (ISequenceInstance) taskInstance) {
                    calculateStatistics(child, session, statistics);
                }
            }
            else if (taskInstance instanceof IIterationInstance) {
                for (ITaskInstance child : (IIterationInstance) taskInstance) {
                    calculateStatistics(child, session, statistics);
                }
            }
            else if (taskInstance instanceof ISelectionInstance) {
                calculateStatistics
                    (((ISelectionInstance) taskInstance).getChild(), session, statistics);
            }
            else if (taskInstance instanceof IOptionalInstance) {
                calculateStatistics
                    (((IOptionalInstance) taskInstance).getChild(), session, statistics);
            }
            else{
                statistics.incrementNoOfOtherEventTasks();
            }
        }
    }

    /**
     *
     */
    private boolean isTextInput(ITaskInstance taskInstance) {
        if (taskInstance instanceof IEventTaskInstance) {
            return ((IEventTaskInstance) taskInstance).getEvent().getType() instanceof TextInput;
        }

        return false;
    }

    /**
     *
     */
    private void calculateStatistics(IEventTaskInstance  taskInstance,
                                     IUserSession        session,
                                     TextInputStatistics statistics)
    {
        IEventType type = taskInstance.getEvent().getType();
        IEventTarget target = taskInstance.getEvent().getTarget();

        if (type instanceof TextInput) {
            if (target instanceof ITextField) {
                statistics.addTextFieldInput(taskInstance, session);
            }
            else if (target instanceof ITextArea) {
                statistics.addTextAreaInput(taskInstance, session);
            }
        }
    }

    /**
     *
     */
/*    private static String[] determineTextFragments(String enteredText) {
        List<String> fragments = new ArrayList<String>();

        StringBuffer fragment = new StringBuffer();
        char lastChar = 0;

        for (int i = 0; i < enteredText.length(); i++) {
            char currentChar = enteredText.charAt(i);

            if (!isEqualCharacterType(lastChar, currentChar)) {
                // the previous fragment ended. so finalize it and start a new one
                if ((fragment != null) && (fragment.length() > 0)) {
                    String fragmentStr = fragment.toString().trim();
                    
                    if (!"".equals(fragmentStr)) {
                        fragments.add(fragmentStr);
                    }
                    
                    fragment = new StringBuffer();
                }
            }

            fragment.append(currentChar);
            lastChar = currentChar;
        }

        if ((fragment != null) && (fragment.length() > 0)) {
            String fragmentStr = fragment.toString().trim();
            
            if (!"".equals(fragmentStr)) {
                fragments.add(fragmentStr);
            }
        }

        return fragments.toArray(new String[fragments.size()]);
    }

    /**
     *
     */
/*    private static boolean isEqualCharacterType(char char1, char char2) {
        return
            ((char1 == char2) ||
            (Character.isWhitespace(char1) && Character.isWhitespace(char2)) ||
            (Character.isDigit(char1) && Character.isDigit(char2)) ||
            (Character.isLetter(char1) && Character.isLetter(char2)) ||
            (Character.isJavaIdentifierPart(char1) && Character.isJavaIdentifierPart(char2)));
    }*/

    /**
     *
     */
    private static class TextInputStatistics {
        
        /** */
        private List<IEventTaskInstance> textFieldInputs = new ArrayList<IEventTaskInstance>();
        
        /** */
        private Map<ITextField, List<String>> textFields = new HashMap<ITextField, List<String>>();

        /** */
        private List<IEventTaskInstance> textAreaInputs = new ArrayList<IEventTaskInstance>();

        /** */
        private Map<IUserSession, Map<String, TextEntryData>> textEntries =
            new HashMap<IUserSession, Map<String, TextEntryData>>();

        /** */
        private int otherEventsCount;

        /**
         *
         */
        private void addTextFieldInput(IEventTaskInstance instance, IUserSession session) {
            String enteredText = ((TextInput) instance.getEvent().getType()).getEnteredText();
            
            if ((enteredText != null) && (!"".equals(enteredText.trim()))) {
                enteredText = enteredText.trim();
                
                textFieldInputs.add(instance);

                // store text entries into text fields
                List<String> entries = textFields.get(instance.getEvent().getTarget());

                if (entries == null) {
                    entries = new LinkedList<String>();
                    textFields.put((ITextField) instance.getEvent().getTarget(), entries);
                }
                
                entries.add(enteredText);

                // writing down all text entries in text fields to check later for cooccurrences in
                // same session
                Map<String, TextEntryData> sessionTextEntries = textEntries.get(session);

                if (sessionTextEntries == null) {
                    sessionTextEntries = new HashMap<String, TextEntryData>();
                    textEntries.put(session, sessionTextEntries);
                }

                TextEntryData data = sessionTextEntries.get(enteredText);

                if (data == null) {
                    data = new TextEntryData(enteredText);
                    sessionTextEntries.put(enteredText, data);
                }

                data.addTaskInstance(instance);
            }
        }
        
        /**
         *
         */
        public List<String> getAllInputsInto(ITextField textField) {
            return textFields.get(textField);
        }

        /**
         *
         */
        private int getUsageCount(ITextField textField) {
            List<String> entries = textFields.get(textField);
            
            if (entries == null) {
                return 0;
            }
            else {
                return entries.size();
            }
        }

        /**
         *
         */
        private List<TextFieldCorrelation> determineTextFieldCorrelations() {
            System.out.print("determining text field correlations of " + textFields.size() +
                             " text fields ... ");
            List<TextFieldCorrelation> correlations = new ArrayList<TextFieldCorrelation>();
            
            // we need an ordered list of text fields to be able compare all with each other
            // through a nested loop
            List<ITextField> textFieldList = getAllTextFields();
            
            List<TextEntryData> relevantTextEntryData = new LinkedList<TextEntryData>();
            
            for (Map<String, TextEntryData> sessionSpecEntries : textEntries.values()) {
                for (TextEntryData data : sessionSpecEntries.values()) {
                    if (data.textFields.size() > 1) {
                        relevantTextEntryData.add(data);
                    }
                }
            }
            
            for (int i = 0; i < (textFieldList.size() - 1); i++) {
                for (int j = i + 1; j < textFieldList.size(); j++) {
                    // count the number of times, in which the same text was entered in both
                    // text fields within the same session
                    List<String> sameEnteredTexts = new LinkedList<String>();

                    for (TextEntryData data : relevantTextEntryData) {
                        if (data.textFields.contains(textFieldList.get(i)) &&
                            data.textFields.contains(textFieldList.get(j)))
                        {
                            sameEnteredTexts.add(data.enteredText);
                        }
                    }

                    if (sameEnteredTexts.size() > 0) {
                        // for the checked combination of text fields, there is at least once
                        // the same text entered into both text fields during the same session
                        correlations.add(new TextFieldCorrelation(textFieldList.get(i),
                                                                  textFieldList.get(j),
                                                                  sameEnteredTexts));
                    }
                }
            }
            
            System.out.println("done");
            
            return correlations;
        }

        /**
         *
         */
        private void addTextAreaInput(IEventTaskInstance instance, IUserSession session) {
            textAreaInputs.add(instance);
        }

        /**
         *
         */
        private int getNoOfAllEvents() {
            return textFieldInputs.size() + textAreaInputs.size() + otherEventsCount;
        }

        /**
         *
         */
        private int getNoOfTextFieldInputs() {
            return textFieldInputs.size();
        }

        /**
         *
         */
        private int getNoOfTextAreaInputs() {
            return textAreaInputs.size();
        }

        /**
         *
         */
        private void incrementNoOfOtherEventTasks() {
            otherEventsCount++;
        }

        /**
         * 
         */
        private List<ITextField> getAllTextFields() {
            List<ITextField> textFieldList = new ArrayList<ITextField>(textFields.size());
            
            for (ITextField textField : textFields.keySet()) {
                textFieldList.add(textField);
            }
            
            return textFieldList;
        }
    }
    
    /**
     * 
     */
    private static class TextEntryData {
        
        /** */
        private String enteredText;
        
        /** */
        private List<IEventTaskInstance> respectiveTaskInstances =
            new LinkedList<IEventTaskInstance>();

        /** */
        private Set<ITextField> textFields = new HashSet<ITextField>();
        
        /**
         *
         */
        private TextEntryData(String text) {
            this.enteredText = text;
        }

        /**
         *
         */
        private void addTaskInstance(IEventTaskInstance instance) {
            respectiveTaskInstances.add(instance);
            textFields.add((ITextField) instance.getEvent().getTarget());
        }
        
    }

    /**
     * 
     */
    private static class TextFieldCorrelation {
        
        /** */
        private List<String> enteredTexts = new LinkedList<String>();

        /** */
        private ITextField textField1;
        
        /** */
        private ITextField textField2;
        
        /**
         *
         */
        private TextFieldCorrelation(ITextField   textField1,
                                     ITextField   textField2,
                                     List<String> enteredTexts)
        {
            this.textField1 = textField1;
            this.textField2 = textField2;
            this.enteredTexts = enteredTexts;
        }

    }

}
