
import logging
from operator import attrgetter, concat
from collections import namedtuple

import numpy as np
import pandas as pd

from .. import archive

log = logging.getLogger(__name__)

class Monitor( object ):
    """ abstract class keep track of a set of parameters in the model or the
    learning process"""

    ftype_frm = {int : "%d", float : "%.6f", str : "%s"}
    fzero = {int : 0, float : 0.0, str : "NA"}

    @property
    def csv(self, sep=","):
        return sep.join( [self.ftype_frm[t] for t in self._ftypes] ) % self

    @classmethod
    def _empty(cls):
        return tuple.__new__(cls, [cls.fzero[t] for t in cls._ftypes])

    @property
    def report(self):
        return self._report_frm.format(**self._asdict())

    @classmethod
    def _has_changed(cls, what, patience, data, summ_f, cmp_f):
        """compares a summary of the last `patience` values defined by the `what` field
        with the `patience`-1 value"""


        if len(data) < patience + 1:
            raise ValueError("not enough history (%d) for this patience (%d)" % (len(data), patience))

        what_f = attrgetter(what)

        ##extract the values defined by `what` from the last patience epochs
        patience_best = summ_f(map(what_f, data[-patience:]))
        ##extract the value just before the last `patience` items
        history_mark = what_f(data[-patience-1])

        assert type(patience_best) == type(history_mark)
        log.debug("%f, [%f]" % (history_mark, patience_best))

        return cmp_f(patience_best, history_mark)

    @classmethod
    def rel_diff(cls, init, final):
        "(final-init)/max(final, init)"

        M = float( max(final, init) )
        if M:
            return (final-init)/M
        return 0.

    @classmethod
    def is_min_up(cls, what, memory, data):
        return cls._has_changed(what, memory, data, min, lambda summarized, mark: summarized > mark)

    @classmethod
    def is_min_down(cls, what, memory, data):
        return cls._has_changed(what, memory, data, min, lambda summarized, mark: summarized < mark)

    @classmethod
    def is_min_still(cls, what, memory, data):
        return cls._has_changed(what, memory, data, min, lambda summarized, mark: summarized == mark)

    @classmethod
    def is_max_up(cls, what, memory, data):
        return cls._has_changed(what, memory, data, max, lambda summarized, mark: summarized > mark)

    @classmethod
    def is_max_down(cls, what, memory, data):
        return cls._has_changed(what, memory, data, max, lambda summarized, mark: summarized < mark)

    @classmethod
    def is_max_still(cls, what, memory, data):
        return cls._has_changed(what, memory, data, max, lambda summarized, mark: summarized == mark)

    @classmethod
    def _dump(cls, path, data):
        with open(path, 'w') as fd:
            log.info("saving %s log to %s ..." % (str(cls), fd.name))
            fd.write(",".join( cls._fields )+"\n")
            map(lambda r: fd.write("%s\n" % r.csv), data)

    @classmethod
    def _archive_dump(cls, path, train_run_name, data):
        cls_name = str(cls).split(".")[-1][:-2]
        key = "%s/%s/%s" % (archive.basename(path), train_run_name, cls_name)
        obj = pd.DataFrame.from_records(data, columns=cls._fields)

        archive.save_object( archive.archname(path), key, obj )


class DaeLearnMonitor(Monitor, namedtuple("learnmonitor", "epoch lrate traincost validcost")):
    _ftypes = (int, float, float, float)
    _report_frm = ("Epoch {epoch} / lrate: {lrate:.6f}\n\t"
            "Cost: {traincost:.6f} / {validcost:.6f} \n\t")

    @classmethod
    def _from_fs(cls, ds, mtr, epoch, cost_f):
        train_batches = ds.train_batches
        valid_batches = ds.valid_batches

        ct = np.array( map(cost_f, train_batches) ).mean()
        cv = np.array( map(cost_f, valid_batches) ).mean()

        return cls._make( [epoch, mtr.lr, ct, cv] )


class LearnMonitor(Monitor, namedtuple("learnmonitor", "epoch lrate traincost trainCE trainMC validcost validCE validMC") ):
    """learning stats"""

    _ftypes = (int, float,
            float, float,
            float, float,
            float, float)
    _report_frm = ("Epoch {epoch} / lrate: {lrate:.6f}\n\t"
            "Cost: {traincost:.6f} / {validcost:.6f} \n\t"
            "Cross entropy: {trainCE:.6f} / {validCE:.6f} \n\t"
            "Missclass.: {trainMC:.2%} / {validMC:.2%}")

    @classmethod
    def _from_fs(cls, ds, mtr, epoch, cost_f, ce_f, mcl_f):
        train_batches = ds.train_batches
        valid_batches = ds.valid_batches

        train_s = ds.batch_size * len(ds.train_batches)
        valid_s = ds.batch_size * len(ds.valid_batches)
        a = [np.mean(np.array(map(cost_f, train_batches))),
                np.mean(np.array(map(ce_f, train_batches))),
                sum(map(mcl_f, train_batches)) / float(train_s),

                np.mean(np.array(map(cost_f, valid_batches))),
                np.mean(np.array(map(ce_f, valid_batches))),
                sum(map(mcl_f, valid_batches)) / float(valid_s)]

        return cls._make( [epoch, mtr.lr] + a )

class RegrLearnMonitor(Monitor, namedtuple("learnmonitor", "epoch lrate traincost validcost") ):
    """learning stats for a linear regression """

    _ftypes = (int, float, float, float)
    _report_frm = ("Epoch {epoch} / lrate: {lrate:.6f}\n\t"
            "Cost: {traincost:.6f} / {validcost:.6f}")

    @classmethod
    def _from_fs(cls, ds, mtr, epoch, cost_f, train_batches, valid_batches):
        a = [np.mean(np.array(map(cost_f, train_batches))),
                np.mean(np.array(map(cost_f, valid_batches)))]
        return cls._make( [epoch, mtr.lr] + a )


class WeightMonitor( Monitor, namedtuple("w_monitor", "epoch layer wshp wmin wmean wmedian wsd wmax bshp bmin bmean bmedian bsd bmax") ):
    """weight and activity information on the network"""

    _ftypes = (int, int,
            str, float, float, float, float, float,
            str, float, float, float, float, float)
    _report_frm = ("{epoch:d} {layer:d} "
            "W({wshp:s}): {wmin:.6f} {wmean:.6f} {wmedian:.6f} {wsd:.6f} {wmax:.6f}\n\t"
            "B({bshp:s}): {bmin:.6f} {bmean:.6f} {bmedian:.6f} {bsd:.6f} {bmax:.6f}")

    @classmethod
    def _from_model(cls, epoch, l, model):
        def stat_record(a):
            return map(lambda f: f(a), (np.min, np.mean, np.median, np.std, np.max))

        def shape_str(a):
            return str(np.prod(a.shape))

        w_lst = model[l].get_weights()
        w_shapes = map(shape_str, w_lst)
        w_stats = map(stat_record, w_lst)
        w_rec = map(lambda (a,b): [a] + b, zip(w_shapes, w_stats))

        return cls._make( [epoch, l] + reduce(concat, w_rec) )


if __name__ == "__main__":
    for C in (WeightMonitor, LearnMonitor):
        assert len(C._ftypes) == len(C._fields), "%s) C._ftypes %d != C._fields = %d" % (str(C), len(C._ftypes) , len(C._fields))



