/*******************************************************************************
 * 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 java.util.List;
import java.util.Objects;

import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.internal.analysis.os.linux.core.Activator;
import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem;
import org.eclipse.tracecompass.statesystem.core.StateSystemUtils;
import org.eclipse.tracecompass.statesystem.core.exceptions.StateSystemDisposedException;
import org.eclipse.tracecompass.statesystem.core.interval.ITmfStateInterval;

/**
 * This class represents a storage device in the system that behaves like a disk
 * from the operating system point of view. Concretely, it can be an HDD, an
 * SSD, a USB key, etc.
 *
 * @author Geneviève Bastien
 */
public class Disk {

    private static final int MINORBITS = 20;
    private static final int MINORMASK = (1 << MINORBITS) - 1;

    private final int fDev;
    private final int fDiskQuark;
    private final ITmfStateSystem fSs;
    private @Nullable String fDiskName = null;

    /**
     * Constructor
     *
     * @param dev
     *            The device number of the disk
     * @param ss
     *            The state system this disk will be saved to
     * @param diskQuark
     *            The quark of this disk in the state system
     */
    public Disk(Integer dev, ITmfStateSystem ss, int diskQuark) {
        fDev = dev;
        fSs = ss;
        fDiskQuark = diskQuark;
        ITmfStateInterval diskNameInterval = StateSystemUtils.queryUntilNonNullValue(ss, diskQuark, ss.getStartTime(), ss.getCurrentEndTime());
        if (diskNameInterval != null) {
            fDiskName = diskNameInterval.getStateValue().unboxStr();
        }
    }

    /**
     * Get the device ID of this device
     *
     * @return The device ID of this disk
     */
    public int getDeviceId() {
        return fDev;
    }

    /**
     * Get the disk name if available. If the disk name is not set, this method
     * will return the string corresponding to the major, minor value of the
     * disk's ID, ie the return value of {@link #getDeviceIdString()}.
     *
     * @return The disk name or the value returned by
     *         {@link #getDeviceIdString()}
     */
    public String getDiskName() {
        String diskName = fDiskName;
        if (diskName == null) {
            return getDeviceIdString();
        }
        return diskName;
    }

    /**
     * Get the quark
     *
     * @return The quark of this disk in the state system
     */
    public int getQuark() {
        return fDiskQuark;
    }

    /**
     * Set the human readable disk name of this device
     *
     * @param diskname
     *            The human readable name of the disk
     */
    public void setDiskName(String diskname) {
        fDiskName = diskname;
    }

    /**
     * Return the disk's device ID as a major,minor string. Those major,minor
     * numbers correspond to the number of the disk found when listing disk with
     * ls -al /dev, or using lsblk in Linux.
     *
     * @return The device ID string as major,minor
     */
    public String getDeviceIdString() {
        return extractDeviceIdString(fDev);
    }

    /**
     * Extract a string from a device ID
     *
     * @param dev
     *            Device ID
     * @return The extracted string
     */
    public static String extractDeviceIdString(int dev) {
        int major = dev >> MINORBITS;
        int minor = dev & MINORMASK;
        return major + "," + minor; //$NON-NLS-1$
    }

    /**
     * Get the total number of sectors either read or written at the end of a
     * time range. This method will interpolate the requests that are in
     * progress.
     *
     * @param ts
     *            The start of the time range to query
     * @param type
     *            The type of IO operation to query
     * @return The number of sectors affected by operation at the end of the
     *         range
     */
    public double getSectorsAt(long ts, IoOperationType type) {

        ITmfStateSystem ss = fSs;

        /* Get the quark for the number of sector for the requested operation */
        int rwSectorQuark;
        if (type == IoOperationType.READ) {
            rwSectorQuark = ss.optQuarkRelative(fDiskQuark, Attributes.SECTORS_READ);
        } else if (type == IoOperationType.WRITE) {
            rwSectorQuark = ss.optQuarkRelative(fDiskQuark, Attributes.SECTORS_WRITTEN);
        } else {
            return 0;
        }

        long time = Math.max(ts, ss.getStartTime());
        time = Math.min(time, ss.getCurrentEndTime());

        try {
            List<ITmfStateInterval> states = ss.queryFullState(time);
            return extractCount(rwSectorQuark, ss, states, time);
        } catch (StateSystemDisposedException e) {
            Activator.getDefault().logError("Error getting disk IO Activity", e); //$NON-NLS-1$
        }
        return 0;
    }

    /**
     * Get the total number of sectors either read or written at the end of a time
     * range. This method will interpolate the requests that are in progress.
     *
     * @param sectorQuark
     *            the read or write sector quark for this Disk.
     * @param ss
     *            this disk's state system
     * @param states
     *            list of full states queried at the desired time.
     * @param ts
     *            the desired time
     * @return The number of sectors affected by operation at the end of the range
     */
    public static double extractCount(int sectorQuark, ITmfStateSystem ss, List<ITmfStateInterval> states, long ts) {
        // Determine if we are handling read or write requests.
        String sectorName = ss.getAttributeName(sectorQuark);
        IoOperationType rw = sectorName.equals(Attributes.SECTORS_READ) ? IoOperationType.READ : IoOperationType.WRITE;

        // Get the initial value.
        Object stateValue = states.get(sectorQuark).getValue();
        double count = (stateValue instanceof Number) ? ((Number) stateValue).doubleValue() : 0.0;

        // Find the driver queue root attribute
        int diskQuark = ss.getParentAttributeQuark(sectorQuark);
        int driverQ = ss.optQuarkRelative(diskQuark, Attributes.DRIVER_QUEUE);
        if (driverQ == ITmfStateSystem.INVALID_ATTRIBUTE) {
            return count;
        }

        /*
         * Interpolate the part of the requests in progress at requested time
         */
        for (Integer driverSlotQuark : ss.getSubAttributes(driverQ, false)) {
            int sizeQuark = ss.optQuarkRelative(driverSlotQuark, Attributes.REQUEST_SIZE);
            if (sizeQuark != ITmfStateSystem.INVALID_ATTRIBUTE) {
                ITmfStateInterval interval = states.get(sizeQuark);
                Object size = interval.getValue();
                if (size instanceof Number && Objects.equals(rw.ordinal(), states.get(driverSlotQuark).getValue())) {
                    /*
                     * The request is fully completed (and included in the r/w sectors) at interval
                     * end time + 1, so at interval end time, we do not expect the size to be total
                     * size
                     */
                    count += interpolate(ts, interval, (Number) size);
                }
            }
        }
        return count;
    }

    private static double interpolate(long ts, ITmfStateInterval interval, Number size) {
        long runningTime = interval.getEndTime() - interval.getStartTime() + 1;
        return (ts - interval.getStartTime()) * size.doubleValue() / runningTime;
    }

    /**
     * Return whether requests were made on this disk during the trace or not
     *
     * @return {@code true} if there was requests on this disk, {@code false}
     *         otherwise
     */
    public boolean hasActivity() {
        return queueIsActive(Attributes.WAITING_QUEUE) && queueIsActive(Attributes.DRIVER_QUEUE);
    }

    private boolean queueIsActive(String queue) {
        int quark = fSs.optQuarkRelative(fDiskQuark, queue);
        return quark != ITmfStateSystem.INVALID_ATTRIBUTE && !fSs.getSubAttributes(quark, false).isEmpty();
    }

    // ----------------------------------------------------
    // Object methods
    // ----------------------------------------------------

    @Override
    public String toString() {
        return "Disk: [" + getDeviceIdString() + ',' + fDiskName + ']'; //$NON-NLS-1$
    }

    @Override
    public int hashCode() {
        return Objects.hash(fSs, fDev);
    }

    @Override
    public boolean equals(@Nullable Object o) {
        if (o instanceof Disk) {
            Disk disk = (Disk) o;
            return fSs.equals(disk.fSs) && fDev == disk.fDev;
        }
        return false;
    }

}
