/*******************************************************************************
 * Copyright (c) 2009, 2018 Red Hat, Inc.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Red Hat - initial API and implementation
 *******************************************************************************/
package org.eclipse.linuxtools.internal.callgraph;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.eclipse.cdt.core.model.CoreModel;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.linuxtools.internal.callgraph.core.SystemTapParser;
import org.eclipse.linuxtools.internal.callgraph.core.SystemTapUIErrorMessages;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;


/**
 * This class is used only in the case that we are rendering a graph
 * using GEF (specifically Zest).
 *
 * After a stap command is sent to be executed, and after data is stored
 * into some temporary file, the data must be parsed to be used. This class
 * handles all of the parsing. All data is stored into Maps and this class
 * also starts the job responsible for taking the parsed data and rendering it.
 */
public class StapGraphParser extends SystemTapParser {

    public Map<Integer, Long> timeMap;
    public Map<Integer, String> serialMap;
    public Map<Integer, HashMap<Integer, ArrayList<Integer>>> neighbourMaps;
    public Map<String, Long> aggregateTimeMap;
    public Map<String, Integer> countMap;
    public List<Integer> callOrderList;
    public Map<Integer, String> markedMap;
    public Long endingTimeInNS;
    public long totalTime;
    public Map<Integer, Integer> lastFunctionMap;
    public ICProject project;
    private static final String DELIM = ",,"; //$NON-NLS-1$

    private boolean encounteredMain = false;
    private List<Integer> shouldGetEndingTimeForID = new ArrayList <>();

    private Map<Integer, List<String>> nameMaps;
    private Map<Integer, List<Integer>> idMaps;
    private boolean skippedDirectives = false;
    private int firstNode = -1;

    public long startTime = -1;

    @Override
    protected void initialize() {
        //INITIALIZE MAPS
        neighbourMaps = new HashMap<>();
        timeMap = new HashMap<>();
        serialMap = new TreeMap<>();
        aggregateTimeMap = new HashMap<>();
        countMap = new HashMap<>();
        endingTimeInNS = 0l;
        callOrderList = new ArrayList<>();
        markedMap = new HashMap<>();
        lastFunctionMap = new HashMap<>();
        nameMaps = new HashMap<>();
        idMaps = new HashMap<>();
        project = null;
        startTime = -1;
    }


    @Override
    public IStatus nonRealTimeParsing(){
        //Clear maps (in case a previous execution left values hanging)
        neighbourMaps.clear();
        timeMap.clear();
        serialMap.clear();
        aggregateTimeMap.clear();
        countMap.clear();
        callOrderList.clear();
        shouldGetEndingTimeForID.clear();
        nameMaps.clear();
        idMaps.clear();
        encounteredMain = false;
        skippedDirectives = false;
        firstNode = -1;
        startTime = -1;

        BufferedReader buff = null;
        try {
            buff = new BufferedReader(new FileReader(sourcePath));
        } catch (FileNotFoundException e1) {
            Display.getDefault().asyncExec(() -> MessageDialog.openError(new Shell(), Messages.getString("StapGraphParser.FileNotFound"),  //$NON-NLS-1$
			        Messages.getString("StapGraphParser.CouldNotOpen") + sourcePath)); //$NON-NLS-1$
            return Status.CANCEL_STATUS;
        }
        internalData = buff;
        return realTimeParsing();
    }


    private void parseEnd() {

        //CHECK FOR EXIT() CALL
        for (Map.Entry<Integer, List<Integer>> entry : idMaps.entrySet()) {
            List<Integer> idList = entry.getValue();
            int lastFunctionCalled = lastFunctionMap.get(entry.getKey());
            if (idList.size() > 1) {
                for (int val : idList) {
                    String name = serialMap.get(val);
                    long time = endingTimeInNS - timeMap.get(val);
                    timeMap.put(val, time);
                    if (val == firstNode) {
                        showTime(val, time);
                    }
                    if (shouldGetEndingTimeForID.contains(val)) {
                        long cumulativeTime = aggregateTimeMap.get(name)
                                + endingTimeInNS;
                        aggregateTimeMap.put(name, cumulativeTime);
                    }

                    lastFunctionCalled = val;
                }
                String tmp = markedMap.get(lastFunctionCalled);
                if (tmp == null) {
                    tmp = ""; //$NON-NLS-1$
                }
                markedMap
                        .put(lastFunctionCalled,
                                tmp
                                        + "\n" + Messages.getString("StapGraphParser.Term")); //$NON-NLS-1$ //$NON-NLS-2$
            }
        }

        //timecheck is true if the total execution time is less than 10ms
        //and the first function is more than 1% off from the total time.
        boolean timeCheck = totalTime < 50000000 &&
                            (((float)timeMap.get(firstNode)/totalTime) > 1.01 ||
                            ((float)timeMap.get(firstNode)/totalTime) < 0.99);

        /*
         * Indicate whether or not we had to manipulate total time, and why
         */
        if (skippedDirectives || timeCheck) {
            totalTime = timeMap.get(firstNode);
            String markedMessage = ""; //$NON-NLS-1$
            if (markedMap.containsKey(firstNode)) {
                markedMessage = markedMap.get(firstNode) + "\n"; //$NON-NLS-1$
            }
            if (skippedDirectives) {
                markedMessage += Messages.getString("StapGraphParser.CDirectives"); //$NON-NLS-1$
            }
            if (timeCheck) {
                markedMessage += Messages.getString("StapGraphParser.TooFast"); //$NON-NLS-1$
            }

            markedMessage += Messages.getString("StapGraphParser.TimeForThisNode"); //$NON-NLS-1$

            markedMap.put(firstNode, markedMessage);
        }
    }

    private void parseMarked(String msg) {
        /*
         * Append message
         */
        String[] parsed = msg.split(",,", 2); //$NON-NLS-1$

        int key = Integer.parseInt(parsed[0]);

        List<Integer> idList = idMaps.get(key);
        if (idList == null || msg.length() < 1 || idList.size() < 1) {
            return;
        }
        int id = idList.get(idList.size() -1);
        if (parsed[1].equals("<unknown>")) { //$NON-NLS-1$
            parsed[1] = parsed[1] + Messages.getString("StapGraphParser.UnknownMarkers"); //$NON-NLS-1$
        }
        markedMap.put(id, (markedMap.get(id) == null ? "" : markedMap.get(id)) + parsed[1]); //$NON-NLS-1$
    }

    private IStatus parse(String s) {

        try {
        if (s.length() < 1) {
            return Status.OK_STATUS;
        }
        switch (s.charAt(0)) {
            case '<' :
                /*
                 *
                 * Open tag -- function call
                 *
                 *
                 */
                String[] args = s.substring(1, s.length()).split(DELIM);
                // args[0] = name
                // args[1] = id
                // arsg[2] = time of event
                int id = Integer.parseInt(args[1]);
                long time = Long.parseLong(args[2]);
                int tid = Integer.parseInt(args[3]);
                String name = args[0];

                //If we haven't encountered a main function yet and the name isn't clean,
                //and the name contains "__", then this is probably a C directive
                if (!encounteredMain && !isFunctionNameClean(name) && name.contains("__")) { //$NON-NLS-1$
                    skippedDirectives = true;
                    break;
                }

                List<String> nameList = nameMaps.get(tid);
                if (nameList == null) {
                    nameList = new ArrayList<>();
                }

                List<Integer> idList = idMaps.get(tid);
                if (idList == null) {
                    idList = new ArrayList<>();
                }

                HashMap<Integer, ArrayList<Integer>> outNeighbours = neighbourMaps.get(tid);
                if (outNeighbours == null) {
                    outNeighbours = new HashMap<>();
                }

                if (startTime < 1) {
                    startTime = time;
                }
                endingTimeInNS=time;

                name = cleanFunctionName(name);
                if (name.equals("main")) { //$NON-NLS-1$
                    encounteredMain = true;
                }
                if (firstNode == -1) {
                    firstNode = id;
                }

                serialMap.put(id, name);
                timeMap.put(id, time);

                if (aggregateTimeMap.get(name) == null){
                    aggregateTimeMap.put(name, (long) 0);
                }

                //IF THERE ARE PREVIOUS FUNCTIONS WITH THE SAME NAME
                //WE ARE IN ONE OF THEM SO DO NOT ADD TO CUMULATIVE TIME


                if (nameList.indexOf(name) == -1) {
                    long cumulativeTime = aggregateTimeMap.get(name) - time;
                    aggregateTimeMap.put(name, cumulativeTime);
                    shouldGetEndingTimeForID.add(id);
                }


                if (countMap.get(name) == null){
                    countMap.put(name, 0);
                }
                countMap.put(name, countMap.get(name) + 1);

                nameList.add(name);
                idList.add(id);


                if (outNeighbours.get(id) == null){
                    outNeighbours.put(id, new ArrayList<Integer>());
                }

                if (idList.size() > 1) {
                    int parentID = idList.get(idList.size() - 2);
                    outNeighbours.get(parentID).add(id);
                }


                callOrderList.add(id);
                lastFunctionMap.put(tid,id);

                neighbourMaps.put(tid, outNeighbours);
                nameMaps.put(tid, nameList);
                idMaps.put(tid, idList);

                break;
            case '>' :

                /*
                 *
                 * Close tag -- Function return
                 *
                 */

                args = s.substring(1, s.length()).split(DELIM);
                //args[0] = name
                //args[1] = time of event
                name = args[0];
                tid = Integer.parseInt(args[2]);

                nameList = nameMaps.get(tid);
                if (nameList == null) {
                    nameList = new ArrayList<>();
                }

                idList = idMaps.get(tid);
                if (idList == null) {
                    idList = new ArrayList<>();
                }


                //If we haven't encountered a main function yet and the name isn't clean,
                //and the name contains "__", then this is probably a C directive
                if (!encounteredMain && !isFunctionNameClean(name) && name.contains("__")) { //$NON-NLS-1$
                    skippedDirectives = true;
                    break;
                }

                name = cleanFunctionName(name);
                int lastOccurance = nameList.lastIndexOf(name);
                if (lastOccurance < 0) {
                    parsingError(Messages.getString("StapGraphParser.RetMismatch") + name); //$NON-NLS-1$
                    return Status.CANCEL_STATUS;
                }

                nameList.remove(lastOccurance);
                id = idList.remove(lastOccurance);


                if (timeMap.get(id) == null) {
                    parsingError(Messages.getString("StapGraphParser.NoStartTime") + name); //$NON-NLS-1$
                    return Status.CANCEL_STATUS;
                }
                endingTimeInNS=Long.parseLong(args[1]);
                time = endingTimeInNS - timeMap.get(id);
                timeMap.put(id, time);
                if (id == firstNode) {
                    showTime(id, time);
                }


                //IF AN ID IS IN THIS ARRAY IT IS BECAUSE WE NEED THE ENDING TIME
                // TO BE ADDED TO THE CUMULATIVE TIME FOR FUNCTIONS OF THIS NAME
                if (shouldGetEndingTimeForID.contains(id)){
                    long cumulativeTime = aggregateTimeMap.get(name) + Long.parseLong(args[1]);
                    aggregateTimeMap.put(name, cumulativeTime);
                }

                nameMaps.put(tid, nameList);
                idMaps.put(tid, idList);
                break;
            default :
                /*
                 *
                 * Anything else -- error
                 *
                 */

//                    parsingError(Messages.getString("StapGraphParser.14") + s.charAt(0) + //$NON-NLS-1$
//                            Messages.getString("StapGraphParser.15") ); //$NON-NLS-1$
                return Status.CANCEL_STATUS;

        }
        } catch (NumberFormatException e) {
            SystemTapUIErrorMessages mess = new SystemTapUIErrorMessages
                    (Messages.getString("StapGraphParser.BadSymbol"),  //$NON-NLS-1$
                    Messages.getString("StapGraphParser.BadSymbol"),   //$NON-NLS-1$
                    Messages.getString("StapGraphParser.BadSymbolMsg1") + //$NON-NLS-1$
                    Messages.getString("StapGraphParser.BadSymbolMsg2")); //$NON-NLS-1$
            mess.schedule();

            return Status.CANCEL_STATUS;
        }
        return Status.OK_STATUS;
    }


    private IStatus parseDotFile() {
        BufferedReader buff = internalData;

        HashMap <Integer, ArrayList<Integer>> outNeighbours= new HashMap<>();
        ArrayList<String> nameList = new ArrayList<>();
        ArrayList<Integer> idList = new ArrayList<>();
        endingTimeInNS =0l;
        totalTime=10000l;
        try {
            String line;
            while ((line = buff.readLine()) != null) {
                if (line.equals("}")) { //$NON-NLS-1$
                    break;
                }
                if (monitor.isCanceled()) {
                    return Status.CANCEL_STATUS;
                }
                if (line.length() < 1) {
                    continue;
                }

                String[] args = line.split(" ", 2); //$NON-NLS-1$
                if (args[0].contains("->")) { //$NON-NLS-1$
                    //connection
                    int[] ids = new int[2];
                    int called = 1;
                    try {
                        ids[0] = Integer.parseInt(args[0].split("->")[0]); //$NON-NLS-1$
                        ids[1] = Integer.parseInt(args[0].split("->")[1]); //$NON-NLS-1$
                        int index1 = args[1].indexOf("=\""); //$NON-NLS-1$
                        int index2 = args[1].indexOf("\"]"); //$NON-NLS-1$
                        called = Integer.parseInt(args[1].substring(index1 + 2,index2));
                    } catch (NumberFormatException e) {
                        SystemTapUIErrorMessages m = new SystemTapUIErrorMessages(
                                Messages.getString("StapGraphParser.idOrLabel"), Messages.getString("StapGraphParser.idOrLabel"),  //$NON-NLS-1$ //$NON-NLS-2$
                                Messages.getString("StapGraphParser.nonNumericLabel")); //$NON-NLS-1$
                        m.schedule();
                        return Status.CANCEL_STATUS;
                    }

                    //Set neighbour
                    ArrayList<Integer> tmpList = outNeighbours.get(ids[0]);
                    if (tmpList == null) {
                        tmpList = new ArrayList<>();
                    }

                    for (int i = 0; i < called; i++) {
                        tmpList.add(ids[1]);
                    }

                    outNeighbours.put(ids[0], tmpList);
                } else {
                    //node
                    try {
                        int id = Integer.parseInt(args[0]);
                        if (firstNode == -1) {
                            firstNode = id;
                        }
                        int index = args[1].indexOf("=\""); //$NON-NLS-1$
                        String name = args[1].substring(index + 2, args[1].indexOf(' ', index));
                        double dtime = 0.0;
                        dtime = Double.parseDouble(args[1].substring(args[1].indexOf(' ') + 1, args[1].indexOf('%')));
                        long time = (long) (dtime*100);

                        nameList.add(name);
                        idList.add(id);
                        timeMap.put(id, time);
                        serialMap.put(id, name);
                        if (countMap.get(name) == null){
                            countMap.put(name, 0);
                        }
                        countMap.put(name, countMap.get(name) + 1);

                        long cumulativeTime = (aggregateTimeMap.get(name) != null ? aggregateTimeMap.get(name) : 0) + time;
                        aggregateTimeMap.put(name, cumulativeTime);
                    } catch (NumberFormatException e) {
                        SystemTapUIErrorMessages m = new SystemTapUIErrorMessages(
                                Messages.getString("StapGraphParser.idOrTime"), Messages.getString("StapGraphParser.idOrTime"),  //$NON-NLS-1$ //$NON-NLS-2$
                        Messages.getString("StapGraphParser.nonNumericTime")); //$NON-NLS-1$
                        m.schedule();
                        return Status.CANCEL_STATUS;
                    }

                }


            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                buff.close();
            } catch (IOException e) {
                //Do nothing
            }
        }
        neighbourMaps.put(0, outNeighbours);
        nameMaps.put(0, nameList);
        idMaps.put(0, idList);
        try {
            view.update();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return Status.OK_STATUS;
    }

    @Override
    public IStatus realTimeParsing() {
        BufferedReader buff = internalData;

        String line;
        boolean draw = false;
        boolean first = true;
        try {
            while ((line = buff.readLine()) != null) {
                if (monitor.isCanceled()) {
                    return Status.CANCEL_STATUS;
                }
                if (line.length() < 1) {
                    continue;
                }
                if (first && (line.contains(Messages.getString("StapGraphParser.17")))) { //$NON-NLS-1$
                    return parseDotFile();
                }
                first = false;

                draw = true;
                if (line.equals("PROBE_BEGIN")) { //$NON-NLS-1$
                    buff.mark(100);
                    String tmp = buff.readLine();

                    if (tmp != null && tmp.length() > 0) {
                        char tchar = tmp.charAt(0);
                        if (tchar != '-' && tchar != '+' && tchar != '?' && tchar != '>' && tchar != '<') {
                            project = CoreModel.getDefault().getCModel().getCProject(tmp);
                        } else {
                            buff.reset();
                        }
                    }

                } else if (line.charAt(0) == '-') {
                    endingTimeInNS = Long.parseLong(line.substring(1));
                } else if (line.charAt(0) == '+') {
                    totalTime = Long.parseLong(line.substring(1));
                    //Total time should be the last line in the output
                    parseEnd();
                } else if (line.charAt(0) == '?') {
                    if (line.length() > 1) {
                        parseMarked(line.substring(1));
                    }
                } else {
                    if (parse(line) == Status.CANCEL_STATUS) {
                        break;
                    }
                }
            }
            if (draw && view != null) {
                view.update();
                //Repeat until all lines are read
                realTimeParsing();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return Status.OK_STATUS;
    }

    /**
     * Mark node id with a message giving its actual time.
     */
    private void showTime(int id, long time) {
        String tmp = markedMap.get(id);
        if (tmp == null) {
            tmp = ""; //$NON-NLS-1$
        }
        markedMap.put(id, tmp +
                Messages.getString("StapGraphParser.ActualTime") + time/1000000  //$NON-NLS-1$
                + Messages.getString("StapGraphParser.TimeUnits")); //$NON-NLS-1$
    }


}
