/*******************************************************************************
 * Copyright (c) 2015 École Polytechnique de Montréal
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License 2.0 which
 * accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/

package org.eclipse.tracecompass.internal.lttng2.kernel.core.analysis.graph.handlers;

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

import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.analysis.graph.core.base.TmfEdge;
import org.eclipse.tracecompass.analysis.graph.core.base.TmfEdge.EdgeType;
import org.eclipse.tracecompass.analysis.graph.core.base.TmfGraph;
import org.eclipse.tracecompass.analysis.graph.core.base.TmfVertex;
import org.eclipse.tracecompass.analysis.graph.core.base.TmfVertex.EdgeDirection;
import org.eclipse.tracecompass.analysis.os.linux.core.execution.graph.OsExecutionGraphProvider;
import org.eclipse.tracecompass.analysis.os.linux.core.execution.graph.OsExecutionGraphProvider.Context;
import org.eclipse.tracecompass.analysis.os.linux.core.execution.graph.OsInterruptContext;
import org.eclipse.tracecompass.analysis.os.linux.core.execution.graph.OsSystemModel;
import org.eclipse.tracecompass.analysis.os.linux.core.execution.graph.OsWorker;
import org.eclipse.tracecompass.analysis.os.linux.core.kernel.LinuxValues;
import org.eclipse.tracecompass.analysis.os.linux.core.model.HostThread;
import org.eclipse.tracecompass.analysis.os.linux.core.model.ProcessStatus;
import org.eclipse.tracecompass.analysis.os.linux.core.trace.IKernelAnalysisEventLayout;
import org.eclipse.tracecompass.common.core.NonNullUtils;
import org.eclipse.tracecompass.internal.lttng2.kernel.core.TcpEventStrings;
import org.eclipse.tracecompass.tmf.core.event.ITmfEvent;
import org.eclipse.tracecompass.tmf.core.event.ITmfEventField;
import org.eclipse.tracecompass.tmf.core.event.aspect.TmfCpuAspect;
import org.eclipse.tracecompass.tmf.core.event.matching.IMatchProcessingUnit;
import org.eclipse.tracecompass.tmf.core.event.matching.TmfEventDependency;
import org.eclipse.tracecompass.tmf.core.event.matching.TmfEventDependency.DependencyEvent;
import org.eclipse.tracecompass.tmf.core.event.matching.TmfEventMatching;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.core.trace.TmfTraceUtils;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;

/**
 * Event handler that actually builds the execution graph from the events
 *
 * @author Francis Giraldeau
 * @author Geneviève Bastien
 */
public class TraceEventHandlerExecutionGraph extends BaseHandler {

    /*
     * The following IRQ constants was found empirically.
     *
     * TODO: other IRQ values should be determined from the lttng_statedump_interrupt events.
     */
    private static final int IRQ_TIMER = 0;

    private static final NullProgressMonitor DEFAULT_PROGRESS_MONITOR = new NullProgressMonitor();

    private final Table<String, Integer, OsWorker> fKernel;
    private final IMatchProcessingUnit fMatchProcessing;
    private Map<DependencyEvent, TmfVertex> fTcpNodes;
    private TmfEventMatching fTcpMatching;

    /**
     * Constructor
     *
     * @param provider
     *            The parent graph provider
     * @param priority
     *            The priority of this handler. It will determine when it will be
     *            executed
     */
    public TraceEventHandlerExecutionGraph(OsExecutionGraphProvider provider, int priority) {
        super(provider, priority);
        fKernel = HashBasedTable.create();

        fTcpNodes = new HashMap<>();
        fMatchProcessing = new IMatchProcessingUnit() {

            @Override
            public void matchingEnded() {
                // Do nothing
            }

            @Override
            public int countMatches() {
                return 0;
            }

            @Override
            public void addMatch(@Nullable TmfEventDependency match) {
                if (match == null) {
                    return;
                }
                TmfVertex output = fTcpNodes.remove(match.getSource());
                TmfVertex input = fTcpNodes.remove(match.getDestination());
                if (output != null && input != null) {
                    output.linkVertical(input).setType(EdgeType.NETWORK);
                }
            }

            @Override
            public void init(Collection<ITmfTrace> fTraces) {
                // Do nothing
            }

        };

        ITmfTrace trace = provider.getTrace();
        fTcpMatching = new TmfEventMatching(Collections.singleton(trace), fMatchProcessing);
        fTcpMatching.initMatching();
    }

    private OsWorker getOrCreateKernelWorker(ITmfEvent event, Integer cpu) {
        String host = event.getTrace().getHostId();
        OsWorker worker = fKernel.get(host, cpu);
        if (worker == null) {
            HostThread ht = new HostThread(host, -1);
            worker = new OsWorker(ht, "kernel/" + cpu, event.getTimestamp().getValue()); //$NON-NLS-1$
            worker.setStatus(ProcessStatus.RUN);

            fKernel.put(host, cpu, worker);
        }
        return worker;
    }

    @Override
    public void handleEvent(ITmfEvent ev) {
        String eventName = ev.getName();
        IKernelAnalysisEventLayout eventLayout = getProvider().getEventLayout(ev.getTrace());

        if (eventName.equals(eventLayout.eventSchedSwitch())) {
            handleSchedSwitch(ev);
        } else if (eventName.equals(eventLayout.eventSoftIrqEntry())) {
            handleSoftirqEntry(ev);
        } else if (eventLayout.eventsNetworkReceive().contains(eventName) ||
                eventName.equals(TcpEventStrings.INET_SOCK_LOCAL_IN)) {
            handleInetSockLocalIn(ev);
        } else if (eventLayout.eventsNetworkSend().contains(eventName) ||
                eventName.equals(TcpEventStrings.INET_SOCK_LOCAL_OUT)) {
            handleInetSockLocalOut(ev);
        } else if (isWakeupEvent(ev)) {
            handleSchedWakeup(ev);
        }
    }

    private TmfVertex stateExtend(OsWorker task, long ts) {
        TmfGraph graph = NonNullUtils.checkNotNull(getProvider().getAssignedGraph());
        TmfVertex node = new TmfVertex(ts);
        ProcessStatus status = task.getStatus();
        graph.append(task, node, resolveProcessStatus(status));
        return node;
    }

    private TmfVertex stateChange(OsWorker task, long ts) {
        TmfGraph graph = NonNullUtils.checkNotNull(getProvider().getAssignedGraph());
        TmfVertex node = new TmfVertex(ts);
        ProcessStatus status = task.getOldStatus();
        graph.append(task, node, resolveProcessStatus(status));
        return node;
    }

    private static EdgeType resolveProcessStatus(ProcessStatus status) {
        EdgeType ret = EdgeType.UNKNOWN;
        switch (status) {
        case NOT_ALIVE:
            break;
        case EXIT:
        case RUN:
        case RUN_SYTEMCALL:
        case INTERRUPTED:
            ret = EdgeType.RUNNING;
            break;
        case UNKNOWN:
            ret = EdgeType.UNKNOWN;
            break;
        case WAIT_BLOCKED:
            ret = EdgeType.BLOCKED;
            break;
        case WAIT_CPU:
        case WAIT_FORK:
        case WAIT_UNKNOWN:
            ret = EdgeType.PREEMPTED;
            break;
        case ZOMBIE:
            ret = EdgeType.UNKNOWN;
            break;
        default:
            break;
        }
        return ret;
    }

    private void handleSchedSwitch(ITmfEvent event) {
        String host = event.getTrace().getHostId();
        long ts = event.getTimestamp().getValue();
        IKernelAnalysisEventLayout eventLayout = getProvider().getEventLayout(event.getTrace());
        OsSystemModel system = getProvider().getSystem();
        ITmfEventField content = event.getContent();

        Integer next = content.getFieldValue(Integer.class, eventLayout.fieldNextTid());
        Integer prev =content.getFieldValue(Integer.class, eventLayout.fieldPrevTid());

        if (next == null || prev == null) {
            return;
        }

        OsWorker nextTask = system.findWorker(new HostThread(host, next));
        OsWorker prevTask = system.findWorker(new HostThread(host, prev));

        if (prevTask == null || nextTask == null) {
            return;
        }
        stateChange(prevTask, ts);
        stateChange(nextTask, ts);
    }

    private void handleSchedWakeup(ITmfEvent event) {
        TmfGraph graph = NonNullUtils.checkNotNull(getProvider().getAssignedGraph());
        String host = event.getTrace().getHostId();
        Integer cpu = NonNullUtils.checkNotNull(TmfTraceUtils.resolveIntEventAspectOfClassForEvent(event.getTrace(), TmfCpuAspect.class, event));
        IKernelAnalysisEventLayout eventLayout = getProvider().getEventLayout(event.getTrace());
        OsSystemModel system = getProvider().getSystem();

        long ts = event.getTimestamp().getValue();
        Integer tid = event.getContent().getFieldValue(Integer.class, eventLayout.fieldTid());
        if (tid == null) {
            return;
        }

        OsWorker target = system.findWorker(new HostThread(host, tid));
        OsWorker current = system.getWorkerOnCpu(host, cpu);
        if (target == null) {
            return;
        }

        ProcessStatus status = target.getOldStatus();
        switch (status) {
        case WAIT_FORK:
            waitFork(graph, ts, target, current);
            break;
        case WAIT_BLOCKED:
            waitBlocked(event, graph, host, cpu, eventLayout, system, ts, target, current);
            break;
        case NOT_ALIVE:
        case EXIT:
        case RUN:
        case UNKNOWN:
        case WAIT_CPU:
        case ZOMBIE:
        case INTERRUPTED:
        case RUN_SYTEMCALL:
        case WAIT_UNKNOWN:
        default:
            break;
        }
    }

    private void waitBlocked(ITmfEvent event, TmfGraph graph, String host, Integer cpu, IKernelAnalysisEventLayout eventLayout, OsSystemModel system, long ts, OsWorker target, @Nullable OsWorker current) {
        OsInterruptContext context = system.peekContextStack(host, cpu);
        switch (context.getContext()) {
        case HRTIMER:
            // shortcut of appendTaskNode: resolve blocking source in situ
            graph.append(target, new TmfVertex(ts), EdgeType.TIMER);
            break;
        case IRQ:
        case COMPLETE_IRQ:
            irq(graph, eventLayout, ts, target, context);
            break;
        case SOFTIRQ:
            softIrq(event, graph, cpu, eventLayout, ts, target, context);
            break;
        case IPI:
            graph.append(target, new TmfVertex(ts), EdgeType.IPI);
            break;
        case NONE:
            none(ts, target, current);
            break;
        case PACKET_RECEPTION:
            receivingFromNetwork(event, host, cpu, system, graph, ts, target, current);
            break;
        default:
            break;
        }
    }

    private void receivingFromNetwork(ITmfEvent event, String host, Integer cpu, OsSystemModel system, TmfGraph graph, long ts, OsWorker target, @Nullable OsWorker current) {
        // Look at the inner context to see if the current worker is the
        // receptor or if we are in irq context
        Context innerCtx = peekInnerContext(host, cpu, system);
        OsWorker source = current;
        if (innerCtx == Context.SOFTIRQ || innerCtx == Context.IRQ) {
            source = getOrCreateKernelWorker(event, cpu);
        }

        // Append a vertex to the target and set the edge type to network
        TmfVertex wupTarget = new TmfVertex(ts);
        TmfEdge netLink = graph.append(target, wupTarget);
        if (netLink != null) {
            if (source != null && innerCtx != Context.SOFTIRQ && innerCtx != Context.IRQ) {
                netLink.setType(EdgeType.NETWORK, source.getName());
            } else {
                netLink.setType(EdgeType.NETWORK);
            }
        }

        if (source == null) {
            return;
        }
        // See if we can directly link from the packet reception.
        TmfVertex tail = graph.getTail(source);
        if (tail != null) {
            replaceIncomingNetworkEdge(tail, wupTarget);
        }
    }

    /**
     * If the last tail vertex has an incoming vertical edge of network type, it
     * would be the packet reception that caused the wakeup. So here, we have
     * that incoming edge point to the target instead of passing through the
     * current worker and being woken up later. Because we know from context
     * that it's the packet who caused the wakeup and not the current process
     *
     * @param tail
     *            The last vertex of the wakeup source
     * @param wupTarget
     *            The wakeup target
     * @return <code>true</code> if the edge was replaced, <code>false</code>
     *         otherwise. That last response would indicate the source of the
     *         packet couldn't be identified, so proper wakeup link should be
     *         done
     */
    private static boolean replaceIncomingNetworkEdge(TmfVertex tail, TmfVertex wupTarget) {
        TmfEdge edge = tail.getEdge(EdgeDirection.INCOMING_VERTICAL_EDGE);

        if (edge == null || edge.getType() != EdgeType.NETWORK) {
            return false;
        }
        TmfEdge newLink = edge.getVertexFrom().linkVertical(wupTarget);
        newLink.setType(EdgeType.NETWORK);
        tail.removeEdge(EdgeDirection.INCOMING_VERTICAL_EDGE);
        return true;
    }

    /* Extend the source worker to ts, and add a vertical link to the target vertex */
    private void extendAndLink(OsWorker worker, long ts, TmfVertex targetVertex) {
        // Extend the source worker to the timestamp
        TmfVertex wupSource = stateExtend(worker, ts);
        // Add a vertical link to target
        wupSource.linkVertical(targetVertex);
    }

    private void softIrq(ITmfEvent event, TmfGraph graph, Integer cpu, IKernelAnalysisEventLayout eventLayout, long ts, OsWorker target, OsInterruptContext context) {
        TmfVertex wupTarget = new TmfVertex(ts);
        TmfEdge hLink = graph.append(target, wupTarget);
        ITmfEventField content = context.getEvent().getContent();
        Long vec = content.getFieldValue(Long.class, eventLayout.fieldVec());
        if (hLink != null) {
            // Try to resolve the type of the target edge to the softirq's source
            hLink.setType(resolveSoftirq(vec));
        }
        /*
         * special case for network related softirq. This code supports network
         * drivers that receive packets within softirq. The newer
         * Packet_Reception context replaces this if the right events are
         * enabled.
         */
        if (vec != null && (vec == LinuxValues.SOFTIRQ_NET_RX || vec == LinuxValues.SOFTIRQ_NET_TX)) {
            // create edge if wake up is caused by incoming packet
            OsWorker k = getOrCreateKernelWorker(event, cpu);
            TmfVertex tail = graph.getTail(k);
            if (tail != null) {
                replaceIncomingNetworkEdge(tail, wupTarget);
            }
        }
    }

    private void none(long ts, OsWorker target, @Nullable OsWorker current) {
        // task context wakeup
        if (current != null) {
            TmfVertex n1 = stateChange(target, ts);
            extendAndLink(current, ts, n1);
        } else {
            stateChange(target, ts);
        }
    }

    private static void irq(TmfGraph graph, IKernelAnalysisEventLayout eventLayout, long ts, OsWorker target, OsInterruptContext context) {
        TmfVertex wup = new TmfVertex(ts);
        TmfEdge link = graph.append(target, wup);
        if (link != null) {
            ITmfEventField content = context.getEvent().getContent();
            Integer vec = content.getFieldValue(Integer.class, eventLayout.fieldIrq());
            EdgeType edgeType = resolveIRQ(vec);
            if (edgeType.equals(EdgeType.BLOCKED)) {
                // Blocked for unknown reason, add the irq name as qualifier
                String irqName = content.getFieldValue(String.class, eventLayout.fieldName());
                link.setType(edgeType, irqName);
            } else {
                link.setType(edgeType);
            }
        }
    }

    private void waitFork(TmfGraph graph, long ts, OsWorker target, @Nullable OsWorker current) {
        if (current != null) {
            TmfVertex n0 = stateExtend(current, ts);
            TmfVertex n1 = stateChange(target, ts);
            graph.link(n0, n1);
        } else {
            stateChange(target, ts);
        }
    }

    private static EdgeType resolveIRQ(@Nullable Integer vec) {
        EdgeType ret = EdgeType.UNKNOWN;
        if (vec == null) {
            return ret;
        }
        switch (vec) {
        case IRQ_TIMER:
            ret = EdgeType.INTERRUPTED;
            break;
        default:
            ret = EdgeType.BLOCKED;
            break;
        }
        return ret;
    }

    private static EdgeType resolveSoftirq(@Nullable Long vec) {
        EdgeType ret = EdgeType.UNKNOWN;
        if (vec == null) {
            return ret;
        }
        switch (vec.intValue()) {
        case LinuxValues.SOFTIRQ_HRTIMER:
        case LinuxValues.SOFTIRQ_TIMER:
            ret = EdgeType.TIMER;
            break;
        case LinuxValues.SOFTIRQ_BLOCK:
        case LinuxValues.SOFTIRQ_BLOCK_IOPOLL:
            ret = EdgeType.BLOCK_DEVICE;
            break;
        case LinuxValues.SOFTIRQ_NET_RX:
        case LinuxValues.SOFTIRQ_NET_TX:
            ret = EdgeType.NETWORK;
            break;
        case LinuxValues.SOFTIRQ_SCHED:
            ret = EdgeType.INTERRUPTED;
            break;
        default:
            ret = EdgeType.BLOCKED;
            break;
        }
        return ret;
    }

    private static Context peekInnerContext(String host, Integer cpu, OsSystemModel system) {
        // Get the inner context: pop, peek then push back context
        OsInterruptContext lastCtx = system.popContextStack(host, cpu);
        OsInterruptContext innerCtx = system.peekContextStack(host, cpu);
        if (lastCtx != null) {
            system.pushContextStack(host, cpu, lastCtx);
        }
        return innerCtx.getContext();
    }

    private void handleInetSockLocalIn(ITmfEvent event) {
        Integer cpu = NonNullUtils.checkNotNull(TmfTraceUtils.resolveIntEventAspectOfClassForEvent(event.getTrace(), TmfCpuAspect.class, event));
        String host = event.getTrace().getHostId();
        OsSystemModel system = getProvider().getSystem();

        OsInterruptContext intCtx = system.peekContextStack(host, cpu);
        Context context = intCtx.getContext();
        if (context == Context.PACKET_RECEPTION) {
            context = peekInnerContext(host, cpu, system);
        }
        OsWorker receiver = null;
        if (context == Context.SOFTIRQ || context == Context.IRQ) {
            receiver = getOrCreateKernelWorker(event, cpu);
        } else {
            receiver = system.getWorkerOnCpu(event.getTrace().getHostId(), cpu);
        }
        if (receiver == null) {
            return;
        }
        TmfVertex endpoint;
        endpoint = stateExtend(receiver, event.getTimestamp().getValue());
        fTcpNodes.put(new DependencyEvent(event), endpoint);
        fTcpMatching.matchEvent(event, event.getTrace(), DEFAULT_PROGRESS_MONITOR);
    }

    private void handleInetSockLocalOut(ITmfEvent event) {
        Integer cpu = NonNullUtils.checkNotNull(TmfTraceUtils.resolveIntEventAspectOfClassForEvent(event.getTrace(), TmfCpuAspect.class, event));
        String host = event.getTrace().getHostId();
        OsSystemModel system = getProvider().getSystem();

        OsInterruptContext intCtx = system.peekContextStack(host, cpu);
        Context context = intCtx.getContext();
        if (context == Context.PACKET_RECEPTION) {
            context = peekInnerContext(host, cpu, system);
        }

        OsWorker sender = null;
        if (context == Context.NONE) {
            sender = system.getWorkerOnCpu(event.getTrace().getHostId(), cpu);
        } else if (context == Context.SOFTIRQ) {
            sender = getOrCreateKernelWorker(event, cpu);
        }
        if (sender == null) {
            return;
        }
        TmfVertex endpoint = stateExtend(sender, event.getTimestamp().getValue());
        fTcpNodes.put(new DependencyEvent(event), endpoint);
        // TODO, add actual progress monitor
        fTcpMatching.matchEvent(event, event.getTrace(), DEFAULT_PROGRESS_MONITOR);
    }

    private void handleSoftirqEntry(ITmfEvent event) {
        IKernelAnalysisEventLayout eventLayout = getProvider().getEventLayout(event.getTrace());
        TmfGraph graph = NonNullUtils.checkNotNull(getProvider().getAssignedGraph());
        Long vec = event.getContent().getFieldValue(Long.class, eventLayout.fieldVec());
        if (vec != null && (vec == LinuxValues.SOFTIRQ_NET_RX || vec == LinuxValues.SOFTIRQ_NET_TX)) {
            Integer cpu = NonNullUtils.checkNotNull(TmfTraceUtils.resolveIntEventAspectOfClassForEvent(event.getTrace(), TmfCpuAspect.class, event));
            OsWorker k = getOrCreateKernelWorker(event, cpu);
            graph.add(k, new TmfVertex(event.getTimestamp().getValue()));
        }
    }

}
