//   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.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.Event;
import de.ugoe.cs.autoquest.eventcore.guimodel.GUIModel;
import de.ugoe.cs.autoquest.eventcore.guimodel.IButton;
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.IGUIView;
import de.ugoe.cs.autoquest.eventcore.guimodel.IListBox;
import de.ugoe.cs.autoquest.eventcore.guimodel.IMenuButton;
import de.ugoe.cs.autoquest.eventcore.guimodel.ITextArea;
import de.ugoe.cs.autoquest.eventcore.guimodel.ITextField;
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 UnusedGUIElementsRule implements UsabilityEvaluationRule {

    /*
     * (non-Javadoc)
     * 
     * @see de.ugoe.cs.usability.UsabilityEvaluationRule#evaluate(TaskTree)
     */
    @Override
    public UsabilityEvaluationResult evaluate(ITaskModel taskModel) {
        UsabilityEvaluationResult results = new UsabilityEvaluationResult(taskModel);
        
        Map<IGUIView, List<Set<IGUIElement>>> viewDisplays =
            getViewDisplays(taskModel.getUserSessions());
        
        Map<IGUIView, Set<IGUIElement>> allGUIElements = getAllGUIElements(taskModel);
        
        for (Map.Entry<IGUIView, List<Set<IGUIElement>>> viewDisplay : viewDisplays.entrySet()) {
            handleUnusedGUIElements
                (allGUIElements, viewDisplay.getKey(), viewDisplay.getValue(), results);
        }

        return results;
    }

    /**
     * @param results 
     *
     */
    private void handleUnusedGUIElements(Map<IGUIView, Set<IGUIElement>> usedGUIElements,
                                         IGUIView                        view,
                                         List<Set<IGUIElement>>          viewUsages,
                                         UsabilityEvaluationResult       results)
    {
        Set<IGUIElement> allElementsInView = usedGUIElements.get(view);
        
        if (allElementsInView == null) {
            return;
        }
        
        Map<Integer, List<IGUIElement>> usageCounters = new HashMap<>();
        
        for (IGUIElement relevantElement : allElementsInView) {
            int usageCounter = 0;
            
            for (Set<IGUIElement> viewUsage : viewUsages) {
                if (viewUsage.contains(relevantElement)) {
                    usageCounter++;
                }
            }
            
            List<IGUIElement> elementsWithSameUsage = usageCounters.get(usageCounter);
            
            if (elementsWithSameUsage == null) {
                elementsWithSameUsage = new LinkedList<>();
                usageCounters.put(usageCounter, elementsWithSameUsage);
            }
            
            elementsWithSameUsage.add(relevantElement);
        }
        
        int cumulativeGuiElementUsage = 0;
        for (Set<IGUIElement> viewUsage : viewUsages) {
            cumulativeGuiElementUsage += viewUsage.size();
        }
        
        List<IGUIElement> unusedElements = usageCounters.get(0);
        
        if (unusedElements != null) {
            int ratio = 1000 * unusedElements.size() / allElementsInView.size();

            UsabilitySmellIntensity severity = UsabilitySmellIntensity.getIntensity
                (ratio, cumulativeGuiElementUsage, -1);

            if (severity != null) {
                Map<String, Object> parameters = new HashMap<String, Object>();

                parameters.put("ratio", ratio / 10);
                parameters.put("allDisplays", viewUsages.size());
                parameters.put("view", view);
                parameters.put("unusedGuiElements", unusedElements);
                parameters.put("allGuiElements", allElementsInView.size());

                results.addSmell
                    (severity, UsabilitySmellDescription.UNUSED_GUI_ELEMENTS, parameters);
            }
        }
    }

    /**
     *
     */
    private Map<IGUIView, Set<IGUIElement>> getAllGUIElements(ITaskModel taskModel) {
        Map<IGUIView, Set<IGUIElement>> result = new HashMap<>();
        
        for (ITask task : taskModel.getTasks()) {
            if (task instanceof IEventTask) {
                for (ITaskInstance instance : task.getInstances()) {
                    Event event = ((IEventTaskInstance) instance).getEvent();
                    
                    if ((event.getTarget() instanceof IGUIElement) &&
                        (isRelevant((IGUIElement) event.getTarget())))
                    {
                        IGUIView view = ((IGUIElement) event.getTarget()).getView();
                        
                        Set<IGUIElement> elements = result.get(view);
                        
                        if (elements == null) {
                            elements = new HashSet<>();
                            result.put(view, elements);
                        }
                        
                        elements.add((IGUIElement) event.getTarget());
                    }
                }
            }
        }
        
        if (result.size() <= 0) {
            return result;
        }

        // the problem is, that using the GUI model does not allow to find all in a specific view
        // as the GUI model may return a merged element instead. But anyway, we can add those, which
        // are in the GUI model and have the same view.
        
        GUIModel model = result.values().iterator().next().iterator().next().getGUIModel();
        
        GUIModel.Traverser traverser = model.getTraverser();

        IGUIElement currentGUIElement = null;
        do {
            if (traverser.hasFirstChild()) {
                currentGUIElement = traverser.firstChild();
            }
            else if (traverser.hasNextSibling()) {
                currentGUIElement = traverser.nextSibling();
            }
            else {
                while (currentGUIElement != null) {
                    currentGUIElement = traverser.parent();
                    if (traverser.hasNextSibling()) {
                        currentGUIElement = traverser.nextSibling();
                        break;
                    }
                }
            }

            if (isRelevant(currentGUIElement)) {
                IGUIView view = currentGUIElement.getView();
                
                Set<IGUIElement> elements = result.get(view);
                
                if (elements == null) {
                    elements = new HashSet<>();
                    result.put(view, elements);
                }
                
                elements.add(currentGUIElement);
            }
        }
        while (currentGUIElement != null);
        
        return result;
    }

    /**
     * 
     */
    private Map<IGUIView, List<Set<IGUIElement>>> getViewDisplays(List<IUserSession> sessions) {
        final IGUIView[] currentView = new IGUIView[1];
        final List<IEventTaskInstance> actionInstances = new ArrayList<>();
        final Map<IGUIView, List<Set<IGUIElement>>> result = new HashMap<>();
        
        for (IUserSession session : sessions) {
            currentView[0] = null;
            actionInstances.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;
                                actionInstances.clear();
                            }
                            else if ((currentView[0] != null) && (!currentView[0].equals(view))) {
                                addRelevantTargets(currentView[0], actionInstances, result);
                                
                                currentView[0] = view;
                                actionInstances.clear();
                            }
                        }
                        
                        if (eventTaskInstance.getEvent().getTarget() instanceof IGUIElement) {
                            actionInstances.add(eventTaskInstance);
                        }
                    }
                });
            }
            
            // add the used GUI elements of the last shown view in the session
            if (currentView[0] != null) {
                addRelevantTargets(currentView[0], actionInstances, result);
            }
        }
        
        return result;
    }

    /**
     *
     */
    private void addRelevantTargets(IGUIView                              view,
                                    List<IEventTaskInstance>              actionInstances,
                                    Map<IGUIView, List<Set<IGUIElement>>> result)
    {
        List<Set<IGUIElement>> usedGUIElements = result.get(view);
        
        if (usedGUIElements == null) {
            usedGUIElements = new LinkedList<>();
            result.put(view, usedGUIElements);
        }
        
        Set<IGUIElement> elementsInViewDisplay = new HashSet<>();
        
        for (IEventTaskInstance actionInstance : actionInstances) {
            IGUIElement element = (IGUIElement) actionInstance.getEvent().getTarget();
            
            while (element != null) {
                if (isRelevant(element)) {
                    elementsInViewDisplay.add(element);
                }
                
                element = element.getParent();
            }
        }
        
        usedGUIElements.add(elementsInViewDisplay);
    }

    /**
     *
     */
    private boolean isRelevant(IGUIElement currentGUIElement) {
        if (currentGUIElement == null) {
            return false;
        }
        
        if ((currentGUIElement instanceof IButton) ||
            (currentGUIElement instanceof ICheckBox) ||
            (currentGUIElement instanceof IComboBox) ||
            (currentGUIElement instanceof IListBox) ||
            (currentGUIElement instanceof IMenuButton) ||
            (currentGUIElement instanceof ITextArea) ||
            (currentGUIElement instanceof ITextField))
        {
            return true;
        }
        
        return false;
    }
}
