#
# Copyright (c) 2018 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA. 
#

"""
  StatStruct -- classes for sampling statistics from a VDO via ioctls

  $Id: //eng/vdo-releases/magnesium-rhel7.6/src/python/vdo/statistics/StatStruct.py#1 $
"""

from ctypes import *
import collections
import fcntl
import os
import sys

from Field import Field
from LabeledValue import LabeledValue

class Samples(object):
  """
  An object which represents a collection of samples from a VDO.
  """
  def __init__(self, assays, device, mustBeVDO):
    """
    Create a new set of samples by sampling a VDO device.

    :param assays:    The types of samples to take
    :param devices:   The device to sample (a dictionary containing
                      the user-supplied name and the name to use for sampling)
    :param mustBeVDO: If set to False, errors resulting from the device not
                      being a VDO will be suppressed
    """
    self.device = device["user"]
    self.samples = [assay.sample(os.path.basename(device["sample"]))
                    for assay in assays]

  def getDevice(self):
    """
    Get the name of the device which was sampled.

    :return: The name of the device
    """
    return self.device

  def getSamples(self):
    """
    Get the list of samples for the device.

    :return: The list of samples
    """
    return self.samples

  @staticmethod
  def assay(assays, device, mustBeVDO=True):
    """
    Assay a device.

    :param assays:    The types of samples to take
    :param devices:   The device to sample (a dictionary containing
                      the user-supplied name and the name to use for sampling)
    :param mustBeVDO: If set to False, errors resulting from the device not
                      being a VDO will be suppressed

    :return: The results of the assays or None if the device is not a VDO and
             mustBeVDO is set to False
    """
    try:
      return Samples(assays, device, mustBeVDO)
    except IOError as ioe:
      user = device["user"]
      if (ioe.errno == 22):
        if mustBeVDO:
          raise Exception("Device {0} is not a VDO".format(user))
        return None
      raise Exception("Error sampling device {0}: {1}".format(user, ioe))

  @staticmethod
  def assayDevices(assays, devices, mustBeVDO=True):
    """
    Assay a list of devices.

    :param assays:    The types of samples to take
    :param devices:   The devices to sample (a list of dictionaries containing
                      the user-supplied name and the name to use for sampling)
    :param mustBeVDO: If set to False, errors resulting from the device not
                      being a VDO will be suppressed

    :return: The results of the assays or None if the device is not a VDO and
             mustBeVDO is set to False
    """
    return filter(None, [Samples.assay(assays, device, mustBeVDO)
                         for device in devices])

  @staticmethod
  def samplingDevice(user, sample):
    """
    Returns a dictionary used for sampling purposes.

    The dictionary is structured as:
      { "user"   : <user-specified name>,
        "sample" : <sample name> }

    The user-specified name is used for display purposes.
    The sample name is used to perform the actual sampling.
    The two names may be identical.

    :param user:    user-specified name
    :param sample:  the name to use for sampling

    :return:  A sampling dictionary.
    """
    return { "user" : user, "sample" : sample }

class Sample(object):
  """
  An object which represents a single sample (ioctl) of a VDO.
  """
  def __init__(self, statStruct, sample):
    """
    Create a new sample.

    :param statStruct: The structure representing the type of this sample
    :param sample:     The sampled values
    """
    self.statStruct = statStruct
    self.sample     = sample

  def getType(self):
    """
    Get the object representing the type of this sample.

    :return: The StatStruct which represents the type of this sample
    """
    return self.statStruct

  def labeled(self):
    """
    Get the sampled values as a collection of LabeledValues.

    :return: A LabeledValue representing the sampled values
    """
    return self.statStruct.labeled(self.sample)

  def getStat(self, statName):
    """
    Get the value of a named statistic.

    :param statName:     The name of the statistic, either as a string, or as
                         a list of strings, one for each level of the sample
                         hierarchy

    :return: The value of the named statistic
    """
    if not isinstance(statName, list):
      return self.sample[statName]

    stats = self.sample
    for name in statName[:-1]:
      stats = stats[name]
    return stats[statName[-1]]

  def statEqual(self, other, statName):
    """
    Check whether the value of a given statistic is the same in this sample
    and some other sample.

    :param other: The other sample
    :param statName: The name of the statistic as would be specified to
                     getStat()

    :return: True if the value of the named statistic is the same in this and
             the other sample
    """
    return (self.getStat(statName) == other.getStat(statName))

class StatStruct(Field):
  """
  Base class for objects representing a VDO statistics structure. This object
  can be used to sample a VDO via an ioctl and convert the result from the
  C format in the ioctl to a Sample object.
  """

  """
  The dict of C classes
  """
  cClasses = {}

  def __init__(self, name, fields, **kwargs):
    """
    Create a new statistics structure.

    :param name:     The name of statistics structure
    :param fields:   A list of Field objects specifying the format of the C
                     structure returned by the ioctl
    :param **kwargs: Keyword args which may be:
                       labelPrefix: The prefix to prepend to the label for
                                    each field in this structure
                       ioctl:       The value of the ioctl to use for sampling
                                    a VDO
                       Field:       Any of the keyword arguments for the Field
                                    base class
    """
    labelPrefix      = kwargs.pop('labelPrefix', None)
    self.labelPrefix = labelPrefix + ' ' if (labelPrefix != None) else ''
    self.ioctl       = kwargs.pop('ioctl', None)
    self.procFile    = kwargs.pop('procFile', None)
    self.procRoot    = kwargs.pop('procRoot', None)
    self.fields      = fields
    super(StatStruct, self).__init__(name, self._getCClass(), **kwargs)
    self.fieldsByName = dict()
    for field in fields:
      self.fieldsByName[field.name] = field

  def _getCClass(self):
    """
    Get the Structure class which represents the C struct for a StatStruct
    or Field. If the Structure class hasn't yet been made for the given type,
    it will be created.

    :return: The class defined by the specified set of Fields
    """
    className = type(self).__name__ + '.c'
    cType     = self.cClasses.get(className)
    if not cType:
      fieldList = [(field.name, field.cType) for field in self.fields
                   if field.inStruct]
      cType = type(className, (Structure,), { '_fields_': fieldList });
      self.cClasses[className] = cType

    return cType

  def sample(self, name):
    """
    Get a sample from a VDO via an ioctl.

    :param name: The name of the proc directory from which to read

    :return: The sample
    """
    stats    = self.cType()
    procPath = os.path.join("/proc", self.procRoot, name, self.procFile)
    with open(procPath, 'rb') as fd:
      fd.readinto(stats)
    return self._extract(stats)

  def _extract(self, stats):
    """
    Extract the sampled values from the return of an ioctl call.

    :param stats: The structure returned from an ioctl

    :return: The sample as a Sample
    """
    sample = dict()
    for field in self.fields:
      sample[field.name] = field.extractSample(stats, self)
    return Sample(self, sample)

  def extractSample(self, stats, parent):
    """
    :inherit:
    """
    myStats = getattr(stats, self.name, stats)
    if (self.length > 1) and (myStats != stats):
      # The current field is actually an array, so make a list of the
      # extractions of each element of the array.
      return [self.extractSample(s, self) for s in myStats]

    # The current field is a struct which is not an array, so recursively
    # extract each sub-field of that struct
    sample = dict()
    for field in self.fields:
      sample[field.name] = field.extractSample(myStats, self)
    return sample

  def getSampleValue(self, stats, fieldName):
    """
    Get the value of one of the fields at the current level of the C structure
    from the ioctl.

    :param stats:     The current level of the C structure
    :param fieldName: The name of the field to get

    :return: The value of the specified field
    """
    return self.fieldsByName[fieldName].extractSample(stats, self)

  def labeled(self, sample, prefix = ''):
    """
    :inherit:
    """
    prefix += self.labelPrefix
    label   = prefix + self.label
    if isinstance(sample, list):
      # The current field is an array, so convert each array element
      labeledFields = [self.labeled(s, prefix) for s in sample]
    else:
      # The current field is a struct, so recursively convert each sub-field
      labeledFields = [field.labeled(sample[field.name], prefix)
                       for field in self.fields if field.display]

    return LabeledValue.make(label, labeledFields)
