package de.ugoe.cs.quest.usability;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import de.ugoe.cs.quest.eventcore.guimodel.ITextArea;
import de.ugoe.cs.quest.eventcore.guimodel.ITextField;
import de.ugoe.cs.quest.tasktrees.treeifc.ITaskTree;
import de.ugoe.cs.quest.tasktrees.treeifc.ITaskTreeNode;
import de.ugoe.cs.quest.tasktrees.treeifc.ITextInputEventTask;

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

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

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

        return results;
    }

    /**
     * TODO: comment
     * 
     * @param statistics
     * @param results
     */
    private void analyzeStatistics(TextInputStatistics       statistics,
                                   UsabilityEvaluationResult results)
    {
        checkTextInputRatio(statistics, results);
        checkTextFieldEntryRepetitions(statistics, results);
        checkTextFieldNoLetterOrDigitInputs(statistics, results);
    }

    /**
     * TODO: comment
     * 
     * @param statistics
     * @param 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
                (new UsabilityDefect(severity, UsabilityDefectDescription.TEXT_FIELD_INPUT_RATIO,
                                     parameters));
        }
    }

    /**
     * TODO: comment
     * 
     * @param statistics
     * @param results
     */
    private void checkTextFieldEntryRepetitions(TextInputStatistics       statistics,
                                                UsabilityEvaluationResult results)
    {
        Map<String, Integer> words = new HashMap<String, Integer>();
        int numberOfRepeatedWords = 0;
        int maxRepetitions = 0;

        for (int i = 0; i < statistics.getNoOfTextFieldInputs(); i++) {
            String[] fragments = statistics.getTextFieldInputFragments(i);
            for (String fragment : fragments) {
                if (!"".equals(fragment.trim())) {
                    Integer count = words.get(fragment);
                    if (count == null) {
                        words.put(fragment, 1);
                    }
                    else {
                        count++;
                        words.put(fragment, count);
                        maxRepetitions = Math.max(count, maxRepetitions);

                        if (count == 2) {
                            // do not calculate repeated words several times
                            numberOfRepeatedWords++;
                        }
                    }
                }
            }
        }

        UsabilityDefectSeverity severity = null;
        if ((numberOfRepeatedWords > 10) || (maxRepetitions > 10)) {
            severity = UsabilityDefectSeverity.HIGH;
        }
        else if ((numberOfRepeatedWords > 4) || (maxRepetitions > 4)) {
            severity = UsabilityDefectSeverity.MEDIUM;
        }
        else if ((numberOfRepeatedWords > 2) || (maxRepetitions > 2)) {
            severity = UsabilityDefectSeverity.LOW;
        }
        else if ((numberOfRepeatedWords > 1) || (maxRepetitions > 1)) {
            severity = UsabilityDefectSeverity.INFO;
        }

        if (severity != null) {
            Map<String, String> parameters = new HashMap<String, String>();
            parameters.put("textRepetitionRatio", numberOfRepeatedWords +
                           " repeated tokens, up to " + maxRepetitions + " repetitions per token");

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

    /**
     * TODO: comment
     * 
     * @param statistics
     * @param results
     */
    private void checkTextFieldNoLetterOrDigitInputs(TextInputStatistics       statistics,
                                                     UsabilityEvaluationResult results)
    {
        int allCharactersCount = 0;
        int noLetterOrDigitCount = 0;

        for (int i = 0; i < statistics.getNoOfTextFieldInputs(); i++) {
            String[] fragments = statistics.getTextFieldInputFragments(i);
            for (String fragment : fragments) {
                String effectiveFragment = fragment.trim();
                for (int j = 0; j < effectiveFragment.length(); j++) {
                    if (!Character.isWhitespace(effectiveFragment.charAt(j))) {
                        if (!Character.isLetterOrDigit(effectiveFragment.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("noLetterOrDigitRatio", allCharactersCount + " entered characters of " +
                           "which " + noLetterOrDigitCount + " were no letter or digit");

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

    /**
     * TODO: comment
     * 
     * @param taskTree
     * @param statistics
     */
    private void calculateStatistics(ITaskTreeNode node, TextInputStatistics statistics) {
        if (node instanceof ITextInputEventTask) {
            calculateStatistics((ITextInputEventTask) node, statistics);
        }
        else {
            if ((node.getChildren() == null) || (node.getChildren().size() == 0)) {
                statistics.incrementNoOfOtherEventTasks();
            }
            else {
                for (ITaskTreeNode child : node.getChildren()) {
                    calculateStatistics(child, statistics);
                }
            }
        }
    }

    /**
     * TODO: comment
     * 
     * @param taskTree
     * @param statistics
     */
    private void calculateStatistics(ITextInputEventTask node, TextInputStatistics statistics) {
        String[] fragments = determineTextFragments(node.getEnteredText());

        if (node.getEventTarget() instanceof ITextField) {
            statistics.addTextFieldInput(node, fragments);
        }
        else if (node.getEventTarget() instanceof ITextArea) {
            statistics.addTextAreaInput(node, fragments);
        }
    }

    /**
     * TODO: comment
     * 
     * @param enteredText
     * @return
     */
    private 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)) {
                    fragments.add(fragment.toString());
                    fragment = new StringBuffer();
                }
            }

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

        if ((fragment != null) && (fragment.length() > 0)) {
            fragments.add(fragment.toString());
        }

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

    /**
     * TODO: comment
     * 
     * @param lastChar
     * @param currentChar
     * @return
     */
    private 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)));
    }

    /**
     * TODO comment
     * 
     * @version $Revision: $ $Date: 16.07.2012$
     * @author 2012, last modified by $Author: pharms$
     */
    public static class TextInputStatistics {
        
        /** */
        private List<Object[]> textFieldInputs = new ArrayList<Object[]>();

        /** */
        private List<Object[]> textAreaInputs = new ArrayList<Object[]>();

        /** */
        private int otherEventsCount;

        /**
         * TODO: comment
         * 
         * @param node
         * @param fragments
         * 
         */
        public void addTextFieldInput(ITextInputEventTask node, String[] fragments) {
            textFieldInputs.add(new Object[] { node, fragments });
        }

        /**
         * TODO: comment
         * 
         * @param node
         * @param fragments
         * 
         */
        public void addTextAreaInput(ITextInputEventTask node, String[] fragments) {
            textAreaInputs.add(new Object[] { node, fragments });
        }

        /**
         * TODO: comment
         * 
         * @return
         */
        public int getNoOfAllEvents() {
            return textFieldInputs.size() + textAreaInputs.size() + otherEventsCount;
        }

        /**
         * TODO: comment
         * 
         * @return
         */
        public int getNoOfTextFieldInputs() {
            return textFieldInputs.size();
        }

        /**
         * TODO: comment
         * 
         * @param i
         * @return
         */
        public String[] getTextFieldInputFragments(int index) {
            return (String[]) textFieldInputs.get(index)[1];
        }

        /**
         * TODO: comment
         * 
         * @return
         */
        public int getNoOfTextAreaInputs() {
            return textAreaInputs.size();
        }

        /**
         * TODO: comment
         * 
         * @param i
         * @return
         */
        public String[] getTextAreaInputFragments(int index) {
            return (String[]) textAreaInputs.get(index)[1];
        }

        /**
         * TODO: comment
         * 
         */
        public void incrementNoOfOtherEventTasks() {
            otherEventsCount++;
        }

    }

}
