""" Modules will run collectl in playback mode and collect various process
statistics for a given pid's process and process ancestors.
"""
import collections
import csv
import logging
import sys
import tempfile

from galaxy import util
from ..collectl import stats

if sys.version_info > (3,):
    long = int

log = logging.getLogger(__name__)

# Collectl process information cheat sheet:
#
# Record process information for current user.
# %  collectl -sZ -f./__instrument_collectl  -i 10:10 --procfilt U$USER
#
# TSV Replay of processing information in plottable mode...
#
# % collectl -sZ -P --sep=9 -p __instrument_collectl-jlaptop13-20140322-120919.raw.gz
#
# Has following columns:
#   Date   Time    PID     User    PR      PPID    THRD    S       VmSize  VmLck   VmRSS   VmData  VmStk   VmExe   VmLib   CPU       SysT    UsrT    PCT     AccumT  RKB     WKB     RKBC    WKBC    RSYS    WSYS    CNCL    MajF    MinF    Command
#

# Process data dumped one row per process per interval.
# http://collectl.sourceforge.net/Data-detail.html
PROCESS_COLUMNS = [
    "#Date",  # Date of interval - e.g. 20140322
    "Time",  # Time of interval - 12:18:58
    "PID",  # Process pid.
    "User",  # Process user.
    "PR",  # Priority of process.
    "PPID",  # Parent PID of process.
    "THRD",  # Thread???
    "S",  # Process state - S - Sleeping, D - Uninterruptable Sleep, R - Running, Z - Zombie or T - Stopped/Traced
    # Memory options - http://ewx.livejournal.com/579283.html
    "VmSize",
    "VmLck",
    "VmRSS",
    "VmData",
    "VmStk",
    "VmExe",
    "VmLib",
    "CPU",  # CPU number of process
    "SysT",  # Amount of system time consumed during interval
    "UsrT",  # Amount user time consumed during interval
    "PCT",  # Percentage of current interval consumed by task
    "AccumT",  # Total accumulated System and User time since the process began execution
    # kilobytes read/written - requires I/O level monitoring to be enabled in kernel.
    "RKB",  # kilobytes read by process - requires I/O monitoring in kernel
    "WKB",
    "RKBC",
    "WKBC",
    "RSYS",  # Number of read system calls
    "WSYS",  # Number of write system calls
    "CNCL",
    "MajF",  # Number of major page faults
    "MinF",  # Number of minor page faults
    "Command",  # Command executed
]

# Types of statistics this module can summarize
STATISTIC_TYPES = ["max", "min", "sum", "count", "avg"]

COLUMN_INDICES = dict([(col, i) for i, col in enumerate(PROCESS_COLUMNS)])
PID_INDEX = COLUMN_INDICES["PID"]
PARENT_PID_INDEX = COLUMN_INDICES["PPID"]

DEFAULT_STATISTICS = [
    ("max", "VmSize"),
    ("avg", "VmSize"),
    ("max", "VmRSS"),
    ("avg", "VmRSS"),
    ("sum", "SysT"),
    ("sum", "UsrT"),
    ("max", "PCT"),
    ("avg", "PCT"),
    ("max", "AccumT"),
    ("sum", "RSYS"),
    ("sum", "WSYS"),
]


def parse_process_statistics(statistics):
    """ Turn string or list of strings into list of tuples in format ( stat,
    resource ) where stat is a value from STATISTIC_TYPES and resource is a
    value from PROCESS_COLUMNS.
    """
    if statistics is None:
        statistics = DEFAULT_STATISTICS

    statistics = util.listify(statistics)
    statistics = [_tuplize_statistic(_) for _ in statistics]
    # Check for validity...
    for statistic in statistics:
        if statistic[0] not in STATISTIC_TYPES:
            raise Exception("Unknown statistic type encountered %s" % statistic[0])
        if statistic[1] not in PROCESS_COLUMNS:
            raise Exception("Unknown process column encountered %s" % statistic[1])
    return statistics


def generate_process_statistics(collectl_playback_cli, pid, statistics=DEFAULT_STATISTICS):
    """ Playback collectl file and generate summary statistics.
    """
    with tempfile.NamedTemporaryFile() as tmp_tsv:
        collectl_playback_cli.run(stdout=tmp_tsv)
        with open(tmp_tsv.name, "r") as tsv_file:
            return _read_process_statistics(tsv_file, pid, statistics)


def _read_process_statistics(tsv_file, pid, statistics):
    process_summarizer = CollectlProcessSummarizer(pid, statistics)
    current_interval = None

    for row in csv.reader(tsv_file, dialect="excel-tab"):
        if current_interval is None:
            for header, expected_header in zip(row, PROCESS_COLUMNS):
                if header.lower() != expected_header.lower():
                    raise Exception("Unknown header value encountered while processing collectl playback - %s" % header)

            # First row, check contains correct header.
            current_interval = CollectlProcessInterval()
            continue

        if current_interval.row_is_in(row):
            current_interval.add_row(row)
        else:
            process_summarizer.handle_interval(current_interval)
            current_interval = CollectlProcessInterval()

    # Do we have unsummarized rows...
    if current_interval and current_interval.rows:
        process_summarizer.handle_interval(current_interval)

    return process_summarizer.get_statistics()


class CollectlProcessSummarizer(object):

    def __init__(self, pid, statistics):
        self.pid = pid
        self.statistics = statistics
        self.columns_of_interest = set([s[1] for s in statistics])
        self.tree_statistics = collections.defaultdict(stats.StatisticsTracker)
        self.process_accum_statistics = collections.defaultdict(stats.StatisticsTracker)
        self.interval_count = 0

    def handle_interval(self, interval):
        self.interval_count += 1
        rows = self.__rows_for_process(interval.rows, self.pid)
        for column_name in self.columns_of_interest:
            column_index = COLUMN_INDICES[column_name]

            if column_name == "AccumT":
                # Should not sum this across pids each interval, sum max at end...
                for r in rows:
                    pid_seconds = self.__time_to_seconds(r[column_index])
                    self.process_accum_statistics[r[PID_INDEX]].track(pid_seconds)
            else:
                # All other stastics should be summed across whole process tree
                # at each interval I guess.
                if column_name in ["SysT", "UsrT", "PCT"]:
                    to_num = float
                else:
                    to_num = long

                interval_stat = sum(to_num(r[column_index]) for r in rows)
                self.tree_statistics[column_name].track(interval_stat)

    def get_statistics(self):
        if self.interval_count == 0:
            return []

        computed_statistics = []
        for statistic in self.statistics:
            statistic_type, column = statistic
            if column == "AccumT":
                # Only thing that makes sense is sum
                if statistic_type != "max":
                    log.warning("Only statistic max makes sense for AccumT")
                    continue

                value = sum(v.max for v in self.process_accum_statistics.values())
            else:
                statistics_tracker = self.tree_statistics[column]
                value = getattr(statistics_tracker, statistic_type)

            computed_statistic = (statistic, value)
            computed_statistics.append(computed_statistic)

        return computed_statistics

    def __rows_for_process(self, rows, pid):
        process_rows = []
        pids = self.__all_child_pids(rows, pid)
        for row in rows:
            if row[PID_INDEX] in pids:
                process_rows.append(row)
        return process_rows

    def __all_child_pids(self, rows, pid):
        pids_in_process_tree = set([str(self.pid)])
        added = True
        while added:
            added = False
            for row in rows:
                pid = row[PID_INDEX]
                parent_pid = row[PARENT_PID_INDEX]
                if parent_pid in pids_in_process_tree and pid not in pids_in_process_tree:
                    pids_in_process_tree.add(pid)
                    added = True
        return pids_in_process_tree

    def __time_to_seconds(self, minutes_str):
        parts = minutes_str.split(":")
        seconds = 0.0
        for i, val in enumerate(parts):
            seconds += float(val) * (60 ** (len(parts) - (i + 1)))
        return seconds


class CollectlProcessInterval(object):
    """ Represent all rows in collectl playback file for given time slice with
    ability to filter out just rows corresponding to the process tree
    corresponding to a given pid.
    """

    def __init__(self):
        self.rows = []

    def row_is_in(self, row):
        if not self.rows:  # No rows, this row defines interval.
            return True
        first_row = self.rows[0]
        return first_row[0] == row[0] and first_row[1] == row[1]

    def add_row(self, row):
        self.rows.append(row)


def _tuplize_statistic(statistic):
    if not isinstance(statistic, tuple):
        statistic_split = statistic.split("_", 1)
        statistic = (statistic_split[0].lower(), statistic_split[1])
    return statistic


__all__ = ('generate_process_statistics', )
