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

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 java.util.regex.Matcher;
import java.util.regex.Pattern;

import de.ugoe.cs.autoquest.eventcore.Event;
import de.ugoe.cs.autoquest.eventcore.IEventTarget;
import de.ugoe.cs.autoquest.eventcore.IEventType;
import de.ugoe.cs.autoquest.eventcore.StringEventType;
import de.ugoe.cs.autoquest.eventcore.gui.MouseButtonInteraction;
import de.ugoe.cs.autoquest.eventcore.gui.MouseClick;
import de.ugoe.cs.autoquest.eventcore.gui.Scroll;
import de.ugoe.cs.autoquest.eventcore.gui.TextInput;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IEventTask;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IEventTaskInstance;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IIteration;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IIterationInstance;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IOptional;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IOptionalInstance;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ISelection;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ISelectionInstance;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ISequence;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ISequenceInstance;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ITask;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskBuilder;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskFactory;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskInstance;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskInstanceList;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IUserSession;
import de.ugoe.cs.autoquest.test.DummyGUIElement;
import de.ugoe.cs.autoquest.test.DummyTextField;

/**
 * This is a convenience class to parse a textual specification of an expected task structure to
 * allow for a subsequent comparison of the specified structure with an existing one.
 * 
 * @version $Revision: $ $Date: 01.04.2012$
 * @author 2012, last modified by $Author: patrick$
 */
public class TaskTreeDecoder {
    
    /** */
    private static Pattern taskInstancePattern = Pattern.compile("([^{}]+)\\{|\\}");
    
    /** */
    private static Pattern taskInstanceDetailsPattern =
        Pattern.compile("\\s*(\\w*)\\s*([\\w\\(\\)\"]*)\\s*(\\(([\\w*\\s*]*)\\))?((\\w*)|(\".*\"))?");

    /** */
    private ITaskFactory taskFactory;
    
    /** */
    private ITaskBuilder taskBuilder;
    
    /** */
    Map<String, IEventTarget> targets = new HashMap<String, IEventTarget>();
    
    /** */
    Map<String, ITask> tasks = new HashMap<String, ITask>();
    
    /**
     *
     */
    public TaskTreeDecoder(ITaskFactory taskFactory, ITaskBuilder taskBuilder) {
        super();
        this.taskFactory = taskFactory;
        this.taskBuilder = taskBuilder;
    }

    /**
     *
     */
    public ITaskInstanceList decode(String taskTreeSpec) {
        ITaskInstanceList taskInstanceList = null;

        Matcher taskMatcher = taskInstancePattern.matcher(taskTreeSpec);

        if (taskMatcher.find()) {
            taskInstanceList = parseTaskInstanceList(taskMatcher);
        }
        
        if (taskMatcher.find()) {
            throw new IllegalArgumentException("too many tasks specified");
        }
        
        correctTargets(taskInstanceList);
        
        // validate the instance list
        try {
            new TaskTreeValidator().validate(taskInstanceList);
        }
        catch (java.lang.AssertionError e) {
            throw new IllegalArgumentException(e.getMessage(), e);
        }
        
        return taskInstanceList;
    }

    /**
     * 
     */
    private ITaskInstanceList parseTaskInstanceList(Matcher taskMatcher) {
        if ("}".equals(taskMatcher.group(1))) {
            throw new IllegalArgumentException("invalid task instance list specification");
        }
        
        String taskDetails = taskMatcher.group(1);
        
        Matcher matcher = taskInstanceDetailsPattern.matcher(taskDetails);
        
        if (!matcher.find()) {
            throw new IllegalArgumentException("could not parse task details");
        }

        String type = matcher.group(1);
        
        ITaskInstanceList list;
        
        if ("UserSession".equals(type)) {
            list = taskFactory.createUserSession();
        }
        else if ("TaskInstances".equals(type)) {
            list = taskFactory.createNewTaskInstance(taskFactory.createNewSequence());
        }
        else {
            throw new IllegalArgumentException("unknown type of task instance list: " + type);
        }
        
        while (taskMatcher.find() && !"}".equals(taskMatcher.group(0))) {
            ITaskInstance childInstance = parseTaskInstance(taskMatcher);
            
            if (!(list instanceof IUserSession)) {
                taskBuilder.addChild
                    ((ISequence) ((ITaskInstance) list).getTask(), childInstance.getTask());
            }

            taskBuilder.addTaskInstance(list, childInstance);
        }

        return list;
    }

    /**
     * 
     */
    private ITaskInstance parseTaskInstance(Matcher taskMatcher) {
        if ("}".equals(taskMatcher.group(1))) {
            throw new IllegalArgumentException("invalid task instance specification");
        }
        
        String taskDetails = taskMatcher.group(1);
        
        Matcher matcher = taskInstanceDetailsPattern.matcher(taskDetails);
        
        if (!matcher.find()) {
            throw new IllegalArgumentException("could not parse task details");
        }

        String type = matcher.group(1);
        String id = matcher.group(2);
        
        // determine the children before creating this instance
        List<ITaskInstance> children = new LinkedList<ITaskInstance>();
        while (taskMatcher.find() && !"}".equals(taskMatcher.group(0))) {
            children.add(parseTaskInstance(taskMatcher));
        }
        
        // get the model
        ITask task = getCreateTask(id, type, children);
        
        // create the respective instance
        ITaskInstance instance;
        
        if (task instanceof ISequence) {
            instance = taskFactory.createNewTaskInstance((ISequence) task);
        }
        else if (task instanceof ISelection) {
            instance = taskFactory.createNewTaskInstance((ISelection) task);
        }
        else if (task instanceof IIteration) {
            instance = taskFactory.createNewTaskInstance((IIteration) task);
        }
        else if (task instanceof IOptional) {
            instance = taskFactory.createNewTaskInstance((IOptional) task);
        }
        else {
            Event event = createUserInteractionEvent(type, id, matcher.group(4));
            instance = taskFactory.createNewTaskInstance((IEventTask) task, event);
        }  
        
        // add the children to the instance
        for (ITaskInstance childInstance : children) {
            if (instance instanceof ISequenceInstance) {
                taskBuilder.addChild((ISequenceInstance) instance, childInstance);
            }
            else if (instance instanceof ISelectionInstance) {
                if (((ISelectionInstance) instance).getChild() == null) {
                    taskBuilder.setChild((ISelectionInstance) instance, childInstance);
                }
                else {
                    throw new IllegalArgumentException("can not add several children to one " +
                                                       "selection instance");
                }
            }
            else if (instance instanceof IIterationInstance) {
                taskBuilder.addChild((IIterationInstance) instance, childInstance);
            }
            else if (instance instanceof IOptionalInstance) {
                if (((IOptionalInstance) instance).getChild() == null) {
                    taskBuilder.setChild((IOptionalInstance) instance, childInstance);
                }
                else {
                    throw new IllegalArgumentException("can not add several children to one " +
                                                       "optional instance");
                }
            }
        }

        return instance;
    }

    /**
     *
     */
    private ITask getCreateTask(String id, String type, List<ITaskInstance> children) {
        String effectiveId = id;
        ITask task = tasks.get(effectiveId);
        
        if (task == null) {
            if ("Sequence".equals(type)) {
                task = taskFactory.createNewSequence();
            }
            else if ("Selection".equals(type)) {
                task = taskFactory.createNewSelection();
            }
            else if ("Iteration".equals(type)) {
                task = taskFactory.createNewIteration();
            }
            else if ("Optional".equals(type)) {
                task = taskFactory.createNewOptional();
            }
            else {
                effectiveId = type + " --> " + id;
                task = tasks.get(effectiveId);
                
                if (task == null) {
                    task = taskFactory.createNewEventTask(effectiveId);
                }
            }  
            tasks.put(effectiveId, task);

            for (ITaskInstance childInstance : children) {
                if (task instanceof ISequence) {
                    taskBuilder.addChild((ISequence) task, childInstance.getTask());
                }
                else if (task instanceof ISelection) {
                    taskBuilder.addChild((ISelection) task, childInstance.getTask());
                }
                else if (task instanceof IIteration) {
                    if (((IIteration) task).getMarkedTask() == null) {
                        taskBuilder.setMarkedTask((IIteration) task, childInstance.getTask());
                    }
                    else if (!((IIteration) task).getMarkedTask().equals(childInstance.getTask())) {
                        throw new IllegalArgumentException
                            ("can not add more than one child to an iteration");
                    }
                }
                else if (task instanceof IOptional) {
                    if (((IOptional) task).getMarkedTask() == null) {
                        taskBuilder.setMarkedTask((IOptional) task, childInstance.getTask());
                    }
                    else if (!((IOptional) task).getMarkedTask().equals(childInstance.getTask())) {
                        throw new IllegalArgumentException
                            ("can not add more than one child to an optional");
                    }
                }
                else {
                    throw new IllegalArgumentException("can not add children to something that " +
                                                       "is no sequence, selection, iteration, or " +
                                                       "optional");
                }
            }
        }
        else if ("Selection".equals(type)) {
            // update the selection with further alternatives, if specified by the current
            // child instance
            if (children.size() == 1) {
                ITask newChildTask = children.get(0).getTask();
                
                boolean found = false;
                for (ITask childTask : ((ISelection) task).getChildren()) {
                    if (childTask.equals(newChildTask)) {
                        found = true;
                        break;
                    }
                }
                
                if (!found) {
                    taskBuilder.addChild((ISelection) task, newChildTask);
                }
            }
        }
        else if ("Optional".equals(type)) {
            // update the optional with what is optional if available
            if (children.size() == 1) {
                ITask newChildTask = children.get(0).getTask();
                
                if (((IOptional) task).getMarkedTask() == null) {
                    taskBuilder.setMarkedTask((IOptional) task, newChildTask);
                }
                else if (!((IOptional) task).getMarkedTask().equals(newChildTask)) {
                    throw new IllegalArgumentException("all children of the same optional must " +
                                                       "be of an identical task model.");
                }
            }
        }

        return task;
    }

    /**
     * <p>
     * </p>
     *
     * @param matcher
     * @return
     */
    private Event createUserInteractionEvent(String type, String targetId, String furtherInfo) {
        IEventTarget eventTarget = targets.get(targetId);
        if (eventTarget == null) {
            eventTarget = new DummyGUIElement(targetId);
            targets.put(targetId, eventTarget);
        }
        
        IEventType eventType = determineType(type, furtherInfo);
        
        return new Event(eventType, eventTarget);
    }

    /**
     * <p>
     * </p>
     *
     * @param type
     * @param enteredText
     * @return
     */
    private IEventType determineType(String type, String additionalInfo) {
        if ("TextInput".equals(type)) {
            return new TextInput(additionalInfo, new ArrayList<Event>());
        }
        else if ("MouseClick".equals(type)) {
            return new MouseClick(MouseButtonInteraction.Button.LEFT, 5, 6);
        }
        else if ("Scroll".equals(type)) {
            int x = 0;
            int y = 0;
            if (additionalInfo != null) {
                String[] parts = additionalInfo.split(" ");
                if (parts.length > 0) {
                    try {
                        x = Integer.parseInt(parts[0]);
                    }
                    catch (NumberFormatException e) {
                        throw new IllegalArgumentException("could not parse scroll coordinates", e);
                    }
                }
                if (parts.length > 1) {
                    try {
                        y = Integer.parseInt(parts[1]);
                    }
                    catch (NumberFormatException e) {
                        throw new IllegalArgumentException("could not parse scroll coordinates", e);
                    }
                }
            }
            return new Scroll(x, y);
        }
        else {
            return new StringEventType(type);
        }
    }

    /**
     *
     */
    private void correctTargets(ITaskInstanceList taskInstanceList) {
        Set<IEventTarget> textInputTargets = new HashSet<IEventTarget>();
        
        for (ITaskInstance instance : taskInstanceList) {
            getTextInputTargets(instance, textInputTargets);
        }
        
        Map<IEventTarget, IEventTarget> replacements = new HashMap<IEventTarget, IEventTarget>();
        
        for (IEventTarget oldTarget : textInputTargets) {
            replacements.put(oldTarget, new DummyTextField(oldTarget.toString()));
        }
        
        for (int i = 0; i < taskInstanceList.size(); i++) {
            taskBuilder.setTaskInstance
                (taskInstanceList, i, replaceTargets(taskInstanceList.get(i), replacements));
        }
        
    }

    /**
     *
     */
    private void getTextInputTargets(ITaskInstance instance, Set<IEventTarget> textInputTargets) {
        if (instance instanceof ISequenceInstance) {
            for (ITaskInstance child : (ISequenceInstance) instance) {
                getTextInputTargets(child, textInputTargets);
            }
        }
        else if (instance instanceof ISelectionInstance) {
            getTextInputTargets(((ISelectionInstance) instance).getChild(), textInputTargets);
        }
        else if (instance instanceof IIterationInstance) {
            for (ITaskInstance child : (IIterationInstance) instance) {
                getTextInputTargets(child, textInputTargets);
            }
        }
        else if (instance instanceof IOptionalInstance) {
            getTextInputTargets(((IOptionalInstance) instance).getChild(), textInputTargets);
        }
        else if ((instance instanceof IEventTaskInstance) &&
                 (((IEventTaskInstance) instance).getEvent().getType() instanceof TextInput))
        {
            textInputTargets.add(((IEventTaskInstance) instance).getEvent().getTarget());
        }
    }

    /**
     *
     */
    private ITaskInstance replaceTargets(ITaskInstance                   instance,
                                         Map<IEventTarget, IEventTarget> replacements)
    {
        ITaskInstance replacement = instance;
        if (instance instanceof ITaskInstanceList) {
            for (int i = 0; i < ((ITaskInstanceList) instance).size(); i++) {
                taskBuilder.setTaskInstance
                    ((ITaskInstanceList) instance, i,
                     replaceTargets(((ITaskInstanceList) instance).get(i), replacements));
            }
        }
        else if (instance instanceof ISelectionInstance) {
            ISelectionInstance selInst = (ISelectionInstance) instance;
            taskBuilder.setChild(selInst, replaceTargets(selInst.getChild(), replacements));
        }
        else if (instance instanceof IOptionalInstance) {
            IOptionalInstance optInst = (IOptionalInstance) instance;
            if (optInst.getChild() != null) {
                taskBuilder.setChild(optInst, replaceTargets(optInst.getChild(), replacements));
            }
        }
        else if ((instance instanceof IEventTaskInstance) &&
                 (replacements.containsKey(((IEventTaskInstance) instance).getEvent().getTarget())))
        {
            Event origEvent = ((IEventTaskInstance) instance).getEvent();
            replacement = taskFactory.createNewTaskInstance
                (((IEventTaskInstance) instance).getEventTask(),
                 new Event(origEvent.getType(), replacements.get(origEvent.getTarget())));
            
            taskBuilder.discardTaskInstance(instance);
        }
        
        return replacement;
    }

}
