import django_tables2 as tables
from django_tables2 import A
import random
from pandas import isna, Series, DataFrame as DF
from math import ceil
import json
from django.utils.html import mark_safe
from django_pandas.io import read_frame

class CheckFkColumn(tables.Column):
    ''' A column checks and displays the existence of a FK relationship '''
    def __init__(self, *args, fk_attr=None, present_symbol='✔', absent_symbol='X', **kwargs):
        super().__init__(*args, **kwargs)
        self.fk_attr = fk_attr if fk_attr.endswith('_id') else f'{fk_attr}_id'
        self.psym = present_symbol
        self.asym = absent_symbol

    def render(self, value, record):
        fk_obj = getattr(record, self.fk_attr)
        check = self.asym if fk_obj==None else self.psym
        return f'{value}{check}'

class RoundNumberColumn(tables.Column):
    ''' A column that will round a number and add commas for display '''

    def __init__(self, *args, money=False, round_to=2, prefix='', **kwargs):
        super().__init__(*args, **kwargs)
        self.money = money
        self.round_to = round_to
        self.prefix = prefix
    
    def render(self, value, record):
        val = round(value, self.round_to)
        if self.round_to <= 0:
            rstr = f'{val:,.0f}'
        else:
            rstr = f'{val:,.{self.round_to}f}'
        if self.money: 
            rstr = '$' + rstr
        return self.prefix + rstr

      
class CollapseColumnBase(tables.Column):
    """ Base class for CollapseColumn, extends django-tables2 Column class
    Args:
        *args: arguments to be passed to djang-tables2 derived Column __init__ method
        **kwargs: arguments to be passed to djang-tables2 derived Column __init__ method
        label (:obj:`str`, optional): The string to be displayed on the collapse target. Defaults to "show"
        label_accessor (:obj:`str`, optional): Passed to django-tables2 Accessor object along with the cell value. 
            Result used as collapse target label
        label_extra (:obj:`str`, optional): Extra text that will be appended to the collapse target label
        style (:obj:`str`, optional): css expression that will be passed to the collapasable divs style parameter
    """    
    def __init__(
        self, 
        *args, 
        label='Show', label_accessor=None, label_extra='', style=None, nowrap=False,
        **kwargs   
    ):
        super().__init__(*args, **kwargs)
        self.label = label
        self.label_accessor = label_accessor
        self.label_extra = label_extra
        self.style = style
        self.nowrap = nowrap

    def get_style(self):
        ''' method returns the style to be applied to the collapsable div '''
        if self.style:
            r = f'{self.style}'
        else:
            r = 'max-width: 25vw;'
        if self.nowrap:
            r += 'white-space: nowrap;'
        return f'"{r}"'

    def get_label(self, value=None, record=None, val=None):
        if self.label_accessor:
            rval = A(self.label_accessor).resolve(record)
        else:
            rval = self.label
        if value in [None, {}] or val==None:
            return ''     
        elif getattr(self, 'iterable', None):
            if len(value)==0:
                return ''            
        return str(rval) + self.label_extra

    def final_render(self, value, record, val):
        randnum = random.randint(1, 1_000_000_000)
        label = self.get_label(value=value, record=record, val=val)
        if label != '':
            rval = (
                f'''
                <a href="#unique{record.pk}{randnum}" data-toggle="collapse" aria-expanded="false" class="dropdown-toggle">
                    {label}
                </a>
                <ul class="collapse list-styled" id="unique{record.pk}{randnum}">
                    {val}
                </ul>
                '''
            )
            return mark_safe(rval)
        else:
            return ''  

class CollapseDataFrameColumn(CollapseColumnBase):
    """Custom django-tables2 column that will render a queryset as a pandas.DataFrame using the 
        pandas.DataFrame.to_html method in a collapsable div.

    Args:
        label (str, optional): text to be used on the collapse link. Defaults to 'Show'.
        filter_args (list, optional): args passed to qs.filter method. Defaults to [].
        filter_kwargs (dict, optional): kwargs passed to qs.filter method. Defaults to {}.
        annotate_kwargs (dict, optional): kwargs passed to qs.annotate method. Ignored if use_read_frame=True.Defaults to {}.
        values_args (list, optional): args passed to qs.values method. Ignored if use_read_frame=True. Defaults to [].
        values_kwargs (dict, optional): args passed to qs.values method. Ignored if use_read_frame=True. Defaults to {}.
        order_by_args (list, optional): args passed to qs.order_by method. Defaults to [].
        limit (int, optional): limits the qs by slicing it qs[:limit]. Defaults to None.
        use_read_frame (bool, optional): Boolean indicating if django_pandas.io.read_frame
            should be used to convert the qs to a pandas.DataFrame. Defaults to True.
        fieldnames (list, optional): Passed to django_pandas.io.read_frame fieldnames kwarg. 
            ignored if user_read_frame==False. Defaults to None.
        column_names (list, optional): Passed to django_pandas.io.read_frame column_names kwarg. 
            ignored if user_read_frame==False. Defaults to None.
        to_html_kwargs (dict, optional): kwargs to be passed to df.to_html method. 
            Defaults to dict(classes = ['table-bordered', 'table-striped', 'table-sm'], index=False, justify='left').
        to_html_kwargs_extra (dict, optional): kwargs to be added to to_html_kwargs. Defaults to {}.
    """  
    def __init__(
        self, *args, 
        label='Show',
        filter_kwargs = None,
        filter_args = None, 
        annotate_kwargs = None, 
        values_kwargs = None, 
        values_args = None, 
        order_by_args = None, 
        limit = None, 
        use_read_frame = True, 
        fieldnames = None, 
        column_names = None,
        to_html_kwargs = None, 
        to_html_kwargs_extra = None, 
        **kwargs   
    ):                
        super().__init__(*args, **kwargs)
        self.label = label
        self.limit = limit
        self.filter_kwargs = {} if filter_kwargs==None else filter_kwargs
        self.filter_args = [] if filter_args==None else filter_args
        self.annotate_kwargs = {} if annotate_kwargs==None else annotate_kwargs
        self.values_kwargs = {} if values_kwargs==None else values_kwargs
        self.values_args = [] if values_args==None else values_args
        self.order_by_args = [] if order_by_args==None else order_by_args
        self.use_read_frame = use_read_frame
        self.fieldnames = fieldnames
        self.column_names = column_names
        if to_html_kwargs==None:
            self.to_html_kwargs = dict(
                classes = ['table-bordered', 'table-striped', 'table-sm'],
                index=False, justify='left'
            )                    
        else: 
            self.to_html_kwargs = to_html_kwargs
        self.to_html_kwargs_extra = {} if to_html_kwargs_extra==None else to_html_kwargs_extra
        self.to_html_kwargs.update(self.to_html_kwargs_extra)
        self.no_wrap=False
         
    def get_read_frame_kwargs(self):
        """ Reuturns the kwargs to be passed to read_frame function """ 
        kwargs = {}
        if self.fieldnames:
            kwargs['fieldnames'] = self.fieldnames
        if self.column_names:
            kwargs['column_names'] = self.column_names
        return kwargs       

    def get_queryset(self, value):
        ''' method applies user passed kwargs/args to qs methods '''
        qs = value.filter(
            *self.filter_args, **self.filter_kwargs
        ).order_by(*self.order_by_args)
        if self.use_read_frame == False:
            qs = qs.annotate(
                **self.annotate_kwargs
            ).values(
                *self.values_args, **self.values_kwargs
            )
        return qs if self.limit==None else qs[:self.limit]

    def get_df_html(self, qs):
        if self.use_read_frame:
            df = read_frame(qs, **self.get_read_frame_kwargs())
        else:
            df = DF(qs)
        return df.to_html(**self.to_html_kwargs)

    def render(self, value, record):
        qs = self.get_queryset(value)
        if qs.count() == 0: 
            val = None
        else:
            val= self.get_df_html(qs)
        return self.final_render(value=value, record=record, val=val)



class CollapseColumn(CollapseColumnBase):
    ''' Column is meant for columns that have lots of data in each cell to make viewing cleaner'''

    def __init__(
        self, *args, hyperlink=False, href_attr=None,
        iterable=False, str_attr=None, order_by=None, fkwargs=None, property_attr=None, dictionary=False,
        **kwargs
    ):  # Note on kwargs: lavel_accessor used to make dynamic labels, label_extra is a str that adds on to the returned value
        super().__init__(*args, **kwargs)
        self.hyperlink = hyperlink  # Attempts to linkify the elements of an iterable
        # the attribute name to be used to pull the href value if None provided get_absolute_url will be called
        self.href_attr = href_attr
        self.iterable = iterable
        self.str_attr = str_attr
        self.order_by = order_by
        self.fkwargs = fkwargs
        self.property_attr = property_attr
        self.dictionary = dictionary

    def get_href(self, obj):
        ''' Method derives the href value to be used in hyperlinking list items '''
        if self.href_attr == None:
            return obj.get_absolute_url()
        else:
            return getattr(obj, self.href_attr)

    def render(self, value, record):
        if self.property_attr:
            value = getattr(record, self.property_attr)
        if self.dictionary:
            val = self.get_dictionary_val(value=value)        
        elif self.iterable == False:
            val = self.get_noniterable_val(value=value, record=record)
        else: 
            val = self.get_iterable_val(value)
        return self.final_render(value=value, record=record, val=val)
    
    def get_iterable_val(self, value):
        if self.order_by:
            value = value.order_by(self.order_by)
        if self.fkwargs:
            value = value.filter(**self.fkwargs)
        val = ''
        style = self.get_style()
        for obj in value:
            obj_val = str(obj) if self.str_attr == None else getattr(obj, self.str_attr)
            if self.hyperlink:
                href = self.get_href(obj)
                obj_val = f'<a href={href}>{obj_val}</a>'
            val = val + f'<li style={style}>{obj_val}</li>'
        return val

    def get_noniterable_val(self, value, record):
        if self.hyperlink:
            href = self.get_href(record)
            val = f'<a href={href}>{value}</a>'
        else:
            val = value
        return f'<div style={self.get_style()}>{val}</div>'

    def get_dictionary_val(self, value):
        if isna(value):
            value = {}        
        if type(value) != dict:
            value = json.loads(value)
        df = DF(Series(value), columns=['value'])
        df = df.reset_index().rename(columns={'index':'key'})
        df_html = df.to_html(
            classes = ['table-bordered', 'table-striped', 'table-sm'],
            index=False, justify='left', header=False
        )
        return f'<div style={self.get_style()}>{df_html}</div>'


def get_background(value, record, table, bound_column):
    val = (str(value).split('/')[0]).replace(',','')
    val = float(val)
    vals = [getattr(row, bound_column.name) for row in table.data]
    ser = Series(vals).fillna(0)
    max = ser.max()
    if max == 0:
        w = 0
    else:
        w = 100*val/max
    style_str = f'background:linear-gradient(90deg, {bound_column.column.color} {w}%, transparent {w}%)'
    return style_str
class BarChartColumn(tables.Column):
    def __init__(
        self, *args,
        round_to=0,
        color='lightgrey',
        goal_attr=None,
        **kwargs
    ):
        super().__init__(*args, **kwargs)
        self.round_to = round_to
        self.color = color
        self.goal_attr = goal_attr
        td_dict = self.attrs.get('td', {})
        td_dict['style'] = get_background
        self.attrs['td'] = td_dict

    def render(self, value, record):
        if value==None:
            return
        value = round(value, self.round_to)
        if self.round_to > 0:
            rval = f'{value:,}'
        else:
            rval = f'{value:,.0f}'

        if self.goal_attr:
            goal = getattr(record, self.goal_attr, '')
            goal = ceil(goal)
            rval = str(rval) + f'/{goal}'
        return rval

    def value(self, value):
        value = str(value).split('/')[0]
        if not str(value).isnumeric():
            value = 0
        return value


class LastChangeDateColumn(tables.Column):
    ''' This Column can be used with tables that have a model defined and 
    are using django simple-history to track changes'''

    def render(self, record):
        if not hasattr(record, 'history'):
            return None
        else:
            return record.history.first().history_date


class LastChangeUserColumn(tables.Column):
    ''' This Column can be used with tables that have a model defined and 
    are using django simple-history to track changes'''

    def render(self, record):
        if not hasattr(record, 'history'):
            return None
        else:
            user = record.history.first().history_user
            if user == None:
                return 'Automated'
            return user.employee


class LastChangeTypeColumn(tables.Column):
    ''' This Column can be used with tables that have a model defined and 
    are using django simple-history to track changes'''

    def render(self, record):
        if not hasattr(record, 'history'):
            return None
        else:
            return record.history.first().history_type
