//   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.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import org.apache.commons.math3.stat.inference.ChiSquareTest;

import de.ugoe.cs.autoquest.eventcore.Event;
import de.ugoe.cs.autoquest.eventcore.IEventTargetSpec;
import de.ugoe.cs.autoquest.eventcore.IHierarchicalEventTarget;
import de.ugoe.cs.autoquest.eventcore.IHierarchicalEventTargetModel;
import de.ugoe.cs.autoquest.eventcore.gui.MouseClick;
import de.ugoe.cs.autoquest.eventcore.gui.TextInput;
import de.ugoe.cs.autoquest.eventcore.gui.ValueSelection;
import de.ugoe.cs.autoquest.eventcore.guimodel.GUIModel;
import de.ugoe.cs.autoquest.eventcore.guimodel.ICheckBox;
import de.ugoe.cs.autoquest.eventcore.guimodel.IComboBox;
import de.ugoe.cs.autoquest.eventcore.guimodel.IGUIElement;
import de.ugoe.cs.autoquest.eventcore.guimodel.IGUIElementSpec;
import de.ugoe.cs.autoquest.eventcore.guimodel.IGUIView;
import de.ugoe.cs.autoquest.eventcore.guimodel.IRadioButton;
import de.ugoe.cs.autoquest.tasktrees.treeifc.DefaultTaskInstanceTraversingVisitor;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IEventTask;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IEventTaskInstance;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ITask;
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 DefaultValueRule implements UsabilityEvaluationRule {
    
    /** */
    private static String DEFAULT_VALUE = "DEFAULT_VALUE";
    
    /*
     * (non-Javadoc)
     * 
     * @see de.ugoe.cs.usability.UsabilityEvaluationRule#evaluate(TaskTree)
     */
    @Override
    public UsabilityEvaluationResult evaluate(ITaskModel taskModel) {
        System.out.println("determining value selection targets");
        Set<ValueSelectionTarget> targets = getValueSelectionTargets(taskModel.getTasks());
        System.out.println("found " + targets.size() + " targets");
        
        System.out.println("grouping radio buttons and check boxes targets");
        condenseRadioButtonAndCheckBoxGroups(targets);
        System.out.println(targets.size() + " remaining");
        
        System.out.println("calculating statistics");
        ValueChangeStatistics statistics = new ValueChangeStatistics(targets);
        calculateStatistics(taskModel.getUserSessions(), statistics);

        System.out.println("analyzing statistics");
        UsabilityEvaluationResult results = new UsabilityEvaluationResult(taskModel);
        analyzeStatistics(statistics, results);

        return results;
    }

    /**
     *
     */
    private void analyzeStatistics(ValueChangeStatistics     statistics,
                                   UsabilityEvaluationResult results)
    {
        System.out.println
            ("determined " + statistics.getViewsWithValueSelections().size() + " views");
        
        // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> TEST IMPLEMENTATION >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        int valueSelectionTargetCounter = 0;
        
        for (IGUIView view : statistics.getViewsWithValueSelections()) {
            valueSelectionTargetCounter += statistics.getValueSelectionTargetsInView(view).size();
        }
        
        if (valueSelectionTargetCounter != statistics.selectedValues.size()) {
            throw new IllegalStateException("this should not happen");
        }
        // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< TEST IMPLEMENTATION <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
        
        for (IGUIView view : statistics.getViewsWithValueSelections()) {
            //System.out.println("view " + view + " has " +
            //                   statistics.getValueSelectionTargetsInView(view) +
            //                   " value selection targets");
            for (ValueSelectionTarget target : statistics.getValueSelectionTargetsInView(view)) {
                analyzeStatistics
                    (view, target, statistics.getStatisticsForValueSelectionTarget(target), results);
            }
        }
    }

    /**
     *
     */
    private void analyzeStatistics(IGUIView                  view,
                                   ValueSelectionTarget      target,
                                   Map<Object, Integer>      selectedValues,
                                   UsabilityEvaluationResult results)
    {
        //System.out.println("  analyzing selected values for " + target);
        long[] observed = new long[selectedValues.size()];
        long allObserved = 0;
        long maxObserved = 0;
        LinkedList<Object> mostOftenSelected = new LinkedList<>();
        
        int i = 0;
        for (Map.Entry<Object, Integer> selectedValue : selectedValues.entrySet()) {
            //System.out.println("    " + selectedValue.getValue() + " \t " + selectedValue.getKey());
            
            observed[i++] = selectedValue.getValue();
            allObserved += selectedValue.getValue();
            maxObserved = Math.max(maxObserved, selectedValue.getValue());
            
            boolean added = false;
            ListIterator<Object> iterator = mostOftenSelected.listIterator();
            while (iterator.hasNext()) {
                if (selectedValues.get(iterator.next()) < selectedValue.getValue()) {
                    iterator.previous();
                    iterator.add(selectedValue.getKey());
                    added = true;
                    break;
                }
            }
            
            if (!added) {
                mostOftenSelected.add(selectedValue.getKey());
            }
            
            while (mostOftenSelected.size() > 5) {
                mostOftenSelected.removeLast();
            }
        }
        
        double[] expected = new double[observed.length];
        double expectedFrequency = ((double) allObserved) / expected.length;
        
        for (i = 0; i < expected.length; i++) {
            expected[i] = expectedFrequency;
        }
        
        if ((expected.length > 1) &&
            (new ChiSquareTest().chiSquareTest(expected, observed, 0.05)))
        {
            // values are not equally distributed.
            
            // if the default value is most often selected, everything is fine. If not, smell is
            // detected
            if (!DEFAULT_VALUE.equals(mostOftenSelected.get(0))) {
                UsabilitySmellIntensity intensity = UsabilitySmellIntensity.getIntensity
                    ((int) (1000 * maxObserved / allObserved), (int) allObserved, -1);
                
                if (intensity != null) {
                    List<String> mostOftenSelectedValues =
                        new ArrayList<>(mostOftenSelected.size());
                
                    for (Object oftenSelected : mostOftenSelected) {
                        mostOftenSelectedValues.add
                            (oftenSelected + " (" +
                             (100.0 * selectedValues.get(oftenSelected) / allObserved) + "%)");
                    }
                
                    Map<String, Object> parameters = new HashMap<String, Object>();
                    parameters.put("view", view);
                    parameters.put("guiElement", target);
                    parameters.put("selectedValues", mostOftenSelectedValues);

                    results.addSmell(intensity, UsabilitySmellDescription.GOOD_DEFAULTS, parameters);
                }
            }
        }
    }

    /**
     * 
     */
    private Set<ValueSelectionTarget> getValueSelectionTargets(Collection<ITask> tasks) {
        Set<ValueSelectionTarget> result = new HashSet<>();
        
        for (ITask task : tasks) {
            if (task instanceof IEventTask) {
                for (ITaskInstance instance : task.getInstances()) {
                    if (isValueSelection(instance)) {
                        result.add(newValueSelectionTarget(instance));
                    }
                }
            }
        }
        
        return result;
    }

    /**
     *
     */
    private boolean isValueSelection(ITaskInstance instance) {
        if (instance instanceof IEventTaskInstance) {
            Event event = ((IEventTaskInstance) instance).getEvent();
        
            if ((event.getType() instanceof TextInput) ||
                (event.getType() instanceof ValueSelection))
            {
                return true;
            }
            
            if (("JFC".equals(event.getTarget().getPlatform())) &&
                (event.getTarget() instanceof ICheckBox) &&
                (event.getType() instanceof MouseClick))
            {
                return true;
            }
        }
        
        return false;
    }

    /**
     *
     */
    private static ValueSelectionTarget newValueSelectionTarget(ITaskInstance instance) {
        Event event = ((IEventTaskInstance) instance).getEvent();
        return new ValueSelectionTarget((IGUIElement) event.getTarget());
    }
    
    /**
     *
     */
    private void condenseRadioButtonAndCheckBoxGroups(Set<ValueSelectionTarget> targets) {
        // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> TEST IMPLEMENTATION >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        Set<IGUIView> viewsBefore = new HashSet<IGUIView>();
        
        for (ValueSelectionTarget target : targets) {
            viewsBefore.add(target.getView());
        }
        // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< TEST IMPLEMENTATION <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
        
        condenseGuiElementGroups(targets, IRadioButton.class);
        condenseGuiElementGroups(targets, ICheckBox.class);
        
        // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> TEST IMPLEMENTATION >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        Set<IGUIView> viewsAfter = new HashSet<IGUIView>();
        
        for (ValueSelectionTarget target : targets) {
            viewsAfter.add(target.getView());
        }
        
        if (viewsBefore.size() != viewsAfter.size()) {
            throw new IllegalStateException("this should not happen");
        }
        
        for (IGUIView view : viewsBefore) {
            if (!viewsAfter.contains(view)) {
                throw new IllegalStateException("this should not happen too");
            }
        }
        // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< TEST IMPLEMENTATION <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    }
    
    /**
     *
     */
    private void condenseGuiElementGroups(Set<ValueSelectionTarget> targets, Class<?> type) {
        Map<IGUIView, List<IGUIElement>> guiElementsInViews = new HashMap<>();
        
        // determine the targets under consideration
        for (ValueSelectionTarget target : targets) {
            if (type.isInstance(target.getTarget())) {
                List<IGUIElement> guiElementsInView = guiElementsInViews.get(target.getView());

                if (guiElementsInView == null) {
                    guiElementsInView = new LinkedList<IGUIElement>();
                    guiElementsInViews.put(target.getView(), guiElementsInView);
                }
                
                guiElementsInView.add(target);
            }
        }

        for (Map.Entry<IGUIView, List<IGUIElement>> guiElements :
                 guiElementsInViews.entrySet())
        {
            Map<IGUIElement, List<IGUIElement>> groups =
                RuleUtils.getGroups(guiElements.getValue(), 3);

            for (Map.Entry<IGUIElement, List<IGUIElement>> group : groups.entrySet()) {
                //System.out.println("replacing");

                // it is important to provide the correct view here, as the view of group.getKey()
                // for HTML stuff may not be correct due to GUI model condensing
                ValueSelectionTargetGroup valueSelectionTargetGroup =
                    new ValueSelectionTargetGroup(group.getKey(), guiElements.getKey());
                
                for (IGUIElement toBeMerged : group.getValue()) {
                    //System.out.println(targets.size() + "  " + toBeMerged.getView() + "  " +
                    //                   toBeMerged);
                    
                    valueSelectionTargetGroup.addTargetToGroup(toBeMerged);
                    targets.remove(toBeMerged);
                }
                
                //System.out.println("with " + valueSelectionTargetGroup.getView() + "  " +
                //                   valueSelectionTargetGroup);
                targets.add(valueSelectionTargetGroup);
            }
        }
    }

    /**
     * 
     */
    private void calculateStatistics(List<IUserSession>          sessions,
                                     final ValueChangeStatistics statistics)
    {
        final IGUIView[] currentView = new IGUIView[1];
        final List<IEventTaskInstance> valueChangesInViewDisplay = new ArrayList<>();
        
        for (IUserSession session : sessions) {
            currentView[0] = null;
            valueChangesInViewDisplay.clear();
            
            for (final ITaskInstance currentRoot : session) {
                currentRoot.accept(new DefaultTaskInstanceTraversingVisitor() {
                    @Override
                    public void visit(IEventTaskInstance eventTaskInstance) {
                        if (eventTaskInstance.getEvent().getTarget() instanceof IGUIElement) {
                            IGUIView view =
                                ((IGUIElement) eventTaskInstance.getEvent().getTarget()).getView();
                            
                            if ((currentView[0] == null) && (view != null)) {
                                currentView[0] = view;
                                valueChangesInViewDisplay.clear();
                            }
                            else if ((currentView[0] != null) && (!currentView[0].equals(view))) {
                                statistics.addValueChangesInViewDisplay
                                    (currentView[0], valueChangesInViewDisplay);
                                
                                currentView[0] = view;
                                valueChangesInViewDisplay.clear();
                            }
                        }
                        
                        if (isValueSelection(eventTaskInstance)) {
                            valueChangesInViewDisplay.add(eventTaskInstance);
                        }
                    }
                });
            }
            
            // add the selected values of the last shown view in the session
            if (currentView[0] != null) {
                statistics.addValueChangesInViewDisplay(currentView[0], valueChangesInViewDisplay);
            }
        }
    }

    /**
     *
     */
    private static class ValueChangeStatistics {
        
        /** */
        private Map<ValueSelectionTarget, ValueSelectionTarget> targetReplacements =
            new HashMap<>();
        
        /** */
        private Map<IGUIView, List<ValueSelectionTarget>> valueSelectionTargetsInView =
            new HashMap<>();
        
        /** */
        private Map<ValueSelectionTarget, Map<Object, Integer>> selectedValues = new HashMap<>();

        /**
         *
         */
        public ValueChangeStatistics(Set<ValueSelectionTarget> targets) {
            for (ValueSelectionTarget target : targets) {
                if (target instanceof ValueSelectionTargetGroup) {
                    for (IGUIElement groupElement : (ValueSelectionTargetGroup) target) {
                        targetReplacements.put((ValueSelectionTarget) groupElement, target);
                    }
                }
                else {
                    targetReplacements.put((ValueSelectionTarget) target, target);
                }
                
                List<ValueSelectionTarget> targetsInView =
                    valueSelectionTargetsInView.get(target.getView());
                
                if (targetsInView == null) {
                    targetsInView = new LinkedList<ValueSelectionTarget>();
                    valueSelectionTargetsInView.put(target.getView(), targetsInView);
                }
                
                targetsInView.add((ValueSelectionTarget) target);
            }
        }

        /**
         *
         */
        private void addValueChangesInViewDisplay(IGUIView                 view,
                                                  List<IEventTaskInstance> valueChanges)
        {
            if (!valueSelectionTargetsInView.containsKey(view)) {
                if (!valueChanges.isEmpty()) {
                    throw new IllegalStateException("this should not happen, as we already " +
                                                    "recorded the views with possible value " +
                                                    "changes at this point");
                }
                
                // view does not contain value changes --> return 
                return;
            }
            
            //System.out.println
            //    ("handling " + valueChanges.size() + " value changes on view " + view);
        
            Map<ValueSelectionTarget, Object> lastSelectedValues = new HashMap<>();
            
            // determine the last selected value for each of the value selection targets that were
            // actively selected by the user
            for (IEventTaskInstance valueChange : valueChanges) {
                ValueSelectionTarget target = newValueSelectionTarget(valueChange);
                Object selectedValue;
                
                if (valueChange.getEvent().getType() instanceof TextInput) {
                    selectedValue = ((TextInput) valueChange.getEvent().getType()).getEnteredText();
                }
                else if (valueChange.getEvent().getType() instanceof ValueSelection) {
                    selectedValue =
                        ((ValueSelection<?>) valueChange.getEvent().getType()).getSelectedValue();
                    
                    if ((target.target instanceof IRadioButton) ||
                        (target.target instanceof ICheckBox))
                    {
                        selectedValue = selectedValue + " (" + target + ")";
                    }
                    else if (target.target instanceof IComboBox) {
                        if (selectedValue == null) {
                            // this may have happened due to the recording issue that selected
                            // values of combo boxes are not logged correctly. In this case,
                            // pretend to have the a random value selected
                            selectedValue = "randomValueDueToRecordingBug_" + Math.random(); 
                        }
                    }
                }
                else if (valueChange.getEvent().getType() instanceof MouseClick) {
                    if ((target.target instanceof IRadioButton) ||
                        (target.target instanceof ICheckBox))
                    {
                        selectedValue = target.toString();
                    }
                    else {
                        throw new IllegalStateException("the implementation needs to be extended " +
                                                        "to fully support clicks as value changes");
                    }
                }
                else {
                    throw new IllegalStateException("the implementation needs to be extended to " +
                                                    "handle further value change types");
                }
                
                target = targetReplacements.get(target);
                lastSelectedValues.put(target, selectedValue);
            }
            
            // add the default value selection for the unchanged value selection targets
            for (ValueSelectionTarget target : valueSelectionTargetsInView.get(view)) {
                if (!lastSelectedValues.containsKey(target)) {
                    lastSelectedValues.put(target, DEFAULT_VALUE);
                }
            }
            
            // and now store the statistics
            for (Map.Entry<ValueSelectionTarget, Object> selectedValue :
                     lastSelectedValues.entrySet())
            {
                Map<Object, Integer> statistics = selectedValues.get(selectedValue.getKey());
                
                if (statistics == null) {
                    statistics = new HashMap<>();
                    selectedValues.put(selectedValue.getKey(), statistics);
                }
                
                Integer counter = statistics.get(selectedValue.getValue());
                
                if (counter == null) {
                    statistics.put(selectedValue.getValue(), 1);
                }
                else {
                    statistics.put(selectedValue.getValue(), counter + 1);
                }
            }
        }

        /**
         * 
         */
        private Collection<IGUIView> getViewsWithValueSelections() {
            return valueSelectionTargetsInView.keySet();
        }

        /**
         * 
         */
        private Collection<ValueSelectionTarget> getValueSelectionTargetsInView(IGUIView view) {
            return valueSelectionTargetsInView.get(view);
        }

        /**
         * 
         */
        private Map<Object, Integer> getStatisticsForValueSelectionTarget
            (ValueSelectionTarget target)
        {
            return selectedValues.get(target);
        }
    }

    /**
     *
     */
    private static class ValueSelectionTarget implements IGUIElement {

        /**  */
        private static final long serialVersionUID = 1L;

        /** */
        private IGUIView view;
        
        /** */
        private IGUIElement target;
        
        /**
         *
         */
        private ValueSelectionTarget(IGUIElement target) {
            this.view = target.getView();
            this.target = target;
        }
        
        /**
         *
         */
        private ValueSelectionTarget(IGUIElement target, IGUIView view) {
            this.view = view;
            this.target = target;
        }

        /**
         *
         */
        public IGUIElementSpec getSpecification() {
            return target.getSpecification();
        }

        /**
         *
         */
        public String getPlatform() {
            return target.getPlatform();
        }

        /**
         *
         */
        public IGUIElement getParent() {
            return target.getParent();
        }

        /**
         *
         */
        public String getStringIdentifier() {
            return target.getStringIdentifier();
        }

        /**
         *
         */
        public GUIModel getGUIModel() {
            return target.getGUIModel();
        }

        /**
         * 
         */
        public IHierarchicalEventTargetModel<?> getEventTargetModel() {
            return target.getEventTargetModel();
        }

        /**
         *
         */
        public boolean isUsed() {
            return target.isUsed();
        }

        /**
         *
         */
        public void markUsed() {
            target.markUsed();
        }

        /**
         *
         */
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            else if (obj instanceof ValueSelectionTarget) {
                ValueSelectionTarget other = (ValueSelectionTarget) obj;
                return ((this.view == null) ? other.view == null : other.view.equals(this.view) &&
                        other.target.equals(this.target));
            }
            
            return false;
        }

        /**
         *
         */
        public int hashCode() {
            if (view != null) {
                return view.hashCode();
            }
            else {
                return 0;
            }
        }

        /**
         *
         */
        public void updateSpecification(IEventTargetSpec furtherSpec) {
            target.updateSpecification(furtherSpec);
        }

        /**
         * 
         */
        public void addEqualEventTarget(IHierarchicalEventTarget equalElement) {
            target.addEqualEventTarget(equalElement);
        }

        /**
         *
         */
        public double getDistanceTo(IGUIElement otherElement) {
            return target.getDistanceTo(otherElement);
        }

        /* (non-Javadoc)
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            return target.toString();
        }

        /**
         *
         */
        public IGUIView getView() {
            return view;
        }

        /**
         *
         */
        private IGUIElement getTarget() {
            return target;
        }

    }

    /**
     *
     */
    private static class ValueSelectionTargetGroup extends ValueSelectionTarget
        implements Iterable<IGUIElement>
    {

        /**  */
        private static final long serialVersionUID = 1L;
        
        /** */
        private List<IGUIElement> groupedTargets = new LinkedList<>();
        
        /**
         *
         */
        private ValueSelectionTargetGroup(IGUIElement target, IGUIView view) {
            super(target, view);
        }

        /**
         * 
         */
        private void addTargetToGroup(IGUIElement target) {
            groupedTargets.add(target);
        }

        /* (non-Javadoc)
         * @see java.lang.Iterable#iterator()
         */
        @Override
        public Iterator<IGUIElement> iterator() {
            return groupedTargets.iterator();
        }

        /* (non-Javadoc)
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            return "group(" + groupedTargets.size() + " targets, view " + super.getView() + ")";
        }
    }
}
