#  Copyright (c) 2014-2019, Centre for Genomic Regulation (CRG).
#  Copyright (c) 2014-2019, Jose Espinosa-Carrasco and the respective authors.
#
#  This file is part of Pergola.
#
#  Pergola is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  Pergola is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with Pergola.  If not, see <http://www.gnu.org/licenses/>.

"""
======================
Module: pergola.tracks
======================

.. module:: tracks

This module  provides the structures to keep the data in genomic format.
It provides a generic class :class:`~pergola.tracks.GenomicContainer` which has 
the general attributes and methods shared by all the other subclasses. 

These subclasses provide special features for each type of data and are:
 
:py:class:`~pergola.tracks.Track` objects are generated by
:func:`pergola.intervals.IntData.read` and hold the parsed and manipulated
data as an iterator and the attributes generated when reading file 

:py:class:`~pergola.tracks.Bed` :class:`~pergola.tracks.GenomicContainer` class 
for Bed files.

:py:class:`~pergola.tracks.BedGraph` :class:`~pergola.tracks.GenomicContainer` 
class for BedGraph files.

"""
from __future__ import print_function

from os         import getcwd
from sys        import stderr, exit
from os.path    import join
from operator   import itemgetter
from itertools  import groupby
from numpy      import arange
import tempfile
from pybedtools import BedTool
from ntpath import split as path_split

## Contains class and file extension
_dict_file = {'bed' : ('Bed', 'track_convert2bed', '.bed'),              
              'bedGraph': ('BedGraph', 'track_convert2bedGraph', '.bedGraph'),
              'gff': ('Gff', 'track_convert2gff', '.gff'),
              'txt': ('Track', '', '.txt')}

# From light to dark
# n_interval = 9

# _black_gradient = ["226,226,226", "198,198,198", "170,170,170", "141,141,141", "113,113,113", "85,85,85", "56,56,56", "28,28,28", "0,0,0"]
# _blue_gradient = ["224,255,255", "173,216,230", "135,206,250", "135,206,235", "65,105,225", "0,0,255", "0,0,205", "0,0,139", "0,0,128"] 
# _red_gradient = ["254,172,182", "254,153,162", "254,134,142", "254,115,121", "254,96,101", "254,77,81", "254,57,61", "254,38,40", "254,19,20"]
# _green_gradient = ["203,254,203", "178,254,178", "152,254,152", "127,254,127", "102,254,102", "76,254,76", "51,254,51", "0,254,0", "25,115,25"]
# _orange_gradient = ["255,223,0", "255,240,0", "255,220,0", "255,200,0", "255,180,0", "255,160,0", "255,140,0", "250,115,0", "255,100,0"]

n_interval = 4

## short gradients
_black_gradient = ["170,170,170", "113,113,113", "85,85,85", "56,56,56", "28,28,28", "0,0,0"]
_blue_gradient = ["135,206,250", "65,105,225", "0,0,255", "0,0,205", "0,0,139", "0,0,128"] 
_red_gradient = ["254,134,142", "254,96,101", "254,77,81", "254,57,61", "254,38,40", "254,19,20"]
_green_gradient = ["152,254,152", "102,254,102", "76,254,76", "51,254,51", "0,254,0", "25,115,25"]
_orange_gradient = ["255,220,0", "255,180,0", "255,160,0", "255,140,0", "250,115,0", "255,100,0"]
_yellow_gradient = ["255,255,0", "255,255,31", "255,255,51", "255,255,102", "255,255,153", "255,255,204"]
_pink_gradient = ["229,0,174", "234,38,190", "239,77,206", "244,116,222", "246,135,230", "254,194,254"]
_cyan_gradient = ["0,186,229", "38,199,234", "58,205,236", "116,225,244", "135,231,246", "174,244,251"]

_dict_colors = {
                'black' : _black_gradient,
                'blue' : _blue_gradient,
                'red' : _red_gradient,
                'green' : _green_gradient,
                'orange' : _orange_gradient,
                'yellow' : _yellow_gradient,
                'pink' : _pink_gradient,
                'cyan': _cyan_gradient
                }

# _intervals = [0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 1, 1000] #del

_max_file_name_len = 100


class GenomicContainer(object):
    """
    This class provides the general attributes and methods shared by all the other
    subclasses that contain the data after being read :func:`pergola.intervals.IntData.read`
    or converted by :py:func:`pergola.tracks.Track.convert`

    .. attribute:: data
    
       Iterator yielding record of the genomic data
    
    .. attribute:: fields
    
       List of name of each of the fields contained in records of data
    
    .. attribute:: format
       
       Format to write file (extension) 
    
    .. attribute:: track
       
       String with the name of the track
    
    .. attribute:: range_values
       
       Range of values inside data_value field
       
    ..
       Indicates the presence of a header.
       * `False` if there is no header. Fields should the be provided using fields param
       * `True` if the file have a header line with names. This names should match names in ontology_dict (default).
       
    :returns: GenomicContainer object
        
    """

    def __init__(self, data, fields=None, data_types=None, **kwargs):
        if isinstance(data,(tuple)):            
            data = iter(data)
        
        if not fields:
            raise ValueError("Must specify a 'fields' attribute for %s." % self.__str__())
        
        self.data = data
        self.fields = fields
        self.data_types = data_types     
        self.format = kwargs.get("format",'txt')
        self.track = kwargs.get('track', "1")
        self.range_values = kwargs.get('range_values', None)
        
    def __iter__(self):
        return self.data

    def next(self):
        """        
        :returns: next item in iterator
         
        """

        return next(self.data)
    
    def save_track(self, mode="w", path=None, name_file=None, track_line=True, bed_label=False, gff_label=False):
        """
        Save the data in a file of format set by *self.format* 
        
        :param "w" mode: :py:func:`str` Only implemented write 'w' (default). 
        
        :param None path: Path to create file, py:func:`str`. If None (default) the 
            file is dumped in the current working directory and prints a warning.  
        
        :param None track: Path to create file, py:func:`str`. If None (default) the 
            file is dumped in the current working directory and prints a warning.
            
        :param True track_line: If it is set to True includes the track_line 
        
        :param None name_file: :py:func: `str` to set name of output file
        
        :param False bed_label: Whether to include or not the labels of each interval, 
            default False in bed files
        
        :returns: Void
        
        """
        
        if not path: 
            pwd = getcwd()
            print("No path selected, files dump into path: ", pwd, file=stderr) 
        else:
            pwd = path
            print("Files dump into path: ", pwd, file=stderr)
         
                             
        if not(isinstance(self, GenomicContainer)):
            raise Exception("Not writable object, type not supported '%s'."%(type(self)))    
        
        try:
            file_ext = _dict_file.get(self.format)[2]      
        except KeyError:
            raise ValueError("File types not supported \'%s\'"%(self.format))
        
        if name_file is None:
            conc_data_types = self.data_types
            if isinstance(conc_data_types, set):
                conc_data_types="_".join(self.data_types)        
            
            if len ("tr_" + self.track + "_dt_" + conc_data_types + file_ext) < _max_file_name_len: 
                name_file = "tr_" + self.track + "_dt_" + conc_data_types + file_ext
            else:
                name_file = "tr_" + self.track + "all_data_types" +  file_ext 
                
        else:
            if not name_file.endswith('.tmp'):
                name_file = name_file + file_ext
                
        print("File %s generated" % name_file, file=stderr)       

        track_file = open(join(pwd, name_file), mode)
                
        ## Annotation track to set the genome browser interface
        annotation_track = ''
        data_out = []
        
        if self.format == 'bed' and track_line:
            annotation_track = 'track ' + 'name=\"' +  self.track + "_" + self.data_types + '\"' + " " + 'description=\"' + self.track + " " + self.data_types + '\"' + " " + "visibility=2 itemRgb=\"On\" priority=20"
            track_file.write (annotation_track + "\n")
            
        elif self.format == 'bedGraph' and track_line:
            annotation_track = 'track ' + 'name=\"' + self.track + "_" + self.data_types + '\"' + " " + 'description=\"' + self.track + "_" + self.data_types + '\"' + " " + 'visibility=full color=' + self.color_gradient[n_interval-1] + ' altColor=' + self.color_gradient[n_interval] + ' priority=20'        #                         
            track_file.write (annotation_track + "\n")        
        
        if self.format == 'gff':
            file_format_line = '##gff-version 3'
            track_file.write (file_format_line + "\n")
            track_file.write ('##sequence-region 1' + "\t" + "1"  "\t" + "1" + "\t" + "50" +  "\n")

        data_out = sorted(self.data, key=itemgetter(self.fields.index('start')))
                
        # for row in self.data:
        for row in data_out:  
            
            if self.format == 'bed' and not bed_label:
                index_name = self.fields.index ('name')                 
                empty_label = "."
                row = [empty_label if (i == index_name) else (v) for (i, v) in enumerate(row)]
            
            elif self.format == 'gff' and not bed_label:
                index_name = self.fields.index ('feature')                 
                empty_label = "."
                row = [empty_label if (i == index_name) else (v) for (i, v) in enumerate(row)]
                    
            track_file.write('\t'.join(str(v) for v in row))           
            track_file.write("\n")
                  
        track_file.close()
    
    
#     def _tmp_bed(self):
#         tmp_bed = tempfile.NamedTemporaryFile(prefix='pergola.',
#                                             suffix='.tmp',
#                                             delete=False)
#         tmp_bed = tmp_bed.name
#         return tmp_bed
    
    # Quiza mejor hacerlo fuera porque asi puedo hacer excepciones en plan no se ha instaldo pybedtools
    # sino tengo dependencia con pybedtools 
#     def create_pybedtools(self): #del
#         f_bed = self._tmp_bed()
#         path_bed, name_bed = path_split(f_bed)
#         self.save_track(path = path_bed, name_file=name_bed, bed_label=True)
#         return BedTool(f_bed)
    
    
class Track(GenomicContainer):
    """
    This class contains all the methods to operate and convert the data list into the 
    different possible classes
    
    .. attribute:: data

       Iterator yielding record of the genomic data

    .. attribute:: fields

       List of name of each of the fields contained in records of data

    .. attribute:: data_types

       List of data types contained in the data 

    .. attribute:: list_tracks

       List of tracks contained in the data 

    .. attribute:: min

       Minimum value contained in the data
    
    .. attribute:: max

       Maximum value contained in the data
    
    :returns: Dictionary of Bed or BedGraph objects
    
    """

    def __init__(self, data, fields=None, data_types=None, list_tracks=None, min=0, max=0, **kwargs):
        self.list_tracks = list_tracks
        self.list_tracks_filt = []
        self.list_data_types = data_types
        self.min = min
        self.max = max

        GenomicContainer.__init__(self, data, fields, data_types, **kwargs)
        
    def convert(self, mode='bed', range_color=None, **kwargs):
        """
        Calls function to convert data (as a list of tuples) into a dictionary of 
        the one or several object of class set by mode
        
        :param bed mode: class of the output objects returned set to `bed` by
            default
            
        :param range_color: :py:func:`list` with range of values to set colors 
            of bed file. If set modifies self.range_values
            
        :param tracks2remove: :py:func:`list` of tracks to remove from the dict_t
        
        :returns: dictionary containing object/s of the class set by mode 
        
        """

        kwargs['relative_coord'] = kwargs.get("relative_coord", False)
        
        if mode not in _dict_file: 
            raise ValueError("Mode \'%s\' not available. Possible convert() modes are %s"%(mode,', '.join(['{}'.format(m) for m in _dict_file.keys()])))
        
        # User set values for bed file colors
        if range_color:
            if (len(range_color) != 2 or not(all(isinstance(n, (int, float)) for n in range_color))):
                raise ValueError ("Range color must be a list with two numeric values" \
                                % (range_color))
        
            self.range_values = range_color

        dict_tracks = (self._convert2single_track(self.data, mode, **kwargs)) 
        
        return (dict_tracks)
        
    def _convert2single_track(self, data_tuples,  mode=None, **kwargs):
        """
        Split data (as a list of tuples) into one or several objects depending on options 
        selected. Each object will correspond to a track in the genome browser
        
        :param data_tuples: list of tuples containing data
        :param None mode: class of each single track that will be hold in dictionary
            
        :returns: dictionary of tracks
        
        """
           
        dict_split = {}
        
        ### Data is separated by track and data_types
        idx_fields2split = [self.fields.index("track"), self.fields.index("data_types")]
        data_tuples = sorted(data_tuples, key=itemgetter(*idx_fields2split))
        
        for key,group in groupby(data_tuples, itemgetter(*idx_fields2split)):
            if key[0] not in dict_split:
                dict_split [key[0]] = {}
            dict_split [key[0]][key[1]] = tuple(group)
        
        #Generates dictionary of original fields and color gradients
        color_restrictions = kwargs.get('color_restrictions', None)
        _dict_col_grad = dict()
        
        self.list_tracks_filt = self.list_tracks
        
        ### Tracks not set in tracks option are filtered out
        sel_tracks = []
        if not kwargs.get('tracks'):
            pass
        else:
            sel_tracks = map(str, kwargs.get("tracks",[]))
                   
        ### When any tracks are selected we consider that no track should be removed
        if sel_tracks != []:            
            ori_tracks = set(self.list_tracks)
            tracks2rm = self.list_tracks.difference(sel_tracks)
#             tracks2rm = sel_tracks                           
            dict_split = self.remove (dict_split, tracks2rm)
            print("Removed tracks are:", ' '.join(sorted(tracks2rm, key=int)), file=stderr)
            
            # I have to keep the original list otherwise the original object is changed
            self.list_tracks = ori_tracks
            
        # Eventually I can eliminate tracks of not selected tracks
#         self.data = [y for y in data_tuples if y[0] in self.list_tracks]
        
        sel_data_types = []
        if not kwargs.get('data_types'):
            pass
        else:            
            sel_data_types = map(str, kwargs.get("data_types",[]))
        
        new_dict_split = {} 
           
        ### When any data_types are selected we consider that no data_types should be removed
        if sel_data_types  != []:
            data_types2rm = self.list_data_types.difference(sel_data_types)
            for track, track_dict in dict_split.items():    
                dict_data_type = self.remove (track_dict, data_types2rm)        
                new_dict_split [track] = dict_data_type
            print("Removed data types are:", ' '.join(data_types2rm), file=stderr)        

        if new_dict_split: 
            dict_split = new_dict_split
                
        d_track_merge = {} 
        
        ### If tracks_merge is set we combine tracks selected                 
        if not kwargs.get('tracks_merge'):
            d_track_merge = dict_split
        else:
#             tracks_merge = kwargs.get('tracks_merge',self.list_tracks)
            tracks_merge = kwargs.get('tracks_merge',self.list_tracks_filt)
#             if not all(tracks in self.list_tracks for tracks in tracks_merge):
            if not all(tracks in self.list_tracks_filt for tracks in tracks_merge):
                raise ValueError ("Tracks to merge: %s, are not in the track list: " % 
                                  ",".join("'{0}'".format(n) for n in tracks_merge), 
                                  ",".join("'{0}'".format(n) for n in self.list_tracks_filt))
            print(("Tracks that will be merged are: %s" %  " ".join(tracks_merge)), file=stderr)
            
            d_track_merge = self.join_by_track(dict_split, tracks_merge)       
        
        d_data_types_merge = {}
        
        ### If set we join the data_types or natures
        if not kwargs.get('data_types_actions') or kwargs.get('data_types_actions') == 'one_per_channel':
            d_data_types_merge = d_track_merge
        elif kwargs.get('data_types_actions') == 'all':
            d_data_types_merge = self.join_by_dataType(d_track_merge, mode)
        
        if mode == 'bedGraph':     
            _dict_col_grad = assign_color (self.data_types, color_restrictions)
         
        track_dict = {}                        
   
        ### Generating track dict (output)                         
        window = kwargs.get("window", 300)
        mean_win = kwargs.get("mean_win", False)
        mean_value = kwargs.get("mean_value", False)

        ### Assigning data to output dictionary
        for k, d in d_data_types_merge.items():
            if not isinstance(d,dict):
                raise ValueError ("The structure that holds the tracks should be a dictionary of dictionaries")
            
            for k_2, d_2 in d.items():
                if not k_2 in _dict_col_grad and mode == "bed" or mode == "gff":
                    _dict_col_grad[k_2] = ""
                
                range_val = self._get_range(d_2)

                i_chr_start = self.fields.index("start")
                i_chr_end = self.fields.index("end")
                min_time = kwargs.get('min_time', self.min)
                max_time = kwargs.get('max_time', self.max)

                d_2 = [r for r in d_2 if r[i_chr_start] >= min_time and r[i_chr_end] <= max_time]

                if d_2:
                    track_dict[k,k_2] = globals()[_dict_file[mode][0]](getattr(self,_dict_file[mode][1])(d_2,
                                                                                                         True,
                                                                                                         window=window,
                                                                                                         mean_win=mean_win,
                                                                                                         mean_value=mean_value,
                                                                                                         color_restrictions=color_restrictions,
                                                                                                         min_t = self.min,
                                                                                                         max_t = self.max,
                                                                                                         min_time=kwargs.get('min_time', self.min),
                                                                                                         max_time=kwargs.get('max_time', self.max)),
                                                                                                         track=k, data_types=k_2,
                                                                                                         range_values=range_val,
                                                                                                         color=_dict_col_grad[k_2])
                else:
                    print(("WARNING: Processed track %s, %s will be deleted because is empty with current " \
                                      "time boundaries." % (k, k_2)), file=stderr)

        return track_dict
    
    def _get_range(self, data_tr):
        """
        Calculates the range of values in data_value field 
        
        :param data_tr: :py:func:`tuple` of tuples containing data of a single track    
                
        :returns: range_list list with minimum and maximum value in data

        """

        try:
            i_data_value = self.fields.index("data_value")
        except KeyError:
            raise ValueError("Data value index is not set")
        
        min = -10000000
        max = -10000000
        
        for r in data_tr:
            v = r[i_data_value]
            
            if min == -10000000: min = v
            if min > v: min = v
            
            if max == -10000000: max = v
            if max < v: max = v
            
        range_list = [min, max]
           
        return range_list
    
    def remove(self, dict_t, tracks2remove):
        """
        Removes selected tracks from a dictionary of tracks that is the input of the function those that are 
        set by tracks2remove
        
        :param dict_t: py:func:`dict` dictionary containing one or more tracks, keys represent each 
            of these tracks
        :param tracks2remove: :py:func:`list` of tracks to remove from the dict_t
             
        :returns: dict_t dictionary, that contains the remaining tracks
        
        #TODO I can make this function more general as remove from dictionary it can be use outside
        # in fact I am using now it to remove data_types and not only tracks
        
        """

        for key in tracks2remove:
            key = str(key)
    
            dict_t.pop(key, None)
            
#             if key in self.list_tracks:        #OJO
#                 self.list_tracks.remove(key)
            
            if key in self.list_tracks_filt:        
                self.list_tracks_filt.remove(key)
            
#             if key in self.list_data_types:
#                 self.list_data_types.remove(key)
                    
        return (dict_t)
    
    def join_by_track(self, dict_t, tracks2join):  
        """
        Join tracks by track name or id 
        
        :param dict_t: py:func:`dict` containing one or more tracks, keys 
            represent each of these tracks
        :param tracks2join: :py:func:`list` of tracks to join in a single track
             
        :returns: d_track_merge dictionary that contains the joined tracks
        
        TODO What if I give to the function only some tracks to join, what happen
            to the remaining tracks
         
        """
        
        d_track_merge = {} 
        new_tracks = set()
        
        for key, nest_dict in dict_t.items():
            
            if key not in tracks2join: 
                print("Track not use because was not set when join_by_track is called: %s" % key)
                continue
            
            if '_'.join(tracks2join) not in d_track_merge:
                d_track_merge['_'.join(tracks2join)] = {}
                new_tracks.add('_'.join(tracks2join))
            
            for key_2, data in nest_dict.items():                            
                if key_2 not in d_track_merge['_'.join(tracks2join)]:
                    d_track_merge['_'.join(tracks2join)] [key_2]= data
                else:  
                    d_track_merge['_'.join(tracks2join)] [key_2] = d_track_merge['_'.join(tracks2join)] [key_2] + data

#         self.list_tracks = new_tracks
                   
        return (d_track_merge)
    
    
    def join_by_dataType (self, dict_d, mode):
        """
        Join tracks by dataType
        
        :param dict_d: py:func:`dict` containing one or more tracks, primary key 
            are tracks id and secondary tracks are data_types
        :param mode: :py:func:`str` class of the object that is going to be 
            generated
             
        :returns: d_data_types_merge dictionary that contains the joined tracks and
                  the new dictionary with colors assign by data type
         
        """
        d_data_types_merge = {}
        new_data_types = set()
        
        for key, nest_dict in dict_d.items():
            
            d_data_types_merge[key] = {}                        
            
            for key_2, data in nest_dict.items(): 
                
                if '_'.join(nest_dict.keys()) not in d_data_types_merge[key]:
                    d_data_types_merge[key]['_'.join(nest_dict.keys())] = data                    
                    new_data_types.add('_'.join(nest_dict.keys())) 
                else:                    
                    d_data_types_merge[key]['_'.join(nest_dict.keys())] = d_data_types_merge[key]['_'.join(nest_dict.keys())] + data
                    new_data_types.add('_'.join(nest_dict.keys()))          
        
        # New data_types only set if objects is bedGraph. Bed objects needs to
        # know all original data_types to display them with different colors
        if mode == 'bedGraph':
            self.data_types = new_data_types
            
        return (d_data_types_merge)
    
    def track_convert2bed(self, track, in_call=False, **kwargs):
        """
        Converts data belonging to a single track (in the form of a list of tuples) in
        an object of class Bed
        
        :param track: :py:func:`list` of tuples containing data of a single track
        :param False in_call: If False the call to the function is from the user otherwise
            is from inside :py:func: `convert2single_track()`
        :param None color_restrictions: Set colors not to be used #TODO this is not clear example??        
                
        :returns: Bed object
        
        """        

        # This fields are mandatory in objects of class Bed
        _bed_fields = ["track","start","end","data_types", "data_value"]        
        
        # Check whether these fields are in the original otherwise raise exception
        try:
            [self.fields.index(f) for f in _bed_fields]
        except ValueError:
            raise ValueError("Mandatory field for Bed object creation '%s' is missing." % (f))

#         if (not in_call and len(self.list_tracks) != 1):
        if (not in_call and len(self.list_tracks_filt) != 1):
            raise ValueError("Your file '%s' has more than one track, only single tracks can be converted to bed" % (self.path))
        
        i_track = self.fields.index("track")
        i_chr_start = self.fields.index("start")
        i_chr_end = self.fields.index("end")
        i_data_value = self.fields.index("data_value")
        i_data_types = self.fields.index("data_types")
        
        #Generate dictionary of field and color gradients
        color_restrictions = kwargs.get('color_restrictions', None)
        _dict_col_grad = assign_color (self.data_types, color_restrictions)
        
        step = (float(self.range_values[1]) - float(self.range_values[0])) / n_interval

        if step == 0: 
            _intervals = [0, self.range_values[1]]
        else: 
            _intervals = list(arange(float(self.range_values[0]), float(self.range_values[1]), step))

        for row in track:
            temp_list = []
            temp_list.append("chr1")
            temp_list.append(row[i_chr_start])
            temp_list.append(row[i_chr_end])
            temp_list.append(row[i_data_types])  
            temp_list.append(row[i_data_value])   
            temp_list.append("+")
            temp_list.append(row[i_chr_start])
            temp_list.append(row[i_chr_end])
            
            if step != 0:
                for i,v in enumerate(_intervals):    
                    d_type = row [self.fields.index("data_types")]
                    global color
                    color = _dict_col_grad[d_type][len(_intervals)-1]

                    if float(row[i_data_value]) <= v:                    
                        color = _dict_col_grad[d_type][i]                               
                        break
            else:
                 d_type = row [self.fields.index("data_types")]
                 # global color
                 color = _dict_col_grad[d_type][-1]
                 
            temp_list.append(color)          
            
            yield(tuple(temp_list))
    
    def track_convert2gff(self, track, in_call=False, **kwargs):
        """
        Converts data belonging to a single track (in the form of a list of tuples) in
        an object of class Gff
        
        :param track: :py:func:`list` of tuples containing data of a single track
        :param False in_call: If False the call to the function is from the user otherwise
            is from inside :py:func: `convert2single_track()`
        :param None color_restrictions: Set colors not to be used #TODO this is not clear example??        
                
        :returns: Gff object
        
        """        

        #This fields are mandatory in objects of class Bed
#         _bed_fields = ["track","chrom_start","chrom_end","data_types", "data_value"]        

#         _gff_fields = ['seqname','source','feature','start','end','score', 'strand','frame','attribute']
        
        ## Following specifications of:
        ## http://gmod.org/wiki/GFF3
#         _bed_fields = ["track","chrom_start","chrom_end","data_types", "data_value"]        
        
#         _gff_fields_mand = ['seqname','source','type','start','end','score', 'strand','frame','attribute']
        _gff_fields_mand = ["track", "data_types", "start","end", "data_value"] 
#         _gff_fields_mand = ["seqname", "feature", "chrom_start", "chrom_end", "score"]
            
        '''
        'seqname' track
        'source'  NA NOT MANDATORY "." (a period) in this field.
        'type' "data_types"
        'start'   "start" REQUIRED
        'end'     "end"   REQUIRED
        'score'   "data_value" Not required
        'strand'  NA AND NOT MANDATORY "." (a period) in this field.
        'frame'   NA AND NOT MANDATORY "." (a period) in this field.
        'attribute'  Not required 
        http://gmod.org/wiki/GFF3
        ''' 
        #Check whether these fields are in the original otherwise raise exception
        try:
            [self.fields.index(f) for f in _gff_fields_mand]
        except ValueError:
            raise ValueError("Mandatory field for Gff object creation '%s' is missing." % (f))

#         if (not in_call and len(self.list_tracks) != 1):
        if (not in_call and len(self.list_tracks_filt) != 1):
            raise ValueError("Your file '%s' has more than one track, only single tracks can be converted to bed" % (self.path))
        """
        i_track = self.fields.index("track")
        i_chr_start = self.fields.index("start")
        i_chr_end = self.fields.index("end")
        i_data_value = self.fields.index("data_value")
        i_data_types = self.fields.index("data_types")
        """

#         i_seqname = self.fields.index("seqname")
#         i_types = self.fields.index("feature")        
#         i_start = self.fields.index("chrom_start")
#         i_end = self.fields.index("chrom_end")        
#         i_score = self.fields.index("score")        
        i_seqname = self.fields.index("track")
        i_types = self.fields.index("data_types")        
        i_start = self.fields.index("start")
        i_end = self.fields.index("end")        
        i_score = self.fields.index("data_value") 
        
        #Generate dictionary of field and color gradients
        color_restrictions = kwargs.get('color_restrictions', None)
        _dict_col_grad = assign_color (self.data_types, color_restrictions)
        
        # Step for the color gradient
        step = (float(self.range_values[1]) - float(self.range_values[0])) / n_interval

        if step == 0: 
            _intervals = [0, self.range_values[1]]
        else: 
            _intervals = list(arange(float(self.range_values[0]), float(self.range_values[1]), step))
        
        for row in track:
            temp_list = []
#             temp_list.append(row[i_seqname]) # "seqid"
            temp_list.append(1)  # "seqid"
            temp_list.append(".")  # "source"
#             temp_list.append(row[i_types]) #"type"
            temp_list.append("exon")  # "type"
            temp_list.append(row[i_start] + 1)  # start
            temp_list.append(row[i_end] + 1)  # end
            temp_list.append(row[i_score])  # "score"
            temp_list.append(".")  # "strand"
            temp_list.append(".")  # phase
            
            if step != 0:
                for i,v in enumerate(_intervals):    
                    d_type = row [self.fields.index("data_types")]
                    global color
                    color = _dict_col_grad[d_type][len(_intervals)-1]

                    if float(row[i_score]) <= v:                    
                        color = _dict_col_grad[d_type][i]                               
                        break
            else:
                 d_type = row [self.fields.index("data_types")]
                 # global color
                 color = _dict_col_grad[d_type][-1]
                 
            temp_list.append("color=" + color)          
            
            yield(tuple(temp_list))
            
    def track_convert2bedGraph(self, track, in_call=False, window=300, mean_win=False, mean_value=False,  **kwargs):
        """
        Converts a single data belonging to a single track in a list of tuples in
        an object of class BedGraph. The data is grouped in time windows.
            
        :param track: :py:func:`list` of tuples containing data of a single track
        :param False in_call: If False the call to the function is from the user otherwise
            is from inside :py:func: `convert2single_track()`
        :param window: :py:func:`int` length of windows inside bedGraph file in seconds (default 300)
        :param False mean_win: Calculates average value over the time interval set by window (sum values/length window)
        :param False mean_value: Calculates average value (sum values/count)
        
        :returns: BedGraph object
        """

        #This fields are mandatory in objects of class BedGraph
        _bed_fields = ["track","start","end","data_value"] 
        
        #Check whether these fields are in the original otherwise raise exception
        try:
            idx_f = [self.fields.index(f) for f in _bed_fields]                          
        except ValueError:
              raise ValueError("Mandatory field for BedGraph object creation '%s' is missing." % (f))
        
#         if (not in_call and len(self.list_tracks)  != 1):            
        if (not in_call and len(self.list_tracks_filt)  != 1):
#             raise ValueError("Your file '%s' has more than one track, only single tracks can be converted to bedGraph" % (self.path))
            raise ValueError("Your data has more than one track, only single tracks can be converted to bedGraph.")
        
        i_track = self.fields.index("track")
        i_chr_start = self.fields.index("start")
        i_chr_end = self.fields.index("end")
        i_data_value = self.fields.index("data_value")
        
        ## When the tracks have been join it is necessary to order by chr_start
        track = sorted(track, key=itemgetter(*[i_chr_start]))
        # min_time = kwargs.get('min_time', self.min)
        # max_time = kwargs.get('max_time', self.max)

        ## Filtering by min and max time points
        # track = [row for row in track if row[i_chr_start] >= min_time and row[i_chr_end] <= max_time]

        # Dumping raw data as a bedGraph file, no binning
        if not window or window == 0:  # or false

            for row in track:
                temp_list = []
                temp_list.append("chr1")
                temp_list.append(row[i_chr_start])
                temp_list.append(row[i_chr_end])
    #             temp_list.append(row[i_data_types])
                temp_list.append(row[i_data_value])
    #             temp_list.append("+")
    #             temp_list.append(row[i_chr_start])
    #             temp_list.append(row[i_chr_end])

    #             temp_list.append(color)

                yield(tuple(temp_list))
        
        # binning in windows of time     
        else:
            try:                
                float(window)
            except:               
                raise ValueError("Window option only accepts integers or False. Current value: %s" 
                                % (type(window)))
            
            if float(window) != int(window):
                raise ValueError("Window option only accepts integers or False. Current value: %s" 
                                % (type(window)))
                
            # Ini_window has to be set to the initial value otherwise files with            
            # no relative coordinates contained tones of empty data
    #         ini_window = 0 #ojo
    #         ini_window = 1       
            # I have to find the closest number multiple of the window size so that all 
            # bedGraph have the same intervals
                        
            delta_window = window

            # To read files for instance in GRanges objects all tracks same number of intervals              
            # ini_window_def = divmod(track[0][i_chr_start]/delta_window, 1)[0] * delta_window
            min_t = kwargs.get('min_t', self.min)
            max_t = kwargs.get('max_t', self.max)

            min_time = kwargs.get('min_time', min_t)
            max_time = kwargs.get('max_time', max_t)

            if min_time is not None:
                min_t = min_time
                if min_time < min_t:
                    print(("WARNING: min_time \'%d\' is smaller than minimun time point \'%d\' inside the input file" %(min_time, min_t)), file=stderr)

            if max_time is not None:
                max_t = max_time
                if max_time > max_t:
                    print(("WARNING: max_time \'%d\' is bigger than minimun time point \'%d\' inside the input file" %(max_time, max_t)), file=stderr)

            ini_window = divmod(min_t/delta_window, 1)[0] * delta_window
            end_window = ini_window + delta_window
                        
            partial_value = 0
            counts = 0
            mean_value_l = []
            cross_interv_dict = {}
            
#             last_point =  track[-1][i_chr_end]
            last_point = max_t
            r = last_point % delta_window
            fake_end = last_point # + delta_window - r

            if track:

                last_fake_line = list(track[-1])

                if last_fake_line[i_chr_end] > max_t + 1:
                    exit("FATAL ERROR: Something went wrong during bedGraph window conversion")

                last_fake_line[i_chr_start] = last_fake_line[i_chr_end] + 1
                last_fake_line[i_chr_end] = fake_end
                last_fake_line[i_data_value] = 0
                track.append(tuple(last_fake_line))

                if not r == 1 or r == 0:
                    fake_end = last_point + delta_window - r
                    sec_fake_line = last_fake_line
                    sec_fake_line[i_chr_start] = fake_end + delta_window + 1
                    sec_fake_line[i_chr_end] = fake_end + 2 * delta_window
                    sec_fake_line[i_data_value] = 0
                    track.append(tuple(sec_fake_line))

                for row in track:
                    temp_list = []
                    chr_start = row[i_chr_start]
                    chr_end = row[i_chr_end]
                    data_value = float(row[i_data_value])
                    f = "data_value"
                    self.fields.index(f)

                    # Intervals happening after the current window
                    # if there is a value accumulated it has to be dumped otherwise 0
                    if chr_start > end_window:
                        while end_window < chr_start:
                            partial_value = partial_value + cross_interv_dict.get(ini_window, 0)

                            if mean_value and cross_interv_dict.get(ini_window):
                                mean_value_l.append(cross_interv_dict.get(ini_window, 0))

                            temp_list.append("chr1")
                            temp_list.append(ini_window)
                            temp_list.append(end_window)

                            if mean_win:
                                temp_list.append(partial_value/window)
                            elif mean_value and counts:
                                if float(sum(mean_value_l)) == 0:
                                    temp_list.append(0)
                                else:
                                    temp_list.append(float(sum(mean_value_l)) / float(len(mean_value_l)))
                                # else:
                                #     temp_list.append(partial_value)
                            else:
                                temp_list.append(partial_value)

                            partial_value = 0
                            counts = 0
                            mean_value_l = []
        #                     ini_window += delta_window + 1 #ojo
                            ini_window += delta_window
        #                     end_window += delta_window + 1 #ojo
                            end_window += delta_window

                            if end_window > max_t:
                                break

                            yield(tuple(temp_list))
                            temp_list = []

                        ## Value must to be weighted between intervals
                        if chr_end > end_window:
                            value2weight = data_value
                            end_w = end_window
                            start_new = chr_start
                            end_new = chr_end

                            for start_w in range (int(ini_window), int(chr_end), delta_window):
                                weighted_value = 0.0
                                counts += 1

                                if end_w == start_w:
                                    weighted_value = float(end_w - start_new + 1) / float(end_new - start_new)
                                elif end_new == start_new:
                                    break
                                else:
                                    weighted_value = float(end_w - start_new) / float(end_new - start_new)
                                    # weighted_value= 9/2

                                weighted_value *= value2weight
                                cross_interv_dict[start_w] = float(cross_interv_dict.get(start_w, 0)) + float(weighted_value)
                                start_new = end_w
                                value2weight = value2weight - weighted_value

                                if (end_w + delta_window) >= chr_end:
                                    new_start_w = start_w + delta_window
                                    cross_interv_dict[new_start_w] = cross_interv_dict.get(new_start_w, 0) + value2weight
                                    break

                                end_w = end_w + delta_window
                        else:
                            partial_value = partial_value + data_value

                            if mean_value:
                                counts += 1
                                mean_value_l.append(data_value)

                    # elif chr_start <= end_window and chr_start >= ini_window:
                    elif end_window > chr_start >= ini_window:
                        if chr_end <= end_window:
                            partial_value = partial_value + data_value

                            if mean_value and data_value:
                                mean_value_l.append(data_value)
                                counts += 1

                        else:
                            value2weight = data_value
                            end_w = end_window
                            start_new = chr_start
                            end_new = chr_end

                            for start_w in range (int(ini_window), int(chr_end), delta_window):
                                weighted_value = 0
                                counts += 1

                                if end_w == start_w:
                                    weighted_value = float(end_w - start_new + 1) / float(end_new - start_new)
                                else:
                                    weighted_value = float(end_w - start_new) / float(end_new - start_new)

                                weighted_value *= value2weight
                                cross_interv_dict[start_w] = float(cross_interv_dict.get(start_w,0)) + float(weighted_value)
                                start_new = end_w
                                value2weight = value2weight - weighted_value

                                if end_w + delta_window >= chr_end:
                                    new_start_w = start_w + delta_window
                                    cross_interv_dict[new_start_w] = cross_interv_dict.get(new_start_w,0) + value2weight
                                    break

                                end_w = end_w + delta_window

                    elif chr_start < ini_window:
                        print(("WARNING: Value %d deleted because you set first time point " \
                                          "to a higher value %d") % (chr_start, end_window), file=stderr)
                # else:
                    # print >> stderr, ("FATAL ERROR: Something went wrong during bedGraph window " \
                    #                   "conversion.")

        # Last value just printed out
#         temp_list.append("chr1")        
#         temp_list.append(ini_window)
#         temp_list.append(end_window)
#         temp_list.append(data_value)
#         yield(tuple(temp_list))

class BedToolConvertible(GenomicContainer):
    def __init__(self, data, **kwargs):
        # GenomicContainer.__init__(self,data,**kwargs)
        # GenomicContainer.__init__(self, data, fields, data_types, **kwargs)
        GenomicContainer.__init__(self, data, **kwargs)
    
    def create_pybedtools(self):
        """
        Converts a single data belonging to a single track in a list of tuples in
        an object of class BedGraph. The data is grouped in time windows.
            
        :param track: :py:func:`list` of tuples containing data of a single track
        :param False in_call: If False the call to the function is from the user otherwise
            is from inside :py:func: `convert2single_track()`
        :param window: :py:func:`int` length of windows inside bedGraph file in seconds (default 300)
                 
        :returns: BedGraph objectx
        """
        track_tmp = self._tmp_track()
        
        if not(isinstance(self, GenomicContainer)):
            raise Exception("Object \'%s\' can not be converted to BedTool" % (type(self)))    
        
        try:
            file_ext = _dict_file.get(self.format)[2]      
        except KeyError:
            raise ValueError("File types \'%s\' not convertible to BedTool" % (self.format))
        
        data_tmp = []
        data_tmp = sorted(self.data, key=itemgetter(self.fields.index('start')))
        
        with open(track_tmp, "w") as bedtool_out:         
            for r in data_tmp:  
                bedtool_out.write('\t'.join(str(f) for f in r))           
                bedtool_out.write("\n")
        
        bedtool_obj = BedTool(track_tmp)            
        
        return(bedtool_obj)
        
    def _tmp_track(self):
        """
        TODO: comment
        """
        tmp_track = tempfile.NamedTemporaryFile(prefix='pergola.', suffix='.tmp', delete=False)
        tmp_track = tmp_track.name
        
        return tmp_track    
            
        
                              
# class Bed(GenomicContainer):
class Bed(BedToolConvertible):
    """
    A :class:`~pergola.tracks.GenomicContainer` object designed to include specific 
    fields and features of **bed files**
    
    Default fields are
        ::
        
         ['chr','start','end','name','score','strand',
          'thick_start','thick_end','item_rgb']
    
    :returns: Bed object    
        
    """
    def __init__(self, data, **kwargs):
        kwargs['format'] = 'bed'
        
        # 
        kwargs['fields'] = ['chr','start','end','name','score','strand',
                            'thick_start','thick_end','item_rgb']
        
#         GenomicContainer.__init__(self,data,**kwargs)
        BedToolConvertible.__init__(self,data,**kwargs)
        
        
#     def _tmp_bed(self):
#         tmp_bed = tempfile.NamedTemporaryFile(prefix='pergola.',
#                                             suffix='.tmp',
#                                             delete=False)
#         tmp_bed = tmp_bed.name
#         return tmp_bed
#     
#     # Quiza mejor hacerlo fuera porque asi puedo hacer excepciones en plan no se ha instaldo pybedtools
#     # sino tengo dependencia con pybedtools 
#     def create_pybedtools(self):
#         f_bed = self._tmp_bed()
#         path_bed, name_bed = path_split(f_bed)
#         self.save_track(path = path_bed, name_file=name_bed, bed_label=True)
#         return BedTool(f_bed)

class BedGraph(BedToolConvertible):
# class BedGraph(GenomicContainer):
    """
    A :class:`~pergola.tracks.GenomicContainer` object designed to include specific 
    fields and features of **bedGraph files**
    
    .. attribute:: color
       Gradient of colors that assign by value to display in the genome browser
       
    Default fields are
        ::
        
         ['chr','start','end', 'score']
    
    :returns: BedGraph object  
          
    """

    def __init__(self, data, **kwargs):
        kwargs['format'] = 'bedGraph'
        kwargs['fields'] = ['chr','start','end','score']        
        self.color_gradient = kwargs.get('color',_blue_gradient)
#         GenomicContainer.__init__(self,data,**kwargs)
#         BedToolConvertible.__init__(self, **kwargs)
        BedToolConvertible.__init__(self,data, **kwargs)
        
    def win_mean (self):
        n_tracks = len (self.track.split("_"))

        # TODO if number of tracks is 1 exit returning self
        self.data = self._win_mean(self.data, n_tracks)
        return (self)
    
    def _win_mean (self, data, n_tracks):     
        for row in data:
            temp_list = []
            for i, v in enumerate(row):
                
                if i == 3:
                    temp_list.append (v/n_tracks)
                else:
                    
                    temp_list.append (v)
                
            yield (tuple(temp_list))


class Gff(BedToolConvertible):
    """
    A :class:`~pergola.tracks.GenomicContainer.BedToolConvertible` object designed 
    to include specific fields and features of **gff files**
    
    Default fields are
        ::
        
         ['seqname','source','feature','start','end','score',
          'strand','frame','attribute']
    
    :returns: Gff object    
        
    """
    def __init__(self, data, **kwargs):
        kwargs['format'] = 'gff'

        kwargs['fields'] = ['seqname','source','feature','start','end','score',
                            'strand','frame','attribute'] 
        
#         ['chr','chrom_start','chrom_end','name','score','strand',
#                             'thick_start','thick_end','item_rgb']

        BedToolConvertible.__init__(self,data,**kwargs)


def assign_color(set_data_types, color_restrictions=None):
    """
    Assign colors to fields randomly. It is optional to set given color to given 
    data_types,  a restricted color will no be used for the remaining data_types.
    
    :param set_data_types: :py:func:`set` containing data_types names that should 
        be linked to color gradient
    :param color_restrictions: A :py:func:`dict` that has as key data_types and as  
        values colors that are set by the user          
        ::  
        
            {'dataType': 'black'}
            
        Possible color gradients are
        ::
         
             'black', 'blue', 'red', 'green', 'orange'
    
    :returns: d_dataType_color dictionary with data_types as keys and color gradients
        as values
    
    """
    d_dataType_color = {}
    colors_not_used = []
    
    if color_restrictions is not None:
        rest_colors = (list (color_restrictions.values()))

        ## If there are restricted colors they should be on the default colors list
        if not all(colors in _dict_colors for colors in rest_colors):
            raise ValueError("Not all restricted colors are available") 
             
        ## If there are fields link to related colors they also must be in the data type list
#         if not all(key in set_data_types for key in color_restrictions):                                  
#             raise ValueError("Some values of data types provided as color restriction are not present in the file", (set_data_types, color_restrictions))
            
        for dataType in color_restrictions:
            d_dataType_color[dataType] = _dict_colors[color_restrictions[dataType]] 
    
        colors_not_used = list(__dict_colors.keys())
        colors_not_used.remove (color_restrictions[dataType])

    for dataType in set_data_types:        
        if not colors_not_used:
            colors_not_used = list(_dict_colors.keys())
        
        if dataType in d_dataType_color:
            continue
        else:
            d_dataType_color[dataType] = _dict_colors[colors_not_used.pop(0)]    
    
    return d_dataType_color


def merge_tracks (tr_1, tr_2):
    """
    Merge two tracks objects.    
    
    :param tr_1: Object of class :py:class:`~pergola.tracks.Track`  
    :param tr_2: Object of class :py:class:`~pergola.tracks.Track`  
       
    :returns:  dictionary with data_types as keys and color gradients
        as values
    """    
    merge_track = tr_1
    
    if set(tr_1.fields) != set(tr_2.fields):        
        raise ValueError ("Fields of the two tracks objects do not match" )
    
    if tr_1.format != tr_2.format:        
        raise ValueError ("Format of the two tracks objects do not match" )
    
    merge_track.data_types = tr_1.data_types.union (tr_2.data_types)
    merge_track.range_values = [min(tr_1.range_values[0], tr_2.range_values[0]), max(tr_1.range_values[1], tr_2.range_values[1])]
    
    merge_track.list_data_types = merge_track.data_types    
    
    merge_track.max = max (tr_1.max, tr_2.max)
    merge_track.min = min (tr_1.min, tr_2.min)
    
    merge_track.list_tracks = tr_1.list_tracks.union (tr_2.list_tracks)
    merge_track.data.extend (tr_2.data)                
        
    i_track_1 = tr_1.fields.index('track')
    i_start_1 = tr_1.fields.index('start')
    i_track_2 = tr_2.fields.index('track')
    i_start_2 = tr_2.fields.index('start')
    
    if i_start_1 != i_start_2:
        raise ValueError ("Start field does not match track object to append")
    
    if i_track_1 != i_track_2:
        raise ValueError ("Track field does not match track object to append")

    if all(row[i_track_1].isdigit() for row in merge_track.data):
        merge_track.data = sorted(merge_track.data, key=lambda x: (int(x[i_track_1]), x[i_start_1]))
    else:
        merge_track.data = sorted(merge_track.data, key=itemgetter(i_track_1, idx_fields2int))
                                                  
    return merge_track
