/*******************************************************************************
 * Copyright (c) 2017 Pegasystems Inc. All rights reserved.
 *
 * Contributors:
 *     Manu Varghese
 *******************************************************************************/

package com.pega.gcs.tracerviewer;

import java.awt.Color;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.CancellationException;

import javax.swing.border.EmptyBorder;
import javax.swing.table.DefaultTableColumnModel;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;

import com.pega.gcs.fringecommon.guiutilities.CheckBoxMenuItemPopupEntry;
import com.pega.gcs.fringecommon.guiutilities.FilterColumn;
import com.pega.gcs.fringecommon.guiutilities.FilterTableModel;
import com.pega.gcs.fringecommon.guiutilities.FilterTableModelNavigation;
import com.pega.gcs.fringecommon.guiutilities.ModalProgressMonitor;
import com.pega.gcs.fringecommon.guiutilities.RecentFile;
import com.pega.gcs.fringecommon.guiutilities.SearchTableModelEvent;
import com.pega.gcs.fringecommon.guiutilities.search.SearchData;
import com.pega.gcs.fringecommon.guiutilities.search.SearchModel;
import com.pega.gcs.fringecommon.log4j2.Log4j2Helper;
import com.pega.gcs.tracerviewer.model.TraceEvent;
import com.pega.gcs.tracerviewer.model.TraceEventAlert;
import com.pega.gcs.tracerviewer.model.TraceEventKey;
import com.pega.gcs.tracerviewer.model.TraceEventType;

public class TraceTableModel extends FilterTableModel<TraceEventKey> {

    private static final long serialVersionUID = -2061492402283117131L;

    private static final Log4j2Helper LOG = new Log4j2Helper(TraceTableModel.class);

    private TraceTableModelColumn[] traceTableModelColumnArray;

    // Main list. for reference purpose only, not working on this map.
    private Map<TraceEventKey, TraceEvent> traceEventMap;

    private List<TraceEventKey> traceEventKeyList;

    // search
    private SearchData<TraceEventKey> searchData;
    private SearchModel<TraceEventKey> searchModel;

    // sets for event filters
    private Map<TraceEventType, CheckBoxMenuItemPopupEntry<TraceEventKey>> traceEventTypeCheckBoxMenuItemMap;

    // tree related variables - trace event tree node map
    private TraceEventTreeNode rootTraceEventTreeNode;
    private TraceEventCombinedTreeNode rootTraceEventCombinedTreeNode;

    private LinkedList<TraceEvent> treeBuildTraceEventList;

    private Map<TraceEventKey, TraceEventTreeNode> traceEventTreeNodeMap;
    private Map<TraceEventKey, TraceEventCombinedTreeNode> traceEventCombinedTreeNodeMap;

    // reporting
    private List<TraceEventKey> failedEventKeyList;
    private List<TraceEventKey> exceptionEventKeyList;
    private List<TraceEventKey> alertEventKeyList;
    private List<TraceEventKey> noStartEventKeyList;
    private List<TraceEventKey> noEndEventKeyList;
    private TreeMap<Double, List<TraceEventKey>> ownElapsedEventKeyMap;
    private TreeMap<TraceEventRuleset, TreeSet<TraceEventRule>> rulesInvokedMap;

    public TraceTableModel(RecentFile recentFile, SearchData<TraceEventKey> searchData) {

        super(recentFile);
        this.searchData = searchData;

        resetModel();

    }

    public SearchData<TraceEventKey> getSearchData() {
        return searchData;
    }

    public TraceTableModelColumn[] getTraceTableModelColumnArray() {

        if (traceTableModelColumnArray == null) {
            traceTableModelColumnArray = TraceTableModelColumn.getTraceTableModelColumnArray();
        }

        return traceTableModelColumnArray;
    }

    protected Map<TraceEventKey, TraceEvent> getTraceEventMap() {

        if (traceEventMap == null) {
            traceEventMap = new TreeMap<TraceEventKey, TraceEvent>();
        }

        return traceEventMap;
    }

    private Map<TraceEventKey, TraceEventTreeNode> getTraceEventTreeNodeMap() {

        if (traceEventTreeNodeMap == null) {
            traceEventTreeNodeMap = new HashMap<TraceEventKey, TraceEventTreeNode>();
        }

        return traceEventTreeNodeMap;
    }

    private Map<TraceEventKey, TraceEventCombinedTreeNode> getTraceEventCombinedTreeNodeMap() {

        if (traceEventCombinedTreeNodeMap == null) {
            traceEventCombinedTreeNodeMap = new HashMap<TraceEventKey, TraceEventCombinedTreeNode>();
        }

        return traceEventCombinedTreeNodeMap;
    }

    @Override
    public void resetModel() {

        List<TraceEventKey> traceEventKeyList = getFtmEntryKeyList();
        traceEventKeyList.clear();

        Map<TraceEventKey, TraceEvent> traceEventMap = getTraceEventMap();
        traceEventMap.clear();

        Map<TraceEventKey, TraceEventTreeNode> traceEventTreeNodeMap = getTraceEventTreeNodeMap();
        traceEventTreeNodeMap.clear();

        Map<TraceEventKey, TraceEventCombinedTreeNode> traceEventCombinedTreeNodeMap = getTraceEventCombinedTreeNodeMap();
        traceEventCombinedTreeNodeMap.clear();

        // tree
        TraceEventTreeNode rootTraceEventTreeNode = getRootTraceEventTreeNode();
        rootTraceEventTreeNode.removeAllChildren();

        TraceEventCombinedTreeNode rootTraceEventCombinedTreeNode = getRootTraceEventCombinedTreeNode();
        rootTraceEventCombinedTreeNode.removeAllChildren();

        LinkedList<TraceEvent> treeBuildTraceEventList = getTreeBuildTraceEventList();
        treeBuildTraceEventList.clear();

        Map<FilterColumn, List<CheckBoxMenuItemPopupEntry<TraceEventKey>>> columnFilterMap;
        columnFilterMap = getColumnFilterMap();

        columnFilterMap.clear();

        TraceTableModelColumn[] traceTableModelColumnArray = getTraceTableModelColumnArray();

        for (int columnIndex = 1; columnIndex < traceTableModelColumnArray.length; columnIndex++) {

            TraceTableModelColumn traceTableModelColumn = traceTableModelColumnArray[columnIndex];

            // preventing unnecessary buildup of filter map
            if (traceTableModelColumn.isFilterable()) {

                FilterColumn filterColumn = new FilterColumn(columnIndex);

                filterColumn.setColumnFilterEnabled(false);

                columnFilterMap.put(filterColumn, null);
            }
        }

        traceEventTypeCheckBoxMenuItemMap = new TreeMap<TraceEventType, CheckBoxMenuItemPopupEntry<TraceEventKey>>();

        TraceEventType[] values = TraceEventType.values();

        Arrays.sort(values, new Comparator<TraceEventType>() {

            @Override
            public int compare(TraceEventType o1, TraceEventType o2) {
                return o1.toString().compareTo(o2.toString());
            }
        });

        for (TraceEventType traceEventType : values) {

            CheckBoxMenuItemPopupEntry<TraceEventKey> cbmipe = new CheckBoxMenuItemPopupEntry<TraceEventKey>(
                    traceEventType);

            traceEventTypeCheckBoxMenuItemMap.put(traceEventType, cbmipe);
        }

        // reporting
        List<TraceEventKey> failedEventKeyList = getFailedEventKeyList();
        List<TraceEventKey> exceptionEventKeyList = getExceptionEventKeyList();
        List<TraceEventKey> alertEventKeyList = getAlertEventKeyList();
        List<TraceEventKey> noStartEventKeyList = getNoStartEventKeyList();
        List<TraceEventKey> noEndEventKeyList = getNoEndEventKeyList();

        Map<Double, List<TraceEventKey>> ownElapsedEventKeyMap = getOwnElapsedEventKeyMap();
        Map<TraceEventRuleset, TreeSet<TraceEventRule>> rulesInvokedMap = getRulesInvokedMap();

        failedEventKeyList.clear();
        exceptionEventKeyList.clear();
        alertEventKeyList.clear();
        noStartEventKeyList.clear();
        noEndEventKeyList.clear();

        ownElapsedEventKeyMap.clear();
        rulesInvokedMap.clear();

        clearSearchResults(true);

        fireTableDataChanged();
    }

    @Override
    public List<TraceEventKey> getFtmEntryKeyList() {

        if (traceEventKeyList == null) {
            traceEventKeyList = new ArrayList<TraceEventKey>();
        }

        return traceEventKeyList;
    }

    // this is called from load task. hence the order is expected to be
    // sequential
    public void addTraceEventToMap(TraceEvent traceEvent) {

        TraceEventKey traceEventKey = traceEvent.getKey();

        Map<TraceEventKey, TraceEvent> traceEventMap = getTraceEventMap();
        traceEventMap.put(traceEventKey, traceEvent);

        List<TraceEventKey> traceEventKeyList = getFtmEntryKeyList();

        traceEventKeyList.add(traceEventKey);

        // performing updateColumnFilterMap to avoid re-parsing the full map if
        // we used applyFilterEventSet(null).
        Map<FilterColumn, List<CheckBoxMenuItemPopupEntry<TraceEventKey>>> columnFilterMap = getColumnFilterMap();
        updateColumnFilterMap(traceEvent, columnFilterMap);

        TraceEventType tet = traceEvent.getTraceEventType();

        if (tet != null) {
            CheckBoxMenuItemPopupEntry<TraceEventKey> cbmipe;
            cbmipe = traceEventTypeCheckBoxMenuItemMap.get(tet);
            cbmipe.addRowIndex(traceEventKey);
        }

        buildTree(traceEvent);

    }

    // fix Issue #1 - Compare functionality not working
    // to be overridden in TraceTableCompareModel to avoid building tree for compare
    // view
    protected void buildTree(TraceEvent currentTraceEvent) {

        LinkedList<TraceEvent> treeBuildTraceEventList = getTreeBuildTraceEventList();
        Map<TraceEventKey, TraceEventTreeNode> traceEventTreeNodeMap = getTraceEventTreeNodeMap();
        Map<TraceEventKey, TraceEventCombinedTreeNode> traceEventCombinedTreeNodeMap = getTraceEventCombinedTreeNodeMap();

        TraceEventTreeNode previousParentTraceEventTreeNode = null;
        TraceEventCombinedTreeNode previousParentTraceEventCombinedTreeNode = null;

        TraceEvent previousParentTraceEvent = getMatchingParentTraceEvent(currentTraceEvent);

        // if no parent found. ex if this is first entry. assign to root.
        if (previousParentTraceEvent == null) {
            previousParentTraceEventTreeNode = getRootTraceEventTreeNode();
            previousParentTraceEventCombinedTreeNode = getRootTraceEventCombinedTreeNode();
        } else {
            // get the tree nodes corresponding to key
            TraceEventKey traceEventKey = previousParentTraceEvent.getKey();

            previousParentTraceEventTreeNode = traceEventTreeNodeMap.get(traceEventKey);
            previousParentTraceEventCombinedTreeNode = traceEventCombinedTreeNodeMap.get(traceEventKey);
        }

        TraceEventKey currentTraceEventKey = currentTraceEvent.getKey();

        TraceEventTreeNode currentTraceEventTreeNode = new TraceEventTreeNode(currentTraceEvent);
        TraceEventCombinedTreeNode currentTraceEventCombinedTreeNode = new TraceEventCombinedTreeNode(
                currentTraceEvent);

        traceEventTreeNodeMap.put(currentTraceEventKey, currentTraceEventTreeNode);

        Boolean endEvent = currentTraceEvent.isEndEvent();

        if (endEvent != null) {
            // current event is a block event

            if (endEvent) {
                // find the correct 'begin' node. correct = same INT, RULE#,
                // EVENT_TYPE

                TraceEvent startTraceEvent = getMatchingStartTraceEvent(currentTraceEvent);

                if (startTraceEvent != null) {

                    TraceEventKey startTraceEventKey = startTraceEvent.getKey();

                    // single tree node
                    TraceEventTreeNode startTraceEventTreeNode = traceEventTreeNodeMap.get(startTraceEventKey);

                    TraceEventTreeNode parentTraceEventTreeNode = (TraceEventTreeNode) startTraceEventTreeNode
                            .getParent();
                    parentTraceEventTreeNode.add(currentTraceEventTreeNode);

                    // combined tree node
                    TraceEventCombinedTreeNode startTraceEventCombinedTreeNode = traceEventCombinedTreeNodeMap
                            .get(startTraceEventKey);

                    startTraceEventCombinedTreeNode.setEndEvent(currentTraceEvent);

                    traceEventCombinedTreeNodeMap.put(currentTraceEventKey, startTraceEventCombinedTreeNode);

                    processTraceEventElapsed(startTraceEventTreeNode, currentTraceEventTreeNode);

                } else {
                    LOG.info("Could'nt find a matching start for trace event: " + currentTraceEvent.toDebugString());

                    List<TraceEventKey> noStartEventKeyList = getNoStartEventKeyList();
                    noStartEventKeyList.add(currentTraceEventKey);

                    previousParentTraceEventTreeNode.add(currentTraceEventTreeNode);
                    previousParentTraceEventCombinedTreeNode.add(currentTraceEventCombinedTreeNode);

                    traceEventCombinedTreeNodeMap.put(currentTraceEventKey, currentTraceEventCombinedTreeNode);
                }
            } else {
                // if starting event, add to the stack
                treeBuildTraceEventList.add(currentTraceEvent);

                previousParentTraceEventTreeNode.add(currentTraceEventTreeNode);
                previousParentTraceEventCombinedTreeNode.add(currentTraceEventCombinedTreeNode);

                traceEventCombinedTreeNodeMap.put(currentTraceEventKey, currentTraceEventCombinedTreeNode);
            }
        } else {
            // current event is a singular event.
            previousParentTraceEventTreeNode.add(currentTraceEventTreeNode);
            previousParentTraceEventCombinedTreeNode.add(currentTraceEventCombinedTreeNode);

            traceEventCombinedTreeNodeMap.put(currentTraceEventKey, currentTraceEventCombinedTreeNode);

        }

        processEvent(currentTraceEvent);

    }

    private TraceEvent getMatchingParentTraceEvent(TraceEvent childTraceEvent) {

        TraceEvent parentTraceEvent = null;

        LinkedList<TraceEvent> treeBuildTraceEventList = getTreeBuildTraceEventList();

        Iterator<TraceEvent> descendingIterator = treeBuildTraceEventList.descendingIterator();

        while (descendingIterator.hasNext()) {

            TraceEvent traceEvent = descendingIterator.next();

            if (traceEvent.isMatchingParentTraceEvent(childTraceEvent)) {
                parentTraceEvent = traceEvent;
                break;
            }
        }

        // if no possible parent found, assign it to last start block
        if ((parentTraceEvent == null)) {
            parentTraceEvent = treeBuildTraceEventList.peekLast();
        }

        return parentTraceEvent;
    }

    private TraceEvent getMatchingStartTraceEvent(TraceEvent endTraceEvent) {

        TraceEvent startTraceEvent = null;

        LinkedList<TraceEvent> treeBuildTraceEventList = getTreeBuildTraceEventList();

        int loopCounter = 0;
        int treeBuildTraceEventListSize = treeBuildTraceEventList.size();

        ArrayList<TraceEvent> noEndTraceEventList = new ArrayList<>();

        Iterator<TraceEvent> descendingIterator = treeBuildTraceEventList.descendingIterator();

        while (descendingIterator.hasNext()) {

            TraceEvent traceEvent = descendingIterator.next();
            loopCounter++;

            if (traceEvent.isMatchingStartTraceEvent(endTraceEvent)) {
                startTraceEvent = traceEvent;
                break;
            } else {
                noEndTraceEventList.add(traceEvent);
            }
        }

        // remove the entry from stack
        if (startTraceEvent != null) {
            int removeIndex = treeBuildTraceEventListSize - loopCounter;
            treeBuildTraceEventList.remove(removeIndex);
        }

        if (noEndTraceEventList.size() > 0) {

            List<TraceEventKey> noEndEventKeyList = getNoEndEventKeyList();

            for (TraceEvent traceEvent : noEndTraceEventList) {

                LOG.info("Could'nt find a matching end for trace event: " + traceEvent.toDebugString());

                noEndEventKeyList.add(traceEvent.getKey());

                treeBuildTraceEventList.remove(traceEvent);

            }
        }

        return startTraceEvent;
    }

    private TraceEventTreeNode addToTree(TraceEventTreeNode parentNode, TraceEvent currentTraceEvent) {

        Map<TraceEventKey, TraceEventTreeNode> traceEventTreeNodeMap = getTraceEventTreeNodeMap();

        TraceEventTreeNode startNode = parentNode;

        TraceEventTreeNode currentNode = new TraceEventTreeNode(currentTraceEvent);

        traceEventTreeNodeMap.put(currentTraceEvent.getKey(), currentNode);

        Boolean endEvent = currentTraceEvent.isEndEvent();

        if (endEvent != null) {

            // if end event then move 1 step higher parent
            if (endEvent) {

                // startNode is the begin node at this moment
                processTraceEventElapsed(startNode, currentNode);

                TraceEventTreeNode curParent = (TraceEventTreeNode) startNode.getParent();

                if (curParent != null) {
                    startNode = curParent;
                    startNode.add(currentNode);
                } else {
                    startNode.add(currentNode);
                }

            } else {
                startNode.add(currentNode);
                startNode = currentNode;
            }
        } else {

            startNode.add(currentNode);
            processTraceEventElapsed(currentNode, currentNode);
        }

        processEvent(currentTraceEvent);

        return startNode;
    }

    private TraceEventCombinedTreeNode addToTreeMerged(TraceEventCombinedTreeNode parentNode,
            TraceEvent currentTraceEvent) {

        Map<TraceEventKey, TraceEventCombinedTreeNode> traceEventCombinedTreeNodeMap = getTraceEventCombinedTreeNodeMap();

        TraceEventKey traceEventKey = currentTraceEvent.getKey();

        TraceEventCombinedTreeNode startNode = parentNode;

        TraceEventCombinedTreeNode currentNode = new TraceEventCombinedTreeNode(currentTraceEvent);

        Boolean endEvent = currentTraceEvent.isEndEvent();

        if (endEvent != null) {

            // if end event then move 1 step higher parent
            if (endEvent) {

                startNode.setEndEvent(currentTraceEvent);

                traceEventCombinedTreeNodeMap.put(traceEventKey, startNode);

                TraceEventCombinedTreeNode curParent = (TraceEventCombinedTreeNode) startNode.getParent();

                if (curParent != null) {
                    startNode = curParent;
                }
            } else {
                traceEventCombinedTreeNodeMap.put(traceEventKey, currentNode);

                startNode.add(currentNode);
                startNode = currentNode;
            }
        } else {
            traceEventCombinedTreeNodeMap.put(traceEventKey, currentNode);

            startNode.add(currentNode);
        }

        return startNode;
    }

    @Override
    public int getColumnCount() {
        TraceTableModelColumn[] traceTableModelColumnArray = getTraceTableModelColumnArray();
        return traceTableModelColumnArray.length;
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {

        List<TraceEventKey> traceEventIndexList = getFtmEntryKeyList();

        // supply records in reverse order??.
        int reverseRowIndex = traceEventIndexList.size() - rowIndex - 1;
        TraceEventKey traceEventKey = traceEventIndexList.get(reverseRowIndex);

        // supply in right order
        // TraceEventKey traceEventKey = traceEventIndexList.get(rowIndex);

        TraceEvent traceEvent = getEventForKey(traceEventKey);

        return traceEvent;
    }

    @Override
    public String getColumnName(int column) {

        TraceTableModelColumn[] traceTableModelColumnArray = getTraceTableModelColumnArray();

        return traceTableModelColumnArray[column].toString();
    }

    @Override
    protected int getModelColumnIndex(int column) {
        return column;
    }

    public TraceTableModelColumn getColumn(int column) {
        TraceTableModelColumn[] traceTableModelColumnArray = getTraceTableModelColumnArray();
        return traceTableModelColumnArray[column];
    }

    public Set<TraceEventType> getTraceEventTypeList() {
        return traceEventTypeCheckBoxMenuItemMap.keySet();
    }

    public CheckBoxMenuItemPopupEntry<TraceEventKey> getCheckBoxMenuItem(TraceEventType traceEventType) {
        return traceEventTypeCheckBoxMenuItemMap.get(traceEventType);
    }

    // clearing the columnFilterMap will skip the below loop
    private void updateColumnFilterMap(TraceEvent traceEvent,
            Map<FilterColumn, List<CheckBoxMenuItemPopupEntry<TraceEventKey>>> columnFilterMap) {

        if (traceEvent != null) {

            Iterator<FilterColumn> fcIterator = columnFilterMap.keySet().iterator();

            while (fcIterator.hasNext()) {

                FilterColumn filterColumn = fcIterator.next();

                List<CheckBoxMenuItemPopupEntry<TraceEventKey>> columnFilterEntryList;
                columnFilterEntryList = columnFilterMap.get(filterColumn);

                if (columnFilterEntryList == null) {
                    columnFilterEntryList = new ArrayList<CheckBoxMenuItemPopupEntry<TraceEventKey>>();
                    columnFilterMap.put(filterColumn, columnFilterEntryList);
                }

                int columnIndex = filterColumn.getIndex();

                TraceTableModelColumn traceTableModelColumn = getColumn(columnIndex);

                String traceEventKeyStr = traceEvent.getColumnValueForTraceTableModelColumn(traceTableModelColumn);

                if (traceEventKeyStr == null) {
                    traceEventKeyStr = FilterTableModel.NULL_STR;
                } else if ("".equals(traceEventKeyStr)) {
                    traceEventKeyStr = FilterTableModel.EMPTY_STR;
                }

                CheckBoxMenuItemPopupEntry<TraceEventKey> columnFilterEntry;

                CheckBoxMenuItemPopupEntry<TraceEventKey> searchKey;
                searchKey = new CheckBoxMenuItemPopupEntry<TraceEventKey>(traceEventKeyStr);

                int index = columnFilterEntryList.indexOf(searchKey);

                if (index == -1) {
                    columnFilterEntry = new CheckBoxMenuItemPopupEntry<TraceEventKey>(traceEventKeyStr);
                    columnFilterEntryList.add(columnFilterEntry);
                } else {
                    columnFilterEntry = columnFilterEntryList.get(index);
                }

                TraceEventKey traceEventKey = traceEvent.getKey();

                columnFilterEntry.addRowIndex(traceEventKey);

                boolean filterable = traceTableModelColumn.isFilterable();

                if ((filterable) && (columnFilterEntryList.size() > 1)) {
                    filterColumn.setColumnFilterEnabled(true);
                }

                // boolean columnFilterEnabled =
                // filterColumn.isColumnFilterEnabled();
                //
                // if ((!columnFilterEnabled) && (columnFilterEntryList.size() >
                // 1)) {
                // filterColumn.setColumnFilterEnabled(true);
                // }
            }
        }
    }

    // performing one by one search because of showing progress in the monitor
    // also when cancelling the task we should keep the old search results
    // hence not search result is stored unless the task is completed
    @Override
    public boolean search(TraceEventKey key, Object searchStrObj) {

        TraceEvent traceEvent = getEventForKey(key);

        boolean found = traceEvent.search(searchStrObj);

        return found;
    }

    @Override
    protected FilterTableModelNavigation<TraceEventKey> getNavigationRowIndex(List<TraceEventKey> resultList,
            int selectedRowIndex, boolean forward, boolean first, boolean last, boolean wrap) {

        int currSelectedRowIndex = selectedRowIndex;
        // tracer viewer search results are NOT reversed
        TraceEventKey navigationKey = null;
        int navigationIndex = 0;
        int navigationRowIndex = 0;

        if ((resultList != null) && (resultList.size() > 0)) {

            int resultListSize = resultList.size();

            List<TraceEventKey> traceEventKeyList = getFtmEntryKeyList();

            int traceEventKeyListSize = traceEventKeyList.size();

            if (first) {

                int lastIndex = resultListSize - 1;
                navigationKey = resultList.get(lastIndex);
                navigationIndex = 1;

            } else if (last) {

                navigationKey = resultList.get(0);
                navigationIndex = resultListSize;

            } else if (forward) {
                // NEXT
                if (currSelectedRowIndex >= 0) {

                    if (currSelectedRowIndex < (traceEventKeyListSize - 1)) {
                        currSelectedRowIndex++;
                    } else {
                        if (wrap) {
                            currSelectedRowIndex = 0;
                        }
                    }
                } else {
                    currSelectedRowIndex = 0;
                }

                TraceEventKey currSelectedTraceEventKey = traceEventKeyList
                        .get((traceEventKeyListSize - 1) - currSelectedRowIndex);

                int searchIndex = Collections.binarySearch(resultList, currSelectedTraceEventKey);

                if (searchIndex >= 0) {
                    // exact search found
                    navigationKey = resultList.get(searchIndex);
                } else {

                    searchIndex = (searchIndex * -1) - 1;

                    if ((searchIndex == resultListSize) || (searchIndex == 0)) {

                        searchIndex = resultListSize - 1;
                    } else {
                        searchIndex--;
                    }

                    navigationKey = resultList.get(searchIndex);
                }

                navigationIndex = resultList.indexOf(navigationKey);
                navigationIndex = (resultListSize - 1) - navigationIndex + 1;

            } else {
                // PREVIOUS
                if (currSelectedRowIndex >= 0) {

                    if (currSelectedRowIndex > 0) {
                        currSelectedRowIndex--;
                    } else {
                        if (wrap) {
                            currSelectedRowIndex = traceEventKeyListSize - 1;
                        }
                    }
                } else {
                    currSelectedRowIndex = 0;
                }

                TraceEventKey currSelectedTraceEventKey = traceEventKeyList
                        .get((traceEventKeyListSize - 1) - currSelectedRowIndex);

                int searchIndex = Collections.binarySearch(resultList, currSelectedTraceEventKey);

                if (searchIndex >= 0) {
                    // exact search found
                    navigationKey = resultList.get(searchIndex);
                } else {

                    searchIndex = (searchIndex * -1) - 1;

                    if (searchIndex == resultListSize) {
                        searchIndex = 0;
                    }

                    navigationKey = resultList.get(searchIndex);
                }

                navigationIndex = resultList.indexOf(navigationKey);
                navigationIndex = (resultListSize - 1) - navigationIndex + 1;
            }

            if (navigationKey != null) {

                navigationRowIndex = getIndexOfKey(navigationKey);
                // navigationRowIndex =
                // traceEventKeyList.indexOf(navigationKey);
                //
                // navigationRowIndex = (traceEventKeyListSize - 1)
                // - navigationRowIndex;

            } else {
                navigationRowIndex = currSelectedRowIndex;
            }

        }

        FilterTableModelNavigation<TraceEventKey> ttmn = new FilterTableModelNavigation<TraceEventKey>();
        ttmn.setNavigationIndex(navigationIndex);
        ttmn.setNavigationRowIndex(navigationRowIndex);
        ttmn.setNavigationKey(navigationKey);

        return ttmn;
    }

    @Override
    /**
     * this uses treepmap's comparator which is based on traceeventkey's id
     */
    public TraceEvent getEventForKey(TraceEventKey traceEventKey) {

        TraceEvent traceEvent = null;

        if (traceEventKey != null) {
            Map<TraceEventKey, TraceEvent> traceEventMap = getTraceEventMap();
            traceEvent = traceEventMap.get(traceEventKey);
        }

        return traceEvent;
    }

    /**
     * alternate implementation to getEventForKey to use traceeventkey's
     * traceeventindex. used for reports where entries can change because of compare
     * view.
     */
    public TraceEvent getTraceEventForKey(TraceEventKey traceEventKey) {

        TraceEvent traceEvent = null;

        if (traceEventKey != null) {

            int traceEventIndex = traceEventKey.getTraceEventIndex();

            if (traceEventIndex != -1) {

                Map<TraceEventKey, TraceEvent> traceEventMap = getTraceEventMap();
                Set<Map.Entry<TraceEventKey, TraceEvent>> traceEventEntrySet = traceEventMap.entrySet();

                for (Map.Entry<TraceEventKey, TraceEvent> entry : traceEventEntrySet) {

                    TraceEventKey key = entry.getKey();
                    TraceEvent te = entry.getValue();

                    if (traceEventIndex == key.getTraceEventIndex()) {
                        traceEvent = te;
                        break;
                    }
                }
            } else {
                traceEvent = getEventForKey(traceEventKey);
            }

        }

        return traceEvent;
    }

    public void updateRecentFile(String charset) {

        RecentFile recentFile = getRecentFile();

        if (charset != null) {
            recentFile.setAttribute(RecentFile.KEY_CHARSET, charset);
            // change in character set will trigger reloading of file.
        }

        fireTableDataChanged();

    }

    @Override
    public void clearSearchResults(boolean clearResults) {

        getSearchModel().resetResults(clearResults);

        clearTraceEventSearchResults();
    }

    protected void clearTraceEventSearchResults() {
        List<TraceEventKey> filteredList = getFtmEntryKeyList();

        Iterator<TraceEventKey> listIterator = filteredList.iterator();

        while (listIterator.hasNext()) {

            TraceEventKey traceEventKey = listIterator.next();

            TraceEvent traceEvent = getEventForKey(traceEventKey);
            traceEvent.setSearchFound(false);
        }

        clearAbstractTraceEventTreeNode(rootTraceEventTreeNode);

        clearAbstractTraceEventTreeNode(rootTraceEventCombinedTreeNode);
    }

    private void clearAbstractTraceEventTreeNode(AbstractTraceEventTreeNode abstractTraceEventTreeNode) {

        for (Enumeration<?> e = abstractTraceEventTreeNode.children(); e.hasMoreElements();) {

            AbstractTraceEventTreeNode childNode = (AbstractTraceEventTreeNode) e.nextElement();

            clearAbstractTraceEventTreeNode(childNode);
        }

        abstractTraceEventTreeNode.setSearchFound(false);
    }

    @Override
    public int getIndexOfKey(TraceEventKey traceEventKey) {

        List<TraceEventKey> traceEventIndexList = getFtmEntryKeyList();

        int size = traceEventIndexList.size();

        int reverseIndex = -1;

        if (traceEventIndexList != null) {

            int index = traceEventIndexList.indexOf(traceEventKey);

            if (index != -1) {
                reverseIndex = size - index - 1;
            }
        }

        return reverseIndex;
    }

    @Override
    public TraceEventTreeNode getTreeNodeForKey(TraceEventKey key) {

        TraceEventTreeNode traceEventTreeNode = null;

        Map<TraceEventKey, TraceEventTreeNode> traceEventTreeNodeMap = getTraceEventTreeNodeMap();

        traceEventTreeNode = traceEventTreeNodeMap.get(key);

        return traceEventTreeNode;
    }

    public TraceEventCombinedTreeNode getTraceEventCombinedTreeNodeForKey(TraceEventKey key) {

        TraceEventCombinedTreeNode traceEventCombinedTreeNode = null;

        Map<TraceEventKey, TraceEventCombinedTreeNode> traceEventCombinedTreeNodeMap = getTraceEventCombinedTreeNodeMap();

        traceEventCombinedTreeNode = traceEventCombinedTreeNodeMap.get(key);

        return traceEventCombinedTreeNode;
    }

    private List<TraceEventKey> getFailedEventKeyList() {

        if (failedEventKeyList == null) {
            failedEventKeyList = new ArrayList<TraceEventKey>();
        }

        return failedEventKeyList;
    }

    private List<TraceEventKey> getExceptionEventKeyList() {

        if (exceptionEventKeyList == null) {
            exceptionEventKeyList = new ArrayList<TraceEventKey>();
        }

        return exceptionEventKeyList;
    }

    private List<TraceEventKey> getAlertEventKeyList() {

        if (alertEventKeyList == null) {
            alertEventKeyList = new ArrayList<TraceEventKey>();
        }

        return alertEventKeyList;
    }

    private List<TraceEventKey> getNoStartEventKeyList() {

        if (noStartEventKeyList == null) {
            noStartEventKeyList = new ArrayList<>();
        }

        return noStartEventKeyList;
    }

    private List<TraceEventKey> getNoEndEventKeyList() {

        if (noEndEventKeyList == null) {
            noEndEventKeyList = new ArrayList<>();
        }

        return noEndEventKeyList;
    }

    private TreeMap<Double, List<TraceEventKey>> getOwnElapsedEventKeyMap() {

        if (ownElapsedEventKeyMap == null) {
            ownElapsedEventKeyMap = new TreeMap<Double, List<TraceEventKey>>();
        }

        return ownElapsedEventKeyMap;
    }

    private TreeMap<TraceEventRuleset, TreeSet<TraceEventRule>> getRulesInvokedMap() {

        if (rulesInvokedMap == null) {
            rulesInvokedMap = new TreeMap<TraceEventRuleset, TreeSet<TraceEventRule>>();
        }

        return rulesInvokedMap;
    }

    private void processTraceEventElapsed(TraceEventTreeNode beginNode, TraceEventTreeNode endNode) {

        TraceEvent endTE = (TraceEvent) endNode.getUserObject();

        if (endTE != null) {

            double mainElapsed = endTE.getElapsed();

            if (mainElapsed > 0) {

                double childrenElapsed = 0;

                for (Enumeration<?> e = beginNode.children(); e.hasMoreElements();) {

                    TraceEventTreeNode childNode = (TraceEventTreeNode) e.nextElement();

                    TraceEvent childTE = (TraceEvent) childNode.getUserObject();

                    double childElapsed = childTE.getElapsed();

                    if (childElapsed >= 0) {
                        childrenElapsed += childElapsed;
                    }
                }

                double ownElapsed = mainElapsed - childrenElapsed;

                endTE.setChildrenElapsed((beginNode.getChildCount() > 0) ? childrenElapsed : -1);
                endTE.setOwnElapsed(ownElapsed);

                // add ownElapsed to map for reporting
                TreeMap<Double, List<TraceEventKey>> ownElapsedEventKeyMap = getOwnElapsedEventKeyMap();

                List<TraceEventKey> traceEventKeyList = ownElapsedEventKeyMap.get(ownElapsed);

                if (traceEventKeyList == null) {
                    traceEventKeyList = new ArrayList<TraceEventKey>();
                    ownElapsedEventKeyMap.put(ownElapsed, traceEventKeyList);
                }

                traceEventKeyList.add(endTE.getKey());

            }
        }
    }

    private void processEvent(TraceEvent traceEvent) {

        TraceEventKey traceEventKey = traceEvent.getKey();

        if (traceEventKey.getTraceEventIndex() != -1) {

            List<TraceEventKey> failedEventKeyList = getFailedEventKeyList();
            List<TraceEventKey> exceptionEventKeyList = getExceptionEventKeyList();
            List<TraceEventKey> alertEventKeyList = getAlertEventKeyList();
            Map<TraceEventRuleset, TreeSet<TraceEventRule>> rulesInvokedMap = getRulesInvokedMap();

            // add failed events
            boolean stepStatusFail = traceEvent.isStepStatusFail();

            if (stepStatusFail) {
                failedEventKeyList.add(traceEventKey);
            }

            // add Exception events
            boolean stepStatusException = traceEvent.isStepStatusException();

            if (stepStatusException) {
                exceptionEventKeyList.add(traceEventKey);
            }

            boolean isAlertEvent = (traceEvent instanceof TraceEventAlert);

            if (isAlertEvent) {
                alertEventKeyList.add(traceEventKey);
            }

            String rulesetVersion = traceEvent.getRuleSet();

            if (rulesetVersion == null) {
                rulesetVersion = "<NULL>";
            }

            TraceEventRuleset traceEventRuleset = new TraceEventRuleset(rulesetVersion);

            TreeSet<TraceEventRule> traceEventRules = rulesInvokedMap.get(traceEventRuleset);

            if (traceEventRules == null) {
                traceEventRules = new TreeSet<TraceEventRule>();
                rulesInvokedMap.put(traceEventRuleset, traceEventRules);
            }

            String insKey = traceEvent.getInsKey();

            if ((insKey != null) && (!"".equals(insKey))) {

                TraceEventType traceEventType = traceEvent.getTraceEventType();

                Color background = traceEvent.getColumnBackground(0);

                TraceEventRule traceEventRule = new TraceEventRule(insKey, traceEventType, background);

                boolean success = traceEventRules.add(traceEventRule);

                Boolean isEndEvent = traceEvent.isEndEvent();

                // this is called after processTraceEventElapsed hence timing
                // info should be available
                if ((!success) && (isEndEvent != null) && (isEndEvent)) {

                    for (TraceEventRule ter : traceEventRules) {

                        if (ter.equals(traceEventRule)) {

                            // only increment if the entry is of same type.
                            // in case of activities, the child 'steps' and
                            // 'whens' have the activity's insKey set.
                            if (ter.getTraceEventType().equals(traceEventRule.getTraceEventType())) {
                                ter.incrementExecutionCount();
                            }

                            ter.processElapsed(traceEvent.getOwnElapsed());
                        }
                    }
                } else if (isEndEvent == null) {
                    traceEventRule.incrementExecutionCount();
                    traceEventRule.processElapsed(traceEvent.getOwnElapsed());
                }
            }
        }
    }

    public TraceEventTreeNode getRootTraceEventTreeNode() {

        if (rootTraceEventTreeNode == null) {
            rootTraceEventTreeNode = new TraceEventTreeNode(null);
        }

        return rootTraceEventTreeNode;
    }

    public TraceEventCombinedTreeNode getRootTraceEventCombinedTreeNode() {

        if (rootTraceEventCombinedTreeNode == null) {
            rootTraceEventCombinedTreeNode = new TraceEventCombinedTreeNode(null);
        }

        return rootTraceEventCombinedTreeNode;
    }

    private LinkedList<TraceEvent> getTreeBuildTraceEventList() {

        if (treeBuildTraceEventList == null) {
            treeBuildTraceEventList = new LinkedList<>();
        }

        return treeBuildTraceEventList;
    }

    @Override
    public SearchModel<TraceEventKey> getSearchModel() {

        if (searchModel == null) {

            searchModel = new SearchModel<TraceEventKey>(searchData) {

                @Override
                public void searchInEvents(final Object searchStrObj, final ModalProgressMonitor progressMonitor) {

                    if ((searchStrObj != null) && (!((searchStrObj instanceof SearchEventType)
                            && searchStrObj.equals(SearchEventType.SEPERATOR))
                            || !("".equals(searchStrObj.toString())))) {

                        TraceTableSearchTask ttst = new TraceTableSearchTask(progressMonitor, TraceTableModel.this,
                                searchStrObj) {

                            /*
                             * (non-Javadoc)
                             * 
                             * @see javax.swing.SwingWorker#done()
                             */
                            @Override
                            protected void done() {

                                try {
                                    List<TraceEventKey> searchResultList = get();

                                    if (searchResultList != null) {
                                        // LOG.info("TraceTableSearchTask
                                        // done "
                                        // + searchResultList.size()
                                        // + " entries found");
                                        setSearchResultList(searchStrObj, searchResultList);

                                    }
                                } catch (CancellationException ce) {
                                    LOG.info("TraceTableSearchTask cancelled: ");

                                } catch (Exception e) {
                                    LOG.error("Exception in TraceTableSearchTask", e);

                                } finally {

                                    // general fire will reload the tree,
                                    // collapsing the whole tree.
                                    // hence generating a special to identify
                                    // search action. used in
                                    // TraceTreeTableModelAdapter
                                    // fireTableDataChanged();
                                    fireTableChanged(new SearchTableModelEvent(TraceTableModel.this));

                                    progressMonitor.close();
                                }
                            }
                        };

                        ttst.execute();

                    }
                }

                @Override
                public void resetResults(boolean clearResults) {

                    // clears search result on search model and reset the search
                    // panel
                    resetSearchResults(clearResults);

                    // clear search results from within trace events and tree
                    // nodes
                    clearTraceEventSearchResults();

                    // general fire will reload the tree,
                    // collapsing the whole tree.
                    // hence generating a special to identify
                    // search action. used in
                    // TraceTreeTableModelAdapter
                    // fireTableDataChanged();
                    fireTableChanged(new SearchTableModelEvent(TraceTableModel.this));
                }
            };
        }

        return searchModel;
    }

    public boolean isIncompletedTracerXML() {

        List<TraceEventKey> reportNoEndEventKeyList = getReportNoEndEventKeyList();

        int reportNoEndEventKeyListsize = reportNoEndEventKeyList.size();

        boolean incompleteTracerXML = reportNoEndEventKeyListsize > 0;

        if (incompleteTracerXML) {

            StringBuffer sb = new StringBuffer();

            for (TraceEventKey traceEventKey : reportNoEndEventKeyList) {

                if (sb.length() > 0) {
                    sb.append(", ");
                }

                sb.append(traceEventKey.toString());
            }

            String modelName = getModelName();

            LOG.info("Incomplete tracer xml: " + modelName + " - " + reportNoEndEventKeyListsize + " events - "
                    + sb.toString());
        }

        return incompleteTracerXML;
    }

    @Override
    protected TableColumnModel getTableColumnModel() {

        TableColumnModel tableColumnModel = new DefaultTableColumnModel();

        for (int i = 0; i < getColumnCount(); i++) {

            TableColumn tableColumn = new TableColumn(i);

            String text = getColumnName(i);

            tableColumn.setHeaderValue(text);

            TraceTableModelColumn ttmc = getColumn(i);

            TraceTableCellRenderer ttcr = new TraceTableCellRenderer();
            ttcr.setBorder(new EmptyBorder(1, 3, 1, 1));
            ttcr.setHorizontalAlignment(ttmc.getHorizontalAlignment());

            tableColumn.setCellRenderer(ttcr);

            int colWidth = ttmc.getPrefColumnWidth();
            tableColumn.setPreferredWidth(colWidth);
            // tableColumn.setMinWidth(colWidth);
            tableColumn.setWidth(colWidth);
            tableColumn.setResizable(true);

            tableColumnModel.addColumn(tableColumn);
        }

        return tableColumnModel;
    }

    public List<TraceEventKey> getReportFailedEventKeyList() {
        return Collections.unmodifiableList(getFailedEventKeyList());
    }

    public List<TraceEventKey> getReportExceptionEventKeyList() {
        return Collections.unmodifiableList(getExceptionEventKeyList());
    }

    public List<TraceEventKey> getReportAlertEventKeyList() {
        return Collections.unmodifiableList(getAlertEventKeyList());
    }

    public List<TraceEventKey> getReportNoStartEventKeyList() {
        return Collections.unmodifiableList(getNoStartEventKeyList());
    }

    public List<TraceEventKey> getReportNoEndEventKeyList() {

        List<TraceEventKey> reportNoEndEventKeyList = new ArrayList<TraceEventKey>();

        List<TraceEventKey> noEndEventKeyList = getNoEndEventKeyList();
        List<TraceEvent> treeBuildTraceEventList = getTreeBuildTraceEventList();

        for (TraceEventKey traceEventKey : noEndEventKeyList) {
            reportNoEndEventKeyList.add(traceEventKey);
        }

        for (TraceEvent traceEvent : treeBuildTraceEventList) {
            reportNoEndEventKeyList.add(traceEvent.getKey());
        }

        Collections.sort(reportNoEndEventKeyList);

        return Collections.unmodifiableList(reportNoEndEventKeyList);
    }

    public List<TraceEventKey> getReportOwnElapsedEventKeyList() {

        List<TraceEventKey> reportElapsedTimeEventList = new ArrayList<TraceEventKey>();

        TreeMap<Double, List<TraceEventKey>> ownElapsedEventKeyMap = getOwnElapsedEventKeyMap();

        // select last 50 items
        int mapSize = ownElapsedEventKeyMap.size();
        int size = (mapSize > 50) ? 50 : mapSize;

        Iterator<Double> it = ownElapsedEventKeyMap.navigableKeySet().descendingIterator();

        while (it.hasNext() && size > 0) {

            Double key = it.next();
            List<TraceEventKey> traceEventKeyList = ownElapsedEventKeyMap.get(key);

            for (TraceEventKey traceEventKey : traceEventKeyList) {
                reportElapsedTimeEventList.add(traceEventKey);
            }

            size--;
        }

        return Collections.unmodifiableList(reportElapsedTimeEventList);

    }

    public Map<TraceEventRuleset, TreeSet<TraceEventRule>> getReportRulesInvokedMap() {
        return Collections.unmodifiableMap(getRulesInvokedMap());
    }
}