class Metric(object):
    """Abstract base class for metrics."""
    def __init__(self,
                 description,
                 units=None,
                 higher_is_better=False):
        """
        Initializes a Metric.
        @param description: Description of the metric, e.g., used as label on a
                dashboard chart
        @param units: Units of the metric, e.g. percent, seconds, MB.
        @param higher_is_better: Whether a higher value is considered better or
                not.
        """
        self._description = description
        self._units = units
        self._higher_is_better = higher_is_better
        self._samples = []

    @property
    def description(self):
        """Description of the metric."""
        return self._description

    @property
    def units(self):
        """Units of the metric."""
        return self._units

    @property
    def higher_is_better(self):
        """Whether a higher value is considered better or not."""
        return self._higher_is_better

    @property
    def values(self):
        """Measured values of the metric."""
        if len(self._samples) == 0:
            return self._samples
        return self._aggregate(self._samples)

    @values.setter
    def values(self, samples):
        self._samples = samples

    def _aggregate(self, samples):
        """
        Subclasses can override this to aggregate the metric into a single
        sample.
        """
        return samples

    def _store_sample(self, sample):
        self._samples.append(sample)

    def pre_collect(self):
        """
        Hook called before metrics are being collected.
        """
        pass

    def post_collect(self):
        """
        Hook called after metrics has been collected.
        """
        pass

    def collect_metric(self):
        """
        Collects one sample.

        Implementations should call self._store_sample() once if it's not an
        aggregate, i.e., it overrides self._aggregate().
        """
        pass

    @classmethod
    def from_metric(cls, other):
        """
        Instantiate from an existing metric instance.
        """
        metric = cls(
                description=other.description,
                units=other.units,
                higher_is_better=other.higher_is_better)
        metric.values = other.values
        return metric

class PeakMetric(Metric):
    """
    Metric that collects the peak of another metric.
    """

    @property
    def description(self):
        return 'peak_' + super(PeakMetric, self).description

    def _aggregate(self, samples):
        return max(samples)

class SumMetric(Metric):
    """
    Metric that sums another metric.
    """

    @property
    def description(self):
        return 'sum_' + super(SumMetric, self).description

    def _aggregate(self, samples):
        return sum(samples)

class MemUsageMetric(Metric):
    """
    Metric that collects memory usage in percent.

    Memory usage is collected in percent. Buffers and cached are calculated
    as free memory.
    """
    def __init__(self, system_facade):
        super(MemUsageMetric, self).__init__('memory_usage', units='percent')
        self.system_facade = system_facade

    def collect_metric(self):
        total_memory = self.system_facade.get_mem_total()
        free_memory = self.system_facade.get_mem_free_plus_buffers_and_cached()
        used_memory = total_memory - free_memory
        usage_percent = (used_memory * 100) / total_memory
        self._store_sample(usage_percent)

class CpuUsageMetric(Metric):
    """
    Metric that collects cpu usage in percent.
    """
    def __init__(self, system_facade):
        super(CpuUsageMetric, self).__init__('cpu_usage', units='percent')
        self.last_usage = None
        self.system_facade = system_facade

    def pre_collect(self):
        self.last_usage = self.system_facade.get_cpu_usage()

    def collect_metric(self):
        """
        Collects CPU usage in percent.
        """
        current_usage = self.system_facade.get_cpu_usage()
        # Compute the percent of active time since the last update to
        # current_usage.
        usage_percent = 100 * self.system_facade.compute_active_cpu_time(
                self.last_usage, current_usage)
        self._store_sample(usage_percent)
        self.last_usage = current_usage

class AllocatedFileHandlesMetric(Metric):
    """
    Metric that collects the number of allocated file handles.
    """
    def __init__(self, system_facade):
        super(AllocatedFileHandlesMetric, self).__init__(
                'allocated_file_handles', units='handles')
        self.system_facade = system_facade

    def collect_metric(self):
        self._store_sample(self.system_facade.get_num_allocated_file_handles())

class StorageWrittenAmountMetric(Metric):
    """
    Metric that collects amount of kB written to persistent storage.
    """
    def __init__(self, system_facade):
        super(StorageWrittenAmountMetric, self).__init__(
                'storage_written_amount', units='kB')
        self.last_written_kb = None
        self.system_facade = system_facade

    def pre_collect(self):
        statistics = self.system_facade.get_storage_statistics()
        self.last_written_kb = statistics['written_kb']

    def collect_metric(self):
        """
        Collects total amount of data written to persistent storage in kB.
        """
        statistics = self.system_facade.get_storage_statistics()
        written_kb = statistics['written_kb']
        written_period = written_kb - self.last_written_kb
        self._store_sample(written_period)
        self.last_written_kb = written_kb

class StorageWrittenCountMetric(Metric):
    """
    Metric that collects the number of writes to persistent storage.
    """
    def __init__(self, system_facade):
        super(StorageWrittenCountMetric, self).__init__(
                'storage_written_count', units='count')
        self.system_facade = system_facade

    def pre_collect(self):
        command = ('/usr/sbin/fatrace', '--timestamp', '--filter=W')
        self.system_facade.start_bg_worker(command)

    def collect_metric(self):
        output = self.system_facade.get_and_discard_bg_worker_output()
        # fatrace outputs a line of text for each file it detects being written
        # to.
        written_count = output.count('\n')
        self._store_sample(written_count)

    def post_collect(self):
        self.system_facade.stop_bg_worker()

class TemperatureMetric(Metric):
    """
    Metric that collects the max of the temperatures measured on all sensors.
    """
    def __init__(self, system_facade):
        super(TemperatureMetric, self).__init__('temperature', units='Celsius')
        self.system_facade = system_facade

    def collect_metric(self):
        self._store_sample(self.system_facade.get_current_temperature_max())


class EnergyUsageMetric(Metric):
    """
    Metric that collects the amount of energy used.
    """

    def __init__(self, system_facade):
        super(EnergyUsageMetric, self).__init__('energy_usage',
                                                units='microjoules')
        self.system_facade = system_facade
        self.initial_energy = int(self.system_facade.get_energy_usage())

    def collect_metric(self):
        self._store_sample(
                int(self.system_facade.get_energy_usage()) -
                self.initial_energy)

    def _aggregate(self, samples):
        return samples[-1]


def create_default_metric_set(system_facade):
    """
    Creates the default set of metrics.

    @param system_facade the system facade to initialize the metrics with.
    @return a list with Metric instances.
    """
    cpu = CpuUsageMetric(system_facade)
    mem = MemUsageMetric(system_facade)
    file_handles = AllocatedFileHandlesMetric(system_facade)
    storage_written_amount = StorageWrittenAmountMetric(system_facade)
    temperature = TemperatureMetric(system_facade)
    peak_cpu = PeakMetric.from_metric(cpu)
    peak_mem = PeakMetric.from_metric(mem)
    peak_temperature = PeakMetric.from_metric(temperature)
    sum_storage_written_amount = SumMetric.from_metric(storage_written_amount)
    energy = EnergyUsageMetric(system_facade)
    return [
            cpu, mem, file_handles, storage_written_amount, temperature,
            peak_cpu, peak_mem, peak_temperature, sum_storage_written_amount,
            energy
    ]


class SystemMetricsCollector(object):
    """
    Collects system metrics.
    """
    def __init__(self, system_facade, metrics = None):
        """
        Initialize with facade and metric classes.

        @param system_facade The system facade to use for querying the system,
                e.g. system_facade.SystemFacadeLocal for client tests.
        @param metrics List of metric instances. If None, the default set will
                be created.
        """
        self.metrics = (create_default_metric_set(system_facade)
                        if metrics is None else metrics)

    def pre_collect(self):
        """
        Calls pre hook of metrics.
        """
        for metric in self.metrics:
            metric.pre_collect()

    def post_collect(self):
        """
        Calls post hook of metrics.
        """
        for metric in self.metrics:
            metric.post_collect()

    def collect_snapshot(self):
        """
        Collects one snapshot of metrics.
        """
        for metric in self.metrics:
            metric.collect_metric()

    def write_metrics(self, writer_function):
        """
        Writes the collected metrics using the specified writer function.

        @param writer_function: A function with the following signature:
                 f(description, value, units, higher_is_better)
        """
        for metric in self.metrics:
            writer_function(
                    description=metric.description,
                    value=metric.values,
                    units=metric.units,
                    higher_is_better=metric.higher_is_better)
