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

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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.IMarkingTemporalRelationship;
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.IStructuringTemporalRelationship;
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.ITaskModel;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskInfo;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IUserSession;
import de.ugoe.cs.autoquest.tasktrees.treeifc.TaskMetric;

/**
 * <p>
 * this is the default implementation of the interface {@link ITaskModel}. It
 * does not do anything fancy except implementing the interface. It also calculates on
 * initialisations the measures for diverse metrics of the task belonging to the model
 * </p>
 * 
 * @author Patrick Harms
 */
class TaskModel implements ITaskModel {
    
    /**
     * <p>
     * default serial version UID
     * </p>
     */
    private static final long serialVersionUID = 1L;
    
    /**
     * <p>
     * all metrics calculated by this type of task model
     * </p>
     */
    private static final TaskMetric[] taskMetrics = new TaskMetric[]
        { TaskMetric.COUNT,
          TaskMetric.DEPTH,
          TaskMetric.EVENT_COVERAGE,
          TaskMetric.EVENT_COVERAGE_RATIO,
          TaskMetric.EVENT_COVERAGE_QUANTILE };

    /**
     * <p>
     * the user sessions belonging to the model
     * </p>
     */
    private List<IUserSession> userSessions;

    /**
     * <p>
     * index for effectively accessing the model and calculating statistics about it
     * </p>
     */
    private transient TaskModelIndex index = null;
    
    /**
     * <p>
     * initializes the task model with the user sessions out of which the tasks are extracted
     * </p>
     * 
     * @param userSessions as described
     */
    TaskModel(List<IUserSession> userSessions) {
        if ((userSessions == null) || (userSessions.size() == 0)) {
            throw new IllegalArgumentException("user sessions must not be null");
        }
        
        this.userSessions = userSessions;
    }

    
    /* (non-Javadoc)
     * @see de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskModel#getUserSessions()
     */
    @Override
    public List<IUserSession> getUserSessions() {
        ensureInitialized();
        return Collections.unmodifiableList(userSessions);
    }


    /* (non-Javadoc)
     * @see de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskModel#getTasks()
     */
    @Override
    public Collection<ITask> getTasks() {
        ensureInitialized();
        return Collections.unmodifiableCollection(index.taskMap.keySet());
    }


    /* (non-Javadoc)
     * @see de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskModel#getTaskInfo(ITask)
     */
    @Override
    public ITaskInfo getTaskInfo(ITask task) {
        ensureInitialized();
        return index.taskMap.get(task);
    }

    /* (non-Javadoc)
     * @see de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskModel#getAllMetrics()
     */
    @Override
    public TaskMetric[] getAllMetrics() {
        return taskMetrics;
    }


    /* (non-Javadoc)
     * @see java.lang.Object#clone()
     */
    @Override
    public TaskModel clone() {
        return new TaskModel(userSessions);
    }

    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        ensureInitialized();
        return "Task Model (" + userSessions.size() + " sessions, " + index.taskMap.size() +
            " tasks, hash " + System.identityHashCode(this) + ")";
    }

    /**
     * <p>
     * internal convenience method that initializes the internal index and calculates all measures
     * for metrics available for the tasks
     * </p>
     */
    private synchronized void ensureInitialized() {
        if (index == null) {
            index = new TaskModelIndex();
            
            for (IUserSession session : this.userSessions) {
                for (ITaskInstance taskInstance : session) {
                    index.handleTaskInstance(taskInstance);
                }
            }
            
            // count all events covered
            int allEventsCovered = 0;
            Collection<ITask> tasks = getTasks();
            for (ITask task : tasks) {
                if (task instanceof IEventTask) {
                    allEventsCovered += task.getInstances().size();
                }
            }
            
            int[] eventCoverageRatios = new int[tasks.size()];
            int i = 0;

            // add some further measures
            for (ITask task : tasks) {
                TaskInfo info = index.taskMap.get(task);
                info.addMeasure(TaskMetric.EVENT_COVERAGE_RATIO);
                
                int coveredEvents = info.getMeasureValue(TaskMetric.EVENT_COVERAGE);
                int coverageRatio = 0;
                
                if (allEventsCovered > 0) {
                    coverageRatio = (coveredEvents * 1000) / allEventsCovered;
                }
                
                eventCoverageRatios[i++] = coverageRatio;
                info.setCount(TaskMetric.EVENT_COVERAGE_RATIO, coverageRatio);
            }
            
            Arrays.sort(eventCoverageRatios);
            
            // add some further measures
            for (ITask task : tasks) {
                TaskInfo info = index.taskMap.get(task);
                info.addMeasure(TaskMetric.EVENT_COVERAGE_QUANTILE);
                int quantile = Arrays.binarySearch
                    (eventCoverageRatios, info.getMeasureValue(TaskMetric.EVENT_COVERAGE_RATIO));
                
                quantile = 1000 * quantile / eventCoverageRatios.length;
                
                info.setCount(TaskMetric.EVENT_COVERAGE_QUANTILE, quantile);
            }
            
            //index.dumpToCSV(System.out);
            /*try {
                OutputStream stream = new FileOutputStream(new File("tasks.csv"));
                index.dumpToCSV(new PrintStream(stream));
                stream.close();
            }
            catch (FileNotFoundException e) {
                e.printStackTrace();
            }*/
        }
        
    }

    /**
     * <p>
     * the index of task infos used internally. The index is created once and while that filled
     * with task infos for each observed task containing all measures for metrics belonging
     * to the tasks. 
     * </p>
     * 
     * @author Patrick Harms
     */
    private static class TaskModelIndex {

        /**
         * <p>
         * the tasks contained in the user session belonging to the model as well as statistical
         * infos about them
         * </p>
         */
        private Map<ITask, TaskInfo> taskMap = new HashMap<ITask, TaskInfo>();

        /**
         * <p>
         * called on initialization to fill the index with infos about the given task instance
         * as well as to calculate the appropriate metrics
         * </p>
         */
        private int[] handleTaskInstance(ITaskInstance taskInstance) {
            int eventTaskInstancesCovered = 0;
            int depth = 0;
            
            if (taskInstance instanceof ITaskInstanceList) {
                for (ITaskInstance child : (ITaskInstanceList) taskInstance) {
                    int[] measures = handleTaskInstance(child);
                    eventTaskInstancesCovered += measures[0];
                    depth = Math.max(depth, measures[1]);
                }
                
                if ((((ITaskInstanceList) taskInstance).size() == 0) &&
                    (taskInstance instanceof IIterationInstance))
                {
                    // ensure also empty task infos for unselected variants
                    ensureTaskInfo(((IIteration) taskInstance.getTask()).getMarkedTask());
                }
            }
            else if (taskInstance instanceof ISelectionInstance) {
                ITaskInstance child = ((ISelectionInstance) taskInstance).getChild();
                int[] measures = handleTaskInstance(child);
                eventTaskInstancesCovered += measures[0];
                depth = Math.max(depth, measures[1]);
                
                // ensure also empty task infos for unselected variants
                for (ITask otherChildTask : ((ISelection) taskInstance.getTask()).getChildren()) {
                    ensureTaskInfo(otherChildTask);
                }
            }
            else if (taskInstance instanceof IOptionalInstance) {
                ITaskInstance child = ((IOptionalInstance) taskInstance).getChild();
                if (child != null) {
                    int[] measures = handleTaskInstance(child);
                    eventTaskInstancesCovered += measures[0];
                    depth = Math.max(depth, measures[1]);
                }
                else {
                    // ensure also empty task infos for unselected variants
                    ensureTaskInfo(((IOptional) taskInstance.getTask()).getMarkedTask());
                }
            }
            else if (taskInstance instanceof IEventTaskInstance) {
                eventTaskInstancesCovered = 1;
            }
            
            depth++;
            
            ensureTaskInfo(taskInstance.getTask(), eventTaskInstancesCovered, depth);
            
            return new int[] { eventTaskInstancesCovered, depth };
        }
        
        /**
         * <p>
         * internal convenience method to build the task model during initialization
         * </p>
         */
        private void ensureTaskInfo(ITask task) {
            ensureTaskInfo(task, 0, 0);
            
            if (task instanceof IStructuringTemporalRelationship) {
                for (ITask child : ((IStructuringTemporalRelationship) task).getChildren()) {
                    ensureTaskInfo(child);
                }
            }
            else if (task instanceof IMarkingTemporalRelationship) {
                ensureTaskInfo(((IMarkingTemporalRelationship) task).getMarkedTask());
            }
        
        }
        
        /**
         * <p>
         * internal convenience method to build the task model during initialization. Adds a new
         * task info object to the map for the provided task and fills it with measures. If there
         * are already some task infos for the task, the contained measures are updated according
         * to the parameters.
         * </p>
         */
        private void ensureTaskInfo(ITask task,
                                    int   eventTaskInstancesCovered,
                                    int   depth)
        {
            TaskInfo taskInfo = taskMap.get(task);

            if (taskInfo == null) {
                taskInfo = new TaskInfo(task);
                taskInfo.addMeasure(TaskMetric.COUNT);
                taskInfo.addMeasure(TaskMetric.EVENT_COVERAGE);
                taskInfo.addMeasure(TaskMetric.DEPTH);
                taskMap.put(task, taskInfo);
                
                taskInfo.setCount(TaskMetric.DEPTH, getDepth(task));
            }

            taskInfo.increaseCount(TaskMetric.COUNT, 1);
            taskInfo.increaseCount(TaskMetric.EVENT_COVERAGE, eventTaskInstancesCovered);

            taskInfo.setCount(TaskMetric.DEPTH, depth);
        }

        /**
         * <p>
         * internal convenience method to calculate the maximum depth of a task
         * </p>
         */
        private int getDepth(ITask task) {
            if (task instanceof IMarkingTemporalRelationship) {
                return getDepth(((IMarkingTemporalRelationship) task).getMarkedTask()) + 1;
            }
            else if (task instanceof IStructuringTemporalRelationship) {
                int maxDepth = 0;
                
                for (ITask child : ((IStructuringTemporalRelationship) task).getChildren()) {
                    maxDepth = Math.max(maxDepth, getDepth(child));
                }
                
                return maxDepth + 1;
            }
            else {
                // event tasks
                return 1;
            }
        }

        /**
         *
         */
        /*private void dumpToCSV(PrintStream out) {
            out.println("taskid;depth;count;eventcoverage;eventcoverageratio");
            
            for (Map.Entry<ITask, TaskInfo> entry : taskMap.entrySet()) {
                out.print(entry.getKey().getId());
                out.print(';');
                out.print(entry.getValue().getMeasureValue(TaskMetric.DEPTH));
                out.print(';');
                out.print(entry.getValue().getMeasureValue(TaskMetric.COUNT));
                out.print(';');
                out.print(entry.getValue().getMeasureValue(TaskMetric.EVENT_COVERAGE));
                out.print(';');
                out.print(entry.getValue().getMeasureValue(TaskMetric.EVENT_COVERAGE_RATIO));
                out.println();
            }
        }*/

    }


}
