"""
Treeview of dataset
"""
__license__ = "GPL"
__author__ = "Sylvain Meignier"
__copyright__ = "Copyright 2023 Sylvain Meignier, Le Mans Université, LIUM (https://lium.univ-lemans.fr/)"

import warnings
import re
import copy
import pandas as pd

import numpy as np
import panel as pn
import param
from panel.viewable import Viewer
from pyDendron.tools.location import fullgeocode

from pyDendron.app_logger import logger
from pyDendron.dataname import *
#from pyDendron.gui_panel.dataset_treeview_filter import DatasetTreeviewFilter
from pyDendron.gui_panel.tabulator import (_cell_text_align, _cell_editors, _header_filters, 
                                           _cell_formatters, _hidden_columns, _cell_transform,
                                           _get_selection)

class DatasetTreeview(Viewer):
    """
    Treeview to manage dataset.  
    """
    flat =  param.Boolean(False, doc='show all components / sequences')
    #save_auto =  param.Boolean(True, doc='show all components / sequences')
    show_stat =  param.Boolean(True, doc='show data statistics')
    path = param.List([]) # sets and chronologies (IDX_PARENT) form the root to the current set/chronology
    clicked = param.List([])
    
    def __init__(self, dataset, param_column, param_chronology, **params):
        super(DatasetTreeview, self).__init__(**params)   
        self.param_chronology = param_chronology
        self.dataset = dataset
        self.param_column = param_column
        self.wcolumn_selector = self.param_column.columns

        self.data = self.load() # join components & sequences from dataset        
        self.wpath, self.wstat, self.wtabulator = self.get_tabulator()
        
        bt_size = 90
    
        self.dataset.param.watch(self.on_reload,  ['notify_reload'], onlychanged=True)
        self.dataset.param.watch(self.on_synchronize,  ['notify_synchronize'], onlychanged=True)
        
        self.param.watch(self.set_wstat, ['clicked'], onlychanged=True)
        
        self.wcolumn_selector.param.watch(self.sync_columns, ['value'], onlychanged=True, queued=True)
        
        self.param.watch(self.sync_flat, ['flat'], onlychanged=True)

        offsets_items = [('Normalize offsets', 'o'), ('Offsets to Years', 'o2y'), ('Years to offsets', 'y2o'), ('Get GPS code', 'localisation'), ('Current column values to children', 'propchild'), ('Current values to selected', 'propselect')]
        self.bt_tools = pn.widgets.MenuButton(name='Tools', items=offsets_items, icon='adjustments', button_type='primary', align=('start', 'end'), width=int(bt_size*1.5))
        self.bt_tools.on_click(self.on_tools)

        self.bt_chronology = pn.widgets.Button(name='Average', icon='tournament', button_type='primary', align=('start', 'end'), width=int(bt_size*1.5))
        self.bt_chronology.on_click(self.on_chronology)
        
        create_items = [('Create & add selection', 'a'), ('Regexp create & add selection', 'b')]
        self.bt_create = pn.widgets.MenuButton(name='Create set', items=create_items, split=True, icon='folder-plus', button_type='primary', align=('start', 'end'), width=int(bt_size*1.5))
        self.bt_create.on_click(self.on_create)
        self.w_set_name = pn.widgets.TextInput(name='', placeholder='Create parameters')
        
        self.bt_delete = pn.widgets.Button(name='Delete', icon='trash', button_type='primary', align=('start', 'end'), width=bt_size)
        self.bt_delete.on_click(self.on_delete)
        self.bt_paste = pn.widgets.Button(name='Paste', icon='clipboard', button_type='primary', align=('start', 'end'), width=bt_size)
        self.bt_paste.on_click(self.on_paste)
        self.bt_copy = pn.widgets.Button(name='Copy', icon='copy', button_type='primary', align=('start', 'end'), width=bt_size)
        self.bt_copy.on_click(self.on_copy)
        self.bt_cut = pn.widgets.Button(name='Cut', icon='cut', button_type='primary', align=('start', 'end'), width=bt_size)
        self.bt_cut.on_click(self.on_cut)
        
        self._layout = pn.Column(
                self.wpath,
                self.wtabulator,
                self.wstat,
                pn.layout.Divider(margin=(-10,0)),
                pn.Row(self.bt_chronology, self.bt_tools, self.bt_create, self.w_set_name), 
                pn.Row(self.bt_copy, self.bt_cut, self.bt_paste, self.bt_delete),
                #pn.Row(*self.wtabulator.download_menu())
            )
        if self.dataset.is_empty():
            self._layout.visible = False

    def __panel__(self):
        return self._layout
    
    def get_sidebar(self):
        """
        Create the sidebar Card. 
        """
        
        col = pn.Column(
            #self.param.save_auto, 
            self.param.flat, 
            self.param.show_stat, 
        )
        
        return pn.Card(col,                 
                margin=(5, 0), 
                sizing_mode='stretch_width', 
                title='TreeView',
                collapsed=True)
    
    def set_clipboard(self, selection):
        self.clipboard.value = selection.loc[:, [ICON, KEYCODE]]
    
    def get_tabulator(self):
        """
        Configure the main panel. 
        """   
        wpath = pn.Row(margin=(-10,0))
        #wstat = pn.pane.Markdown(margin=(-45,-10, 0, 0))
        
        
        stylesheet = """
        p {
        padding: 0px;
        margin: 0px;
        }
        """
        
        wstat = pn.pane.Alert(margin=(-27,-10, 0, 0), stylesheets=[stylesheet])
        tab = pn.widgets.Tabulator(pd.DataFrame(columns=list(dtype_view.keys())),
                                    on_click=self.on_click, 
                                    on_edit=self.on_edit, 
                                    hidden_columns=_hidden_columns(self.wcolumn_selector.value), 
                                    text_align=_cell_text_align(dtype_view),
                                    editors=_cell_editors(dtype_view, True), 
                                    header_filters=_header_filters(dtype_view), 
                                    formatters=_cell_formatters(dtype_view),
                                    pagination='local',
                                    page_size=100000,
                                    frozen_columns=[ICON, KEYCODE], 
                                    selectable='checkbox',
                                    sizing_mode='scale_width',
                                    height_policy='max',
                                    max_height=500,
                                    min_height=400,
                                    show_index=False,
                                    layout='fit_data_fill',
                                    margin=(0,0)
                                    ) 

        return wpath, wstat, tab

    def on_synchronize(self, event):
        """ 
        Synchronize Dataset and TreeView, path is keep. 
        """
        self.load_refresh(False)

    def on_reload(self, event):
        """ 
        Synchronize Dataset and TreeView, path is reset.
        """
        self.load_refresh(True)
        
    def load_refresh(self, reset_path=False):
        """ 
        Load data from dataset and show data 
        """
        self.wtabulator.selection = []
        self.data = self.load()
        self._layout.visible = False if self.data is None else True
        if reset_path:
            self.path.clear()
        self.show_data()
    
    def load(self):
        """ 
        Load data from dataset 
        """

        data = None
        if not self.dataset.is_empty():
            try:
                print('load data')
                self._layout.loading = True
                data1 = self.dataset.get_components().reset_index()
                #data1 = self.dataset.components.join(self.dataset.sequences, on=IDX_CHILD, how='left').reset_index()
                data2 = self.dataset.sequences.loc[self.dataset.get_roots(),:].reset_index()
                data2.insert(0, IDX_PARENT, -1)
                data2.rename(columns={IDX:IDX_CHILD}, inplace=True)
                with warnings.catch_warnings():
                    # TODO: pandas 2.1.0 has a FutureWarning for concatenating DataFrames with Null entries
                    warnings.filterwarnings("ignore", category=FutureWarning)
                    data = pd.concat([data2, data1], ignore_index=True)
                data.insert(len(data.columns)-1, OFFSET, data.pop(OFFSET))
                data.insert(0, ICON, data.apply(lambda x: category_html(x), axis=1))
                data = _cell_transform(data)
                data = data.set_index([IDX_PARENT, IDX_CHILD])
            except Exception as inst:
                logger.error(f'init_data : {inst}', exc_info=True)
            finally:
                self._layout.loading = False
        return data

    def show_data(self):
        """ 
        Show data selection according path 
        """
        try:
            view = None
            self._layout.loading = True
            if self.flat:
                view = self.data.reset_index()
            else:
                len_path = len(self.path)
                if len_path == 0: #root nodes
                    parent = pd.DataFrame([{IDX_PARENT:-2, IDX_CHILD:-1, ICON:'/', KEYCODE:'/', CATEGORY:'/'}])
                    mask = self.data.index.get_level_values(IDX_PARENT) == -1
                else: # a node and its children
                    idx_grand_parent = self.path[-2] if len_path > 1 else -1
                    parent = self.data.loc[(idx_grand_parent, self.path[-1])].to_frame().T
                    parent.index.names = [IDX_PARENT, IDX_CHILD]
                    parent = parent.reset_index()
                    mask = self.data.index.get_level_values(IDX_PARENT) == self.path[-1]
                children = self.data.iloc[mask].reset_index()
                view = children
        except Exception as inst:
            logger.error(f'set_data: {inst}', exc_info=True)
            view = pd.DataFrame(columns=list(dtype_view.keys()))
        finally:
            self.wtabulator.value = view 
            self.set_wpath()
            self.set_wstat()
            self._layout.loading = False

    def on_create(self, event):
        """
        Create a new Set and add selections if menu is 'a'.
        """
        if len(self.path) == 0:
            logger.warning(f'cannot perform set creation in root')            
        try:
            self._layout.loading = True
            if event.obj.clicked == 'b':
                d = {}
                selection = self.get_selection()
                for i, row in selection.iterrows():
                    pattern = self.w_set_name.value if self.w_set_name.value != '' else r'(.+?\(\d+\))'
                    res = re.match(pattern, row[KEYCODE])
                    if res:
                        if res.groups():
                            folder = res.group(1)
                            if folder not in d:
                                d[folder] = [i]
                            else:
                                d[folder].append(i)
                        else:
                            pattern = '(\w+?\)'
                            logger.warning(f"Regexp don't find group. For exemple use {pattern} to select the first word as new set.")
                for keycode, idxs in d.items():
                    #print(keycode, idxs)
                    triplets = self.get_triplets(selection.loc[idxs])
                    idx = self.dataset.new(keycode=keycode, category=SET, idx_parent=self.path[-1])
                    self.dataset.cut(triplets, self.path + [idx])
                        
            else:
                if event.obj.clicked == 'a':
                    triplets = self.get_triplets(self.get_selection())
                    #print(triplets)
                keycode = self.w_set_name.value if self.w_set_name.value != '' else 'new set'
                idx = self.dataset.new(keycode=keycode, category=SET, idx_parent=self.path[-1])
                if event.obj.clicked == 'a':
                    self.dataset.cut(triplets, self.path + [idx])
                
            # self.do_save_auto()

        except Exception as inst:
            logger.error(f'on_create: {inst}', exc_info=True)
        finally:
            self._layout.loading = False
    
    def get_selection(self) -> pd.DataFrame:
        """
        Returns the view of selectionned series. 
        """
        return _get_selection(self.wtabulator)
            
    def get_triplets(self, selection):
        if selection is not None:
            lst = []
            for i, row in  selection.iterrows():
                idx_parent = row[IDX_PARENT]
                idx_child = row[IDX_CHILD]
                offset = row[OFFSET]
                if idx_child not in [TRASH, CLIPBOARD, ROOT]:
                    lst.append((idx_parent, idx_child, offset))
                else:
                    logger.warning(f"Cannot copy/cut/paste {ROOT}, {CLIPBOARD} or {TRASH}.")
            return lst
        else:
            return []

    def on_delete(self, event):
        #print('on_cut')
        if len(self.path) == 0:
            logger.warning(f'cannot perform delete in root')            
        try:
            self._layout.loading = True
            triplets = self.get_triplets(self.get_selection())
            if self.path[-1] == TRASH:
                #print('on_delete drop: ', self.path)
                self.dataset.drop(triplets)
            else:
                #print('on_delete soft_drop: ', self.path)
                self.dataset.soft_drop(triplets)
            # self.do_save_auto()
        except Exception as inst:
            logger.error(f'perform: {inst}', exc_info=True)
        finally:
            self._layout.loading = False
        
    def on_copy(self, event):
        #print('on_copy')
        if len(self.path) == 0:
            logger.warning(f'cannot perform copy in root')            
        try:
            self._layout.loading = True
            selection = self.get_selection()
            #self.set_clipboard(selection)
            triplets = self.get_triplets(selection)
            self.dataset.copy(triplets, [CLIPBOARD])
            # self.do_save_auto()
        except Exception as inst:
            logger.error(f'perform: {inst}', exc_info=True)
        finally:
            self._layout.loading = False
        
    def on_cut(self, event):
        #print('on_cut')
        if len(self.path) == 0:
            logger.warning(f'cannot perform cut in root')            
        try:
            self._layout.loading = True
            selection = self.get_selection()
            #self.set_clipboard(selection)
            triplets = self.get_triplets(selection)
            self.dataset.cut(triplets, [CLIPBOARD])
            # self.do_save_auto()
        except Exception as inst:
            logger.error(f'perform: {inst}', exc_info=True)
        finally:
            self._layout.loading = False
    
    def on_paste(self, event):
        #print('on_paste')
        if self.flat: 
            logger.warning(f'cannot perform paste in flat mode')
        if len(self.path) == 0:
            logger.warning(f'cannot perform paste in root')            
        try:
            self._layout.loading = True
            mask = self.data.index.get_level_values(IDX_PARENT) == CLIPBOARD
            selection = self.data.iloc[mask].reset_index()
            triplets = self.get_triplets(selection)
            #self.set_clipboard(None)
            self.dataset.cut(triplets, self.path)
            # self.do_save_auto()
        except Exception as inst:
            logger.error(f'perform: {inst}', exc_info=True)
        finally:
            self._layout.loading = False

    def sync_flat(self, event):
        self.path.clear()
        self.show_data()

    def sync_columns(self, event):
        self.wtabulator.hidden_columns = _hidden_columns(self.wcolumn_selector.value)
    
    def on_path_click(self, event):
        i = event.obj.tags[0]
        logger.debug(f'on_path_click: i={i}, len={len(self.path)}, path={self.path}')
        if i < len(self.path):
            self.path = self.path[:i]
            logger.debug(f'on_path_click: new path={self.path}')
            self.show_data()
    
    def set_wstat(self, event=None):
        if self.show_stat:
            self.wpath.visible = True
            data = self.wtabulator.value
            self.wstat.object = ''
            if data is not None:
                count = len(data)
                date_begin = data[DATE_BEGIN].min()
                date_begin = int(date_begin) if pd.notna(date_begin) else '-'
                date_end = data[DATE_END].max()
                date_end = int(date_end) if pd.notna(date_end) else '-'
                freq = data[CATEGORY].value_counts().to_dict()
                freq_str = ''
                
                for i, (k, v) in enumerate(freq.items()):
                    if i == 0:
                        freq_str += f'{k}: {v}'
                    else:
                        freq_str += f', {k}: {v}'
                self.wstat.object = f'**Date** [{date_begin}, {date_end}]. **Count** {count} ({freq_str}).'
                if len(self.clicked) > 0:
                    self.wstat.object += f' **Current** [{self.clicked[3]}, {self.clicked[1]}] = {self.clicked[2]}'
                
        else:
            self.wpath.visible = False
        
    def set_wpath(self):
        """
        Create a list of Button that represent the path (Breadcrumb navigation).
        """
        def add(i, keycode, icon=None):
            if keycode != '':
                wkeycode = f'{keycode[:15]}...' if len(keycode) > 18 else f'{keycode}'
            else:
                wkeycode = keycode
            bt = pn.widgets.Button(name=wkeycode, icon=icon, button_type='light', button_style='outline', 
                                   align=('start', 'center'), tags=[i], margin=(0, 0), width_policy='min')
            bt.on_click(self.on_path_click)
            tmp_wpath.append(bt)
        
        def add_sep():
            tmp_wpath.append(pn.pane.HTML('<span>\U000025B6</span>', align=('start', 'center'), 
                                    margin=(0, 0), width_policy='min')) 
            
        tmp_wpath = []
        tmp_wpath.append(pn.pane.Markdown('**Path:**', align=('start', 'center'), margin=(0, 0), width_policy='min'))
        add(0, '', icon='folder')
        for i, idx in enumerate(self.path):
            add_sep()
            keycode = self.dataset.sequences.at[idx, KEYCODE]
            cat = self.dataset.sequences.at[idx, CATEGORY]
            icon = 'trees' if cat == CHRONOLOGY else 'folder'
            add(i + 1, keycode, icon)
            
        self.wpath.objects = tmp_wpath 
    
    def localisation(self):
        for idx, row in self.get_selection().iterrows():
            keycode = row[KEYCODE]
            (lat, lon, alt, country, state, district, town, zip_code, site) = fullgeocode(keycode, r"^([\w/-]+(?:\s+[\w/-]+)*)")
            #print(lat, lon, alt, country, state, district, town, zip_code, site)
            if lat != '':
                self.dataset.sequences.loc[row[IDX_CHILD],[SITE_LATITUDE, SITE_LONGITUDE, SITE_CODE]] = [float(lat), float(lon), site]
                self.dataset.notify_changes('localisation')
                # self.do_save_auto()
            
            #self.data.loc[(row[IDX_PARENT], row[IDX_CHILD]),[SITE_LATITUDE, SITE_LONGITUDE, SITE_CODE]] = [lat, lon, site]
            #self.wtabulator.value.loc[idx,[SITE_LATITUDE, SITE_LONGITUDE, SITE_CODE]] = [lat, lon, site]
    
    def value2selection(self):
        if self.clicked is not None:
            idx, col, value, keycode = self.clicked
            if col in [KEYCODE, OFFSET, CATEGORY, DATA_VALUES, DATA_INFO, DATA_LENGTH, DATA_WEIGHTS]:
                logger.warning(f'The {col} column is not suitable for this operation.')
                return
            
            idxs = self.get_selection()[IDX_CHILD]
            self.dataset.sequences.loc[idxs,col] = value
            self.dataset.notify_changes('localisation')
            # self.do_save_auto()

    def value2children(self):
        if self.clicked is not None:
            idx, col, value, keycode = self.clicked
            if col in [KEYCODE, OFFSET, CATEGORY, DATA_VALUES, DATA_INFO, DATA_LENGTH, DATA_WEIGHTS]:
                logger.warning(f'The {col} column is not suitable for this operation.')
                return 
            
            for _, row in self.get_selection().iterrows():
                value = row[col]
                keycode = row[KEYCODE]
                idx = row[IDX_CHILD]
                #print('value2children idx:', idx)
                tree = self.dataset.get_descendants(idx)
                idx_children = [node.idx for node in tree.filter().keys()]
                #print('value2children idx_children', idx_children, len(idx_children))
                #print('value2children ', col, value,' keycode', keycode)
                
                self.dataset.sequences.loc[idx_children, col] = value
            self.dataset.notify_changes('value2children')
            # self.do_save_auto()
                                     
    def on_tools(self, event):
        """
        Set offsets or dates. 
        """
        try:
            self._layout.loading = True
            if event.obj.clicked == 'o':
                self.dataset.shift_offsets(self.path[-1])
            elif event.obj.clicked == 'y2o':
                self.dataset.copy_dates_to_offsets(self.path[-1])
            elif event.obj.clicked == 'o2y':
                self.dataset.set_offsets_to_dates(self.path[-1])
            elif event.obj.clicked == 'localisation':
                self.localisation()
            elif event.obj.clicked == 'propchild':
                self.value2children()
            elif event.obj.clicked == 'propselect':
                self.value2selection()
                
        except Exception as inst:
            logger.error(f'set_data: {inst}', exc_info=True)
            view = pd.DataFrame(columns=list(dtype_view.keys()))
        finally:
            self._layout.loading = False
    
    def on_chronology(self, event):
        """
        Compute Chronology. 
        """
        try:
            self._layout.loading = True
            selection = self.get_selection()
            idx_children = selection.loc[selection[CATEGORY] != TREE, IDX_CHILD].to_list()
            date_as_offset = self.param_chronology.date_as_offset, 
            biweight = self.param_chronology.biweight_mean
            num_threads = self.param_chronology.num_threads
            
            self.dataset.chronologies(idx_children, date_as_offset=date_as_offset, biweight=biweight, num_threads=num_threads)
            
            # for idx in idx_children:
            #     logger.debug(f'on_chronology {idx}, {self.dataset.sequences.at[idx, KEYCODE]}')
            #     date_as_offset = self.param_chronology.date_as_offset, 
            #     biweight = self.param_chronology.biweight_mean
            #     #print(f'on_chronology {biweight}')
            #     self.dataset.chronology(idx, date_as_offset=date_as_offset, biweight=biweight)
            # self.do_save_auto()
        except Exception as inst:
            logger.error(f'set_data: {inst}', exc_info=True)
            self.show_data()
        finally:
            self._layout.loading = False

    def on_click(self, event):
        """
        Navigation between sets and chronologies. 
        """
        if self.flat: 
            return
        #print('on_click', event)
        id_row = event.row
        selected = self.wtabulator.value.iloc[id_row]
        if event.column == ICON:
            len_path = len(self.path)
            if selected[CATEGORY] != TREE:
                self.path.append(selected[IDX_CHILD])
                self.show_data()
        else:
            self.clicked = [id_row, event.column, event.value, selected[KEYCODE]]


    def on_edit(self, event):
        try:
            self._layout.loading = True
            col = event.column
            row = self.wtabulator.value.iloc[event.row]
            new = event.value
            idx_parent, idx_child = row[IDX_PARENT], row[IDX_CHILD]
        
            if col == OFFSET:
                self.dataset.edit_component(idx_parent, idx_child, new)
            else:            
                self.dataset.edit_sequence(idx_child, col, new)
            # self.do_save_auto()
        except Exception as inst:
            #print('**** patch', {event.column: [(event.row, event.old)] })
            self.wtabulator.patch( {event.column: [(event.row, event.old)] })
            logger.error(f'on_edit: {inst}', exc_info=True)
        finally:
            self._layout.loading = False

            
            




