# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""metric contains all the aggregate metrics generated for an experiments."""

import logging
import math

from scapy.all import IP
from scapy.all import IPv6
from scapy.all import TCP
from transperf import CONVERGENCE_BUCKET
from transperf import METRIC_NO_PORT
from transperf import outparser
from transperf import tcp
from transperf import VarBandwidth


LOG = logging.getLogger('transperf/metric')


def parse_float(float_str, default=0):
    """Parses the float_str and returns the value if valid.

    Args:
        float_str: String to parse as float.
        default: Value to return if float_str is not valid.

    Returns:
        Parsed float value if valid or default.
    """
    try:
        return float(float_str)
    except ValueError:
        return default


class Metric(object):
    """Represents port-based metics.

    Arg:
        name: The name of this metric.
        vals: The default values.

    Attributes:
        _name: The name of the metric.
        _values: The dictionary of ports to values.
    """

    def __init__(self, name, vals=None, cc=None):
        self._name = name
        self._values = vals if vals is not None else dict()
        self._cc = cc if cc is not None else dict()

    def name(self):
        """Name of the metric."""
        return self._name

    def has(self, port):
        """Returns whether the metric has a value for the port.

        Args:
            port: the port number.

        Returns:
            whether the metrics has a value for the port.
        """
        return int(port) in self._values.keys()

    def set(self, port, value):
        """Sets the metric value for the port.

        Args:
            port: The port.
            value: The value.
        """
        self._values[int(port)] = value

    def set_cc(self, port, cc):
        """Sets the metric cc for the port.

        Args:
            port: The port.
            cc: The cc algorithm.
        """
        self._cc[int(port)] = cc

    def get_cc(self, port):
        """Gets the metric cc for the port.

        Args:
            port: The port.

        Returns:
            The cc algorithm.
        """
        return self._cc[int(port)]

    def as_array(self):
        """Metric values as an array.

        Returns:
            Metric values sorted by port number.
        """
        return [self._values[port] for port in sorted(self._values.keys())]

    def ports(self):
        """Ports that have a value in this metric.

        Returns:
            The port numbers.
        """
        return sorted(self._values.keys())

    def cc_ports(self, cc):
        """Ports that have a specific cc in this metric.

        Args:
            cc: The cc algorithm.

        Returns:
            The port numbers.
        """
        return sorted([k for k, v in self._cc.items() if v == cc])

    def get(self, port):
        """The value assigned for the port.

        Args:
            port: The port number.

        Returns:
            The value
        """
        port = int(port)
        if port not in self._values:
            return 0
        return self._values[port]

    def __str__(self):
        vals = ','.join([('%s:%s' % (port, val))
                         for port, val in self._values.items()])
        ccs = ','.join([('%s:%s' % (port, cc))
                        for port, cc in self._cc.items()])
        return 'Metric(\'%s\',{%s},{%s})' %  (self._name, vals, ccs)


class MetricPublisher(outparser.Visitor):
    """The abstract base class for metrics.

    MetricPublishers are visitors that can publish metrics in a dictionary.
    """

    def publish_metrics(self):
        """Publishes metrics in the metrics dictionary.

        Returns:
            An array of published metrics.
        """
        pass


class TputMetricPublisher(MetricPublisher):
    """Generates throughput metrics.

    Attributes:
        _tputs: Throughput of each port.
    """

    def __init__(self):
        super(TputMetricPublisher, self).__init__()
        self._tputs = Metric('tputs')
        self._tool_tputs = Metric('tool_tputs')  # tput from netperf or similar
        # We want the throughput for each port. Note that not all the flows
        # start at the same time, and they may be configured to run past the
        # duration of the experiment (ie. per conn dur overrides experiment
        # provided dur; suppose a 10-sec experiment where a conn starts at 9
        # seconds in and has a conn-configured dur of 20 seconds.)
        #
        # All we care is that, for each second with a flow active, where that
        # second falls into the lifespan of the experiment as a *whole* (ie.
        # dur= for the experiment itself in the config), that the tput for all
        # such seconds is above the configured threshold for the test. We ignore
        # seconds where no flow was configured to be active (eg. flow from 0-2
        # seconds, then 1 second with now flows, then a flow from 3-10 seconds
        # would ignore t=2 to t=3 seconds). We also ignore cases where a flow
        # continues past the experiment duration as in the case of the 20 second
        # flow above.
        #
        # As metric, we output a timeline of bandwidths for all flows
        # aggregated.
        self._stats = {}
        self._tputs_timeline = {}
        self._prev_time = 0

    def visit_conn(self, ip, port, tool, cc, params, start, dur, tput):
        """Stores connection throughputs.

        See the MetricPublisher interface.
        """
        self._tputs.set(port, parse_float(tput, 0.0))
        self._tputs.set_cc(port, cc)
        self._tool_tputs.set(port, parse_float(tput, 0.0))
        self._tool_tputs.set_cc(port, cc)
        # Prep for when we visit the ss log.
        port = int(port)
        self._stats[port] = (0, 0)
        self._tputs_timeline[port] = []

    def visit_ss_log(self, time, data):
        if 'port' not in data:
            return

        ts = self._prev_time
        while ts + CONVERGENCE_BUCKET <= time:
            ts += CONVERGENCE_BUCKET
            ports = sorted(self._stats.keys())
            for port in ports:
                
# The values are cumulative, so use the delta.
                prev, curr = self._stats[port]
                tput_for_po
rt = 8 * (curr - prev) / CONVERGENCE_BUCKET
                # Convert to mbps.
                tput_for_port /= (10**6)
                # Track both the timeline for this port in this bucket and the
                # state for the cumulative delta in the next iteration.
                self._tputs_timeline[port].append((ts, tput_for_port))
                self._stats[port] = (curr, curr)
        self._prev_time = ts

        port = data['port']
        prev, curr = self._stats[port]
        if 'bytes_acked' in data:
            curr = max(curr, data['bytes_acked'])
        self._stats[port] = (prev, curr)

    def publish_metrics(self):
        """Publishes an array of tputs.

        See the MetricPublisher interface.
        """
        for port in self._stats:
            if not self._tputs_timeline[port]:
                # If we did not get data from sslog, we have to rely on what
                # netperf reports - which is definitely wrong if the flows did
                # not coincide in time but it (usually) works for simple tests
                # where they do.
                return [self._tputs, self._tool_tputs]
        # If we do have sslog data: rather than assume all flows have the same
        # lifespan of activity, and setting a single per-flow tput value, we
    