//   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 static org.junit.Assert.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import de.ugoe.cs.autoquest.eventcore.gui.TextInput;
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.IIteration;
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.ITask;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskInstance;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskInstanceList;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IUserSession;

/**
 * TODO comment
 * 
 * @version $Revision: $ $Date: 01.04.2012$
 * @author 2012, last modified by $Author: patrick$
 */
public class TaskTreeChecker {
    
    /** */
    private static Pattern taskPattern = Pattern.compile("([^{}]+)\\{|\\}");
    
    /** */
    private static Pattern taskDetailsPattern =
        Pattern.compile("\\s*(\\w*)\\s*([\\w\\(\\)\"]*)\\s*((\\w*)|(\".*\"))?");
    
    /** */
    private boolean doTrace;

    /**
     *
     */
    public TaskTreeChecker() {
        this(false);
    }

    /**
     *
     */
    public TaskTreeChecker(boolean doTrace) {
        this.doTrace = doTrace;
    }

    /**
     *
     */
    public void assertUserSession(String userSessionSpec, IUserSession session) {
        if (doTrace) {
            new TaskTreeEncoder().encode(session, System.err);
        }

        TaskSpec taskInstanceSpec = null;

        Matcher taskMatcher = taskPattern.matcher(userSessionSpec);
        
        Map<String, ITask> tasks = new HashMap<String, ITask>();

        while (taskMatcher.find()) {

            taskInstanceSpec = parseTaskInstance(taskMatcher);
            
            if (taskInstanceSpec != null) {
                assertUserSession(taskInstanceSpec, session, tasks);
            }
        }
    }

    /**
     *
     */
    public void assertTaskInstance(String userSessionSpec, ITaskInstance taskInstance) {
        if (doTrace) {
            new TaskTreeEncoder().encode(taskInstance, System.err);
        }

        TaskSpec taskInstanceSpec = null;

        Matcher taskMatcher = taskPattern.matcher(userSessionSpec);
        
        Map<String, ITask> tasks = new HashMap<String, ITask>();

        while (taskMatcher.find()) {

            taskInstanceSpec = parseTaskInstance(taskMatcher);
            
            if (taskInstanceSpec != null) {
                assertTaskInstance(taskInstanceSpec, taskInstance, tasks);
            }
        }
    }

    /**
     *
     */
    public void assertTaskInstanceListsEqual(ITaskInstanceList expected, ITaskInstanceList checked)
    {
        if (expected == null) {
            assertNull("null", checked);
        }
        else {
            assertNotNull(expected.toString(), checked);
            
            assertEquals(expected.toString() + ": types do not match",
                         expected.getClass(), checked.getClass());
            
            if (expected.size() > 0) {
                assertNotNull(expected.toString() + ": children not there", checked);
                assertEquals(expected.toString() + ": different number of children",
                             expected.size(), checked.size());
                
                Map<ITask, ITask> equalTasksMap = new HashMap<ITask, ITask>();
                for (int i = 0; i < expected.size(); i++) {
                    assertTaskInstancesEqual(expected.get(i), checked.get(i), equalTasksMap);
                }
            }
            else {
                assertTrue(expected.toString() + ": unexpected children",
                           (checked == null) || (checked.size() == 0));
            }
        }
    }

    /**
     *
     */
    public void assertTaskInstancesEqual(ITaskInstance expected, ITaskInstance checked) {
        Map<ITask, ITask> equalTasksMap = new HashMap<ITask, ITask>();
        assertTaskInstancesEqual(expected, checked, equalTasksMap);
    }

    /**
     *
     */
    public void assertEventTaskInstancesEqual(ITaskInstanceList expected,
                                              ITaskInstanceList checked)
    {
        final List<IEventTaskInstance> expectedEventTaskInstances =
            new ArrayList<IEventTaskInstance>();
        
        final List<IEventTaskInstance> checkedEventTaskInstances =
                new ArrayList<IEventTaskInstance>();
            
        for (ITaskInstance instance : expected) {
            instance.accept(new DefaultTaskInstanceTraversingVisitor() {
                @Override
                public void visit(IEventTaskInstance eventTaskInstance) {
                    expectedEventTaskInstances.add(eventTaskInstance);
                }
            });
        }
        
        for (ITaskInstance instance : checked) {
            instance.accept(new DefaultTaskInstanceTraversingVisitor() {
                @Override
                public void visit(IEventTaskInstance eventTaskInstance) {
                    checkedEventTaskInstances.add(eventTaskInstance);
                }
            });
        }
        
        assertEquals("task instance lists differ in the number of event task instances",
                     expectedEventTaskInstances.size(), checkedEventTaskInstances.size());
        
        for (int i = 0; i < expectedEventTaskInstances.size(); i++) {
            assertTaskInstancesEqual
                (expectedEventTaskInstances.get(i), checkedEventTaskInstances.get(i));
        }
    }

    /**
     *
     */
    private void assertTaskInstancesEqual(ITaskInstance     expected,
                                          ITaskInstance     checked,
                                          Map<ITask, ITask> equalTasksMap)
    {
        if (expected == null) {
            assertNull("null", checked);
        }
        else {
            assertNotNull(expected.toString(), checked);
            
            assertEquals(expected.toString() + ": types do not match",
                         expected.getClass(), checked.getClass());
            
            if (equalTasksMap.containsKey(expected.getTask())) {
                assertEquals(expected.toString() + ": tasks do not match",
                             checked.getTask(), equalTasksMap.get(expected.getTask()));
            }
            else {
                equalTasksMap.put(expected.getTask(), checked.getTask());
            }
            
            List<ITaskInstance> expectedChildren = getChildren(expected);
            List<ITaskInstance> checkedChildren = getChildren(checked);
            
            if ((expectedChildren != null) && (expectedChildren.size() > 0)) {
                assertNotNull(expected.toString() + ": children not there", checkedChildren);
                assertEquals(expected.toString() + ": different number of children",
                             expectedChildren.size(), checkedChildren.size());
                
                if (expected instanceof ISequence) {
                    for (int i = 0; i < expectedChildren.size(); i++) {
                        assertTaskInstancesEqual(expectedChildren.get(i), checkedChildren.get(i));
                    }
                }
                else {
                    for (int i = 0; i < expectedChildren.size(); i++) {
                        boolean found = false;
                        for (int j = 0; j < checkedChildren.size(); j++) {
                            try {
                                assertTaskInstancesEqual
                                    (expectedChildren.get(i), checkedChildren.get(j));
                                found = true;
                                break;
                            }
                            catch (AssertionError e) {
                                // try next
                            }
                        }
                        
                        assertTrue("one of the children not found", found);
                    }
                }
            }
            else {
                assertTrue(expected.toString() + ": unexpected children",
                           (checkedChildren == null) || (checkedChildren.size() == 0));
            }
        }
    }

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

        TaskSpec task = new TaskSpec();
        task.type = matcher.group(1);
        
        task.name = matcher.group(2);
        if ((matcher.group(4) != null) && (!"".equals(matcher.group(4).trim()))) {
            task.name += " " + matcher.group(4).trim();
        }
        
        if ((matcher.group(5) != null) && (!"".equals(matcher.group(5).trim()))) {
            task.additionalInfo = matcher.group(5).trim();
        }

        if ("".equals(task.name)) {
            task.name = null;
        }

        List<TaskSpec> children = new ArrayList<TaskSpec>();
        while (taskMatcher.find() && !"}".equals(taskMatcher.group(0))) {
            children.add(parseTaskInstance(taskMatcher));
        }

        if (children.size() > 0) {
            task.children = children.toArray(new TaskSpec[children.size()]);
        }

        return task;
    }

    /**
     * @param task
     * @param taskMapCopy
     */
    private void assertUserSession(TaskSpec           taskSpec,
                                   IUserSession       session,
                                   Map<String, ITask> tasks)
    {
        if (doTrace) {
            System.err.println("\ncomparing " + taskSpec.type + " with " + session + "\n");
        }

        if (!"UserSession".equals(taskSpec.type)) {
            fail("can not compare a task instance with a user session");
        }
        
        List<ITaskInstance> children = session.getExecutedTasks();
        
        if (taskSpec.children.length != children.size()) {
            fail("number of task instances in task instance list does not match");
        }
        
        for (int i = 0; i < children.size(); i++) {
            TaskSpec childSpec = taskSpec.children[i];
            assertTrue(taskSpecEqualsTaskInstance(childSpec, children.get(i), tasks));
        }
    }

    /**
     * @param task
     * @param taskMapCopy
     */
    private void assertTaskInstance(TaskSpec           taskSpec,
                                    ITaskInstance      taskInstance,
                                    Map<String, ITask> tasks)
    {
        if (doTrace) {
            System.err.println("\ncomparing " + taskSpec.type + " with " + taskInstance + "\n");
        }

        if (!"TaskInstances".equals(taskSpec.type)) {
            fail("can not compare a task instance with a task instance list");
        }
        
        List<ITaskInstance> children = getChildren(taskInstance);
        
        if (taskSpec.children.length != children.size()) {
            fail("number of task instances in task instance list does not match");
        }
        
        for (int i = 0; i < children.size(); i++) {
            TaskSpec childSpec = taskSpec.children[i];
            assertTrue(taskSpecEqualsTaskInstance(childSpec, children.get(i), tasks));
        }
    }

    /**
     *
     */
    private boolean taskSpecEqualsTaskInstance(TaskSpec           taskSpec,
                                               ITaskInstance      taskInstance,
                                               Map<String, ITask> tasks)
    {
        if (doTrace) {
            System.err.println("comparing " + taskSpec.name + " with");
            new TaskTreeEncoder().encode(taskInstance, System.err);
        }

        ITask task = taskInstance.getTask();
        
        if (("Event".equals(taskSpec.type) && (!(task instanceof IEventTask))) ||
            ("TextInputEvent".equals(taskSpec.type) &&
             ((!(taskInstance instanceof IEventTaskInstance)) ||
              (!(((IEventTaskInstance) taskInstance).getEvent().getType() instanceof TextInput)))) ||
            ("Sequence".equals(taskSpec.type) && (!(task instanceof ISequence))) ||
            ("Selection".equals(taskSpec.type) && (!(task instanceof ISelection))) ||
            ("Iteration".equals(taskSpec.type) && (!(task instanceof IIteration))))
        {
            if (doTrace) {
                System.err.println("task types do not match: " + taskSpec.type + " != " +
                                   task.getClass().getSimpleName() + "\n");
            }
            return false;
        }
        else if (!"Event".equals(taskSpec.type) &&
                 !"TextInputEvent".equals(taskSpec.type) &&
                 !"Sequence".equals(taskSpec.type) &&
                 !"Selection".equals(taskSpec.type) &&
                 !"Iteration".equals(taskSpec.type))
        {
            fail("unknown task type " + taskSpec.type + " --> please extend test case");
        }

        if ("TextInputEvent".equals(taskSpec.type)) {
            TextInput eventType = (TextInput)
                ((IEventTaskInstance) taskInstance).getEvent().getType();
            
            if ((taskSpec.additionalInfo != null) &&
                !"".equals(taskSpec.additionalInfo) &&
                !(taskSpec.additionalInfo.equals(eventType.getEnteredText())))
            {
                if (doTrace) {
                    System.err.println("expected text \"" + taskSpec.additionalInfo +
                                       "\" is not equal to the text " + "provided by the task \"" +
                                       eventType.getEnteredText() + "\"\n");
                }
                return false;
            }
        }
        else if ((task instanceof IEventTask) &&
                 (((IEventTaskInstance) taskInstance).getEvent().getType() != null) &&
                 (!taskSpec.name.equals(((IEventTaskInstance) taskInstance).getEvent().getType().getName())))
        {
            // simple event names do not match. But what about the event name in
            // combination with the additional info
            String complexName =
                taskSpec.name +
                    (!"".equals(taskSpec.additionalInfo) ? " " + taskSpec.additionalInfo : "");

            if (!complexName.equals(((IEventTaskInstance) taskInstance).getEvent().getType().getName())) {
                if (doTrace) {
                    System.err.println
                        ("event names do not match: " + taskSpec.name + " != " +
                         ((IEventTaskInstance) taskInstance).getEvent().getType().getName() + "\n");
                }
                return false;
            }
        }
        
        // check the task name against the map
        if (tasks.containsKey(taskSpec.name)) {
            if (!tasks.get(taskSpec.name).equals(task)) {
                if (doTrace) {
                    System.err.println("the task instance is not of the expected task: " +
                                       taskSpec.name + " != " + task + "\n");
                }
                return false;
            }
        }
        else if (tasks.containsValue(task)) {
            if (doTrace) {
                System.err.println("the task of the task instance " + taskSpec.name +
                                   " should be different to another one but it isn't\n");
            }
            return false;
        }
        else {
            tasks.put(taskSpec.name, task);
        }

        List<ITaskInstance> children = getChildren(taskInstance);
        
        if (((taskSpec.children == null) && (children.size() > 0)) ||
            ((taskSpec.children != null) &&
             (taskSpec.children.length != children.size())))
        {
            if (doTrace) {
                System.err.println
                    ("numbers of children do not match: " +
                     (taskSpec.children == null ? "0" : taskSpec.children.length) + " != " +
                     (children == null ? "0" : children.size()) + "\n");
            }
            return false;
        }

        Iterator<ITaskInstance> childrenIterator = children.iterator();
        if (taskSpec.children != null) {
            for (TaskSpec child : taskSpec.children) {
                if (!taskSpecEqualsTaskInstance(child, childrenIterator.next(), tasks)) {
                    if (doTrace) {
                        System.err.println("one of the children does not match\n");
                    }
                    return false;
                }
            }
        }

        if (!childrenIterator.hasNext()) {
            if (doTrace) {
                System.err.println("nodes match\n");
            }
            return true;
        }
        else {
            if (doTrace) {
                System.err.println("number of children does not match\n");
            }
            return false;
        }
    }
    
    /**
     * <p>
     * TODO: comment
     * </p>
     *
     * @param taskInstance
     * @return
     */
    private List<ITaskInstance> getChildren(ITaskInstance taskInstance) {
        List<ITaskInstance> result = new LinkedList<ITaskInstance>();
        
        if (taskInstance instanceof ITaskInstanceList) {
            for (ITaskInstance child : (ITaskInstanceList) taskInstance) {
                result.add(child);
            }
        }
        else if (taskInstance instanceof ISelectionInstance) {
            result.add(((ISelectionInstance) taskInstance).getChild());
        }
        else if (taskInstance instanceof IOptionalInstance) {
            result.add(((IOptionalInstance) taskInstance).getChild());
        }
        
        return result;
    }

    /**
     *
     */
    private class TaskSpec {
        public String type;
        public String name;
        public String additionalInfo;
        public TaskSpec[] children;
    }

}
