from datetime import datetime
from operator import itemgetter
from flask import request, url_for, current_app
from flask_sqlalchemy.model import DefaultMeta
from flask_sqlalchemy import Pagination
from .element import CoreElement
from .mixin import ReqInjectScriptMixin
from .prop import IntProp, ValidProp, ValidSequenceProp
from .route import Route
from .element import Element
from .modal import ConfirmModal
from .form import Form
from .button import Button, LinkButton
from sqlalchemy.orm.attributes import InstrumentedAttribute

class TableColItem:
    name = ValidProp(str)
    allow_search = ValidProp(bool)
    is_key=ValidProp(bool)
    is_hidden=ValidProp(bool)
    default_sort=ValidProp(bool)
    default_sort_asc=ValidProp(bool)
    
    def __init__(self, name, column=None, allow_search=True, is_key=False, is_hidden=False, default_sort=False, default_sort_asc=False):
        self.name = name
        self.column = column
        self.allow_search = allow_search
        self.is_key = is_key
        self.is_hidden = is_hidden
        self.default_sort = default_sort
        self.default_sort_asc = default_sort_asc
        
    @property
    def column(self):
        return getattr(self, '_column', None)
    
    @column.setter
    def column(self, value):
        if value is not None:
            if type(value) is not InstrumentedAttribute:
                raise ValueError('sort column must be a type of InstrumentedAttribute.')
        setattr(self, '_column', value)
    
    @property
    def friendly_name(self):
        return self.name.replace('_', ' ').title()
    
    def __repr__(self):
        return f"ColItem(name='{self.name}', column={self.column}, allow_search={self.allow_search})"

class TableDateItem(TableColItem):
    format = ValidProp(str)
    display_format = ValidProp(str)
    
    def __init__(self, name, db_column=None, is_hidden=False, format=None, display_format=None, default_sort=False, default_sort_asc=False):
        super().__init__(name, db_column, allow_search=False, is_hidden=is_hidden, default_sort=default_sort, default_sort_asc=default_sort_asc)
        self.format = format or current_app.config['FLASK_UIO_DATE_FORMAT']
        self.display_format = display_format or current_app.config['FLASK_UIO_DATE_DISPLAY_FORMAT']
        
    def get_value(self, date_string):
        if not isinstance(date_string, str):
            raise ValueError(f'date string must be a string type.')
        return datetime.strptime(date_string, self.format).date()
    
    def get_text(self, date):
        if type(date) not in (datetime.date, datetime):
            raise ValueError(f'date string must be a date type.')
        return datetime.strftime(date, self.display_format)
    
class TableDateTimeItem(TableColItem):
    format = ValidProp(str)
    display_format = ValidProp(str)
    
    def __init__(self, name, db_column=None, is_hidden=False, format=None, display_format=None, default_sort=False, default_sort_asc=False):
        super().__init__(name, db_column, allow_search=False, is_hidden=is_hidden, default_sort=default_sort, default_sort_asc=default_sort_asc)
        self.format = format or current_app.config['FLASK_UIO_DATETIME_FORMAT']
        self.display_format = display_format or current_app.config['FLASK_UIO_DATETIME_DISPLAY_FORMAT']
        
    def get_value(self, datetime_string):
        if not isinstance(datetime_string, str):
            raise ValueError(f'date string must be a string type.')
        return datetime.strptime(datetime_string, self.format).date()
    
    def get_text(self, datetime_):
        if not isinstance(datetime_, datetime):
            raise ValueError(f'date string must be a datetime type.')
        return datetime.strftime(datetime_, self.display_format)
    
class TableStaticLinkItem(TableColItem):
    fp_col_name = ValidProp(str)
    
    def __init__(self, name, db_column=None, is_hidden=False, default_sort=False, default_sort_asc=False, fp_col_name=None):
        super().__init__(name, db_column, allow_search=False, is_hidden=is_hidden, default_sort=default_sort, default_sort_asc=default_sort_asc)
        self.fp_col_name = fp_col_name
    
class Table(CoreElement, ReqInjectScriptMixin):
    
    per_page = IntProp(min_value=1)
    pages = IntProp(min_value=0)
    total = IntProp(min_value=0)
    prev_num = IntProp(min_value=1)
    next_num = IntProp(min_value=1)
    css_class = ValidProp(str)
    inject_script = ValidProp(str)
    reload_route = ValidProp(Route)
    detail_route = ValidProp(Route)
    new_route = ValidProp(Route)
    edit_route = ValidProp(Route)
    delete_route = ValidProp(Route)
    excludes = ValidSequenceProp(str)
    allow_sort = ValidProp(bool)
    allow_search = ValidProp(bool)
    allow_paginate = ValidProp(bool)
    search_text = ValidProp(str)
    search_option = ValidProp(str)
    inject_script = ValidProp(str)
    
    def __init__(
            self, 
            title, 
            items=None, 
            colitems=None, 
            css_class=None, 
            reload_route=None, 
            new_route=None, 
            detail_route=None, 
            edit_route=None, 
            delete_route=None, 
            excludes=None,
            per_page=None, 
            page=None, 
            pages=None, 
            total=None, 
            prev_num=None, 
            next_num=None, 
            allow_sort=False, 
            allow_search=False, 
            allow_paginate=False,
        ):
        
        super().__init__('table')
        self.title = title
        self._ref_title = title.replace(' ', '-').lower()
        self._ref_form_paginate_id = f'form-paginate-table-{self._ref_title}'
        self._ref_form_paginate_text_id = f'form-paginate-text-table-{self._ref_title}'
        self._ref_form_search_id = f'form-search-table-{self._ref_title}'
        self._ref_form_search_text_id = f'form-search-text-table-{self._ref_title}'
        self._ref_form_search_option_id = f'form-search-option-table-{self._ref_title}'
        
        self.items = items or []
        self.colitems = colitems or []
        self.per_page = per_page
        self.pages = pages 
        self.total = total
        
        # page
        if request.method == 'POST':
            page = request.form.get(self._ref_form_paginate_text_id, None)
            if page is None:
                page = request.args.get('_page', None)
        elif request.method == 'GET':
            page = request.args.get('_page', None) or page
        self.page = page
        
        # search
        if request.method == 'POST':
            search_text = request.form.get(self._ref_form_search_text_id, None)
            search_option = request.form.get(self._ref_form_search_option_id, None)
            if search_text and search_option:
                self.page = 1
            if search_text is None:
                search_text = request.args.get('_search_text', None)
            if search_option is None:
                search_option = request.args.get('_search_option', None)
            self.search_text = search_text
            self.search_option = search_option
        elif request.method == 'GET':
            self.search_text = request.args.get('_search_text', None)
            self.search_option = request.args.get('_search_option', None)
        
        # prev page
        self.prev_num = prev_num
        if self.prev_num is None and self.page and self.page > 1:
            self.prev_num = self.page - 1
            
        # next page
        self.next_num = next_num
        if self.next_num is None and self.page:
            self.next_num = self.page + 1
            
        self.reload_route = reload_route
        self.detail_route = detail_route
        self.new_route = new_route
        self.edit_route = edit_route
        self.delete_route = delete_route
        self.excludes = excludes    
        self.allow_sort = allow_sort
        self.allow_search = allow_search
        self.allow_paginate = allow_paginate
        
        default_css = 'ui compact celled table'
        if self.detail_route:
            default_css = 'ui selectable compact celled table'
        self.css_class = css_class or default_css
        self.inject_script = ''
        
    def refresh(self, paginate):
        if not isinstance(paginate, Pagination):
            raise ValueError('paginate must be an instance of Pagination.')
        self.items = paginate.items
        self.page = paginate.page
        self.total = paginate.total
        self.per_page = paginate.per_page
        self.pages = paginate.pages
    
    @property
    def paginate(self):
        return {'page': self.page, 'per_page': self.per_page, 'error_out': False}
        
    @property
    def items(self):
        current_args_dicts = self.current_args_dicts.copy()
        items = getattr(self, '_items', [])
        for key in current_args_dicts.keys():
            header = key[5:]
            if header in self.headers:
                direction = current_args_dicts.get(key)
                if direction == 'asc':
                    items.sort(key=itemgetter(header))
                else:
                    items.sort(key=itemgetter(header), reverse=True)
        return items
    
    @items.setter
    def items(self, data):
        """Table items"""
        if not isinstance(data, list):
            raise ValueError('data must be a list of dict.')
        
        list_dict = []
        for index, item in enumerate(data):
            data_dict = {}
            try:
                if isinstance(item, dict):
                    data_dict = item
                elif hasattr(type(item), '__class__') and type(item).__class__ == DefaultMeta:
                    data_dict = item.__dict__
                    del data_dict['_sa_instance_state']
                else:
                    data_dict = item._asdict()
            except Exception as ex:
                raise Exception(f'item not supported.(index: {index}, {item})')
            list_dict.append(data_dict)
            
        setattr(self, '_items', list_dict)
    
    @property
    def headers(self):
        return [item.name for item in self.colitems]
    
    @property 
    def colitems(self):
        colitems = getattr(self, '_colitems', None)
        if colitems:
            return colitems
        if len(self._items) > 0:
            return [TableColItem(name) for name in list(self._items[0].keys())]
        return []
    
    @colitems.setter
    def colitems(self, value):
        if not isinstance(value, list):
            raise ValueError('colitems must be a list of ColItem.')
        
        for v in value:
            if not isinstance(v, TableColItem):
                raise ValueError('item must be an instance of ColItem.')
        
        setattr(self, '_colitems', value)
    
    @property
    def current_args_dicts(self):
        item = {}
        for h in self.headers:
            if h.lower() not in self.excludes and self.reload_route:
                sort_key = f'sort_{h}'
                sort_value = request.args.get(sort_key, None)
                if sort_value:
                    item.update({sort_key: sort_value})
        if self.search_text:
            item.update({'_search_text': self.search_text})
        if self.search_option:
            item.update({'_search_option': self.search_option})
        if self.page:
            item.update({'_page': self.page})
        
        return item
    
    @property
    def order_by(self):
        current_args_dicts = self.current_args_dicts
        output = []
        for col in self.colitems:
            sort_key = f'sort_{col.name}'
            sort_value = current_args_dicts.get(sort_key, None)
            if sort_value:
                if sort_value == 'asc':
                    output.append(col.column.asc())
                elif sort_value == 'desc':
                    output.append(col.column.desc())
        if len(output) == 0:
            for col in self.colitems:
                if col.default_sort:
                    if col.default_sort_asc:
                        output.append(col.column.asc())
                    else:
                        output.append(col.column.desc())
        return tuple(output)
    
    @property
    def filter(self):
        output = []
        if self.search_text and self.search_option:
            for item in self.colitems:
                if item.name == self.search_option:
                    output.append(item.column.ilike(f'%{self.search_text}%'))
        return tuple(output)

    @property
    def next_sort_dicts(self):
        item = {}
        for h in self.headers:
            if h.lower() not in self.excludes and self.reload_route:
                sort_key = f'sort_{h}'
                sort_value = request.args.get(sort_key, None)
                if sort_value:
                    if sort_value == 'asc':
                        item.update({sort_key: 'desc'})
                else:
                    item.update({sort_key: 'asc'})
        return item
    
    @property
    def column_sort_dicts(self):
        item = {}
        current_args_dicts = self.current_args_dicts.copy()
        for h in self.headers:
            temp_dict = dict(current_args_dicts)
            
            if h.lower() not in self.excludes and self.reload_route:
                sort_key = f'sort_{h}'
                if current_args_dicts.get(sort_key, None):
                    del temp_dict[sort_key]
                
                d = {}
                d.update(temp_dict)
                if self.next_sort_dicts.get(sort_key):
                    d.update({sort_key: self.next_sort_dicts.get(sort_key)})
                sort_dict = {sort_key: d}
                item.update(sort_dict)
        return item

    @property
    def page(self):
        return getattr(self, '_page', 1)
            
    @page.setter
    def page(self, value):
        if value:
            setattr(self, '_page', int(value))
        else:
            setattr(self, '_page', 1)

    def _get_colspans(self):
        colspans = len(self.headers)
        
        if self.edit_route or self.delete_route:
            colspans += 1
            
        for h in self.headers:
            if h.lower() in self.excludes:
                colspans -= 1
                
        return colspans

    def _get_header_html(self):
        current_args_dict = self.current_args_dicts.copy()
        tb_header = ''
        if (self.edit_route or self.delete_route) and len(self.items) > 0:
            tb_header = '<th class="collapsing"></th>'
            
        for col in self.colitems:
            header_name = col.name.replace('_', ' ').title()
            if not col.is_hidden and col.name.lower() not in self.excludes:
                if self.reload_route and self.allow_sort:
                    sort_key = f'sort_{col.name}'
                    sort_value = current_args_dict.get(sort_key, None)
                    url = self.reload_route.get_url(**self.column_sort_dicts.get(sort_key))
                    if sort_value:
                        icon = ''
                        if sort_value == 'asc':
                            icon = '<i class="sort amount down alternate grey icon"></i>'
                        elif sort_value == 'desc':
                            icon = '<i class="sort amount down grey icon"></i>'
                        tb_header += f'<th><a href="{url}">{icon}{header_name}</a></th>'
                    else:
                        tb_header += f'<th><a href="{url}">{header_name}</a></th>'
                else:
                    tb_header += f'<th>{header_name}</th>'
        return f'<thead><tr>{tb_header}</tr></thead>'
        
    def _format_col_data(self, colitem, data, item):
        output = data
        if isinstance(colitem, TableDateItem):
            if isinstance(data, str):
                output = colitem.get_text(colitem.get_value(data))
            elif type(data) in (datetime.date, datetime):
                output = colitem.get_text(data)
        elif isinstance(colitem, TableDateTimeItem):
            if isinstance(data, str):
                output = colitem.get_text(colitem.get_value(data))
            elif isinstance(data, datetime):
                output = colitem.get_text(data)
        elif isinstance(colitem, TableStaticLinkItem):
            fp = item.get(colitem.fp_col_name)
            if fp:
                url = url_for('static', filename=fp.replace('static/', ''))
                output = Element('a', data, _href=url, _target='_blank').get_html()
            else:
                output = str(data)
        return output
        
    def _get_body_html(self):    
        tb_rows = ''
        for item in self.items:
            tb_row = ''
            
            # define key for detail/info route argument
            detail_kwarg = {}
            for col in self.colitems:
                data = item.get(col.name)
                if col.is_key:
                    detail_kwarg.update(**{col.name: data})
                    
                if not col.is_hidden and col.name.lower() not in self.excludes:
                    detail_url = ''
                    if self.detail_route and bool(detail_kwarg):
                        detail_url = f'onclick="window.location=\'{self.detail_route.get_url(**detail_kwarg)}\';"'
                    data = self._format_col_data(col, data, item)    
                    tb_row += f'<td {detail_url}>{data if data is not None else "N/A"}</td>'
                
            # define route for actions (edit/delete)
            tb_action = ''
            if (self.edit_route or self.delete_route) and bool(detail_kwarg):
                actions = Element('div', _class="ui small basic icon buttons")
                if self.edit_route:
                    actions.append(
                        Element('a', 
                            inner_text='<i class="pen icon"></i>', 
                            _class='ui button', 
                            _href= self.edit_route.get_url(**detail_kwarg)
                        )
                    )
                if self.delete_route:
                    delete_info = ''
                    for col in self.colitems:
                        if not col.is_hidden or col.name.lower() not in self.excludes:
                            delete_info += f'<li>{col.friendly_name}: {self._format_col_data(col, item.get(col.name), item)}</li>'
                    delete_info = f'<ul>{delete_info}</ul>'
                    delete_button = Element(
                        'div', 
                        inner_text='<i class="trash alternate red icon"></i>', 
                        hide_id=False,
                        _class='ui button', 
                    )
                    confirm = ConfirmModal(
                        title=self.title,
                        submit_url=self.delete_route.get_url(**detail_kwarg),
                        message='Are you sure to delete the following record?',
                        more_message=delete_info,
                        no='Cancel',
                        yes='Yes, Delete',
                        calling_id=delete_button.id,
                    )
                    actions.append(delete_button)
                    actions.append(confirm)
                    self.inject_script += f'load_modal("{delete_button.id}", "{confirm.id}");'
                tb_action += '<td>' + actions.get_html() + '</td>'
            
            tb_row = f'<tr>{tb_action}{tb_row}</tr>'    
            tb_rows += tb_row
            
        if len(self.items) == 0:
            tb_rows = f'<tbody><tr><td colspans="{self._get_colspans()}">No record!</td></tr></tbody>'    
        else:
            tb_rows = f'<tbody>{tb_rows}</tbody>'
            
        return tb_rows

    def _get_footer_html(self):
        if self.reload_route and len(self.items) > 0:
            pagination = Element('div', 
                _class='ui pagination stackable fluid secondary menu', 
                _style='padding-top:5px; padding-bottom:5px;'
            )
            
            current_args_dict = self.current_args_dicts.copy()
            
            if self.total or self.pages:
                text = f'Pages: {self.pages}'
                text += f', Record: {self.total}'
                pagination.append(Element('div', _class='item', inner_text=text))
            
            if self.total:
                text = f'Record: {self.total}'
                pagination.append(Element('div', _class='item', inner_text=text))
            
            if self.page > 1:
                current_args_dict.update({'_page': 1})
                url = self.reload_route.get_url(**current_args_dict)
                pagination.append(Element('a', _class='item', inner_text='First', _href=url))
                
            if self.prev_num and self.page and self.page > 2:
                current_args_dict.update({'_page': self.prev_num})
                url = self.reload_route.get_url(**current_args_dict)
                pagination.append(Element('a', _class='icon item', inner_text='<i class="angle left icon"></i>', _href=url))
                
            if self.page:
                pagination.append(Element('div', _class='active item', inner_text=f'{self.page}'))
                
            if self.next_num:
                if self.pages is None or (self.pages and self.next_num < self.pages): 
                    current_args_dict.update({'_page': self.next_num})
                    url = self.reload_route.get_url(**current_args_dict)
                    pagination.append(Element('a', _class='icon item', inner_text='<i class="angle right icon"></i>', _href=url))
                
            if self.pages:
                current_args_dict.update({'_page': self.pages})
                url = self.reload_route.get_url(**current_args_dict)
                pagination.append(Element('a', _class='item', inner_text='Last', _href=url))
            
            current_args_dict = self.current_args_dicts.copy()
            if current_args_dict.get('_page'):
                del current_args_dict['_page']
            url = self.reload_route.get_url(**current_args_dict)
            form = Element(
                'form', 
                _class='item', 
                _id=self._ref_form_paginate_id,
                _method='POST',
                _action=url,
            )
            form_action = Element('div', _class='ui action input')
            form_action.append(
                Form.create_csrf_field(),
                Element('', inner_text=f'<input name="{self._ref_form_paginate_text_id}" type="text" placeholder="Page" value="{self.page}" />'),
                Element('', inner_text=f'<button form="{self._ref_form_paginate_id}" class="ui button" type="submit">Go</button>')
            )
            form.append(form_action)
            pagination.append(form)
            return pagination.get_html()
        else:
            current_args_dict = self.current_args_dicts.copy()
            pagination = Element('div', 
                _class='ui pagination stackable fluid secondary menu', 
                _style='padding-top:5px; padding-bottom:5px;',
            )
            if self.total or self.pages:
                text = f'Record: {self.total}'
                text += f', Pages: {self.pages}'
                pagination.append(Element('div', _class='item', inner_text=text))
            
            current_args_dict.update({'_page': 1})
            url = self.reload_route.get_url(**current_args_dict)
            pagination.append(Element('a', _class='item', inner_text='First', _href=url))
            
            url = self.reload_route.get_url(**current_args_dict)
            form = Element(
                'form', 
                _class='item', 
                _id=self._ref_form_paginate_id,
                _method='POST',
                _action=url,
            )
            form_action = Element('div', _class='ui action input')
            form_action.append(
                Form.create_csrf_field(),
                Element('', inner_text=f'<input name="{self._ref_form_paginate_text_id}" type="text" placeholder="Page" value="{self.page}" />'),
                Element('', inner_text=f'<button form="{self._ref_form_paginate_id}" class="ui button" type="submit">Go</button>')
            )
            form.append(form_action)
            pagination.append(form)
            return pagination.get_html()

    def _get_search_html(self):
        current_args_dict = self.current_args_dicts.copy()
        if current_args_dict.get('_search_text'):
            del current_args_dict['_search_text']
        if current_args_dict.get('_search_option'):
            del current_args_dict['_search_option']
        url = self.reload_route.get_url(**current_args_dict)
        
        textbox = Element('input', _type='text', _placeholder='Search...', _name=self._ref_form_search_text_id, _value=self.search_text)
        select = Element('select', _class='ui compact selection dropdown', hide_id=False, _name=self._ref_form_search_option_id)
        options = f'<option value="">Select Option</option>'
        for col in self.colitems:
            if col.allow_search and not col.is_hidden:
                col_value = col.name
                col_text = col.name.replace('_', ' ').title()
                if col_value == self.search_option:
                    options += f'<option value="{col_value}" selected>{col_text}</option>'
                else:
                    options += f'<option value="{col_value}">{col_text}</option>'
        select.inner_text = options
        btn = Element('button', _class='ui button', _type='submit', _form=self._ref_form_search_id, inner_text='Search')
        form = Element('form', _class='ui action input', _method='POST', _id=self._ref_form_search_id, _action=url)
        csrf_field = Form.create_csrf_field()
        form.append(csrf_field ,textbox, select, btn)
        self.inject_script += f'load_dropdown_field("{select.id}");'
        return form.get_html()

    def _get_table_html(self):
        header_html = self._get_header_html()
        body_html = self._get_body_html()
        
        # css class
        css_class = ''
        if self.css_class:
            css_class = f' class="{self.css_class}"'
            
        return f'<table id="{self.id}"{css_class}>{header_html}{body_html}</table>'

    def get_html(self):
        items = []
        if self.new_route:
            btn = LinkButton('New Record', url=self.new_route.get_url(), _class='ui primary button')
            items.append(f'<div class="item">{btn.get_html()}</div>')
        
        if self.allow_search:
            items.append(f'<div class="right item">{self._get_search_html()}</div>')
        
        title_html, table_html, paginate_html, action_html = '', '', '', ''
        
        if len(items) > 0:
            menu = Element('div', _class='ui stackable secondary menu', _style='padding-top:5px; padding-bottom:5px;')
            for item in items:
                menu.append(Element('', inner_text=item))
            action_html = f'<div class="ui fitted attached segment">{menu.get_html()}</div>'

        footer_html = self._get_footer_html()
        if self.allow_paginate and footer_html:
            paginate_html = f'<div class="ui bottom attached vertically fitted segment">{footer_html}</div>' 
        
        if action_html != '' or paginate_html != '':
            title_html = f'<h4 class="ui top attached primary header segment">{self.title}</h4>'
            table_html = f'<div class="ui attached segment" style="overflow-x: auto;">{self._get_table_html()}</div>'
        else:
            title_html = Element('h4', _class='ui dividing primary header', inner_text=self.title).get_html()
            table_html = f'<div style="overflow-x: auto;">{self._get_table_html()}</div>'
        
        return title_html + action_html + table_html + paginate_html