/*******************************************************************************
 * Copyright (c) 2016 É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.analysis.os.linux.core.inputoutput;

import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;

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

import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.analysis.os.linux.core.trace.IKernelAnalysisEventLayout;
import org.eclipse.tracecompass.common.core.NonNullUtils;
import org.eclipse.tracecompass.internal.analysis.os.linux.core.Activator;
import org.eclipse.tracecompass.internal.analysis.os.linux.core.inputoutput.handlers.BlockFrontMergeHandler;
import org.eclipse.tracecompass.internal.analysis.os.linux.core.inputoutput.handlers.BlockRqComplete;
import org.eclipse.tracecompass.internal.analysis.os.linux.core.inputoutput.handlers.BlockRqInsertHandler;
import org.eclipse.tracecompass.internal.analysis.os.linux.core.inputoutput.handlers.BlockRqIssueHandler;
import org.eclipse.tracecompass.internal.analysis.os.linux.core.inputoutput.handlers.MergeRequestsHandler;
import org.eclipse.tracecompass.internal.analysis.os.linux.core.inputoutput.handlers.StateDumpHandler;
import org.eclipse.tracecompass.internal.analysis.os.linux.core.inputoutput.handlers.SysEntryHandler;
import org.eclipse.tracecompass.internal.analysis.os.linux.core.inputoutput.handlers.SysExitHandler;
import org.eclipse.tracecompass.internal.analysis.os.linux.core.kernel.handlers.KernelEventHandler;
import org.eclipse.tracecompass.statesystem.core.ITmfStateSystemBuilder;
import org.eclipse.tracecompass.statesystem.core.exceptions.AttributeNotFoundException;
import org.eclipse.tracecompass.statesystem.core.exceptions.StateValueTypeException;
import org.eclipse.tracecompass.statesystem.core.exceptions.TimeRangeException;
import org.eclipse.tracecompass.tmf.core.event.ITmfEvent;
import org.eclipse.tracecompass.tmf.core.event.ITmfEventField;
import org.eclipse.tracecompass.tmf.core.statesystem.AbstractTmfStateProvider;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;

import com.google.common.collect.ImmutableMap;

/**
 * State provider for the I/O analysis
 *
 * Attribute tree:
 *
 * <pre>
 * |- SYSTEM_CALLS
 * |  |- <TID> -> System Call Name
 * |- THREADS
 * |  |- <TID number>
 * |  |  |- BYTES_READ
 * |  |  |- BYTES_WRITTEN
 * |- Disks
 * |  |- <Disk number> -> Disk Name
 * |  |  |- SECTORS_READ
 * |  |  |- SECTORS_WRITTEN
 * |  |  |- WAITING_QUEUE -> Root for the Attribute pool for waiting queue
 * |  |  |  |- <slot #1> -> Status
 * |  |  |  |  |- CURRENT_REQUEST
 * |  |  |  |  |- REQUEST_SIZE
 * |  |  |  |  |- MERGED_IN
 * |  |  |  |- <slot #2>
 * |  |  |- WAITING_QUEUE_LENGTH
 * |  |  |- DRIVER_QUEUE -> Root for the Attribute pool for driver queue
 * |  |  |  |- <slot #1> -> Status
 * |  |  |  |  |- CURRENT_REQUEST
 * |  |  |  |  |- REQUEST_SIZE
 * |  |  |  |  |- ISSUED_FROM
 * |  |  |  |- <slot #2>
 * |  |  |- DRIVER_QUEUE_LENGTH
 * </pre>
 *
 * @author Houssem Daoud
 * @since 2.0
 */
public class InputOutputStateProvider extends AbstractTmfStateProvider {

    private static final int VERSION = 4;

    private final Map<Integer, DiskWriteModel> fDisks = new HashMap<>();
    private final Map<String, KernelEventHandler> fEventNames;
    private final IKernelAnalysisEventLayout fLayout;

    private final KernelEventHandler fSysEntryHandler;
    private final KernelEventHandler fSysExitHandler;

    /**
     * Instantiate a new state provider plugin.
     *
     * @param trace
     *            The kernel trace to apply this state provider to
     * @param layout
     *            The event layout to use for this state provider.
     */
    public InputOutputStateProvider(ITmfTrace trace, IKernelAnalysisEventLayout layout) {
        super(trace, "Input Output Analysis");//$NON-NLS-1$
        fLayout = layout;
        fEventNames = buildEventNames(layout);
        fSysEntryHandler = new SysEntryHandler(layout);
        fSysExitHandler = new SysExitHandler(layout);
    }

    private Map<String, KernelEventHandler> buildEventNames(IKernelAnalysisEventLayout layout) {
        ImmutableMap.Builder<String, KernelEventHandler> builder = ImmutableMap.builder();

        builder.put(layout.eventBlockRqInsert(), new BlockRqInsertHandler(layout, this));
        builder.put(layout.eventBlockRqIssue(), new BlockRqIssueHandler(layout, this));
        builder.put(layout.eventBlockRqComplete(), new BlockRqComplete(layout, this));
        builder.put(layout.eventBlockBioFrontmerge(), new BlockFrontMergeHandler(layout, this));
        builder.put(layout.eventBlockRqMerge(), new MergeRequestsHandler(layout, this));

        final String eventStatedumpBlockDevice = layout.eventStatedumpBlockDevice();
        if (eventStatedumpBlockDevice != null) {
            builder.put(eventStatedumpBlockDevice, new StateDumpHandler(layout, this));
        }

        return builder.build();
    }

    @Override
    public int getVersion() {
        return VERSION;
    }

    @Override
    public InputOutputStateProvider getNewInstance() {
        return new InputOutputStateProvider(this.getTrace(), this.fLayout);
    }

    @Override
    protected void eventHandle(@Nullable ITmfEvent event) {

        if (event == null) {
            return;
        }

        final String eventName = event.getName();

        try {
            final ITmfStateSystemBuilder ss = NonNullUtils.checkNotNull(getStateSystemBuilder());
            /*
             * Feed event to the history system if it's known to cause a state
             * transition.
             */
            KernelEventHandler handler = fEventNames.get(eventName);
            if (handler == null) {
                if (isSyscallExit(eventName)) {
                    handler = fSysExitHandler;
                } else if (isSyscallEntry(eventName)) {
                    handler = fSysEntryHandler;
                }
            }
            if (handler != null) {
                handler.handleEvent(ss, event);
            }
        } catch (TimeRangeException | StateValueTypeException | AttributeNotFoundException e) {
            Activator.getDefault().logError("Exception while building the IO state system", e); //$NON-NLS-1$
        }

    }

    /**
     * Get a disk identified by a device ID
     *
     * @param deviceId
     *            The device ID of the block device
     * @return The disk corresponding to the device ID
     */
    public DiskWriteModel getDisk(int deviceId) {
        return fDisks.computeIfAbsent(deviceId, diskId -> new DiskWriteModel(diskId, checkNotNull(getStateSystemBuilder())));
    }

    private boolean isSyscallEntry(String eventName) {
        return (eventName.startsWith(fLayout.eventSyscallEntryPrefix())
                || eventName.startsWith(fLayout.eventCompatSyscallEntryPrefix()));
    }

    private boolean isSyscallExit(String eventName) {
        return (eventName.startsWith(fLayout.eventSyscallExitPrefix()) ||
                eventName.startsWith(fLayout.eventCompatSyscallExitPrefix()));
    }

    /**
     * Return the quark corresponding to the threads attributes
     *
     * @param ssb
     *            the state system builder
     * @return The quark of the {@link Attributes#THREADS} node
     */
    public static int getNodeThreads(ITmfStateSystemBuilder ssb) {
        return ssb.getQuarkAbsoluteAndAdd(Attributes.THREADS);
    }

    /**
     * Return the quark corresponding to the system call root attributes
     *
     * @param ssb
     *            the state system builder
     * @return The quark of the {@link Attributes#SYSTEM_CALLS_ROOT} node
     */
    public static int getNodeSyscalls(ITmfStateSystemBuilder ssb) {
        return ssb.getQuarkAbsoluteAndAdd(Attributes.SYSTEM_CALLS_ROOT);
    }

    /**
     * Get the rwbs flag of the block events
     *
     * @param eventField
     *            The rwbs event field
     * @return The int value
     */
    public static IoOperationType getRWBS(@Nullable ITmfEventField eventField) {
        if (eventField == null) {
            return IoOperationType.OTHER;
        }
        Integer fieldValue = eventField.getFieldValue(Integer.class);
        return fieldValue == null ? IoOperationType.OTHER : IoOperationType.getType(fieldValue);
    }

}
