package de.tud.kom.p2psim.impl.common;

import de.tud.kom.p2psim.impl.topology.views.visualization.ui.NodeDebugVisualisation;
import de.tudarmstadt.maki.simonstrator.api.common.graph.INodeID;
import de.tudarmstadt.maki.simonstrator.api.component.core.NodeDebugMonitorComponent;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

/**
 * Default node debug monitor. Provides logging capabilities and data access as well
 * as the option to define filters on which information should be logged.
 * Per node information can be shown by simply clicking on the desired node.
 * Filters can be used as blacklist or as whitelist and
 * are specified via the xml config. <BR><BR>
 *
 * How to define Filters:<BR>
 * The standard XML configuration looks like this:<BR><BR>
 * &lt;DebugMonitor class="de.tud.kom.p2psim.impl.common.DefaultNodeDebugMonitor" static="getInstance" enableVis="$ENABLE_DEBUG_VIS"&gt;<BR>
 *      &#09;&lt;PackageFilter class="de.tud.kom.p2psim.impl.common.PackageFilter"<BR>
 *      &#09;&#09;type="EXCLUDE"<BR>
 *      &#09;&#09;packageName="de.tudarmstadt.maki.simonstrator.peerfact.application.resAlloc.user"/&gt<BR>
 * &lt;/DebugMonitor&gt;<BR><BR>
 *
 * Any package or class can be specified as filter and all
 * subpackages and classes will be filtered accordingly. For types you can use 'EXCLUDE' or 'INCLUDE'
 * depending on if you want to use whitelisting or blacklisting. Multiple
 * &lt;PackageFilter&gt; tags can be defined, however you can't use INCLUDE and EXCLUDE simultaneously.
 */
public class DefaultNodeDebugMonitor implements NodeDebugMonitorComponent
{
    private static DefaultNodeDebugMonitor instance;

    private HashMap<INodeID, HashMap<Class, HashMap<String, Object>>> data = new HashMap<>();
    private LinkedList<NodeDebugUpdateListener> listeners = new LinkedList<>();

    //Marker to prevent creation of two visualisation components
    private boolean visAlreadyEnabled = false;

    //Marker determining if blacklisting or whitelisting is enabled
    private boolean filterInitialised = false;
    private boolean whitelist = false;
    private LinkedList<PackageFilter> filteredPackages = new LinkedList<>();
    private HashMap<Class, Boolean> filteredClasses = new HashMap<>();

    public static DefaultNodeDebugMonitor getInstance()
    {
        if(instance == null)
        {
            instance = new DefaultNodeDebugMonitor();
        }

        return instance;
    }

    @Override
    public void update(Class<?> subject, INodeID nodeID, String entry, Object value)
    {
        if(filteredClasses.containsKey(subject))
        {
            //If Whitelisting and class not contained in filter
            if(whitelist && !filteredClasses.get(subject)) return;
            //If blacklisting and class contained in filter.
            if(!whitelist && filteredClasses.get(subject)) return;
        }

        HashMap<Class, HashMap<String, Object>> nodeData;
        if(data.containsKey(nodeID)) nodeData = data.get(nodeID);
        else {
            nodeData = new HashMap<>();
            data.put(nodeID, nodeData);
        }

        HashMap<String, Object> nodeClassData;
        if(nodeData.containsKey(subject)) nodeClassData = nodeData.get(subject);
        else
            {
            //First update for this class. Check if the class needs to be filtered.
            boolean classFiltered = isFiltered(subject);
            filteredClasses.put(subject, classFiltered);
            //If the class is excluded, don't add the information and return.
            if((whitelist && !classFiltered) || (!whitelist && classFiltered) ) return;

            nodeClassData = new HashMap<>();
            nodeData.put(subject, nodeClassData);
        }

        nodeClassData.put(entry, value);
        listeners.forEach(l -> l.onNodeDebugUpdate(subject, nodeID, entry, value));
    }

    @Override
    public Map<Class, HashMap<String, Object>> getNodeData(INodeID nodeID) {
        if(data.containsKey(nodeID)) return data.get(nodeID);
        else return new HashMap<>();
    }

    @Override
    public void addUpdateListener(NodeDebugUpdateListener listener) {
        listeners.add(listener);
    }

    @Override
    public void removeUpdateListener(NodeDebugUpdateListener listener) {
        listeners.remove(listener);
    }

    @Override
    public void setEnableVis(boolean enabled) {
        if(enabled && !visAlreadyEnabled) new NodeDebugVisualisation();
    }


    /**
     * Adds a filter to the debug monitor. Should only be used via XML config and
     * is not meant to be only called while simulation setup.
     * @param filter the filter to add
     */
    public void setPackageFilter(PackageFilter filter)
    {
        //First entry sets whitelisting/blacklisting
        if(!filterInitialised)
        {
            filterInitialised = true;
            whitelist = filter.isWhitelist();
        }

        // If the added type is not conform with the type of listing, an error is thrown.
        if(filter.isWhitelist() != whitelist) throw new AssertionError("Can't use Includes/Excludes simultaneously!");

        //Don't save filters for packages where a parent package is already filtered.
        for(PackageFilter pf : filteredPackages)
        {
            if(filter.subPackageOf(pf)) return;
            if(pf.subPackageOf(filter))
            {
                filteredPackages.remove(pf);
                filteredPackages.add(filter);
                return;
            }
        }

        filteredPackages.add(filter);
    }

    /**
     * Check if a class is contained in the filter.
     * @param c the class to check
     * @return true if contained, false otherwise.
     */
    private boolean isFiltered(Class c)
    {
        for(PackageFilter pf : filteredPackages)
        {
            if(pf.containsClass(c)) return true;
        }
        return false;
    }

}
