from __future__ import division, print_function, absolute_import
import numpy as np


class AbstractValueProvider(object):

    def __call__(self, seqlet):
        raise NotImplementedError()

    @classmethod
    def from_hdf5(cls, grp):
        the_class = eval(grp.attrs["class"])
        return the_class.from_hdf5(grp) 


class CoorScoreValueProvider(AbstractValueProvider):

    def __call__(self, seqlet):
        return seqlet.coor.score 

    def save_hdf5(self, grp):
        grp.attrs["class"] = type(self).__name__

    @classmethod
    def from_hdf5(cls, grp):
        return cls()


class TransformCentralWindowValueProvider(AbstractValueProvider):

    def __init__(self, track_name, central_window, val_transformer):
        if isinstance(track_name, str):
            self.track_name = track_name
        else: 
            self.track_name = track_name.decode('utf-8')
        self.central_window = central_window
        self.val_transformer = val_transformer

    def __call__(self, seqlet):
        val = self.get_val(seqlet=seqlet)
        return self.val_transformer(val=val)

    def get_val(self, seqlet):
        flank_to_ignore = int(0.5*(len(seqlet)-self.central_window))
        track_values = seqlet[self.track_name]\
                        .fwd[flank_to_ignore:(len(seqlet)-flank_to_ignore)]
        return np.sum(track_values)

    def save_hdf5(self, grp):
        grp.attrs["class"] = type(self).__name__
        grp.attrs["track_name"] = self.track_name
        grp.attrs["central_window"] = self.central_window
        self.val_transformer.save_hdf5(grp.create_group("val_transformer")) 

    @classmethod
    def from_hdf5(cls, grp):
        if isinstance(grp.attrs["track_name"], str):
            track_name = grp.attrs["track_name"]
        else:
            track_name = grp.attrs["track_name"].decode('utf-8')
        central_window = grp.attrs["central_window"] 
        val_transformer = AbstractValTransformer.from_hdf5(
                             grp["val_transformer"]) 
        return cls(track_name=track_name,
                   central_window=central_window,
                   val_transformer=val_transformer)


class AbstractValTransformer(object):

    def __call__(self, val):
        raise NotImplementedError()

    @classmethod
    def from_hdf5(cls, grp):
        the_class = eval(grp.attrs["class"])
        return the_class.from_hdf5(grp) 


class AbsPercentileValTransformer(AbstractValTransformer):

    def __init__(self, distribution):
        self.distribution = np.array(sorted(np.abs(distribution)))

    @classmethod
    def from_hdf5(cls, grp):
        distribution = np.array(grp["distribution"][:])
        return cls(distribution=distribution) 

    def save_hdf5(self, grp):
        grp.attrs["class"] = type(self).__name__
        grp.create_dataset("distribution", data=self.distribution)

    def __call__(self, val):
        return np.sign(val)*np.searchsorted(
                 a=self.distribution,
                 v=abs(val))/float(len(self.distribution))


class SignedPercentileValTransformer(AbstractValTransformer):

    def __init__(self, distribution):
        self.distribution = np.array(distribution)
        self.pos_dist = np.array(sorted(
            [x for x in self.distribution if x > 0]))
        self.abs_neg_dist = np.array(sorted(
            [abs(x) for x in self.distribution if x < 0]))

    @classmethod
    def from_hdf5(cls, grp):
        distribution = np.array(grp["distribution"][:])
        return cls(distribution=distribution) 

    def save_hdf5(self, grp):
        grp.attrs["class"] = type(self).__name__
        grp.create_dataset("distribution", data=self.distribution)

    def __call__(self, val):
        if (val == 0):
            return 0
        elif (val > 0):
            #add 1E-7 for complicated numerical stability issues 
            # basically need robustness when dealing with ties
            return  np.searchsorted(
                     a=self.pos_dist, v=(val+1E-7))/float(len(self.pos_dist))
        else:
            #add 1E-7 for complicated numerical stability issues 
            # basically need robustness when dealing with ties
            return  np.searchsorted(
                     a=self.abs_neg_dist, v=(abs(val)+1E-7))/float(
                        len(self.abs_neg_dist))
