//   Copyright 2015 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.plugin.genericevents.commands;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

import de.ugoe.cs.autoquest.CommandHelpers;
import de.ugoe.cs.autoquest.eventcore.Event;
import de.ugoe.cs.autoquest.tasktrees.treeifc.DefaultTaskInstanceTraversingVisitor;
import de.ugoe.cs.autoquest.tasktrees.treeifc.DefaultTaskTraversingVisitor;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IEventTask;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IEventTaskInstance;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IMarkingTemporalRelationship;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ISequence;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ISequenceInstance;
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.IUserSession;
import de.ugoe.cs.autoquest.tasktrees.treeifc.TaskMetric;
import de.ugoe.cs.util.console.Command;
import de.ugoe.cs.util.console.Console;
import de.ugoe.cs.util.console.GlobalDataContainer;

/**
 * <p>
 * TODO comment
 * </p>
 * 
 * @author Patrick Harms
 */
public class CMDexportSogouQTaskTrees implements Command {

    /* (non-Javadoc)
     * @see de.ugoe.cs.util.console.Command#run(java.util.List)
     */
    @Override
    public void run(List<Object> parameters) {
        String taskTreeName = null;
        String path = null;
        String numberStr = null;
        boolean withTimestamp = false;
        
        try {
            for (int i = 0; i < parameters.size(); i++) {
                String parameter = (String) parameters.get(i);
                if (!parameter.startsWith("-")) {
                    if (taskTreeName == null) {
                        taskTreeName = parameter;
                    }
                    else if (path == null) {
                        path = parameter;
                    }
                    else if (numberStr == null) {
                        numberStr = parameter;
                    }
                    else {
                        throw new IllegalArgumentException("unrecognized parameter: " + parameter);
                    }
                }
                else {
                    if ("-withTimestamp".equals(parameter)) {
                        withTimestamp = true;
                    }
                    else {
                        throw new IllegalArgumentException("unrecognized parameter: " + parameter);
                    }
                }
            }
        }
        catch (IllegalArgumentException e) {
            throw e;
        }
        catch (Exception e) {
            throw new IllegalArgumentException("could not process parameters", e);
        }
        
        if (path == null) {
            throw new IllegalArgumentException("no path to directory provided");
        }
        
        if (taskTreeName == null) {
            throw new IllegalArgumentException("no task tree specified");
        }
        
        if (numberStr == null) {
            throw new IllegalArgumentException("no number of tasks to be exported provided");
        }
        
        int tasksToExport;
        try{
            tasksToExport = Integer.parseInt(numberStr);
        }
        catch (Exception e) {
            throw new IllegalArgumentException("invalid number of tasks to be exported: " +
                                               numberStr, e);
        }

        File directory = new File(path);
        if (!directory.isDirectory()) {
            Console.printerrln(path + " is not a directory");
            return;
        }
        if (directory.exists() && (directory.list().length > 0)) {
            Console.printerrln(path + " is not empty");
            return;
        }

        Object dataObject = GlobalDataContainer.getInstance().getData(taskTreeName);
        if (dataObject == null) {
            CommandHelpers.objectNotFoundMessage(taskTreeName);
            return;
        }
        if (!(dataObject instanceof ITaskModel)) {
            CommandHelpers.objectNotType(taskTreeName, "ITaskTree");
            return;
        }

        ITaskModel taskModel = (ITaskModel) dataObject;
        
        LinkedList<ISequence> sequencesToExport =
            determineSequencesToExport(tasksToExport, taskModel);
        
        int index = 1;
        int width = Integer.toString(tasksToExport).length();
        for (ISequence sequence : sequencesToExport) {
            export(String.format("%0" + width + "d", index++),
                   sequence, taskModel, directory, withTimestamp);
        }
    }

    /**
     * <p>
     * TODO: comment
     * </p>
     *
     * @param tasksToExport
     * @param taskModel
     * @return
     */
    private LinkedList<ISequence> determineSequencesToExport(int        tasksToExport,
                                                             ITaskModel taskModel)
    {
        LinkedList<ISequence> sequencesToExport = new LinkedList<>();
        
        for (ITask task : taskModel.getTasks()) {
            if (task instanceof ISequence) {
                int coverage =
                    taskModel.getTaskInfo(task).getMeasureValue(TaskMetric.EVENT_COVERAGE);
                
                boolean inserted = false;
                ListIterator<ISequence> it = sequencesToExport.listIterator();
                
                while (it.hasNext()) {
                    ISequence candidate = it.next();
                    int candidateCoverage =
                        taskModel.getTaskInfo(candidate).getMeasureValue(TaskMetric.EVENT_COVERAGE);
                    
                    if (coverage > candidateCoverage) {
                        it.previous();
                        it.add((ISequence) task);
                        inserted = true;
                        break;
                    }
                }
                
                if (!inserted) {
                    sequencesToExport.add((ISequence) task);
                }
                
                while (sequencesToExport.size() > tasksToExport) {
                    sequencesToExport.removeLast();
                }
            }
        }
        
        return sequencesToExport;
    }

    /**
     * <p>
     * TODO: comment
     * </p>
     *
     * @param index
     * @param sequence
     * @param path
     * @param withTimestamp
     */
    private void export(String     folderPrefix,
                        ISequence  sequence,
                        ITaskModel taskModel,
                        File       directory,
                        boolean    withTimestamp)
    {
        File exportDirectory = new File(directory, folderPrefix + "_pattern" + sequence.getId());
        
        if (exportDirectory.exists()) {
            throw new IllegalArgumentException(exportDirectory + " already exists");
        }
        else {
            exportDirectory.mkdirs();
        }
        
        createPatternFile(sequence, taskModel, withTimestamp, exportDirectory);
        createSessionFiles(sequence, taskModel, withTimestamp, exportDirectory);
        
        Console.println("exported " + sequence);
    }

    /**
     * <p>
     * TODO: comment
     * </p>
     *
     * @param sequence
     * @param taskModel
     * @param withTimestamp
     * @param exportDirectory
     */
    private void createPatternFile(ISequence  sequence,
                                   ITaskModel taskModel,
                                   boolean    withTimestamp,
                                   File       exportDirectory)
    {
        // write the pattern file
        File patternFile = new File(exportDirectory, "patternDetails.txt");
        PrintStream out = null;
        try {
            out = new PrintStream(new FileOutputStream(patternFile));
            out.println("########################################################################");
            out.println("# Pattern");
            out.println("########################################################################");
            out.println();
            dumpSequence(sequence, out);
            out.println();
            
            out.println("########################################################################");
            out.println("# Statistics");
            out.println("########################################################################");
            out.println();
            int value = taskModel.getTaskInfo(sequence).getMeasureValue(TaskMetric.COUNT);
            out.println("         count: " + TaskMetric.COUNT.formatValue(value));
            value = taskModel.getTaskInfo(sequence).getMeasureValue(TaskMetric.EVENT_COVERAGE);
            out.println("      coverage: " + TaskMetric.EVENT_COVERAGE.formatValue(value));
            value = taskModel.getTaskInfo(sequence).getMeasureValue(TaskMetric.EVENT_COVERAGE_RATIO);
            out.println("coverage ratio: " + TaskMetric.EVENT_COVERAGE_RATIO.formatValue(value));
            
            if (withTimestamp) {
                long averageTimeBetweenEvents = getAverageTimeBetweenEvents(sequence);
                
                out.println("\naverage event time interval: " + averageTimeBetweenEvents + "ms");
            }
            
            out.println();
            out.println();
            out.println("########################################################################");
            out.println("# Execution variants");
            out.println("########################################################################");
            out.println();
            
            int index = 1;
            for (Collection<ITaskInstance> variant : sequence.getExecutionVariants()) {
                dumpExecutionVariant(index++, variant.size(), variant.iterator().next(), out);
                out.println();
            }
        }
        catch (IOException e) {
            throw new IllegalArgumentException("could not write into file " + patternFile);
        }
        finally {
            if (out != null) {
                try {
                    out.close();
                }
                catch (Exception e) {
                    // ignore
                }
            }
        }
    }

    /**
     * <p>
     * TODO: comment
     * </p>
     *
     * @param sequence
     * @return
     */
    private long getAverageTimeBetweenEvents(ISequence sequence) {
        int intervalCount = 0;
        long cummulativeTime = 0;
        for (ITaskInstance instance : sequence.getInstances()) {
            final List<Long> timestamps = new ArrayList<>();
            instance.accept(new DefaultTaskInstanceTraversingVisitor() {
                @Override
                public void visit(IEventTaskInstance eventTaskInstance) {
                    timestamps.add(eventTaskInstance.getEvent().getTimestamp());
                }
            });
            
            for (int i = 1; i < timestamps.size(); i++) {
                cummulativeTime += timestamps.get(i) - timestamps.get(i - 1);
                intervalCount++;
            }
        }
        
        return (cummulativeTime / intervalCount);
    }

    /**
     * <p>
     * TODO: comment
     * </p>
     *
     * @param sequence
     * @param stream
     */
    private void dumpSequence(ISequence sequence, final PrintStream stream) {
        final String[] indent = new String[1];
        indent[0] = "";
        
        sequence.accept(new DefaultTaskTraversingVisitor() {
            @Override
            public void visit(IEventTask eventTask) {
                stream.print(indent[0]);
                stream.println(eventTask.toString());
            }

            @Override
            public void visit(IStructuringTemporalRelationship relationship) {
                stream.print(indent[0]);
                stream.print(relationship.toString());
                stream.println(" {");
                String indentBefore = indent[0];
                indent[0] = indentBefore + "  ";
                super.visit(relationship);
                indent[0] = indentBefore;
                stream.print(indent[0]);
                stream.println("}");
            }

            @Override
            public void visit(IMarkingTemporalRelationship relationship) {
                stream.print(indent[0]);
                stream.print(relationship.toString());
                stream.println(" {");
                String indentBefore = indent[0];
                indent[0] = indentBefore + "  ";
                super.visit(relationship);
                indent[0] = indentBefore;
                stream.print(indent[0]);
                stream.println("}");
            }
        });
    }

    /**
     *
     */
    private void dumpExecutionVariant(int               variantIndex,
                                      int               count,
                                      ITaskInstance     instance,
                                      final PrintStream stream)
    {
        stream.println("\nVariant " + variantIndex + " (executed " + count + " times)\n");
        final String[] indent = new String[1];
        indent[0] = "  ";
        
        instance.accept(new DefaultTaskInstanceTraversingVisitor() {
            @Override
            public void visit(IEventTaskInstance eventTaskInstance) {
                stream.print(indent[0]);
                stream.println(eventTaskInstance.toString());
            }

            @Override
            public void visit(ITaskInstanceList taskInstanceList) {
                stream.print(indent[0]);
                stream.print(taskInstanceList.toString());
                stream.println(" {");
                String indentBefore = indent[0];
                indent[0] = indentBefore + "  ";
                super.visit(taskInstanceList);
                indent[0] = indentBefore;
                stream.print(indent[0]);
                stream.println("}");
            }
        });
    }


    /**
     * <p>
     * TODO: comment
     * </p>
     *
     * @param instance
     * @param taskModel
     */
    private void createSessionFiles(final ISequence  sequence,
                                    final ITaskModel taskModel,
                                    final boolean    withTimestamp,
                                    final File       exportDirectory)
    {
        final File sessionsDirectory = new File(exportDirectory, "sessions");
        
        if (sessionsDirectory.exists()) {
            throw new IllegalArgumentException(sessionsDirectory + " already exists");
        }
        else {
            sessionsDirectory.mkdirs();
        }

        for (IUserSession session : taskModel.getUserSessions()) {
            for (ITaskInstance instance : session) {
                instance.accept(new DefaultTaskInstanceTraversingVisitor() {
                    @Override
                    public void visit(ISequenceInstance sequenceInstance) {
                        if (sequence == sequenceInstance.getTask()) {
                            createSessionFile(sequenceInstance, withTimestamp, sessionsDirectory);
                        }
                        super.visit(sequenceInstance);
                    }
                });
            }
        }
    }


    /**
     * <p>
     * TODO: comment
     * </p>
     *
     * @param instance
     * @param taskModel
     */
    private void createSessionFile(final ISequenceInstance instance,
                                   final boolean           withTimestamp,
                                   final File              exportDirectory)
    {
        // write the session file
        final String userId = getUserOf(instance, exportDirectory);
        
        final File sessionsDirectory = new File(exportDirectory, userId);
        
        if (!sessionsDirectory.exists()) {
            sessionsDirectory.mkdirs();
        }

        File sessionFile;
        
        // a user may have many sessions, search for the next id of it.
        int index = 0;
        
        do {
            sessionFile = new File
                (sessionsDirectory, "session_" + String.format("%09d", ++index) + ".txt");
        }
        while (sessionFile.exists());
        
        PrintStream out = null;
        try {
            out = new PrintStream(new FileOutputStream(sessionFile));
            final PrintStream finalOut = out;
            
            instance.accept(new DefaultTaskInstanceTraversingVisitor() {
                @Override
                public void visit(IEventTaskInstance eventTaskInstance) {
                    Event event = eventTaskInstance.getEvent();
                    
                    if (withTimestamp) {
                        finalOut.print(event.getTimestamp());
                        finalOut.print("\t");
                    }
                    
                    finalOut.print(userId);
                    finalOut.print("\t");
                    
                    finalOut.print(event.getParameter("query"));
                    finalOut.print("\t");
                    
                    finalOut.print(event.getParameter("selectedResultPage"));
                    finalOut.print(" ");
                    
                    finalOut.print(event.getParameter("selectedResultIndex"));
                    finalOut.print("\t");
                    
                    finalOut.println(event.getParameter("selectedResult"));
                }
            });
            
        }
        catch (IOException e) {
            throw new IllegalArgumentException("could not write into file " + sessionFile);
        }
        finally {
            if (out != null) {
                try {
                    out.close();
                }
                catch (Exception e) {
                    // ignore
                }
            }
        }
    }
    
    /**
     * <p>
     * TODO: comment
     * </p>
     *
     * @param instance
     * @return
     */
    private String getUserOf(ISequenceInstance instance, final File exportDirectory) {
        final String[] result = new String[1];
        
        instance.accept(new DefaultTaskInstanceTraversingVisitor() {
            @Override
            public void visit(IEventTaskInstance eventTaskInstance) {
                if (result[0] == null) {
                    result[0] = eventTaskInstance.getEvent().getParameter("userId");
                }
            }
            @Override
            public void visit(ITaskInstanceList taskInstanceList) {
                if (result[0] == null) {
                    super.visit(taskInstanceList);
                }
            }
        });
        
        if (result[0] == null) {
            // no user id stored with events --> generate generic unused one
            File sessionFile = null;
            int index = 0;
            
            do {
                sessionFile = new File
                    (exportDirectory, "session_" + String.format("%09d", ++index) + ".txt");
            }
            while (sessionFile.exists());
            
            result[0] = String.format("%09d", index);
        }
        
        return result[0];
    }

    /* (non-Javadoc)
     * @see de.ugoe.cs.util.console.Command#help()
     */
    @Override
    public String help() {
        return "exportSogouQTaskTrees <tasktree> <directory> <numberOfTasksToExport> " +
            "{-withTimestamp}";
    }

}
