import threading
import math
import sys
import tables
import numpy
import collections
from . import Drawable

## provide gaussian statistics for a series of measured data points
#
# AccumulatedValue provides mean and error of mean after being fed with measured data
# internally it keeps the sum, the sum of squares and the number of data points
class AccumulatedValue:

    def __init__(self, mean=None, mean_err=None, n=None):
        """
        one value with std. deviation
        can be initialized by:
        No argument: no entries
        one argument: first entry
        two arguments: mean and its error, n is set 2
        three arguments: already existing statistics defined by mean, mean's error, n
        """
        if mean is None:
            self.y=0.0
            self.y2=0.0
            self.n=0
        elif mean_err is None and n is None:
            self.y=float(mean)
            self.y2=self.y**2
            self.n=1
        elif mean_err is None:
            self.n=max(1, int(n))
            self.y=float(mean)*self.n
            self.y2=(float(mean)**2)*self.n
        elif n is None:
            self.n=2
            self.y=float(mean)*2
            self.y2=(float(mean_err)**2+float(mean)**2)*2
        else:
            self.n=int(n)
            self.y=float(mean)*self.n
            self.y2=float(mean_err)**2*n*(n-1.0)+float(mean)**2*n

    def __add__(self,y):
        new_one=AccumulatedValue()
        if isinstance(y, AccumulatedValue):
            new_one.y=self.y+y.y
            new_one.y2=self.y2+y.y2
            new_one.n=self.n+y.n
        else:
            new_one.y=self.y+float(y)
            new_one.y2=self.y2+float(y)**2
            new_one.n=self.n+1
        return new_one

    def __iadd__(self,y):
        if isinstance(y, AccumulatedValue):
            self.y+=y.y
            self.y2+=y.y2
            self.n+=y.n
        else:
            self.y+=float(y)
            self.y2+=float(y)**2
            self.n+=1
        return self

    def copy(self):
        a=AccumulatedValue()
        a.y=self.y
        a.y2=self.y2
        a.n=self.n
        return a

    def mean(self):
        """
        returns the mean of all added/accumulated values
        """
        if self.n is None or self.n==0:
            return None
        else:
            return self.y/self.n

    def sigma(self):
        """
        returns the standard deviation added/accumulated values
        """
        if self.n>1:
            variance=(self.y2-(self.y**2)/float(self.n))/(self.n-1.0)
            if variance<0:
                if variance<-1e-20:
                    print("variance=%g<0! assuming 0"%variance)
                return 0.0
            return math.sqrt(variance)
        elif self.n==1:
            return 0.0
        else:
            return None

    def mean_error(self):
        """
        returns the mean's error (=std.dev/sqrt(n)) of all added/accumulated values
        """
        if self.n>1:
            variance=(self.y2-(self.y**2)/float(self.n))/(self.n-1.0)
            if variance<0:
                if variance<-1e-20:
                    print("variance=%g<0! assuming 0"%variance)
                return 0.0
            return math.sqrt(variance/self.n)
        elif self.n==1:
            return 0.0
        else:
            return None

    def __str__(self):
        if self.n==0:
            return "no value"
        elif self.n==1:
            return str(self.y)
        else:
            return "%g +/- %g (%d accumulations)"%(self.mean(),self.mean_error(),self.n)

    def __repr__(self):
        return str(self)

class MeasurementResult(Drawable.Drawable, collections.UserDict):

    def __init__(self, quantity_name):
        """
        convenient accumulation and interface to plot functions

        The dictionary must not contain anything but AccumulatedValue instances
        """
        Drawable.Drawable.__init__(self)
        collections.UserDict.__init__(self)
        self.quantity_name=quantity_name
        self.lock = threading.RLock()

    # get the selected item, if it does not exist, create an empty one
    def __getitem__(self, key):
        if key not in self:
            a=AccumulatedValue()
            self.data[float(key)]=a
            return a
        else:
            return self.data[float(key)]

    def __setitem__(self,key,value):
        if not isinstance(value, AccumulatedValue):
            value=AccumulatedValue(float(value))
        return collections.UserDict.__setitem__(self,
                                             float(key),
                                             value)

    def __add__(self, right_value):
        if right_value==0:
            return self.copy()
        else:
            raise Exception("not implemented")

    def get_title(self):
        return self.quantity_name

    def get_xdata(self):
        """
        sorted array of all dictionary entries without Accumulated Value objects with n==0
        """
        keys=numpy.array([k for k in list(self.data.keys()) if not (isinstance(self.data[k], AccumulatedValue) and self.data[k].n==0)],
                         dtype="float64")
        keys.sort()
        return keys

    def get_ydata(self):
        return self.get_xydata()[1]

    def get_xydata(self):
        k=self.get_xdata()
        v=numpy.array([self.data[key].mean() for key in k], dtype="float64")
        return [k,v]

    def get_errorplotdata(self):
        k=self.get_xdata()
        v=numpy.array([self.data[key].mean() for key in k], dtype="float64")
        e=numpy.array([self.data[key].mean_error() for key in k], dtype="float64")
        return [k,v,e]

    def get_lineplotdata(self):
        k=self.get_xdata()
        v=numpy.array(self.y, dtype="float64") 
        return [k, v]

    def uses_statistics(self):
        """
        drawable interface method returns True
        """
        return True

    def write_to_csv(self,destination=sys.stdout, delimiter=" "):
        """
        writes the data to a file or to sys.stdout
        destination can be a file or a filename
        suitable for further processing
        """
        if isinstance(destination, str):
            with open(destination, "w") as f:
                self._write_to_csv_dest(f, delimiter)
        else:
            self._write_to_csv_dest(destination, delimiter)

    def _write_to_csv_dest(self, dest, delimiter):
        dest.write("# quantity:"+str(self.quantity_name)+"\n")
        dest.write("# x y ysigma n\n")
        for x in self.get_xdata():
            y = self.data[x]
            if isinstance(y, (float, int)):
                dest.write("%e%s%e%s0%s1\n" % (x, delimiter, y, delimiter, delimiter))
            else:
                dest.write("%e%s%e%s%e%s%d\n" % (x,
                                                 delimiter,
                                                 y.mean(),
                                                 delimiter,
                                                 y.mean_error(),
                                                 delimiter,
                                                 y.n))


    def write_to_hdf(self, hdffile, where, name, title, complib=None, complevel=None):
        h5_table_format= {
            "x" : tables.Float32Col(),
            "y" : tables.Float32Col(),
            "y_err" : tables.Float32Col(),
            "n" : tables.Int64Col()
            }
        filter=None
        if complib is not None:
            if complevel is None:
                complevel=9
            filter=tables.Filters(complevel=complevel,complib=complib,shuffle=1)

        mr_table=hdffile.create_table(where=where,name=name,
                                     description=h5_table_format,
                                     title=title,
                                     filters=filter,
                                     expectedrows=len(self))
        mr_table.flavor="numpy"
        mr_table.attrs.damaris_type="MeasurementResult"
        self.lock.acquire()
        try:
            mr_table.attrs.quantity_name=self.quantity_name
            row=mr_table.row
            xdata=self.get_xdata()
            if xdata.shape[0]!=0:
                for x in self.get_xdata():
                    y=self.data[x]
                    row["x"]=x
                    if type(y) in [float, int, int]:
                        row["y"]=y
                        row["y_err"]=0.0
                        row["n"]=1
                    else:
                        row["y"]=y.mean()
                        row["y_err"]=y.mean_error()
                        row["n"]=y.n
                    row.append()

        finally:
            mr_table.flush()
            self.lock.release()

def read_from_hdf(hdf_node):
    """
    reads a MeasurementResult object from the hdf_node
    or None if the node is not suitable
    """

    if not isinstance(hdf_node, tables.Table):
        return None

    if hdf_node._v_attrs.damaris_type!="MeasurementResult":
        return None

    mr=MeasurementResult(hdf_node._v_attrs.quantity_name)
    for r in hdf_node.iterrows():
        mr[r["x"]]=AccumulatedValue(r["y"],r["y_err"],r["n"])

    return mr
