#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@author: pho
User Interface Rendering and interactivity helpers

TODO: REORGANIZE_PLOTTER_SCRIPTS: PyVista

"""
import numpy as np
import pyvista as pv

from pyphocorehelpers.gui.PyVista.PhoCustomVtkWidgets import PhoWidgetHelper

def print_controls_helper_text():
    controls_helper_text_strings = ['[f] - Focus and zoom in on the last clicked point',
        'shift+click - Drag to pan the rendering scene',
        'ctrl+click - Rotate the scene in 2D']
    controls_helper_text = '\n'.join(controls_helper_text_strings)
    print(controls_helper_text)
    return controls_helper_text
    
def _customize_default_slider_gui_style(my_theme):
    my_theme.slider_styles.modern.slider_length = 0.01
    my_theme.slider_styles.modern.slider_width = 0.05 # the height of the slider releative to the bar
    my_theme.slider_styles.modern.slider_color = (0.13, 0.14, 0.15) # (0.43, 0.44, 0.45)
    my_theme.slider_styles.modern.tube_width = 0.02
    my_theme.slider_styles.modern.tube_color = (0.69, 0.40, 0.109)
    my_theme.slider_styles.modern.cap_opacity = 0.0
    my_theme.slider_styles.modern.cap_length = 0.01
    my_theme.slider_styles.modern.cap_width = 0.02
    return my_theme


def get_gradients():
    ## All gradient design are curtosy of https://uigradients.com
    # https://github.com/Ghosh/uiGradients
    # NOTE: Arranged dark->light
    gradient_dict = dict()
    gradient_dict['Deep Space (Dark)'] = ('#000000', '#434343') # 
    gradient_dict['Grade Grey'] = ('#2c3e50', '#bdc3c7') # 'Grade Grey'
    gradient_dict['Purple Paradise'] = ('#1d2b64', '#f8cdda') # 'Purple Paradise'
    # gradient[] = ('#0F2027', '##203A43', '#2C5364') # Unknown
    gradient_dict['Colors of Sky (light, cloud-like)'] = ('#e0eafc', '#cfdef3') # 'Colors of Sky (light, cloud-like)'
    gradient_dict['Clouds (Apple-like white)'] = ('#ECE9E6', '##FFFFFF') # 'Clouds (Apple-like white)'
    return gradient_dict

def customize_default_pyvista_theme():
    my_theme = pv.themes.DefaultTheme()
    my_theme = _customize_default_slider_gui_style(my_theme)
    my_theme.window_size = [1920, 1080]
    my_theme.background  = 'white'
    ## Apply the theme as the active pyvista theme
    print('Applying custom Pyvista theme.')
    pv.global_theme.load_theme(my_theme)
    # pv.global_theme.nan_opacity=0.0
    print('done.')
    
    
###########################    
## Playback Timestamp Slider Adjustments Programmatically Methods:
class MutuallyExclusiveRadioButtonGroup:
    """Enforces the constraint that exactly one is active at a time
    Usage:       
        mutually_exclusive_radiobutton_group = MutuallyExclusiveRadioButtonGroup(len(callbacks), active_element_idx=0, on_element_state_changed_callbacks=callbacks)
        mutually_exclusive_radiobutton_group[5] = True # activate element 5
    """
    def __init__(self, num_elements, active_element_idx=None, on_element_state_changed_callbacks=None, require_active_selection=False, is_debug=False):
        # on_element_state_changed_callbacks: a list of callbacks for each of the num_elements that accept one boolean argument indicating the udpated state of that element. Called upon change.
        # require_active_selection: if True, ensures that at least one active_element_idx is always selected, otherwise, allows active_element_idx to be None
        self.is_debug=is_debug
        self.num_elements = num_elements
        self.require_active_selection = require_active_selection
        self._on_element_state_changed_callbacks = on_element_state_changed_callbacks
     
        # Allocate the boolean array that indicates which element is active
        self._is_element_active = np.full([num_elements,], False)
        
        if self.require_active_selection:
            if active_element_idx is None:
                # if requires an active_selection, set self._active_element_idx to zero if it is None
                active_element_idx = 0
        self._active_element_idx = active_element_idx          
        
        if self._active_element_idx is not None:      
            self._is_element_active[self._active_element_idx] = True # set one element to true

        if self.is_debug:
            print('MutuallyExclusiveRadioButtonGroup(num_elements: {}, active_element_idx: {}, require_active_selection): Initialized'.format(num_elements, self._active_element_idx, require_active_selection))
            
        # Need to update all elements using the callback since they were all initialized to either False/True values:
        for i in np.arange(self.num_elements):
            self.perform_callback(i)

    def __getitem__(self, index):
        return self._is_element_active[index]
    
    def __setitem__(self, index, new_value):
        if new_value:
            # normal case, new active item selected
            self.set_element_active(index)
        else:
            # unusual case, an item is set to be inactivated
            if self.require_active_selection and (index == self._active_element_idx):
                # The index to be deactivated is the active one, so change the active index to 0
                self.set_element_active() # set the default (0) element active since the active one is being inactivated
            else:
                # only when the index to be deactivated is the active one will anything actually happen
                pass
        
    def __repr__(self) -> str:
        return f"<MutuallyExclusiveRadioButtonGroup: num_elements: {self.num_elements}, active_element_idx: {self.active_element_idx}>: {self.is_element_active}"

    def __str__(self) -> str:
        return f"<MutuallyExclusiveRadioButtonGroup: num_elements: {self.num_elements}, active_element_idx: {self.active_element_idx}>: {self.is_element_active}"

    
    def set_element_active(self, proposed_active_element_idx=0):
        if self._active_element_idx == proposed_active_element_idx:
            # element unchanged
            if self.is_debug:
                print('MutuallyExclusiveRadioButtonGroup.set_element_active({}): Active index unchanged'.format(proposed_active_element_idx))
            pass
        else:
            # element changing
            prev_active_element_idx = self._active_element_idx
            if self.is_debug:
                print('MutuallyExclusiveRadioButtonGroup.set_element_active({}): Active index changing from {} to {}'.format(proposed_active_element_idx, prev_active_element_idx, proposed_active_element_idx))
            # Inactivate the old element:
            if prev_active_element_idx is not None:
                self._is_element_active[prev_active_element_idx] = False # set prev element to false
                self.perform_callback(prev_active_element_idx)
            
            # Activate the new element:
            self._active_element_idx = proposed_active_element_idx
            if self._active_element_idx is not None:
                self._is_element_active[self._active_element_idx] = True # set new active element to true
                self.perform_callback(self._active_element_idx)
                
                
    def perform_callback(self, callback_idx):
        if self._on_element_state_changed_callbacks is not None:
                curr_callback = self._on_element_state_changed_callbacks[callback_idx]
                if self.is_debug:
                    print('MutuallyExclusiveRadioButtonGroup.perform_callback({}): {}'.format(callback_idx, self._is_element_active[callback_idx]))
                curr_callback(self._is_element_active[callback_idx]) # pass the new state to the callback

    @property
    def active_element_idx(self):
        return self._active_element_idx

    @active_element_idx.setter
    def active_element_idx(self, proposed_active_element_idx):
        """active_element_index ensures consistency on set"""                
        self.set_element_active(proposed_active_element_idx)

    @property
    def is_element_active(self):
        """
        Returns:
            [Bool]: [Returns a boolean array indicating where each element is active]
        """
        return self._is_element_active


class SetUICheckboxValueCallback:
    """Helper callback to keep a reference to a checkbox widget and allow it to be updated programmatically 
    Usage:
        # Make a separate callback for each widget
        callback = SetUICheckboxValueCallback(actor)
    """
    def __init__(self, checkbox_widget_actor, is_debug=False):
        self.is_debug=is_debug
        self.checkbox_widget_actor = checkbox_widget_actor

    def __call__(self, state=None):
        if state is None:
            # if no state provided, toggle the current state:
            prev_state = self.checkbox_widget_actor.GetRepresentation().GetState()
            state = not prev_state
        self.checkbox_widget_actor.GetRepresentation().SetState(state)
      

class OnUICheckboxChangedCallback:
    """Helper callback to keep a reference to both the checkbox widget and the index which it represents and allow it perform a custom callback when its value is changed interactively
    callback: a function that takes (widget_index: Int, state: Bool)
    Usage:
        def _update_mutually_exclusive_callback(widget_index, state):
            debug_console_widget.add_line_to_buffer('_update_mutually_exclusive_callback(widget[{}]): updated value {})'.format(widget_index, state))
            mutually_exclusive_radiobutton_group[widget_index] = state # set the mutually exclusive active element using the widget changed callback
        
        # add the function that responds to user initiated changes by clicking on the value
        checkbox_changed_callbacks = list()            
        for i, a_checkbox_widget_actor in enumerate(checkboxWidgetActors):
            curr_checkbox_changed_callback = OnUICheckboxChangedCallback(a_checkbox_widget_actor, i, _update_mutually_exclusive_callback, is_debug=is_debug)
            a_checkbox_widget_actor.AddObserver(pv._vtk.vtkCommand.StateChangedEvent, curr_checkbox_changed_callback)
            checkbox_changed_callbacks.append(curr_checkbox_changed_callback)
    """
    def __init__(self, checkbox_widget_actor, widget_index, on_update_callback, is_debug=False):
        self.is_debug=is_debug
        self.checkbox_widget_actor = checkbox_widget_actor
        self.widget_index = widget_index
        self.callback = on_update_callback
    
    def __call__(self, widget, event):
        """ Called automatically when the checkbox emits a StateChangedEvent (when the user clicks it) """
        state = bool(widget.GetRepresentation().GetState())
        if self.is_debug:
            print('OnUICheckboxChangedCallback(widget[{}]): updated value {})'.format(self.widget_index, state))
        widget.ProcessEventsOff()
        self.callback(self.widget_index, state) # perform the callback that will update the mutually exclusive active element using the widget changed callback
        widget.ProcessEventsOn()
      

class CallbackSequence:
    """ Helper class to call a list of callbacks with the same argument sequentally """
    def __init__(self, callbacks_list, is_debug=False):
        self.is_debug=is_debug
        self.callbacks_list = callbacks_list

    def __call__(self, state):    
        # call the callbacks in order
        for i in np.arange(len(self.callbacks_list)):
            self.callbacks_list[i](state)
      
        
class SetVisibilityCallback:
    """Helper callback to keep a reference to the actor being modified. 
    Usage:
        # Make a separate callback for each widget
        callback = SetVisibilityCallback(actor)
    """
    def __init__(self, actor):
        self.actor = actor

    def __call__(self, state):
        self.actor.SetVisibility(state)
    
    

def add_placemap_toggle_checkboxes(p, active_idx_updated_callbacks, colors, widget_check_states=False, widget_size=20, widget_start_pos=12, widget_border_size=3, additional_callback_actions=None, labels=None):
    # """ Adds a list of toggle checkboxes to turn on and off each placemap"""
    """ Adds a list of toggle checkboxes to turn on and off each placemap
    Usage:
        checkboxWidgetActors, tuningCurvePlotActorVisibilityCallbacks = add_placemap_toggle_checkboxes(pActiveTuningCurvesPlotter, tuningCurvePlotActors, pf_colors, widget_check_states=True, is_debug=False)
    """
    num_checkboxes = len(active_idx_updated_callbacks)
    if type(widget_check_states)==bool:
        widget_check_states = np.full([num_checkboxes,], widget_check_states)

    start_positions = widget_start_pos + ((widget_size + (widget_size // 10)) * np.flip(np.arange(num_checkboxes)))
    checkboxWidgetActors = list()
    labelWidgetActors = list()
    
    # callbacks
    final_combined_callbacks = list()
    
    for i, curr_callback in enumerate(active_idx_updated_callbacks):
        curr_widget_position = (5.0, start_positions[i])
        # Make a separate callback for each widget
        if additional_callback_actions is not None:
            curr_custom_callback = additional_callback_actions[i]
        else:
            curr_custom_callback = None
            
        curr_callback(widget_check_states[i]) # perform the callback to update the initial visibility based on the correct state for this object
        
        curr_widget_actor = PhoWidgetHelper.perform_add_custom_button_widget(p, curr_callback, value=widget_check_states[i],
                position=curr_widget_position, size=widget_size,
                border_size=widget_border_size,
                color_on=colors[:,i],
                color_off='grey',
                background_color=colors[:,i], # background_color is used for the border
                render=True
        )
        curr_widget_label_actor = PhoWidgetHelper.perform_add_button_text_label(p, '{}'.format(i), curr_widget_position, font_size=6, color=[1, 1, 1], shadow=False, name='lblPlacemapCheckboxLabel[{}]'.format(i), viewport=False)        
        curr_checkbox_checked_callback = SetUICheckboxValueCallback(curr_widget_actor)
        if isinstance(curr_callback, CallbackSequence):
            curr_combined_callback = curr_callback
            curr_combined_callback.callbacks_list.append(curr_checkbox_checked_callback)
        else:
            curr_combined_callback = CallbackSequence([curr_callback, curr_checkbox_checked_callback]) # otherwise if it isn't a CallbackSequence, we just have to wrap it in one
            
        if curr_custom_callback is not None:
            curr_combined_callback.callbacks_list.append(curr_custom_callback) 
        # append the callbacks to the lists:
        
        final_combined_callbacks.append(curr_combined_callback)
        # append actors to lists:
        labelWidgetActors.append(curr_widget_label_actor)
        checkboxWidgetActors.append(curr_widget_actor)

    return checkboxWidgetActors, final_combined_callbacks



def add_placemap_toggle_mutually_exclusive_checkboxes(p, active_idx_updated_callbacks, colors, active_element_idx=0, widget_size=20, widget_start_pos=12, widget_border_size=3, require_active_selection=False, is_debug=False, debug_console_widget=None, additional_callback_actions=None, labels=None):
    """ Adds a list of toggle checkboxes that only allows one at a time to be selected to turn on and off each placemap
    Usage:
        checkboxWidgetActors, tuningCurvePlotActorVisibilityCallbacks, mutually_exclusive_radiobutton_group = add_placemap_toggle_mutually_exclusive_checkboxes(pActiveTuningCurvesPlotter, tuningCurvePlotActors, pf_colors, active_element_idx=4, require_active_selection=False, is_debug=False)
    """
    num_checkboxes = len(active_idx_updated_callbacks)
    # start_positions = widget_start_pos + ((widget_size + (widget_size // 10)) * np.arange(num_checkboxes))
    start_positions = widget_start_pos + ((widget_size + (widget_size // 10)) * np.flip(np.arange(num_checkboxes)))
    
    checkboxWidgetActors = list()
    labelWidgetActors = list()
    
    # callbacks
    final_combined_callbacks = list()
    
    for i, curr_callback in enumerate(active_idx_updated_callbacks):
        curr_widget_position = (5.0, start_positions[i])
        if additional_callback_actions is not None:
            curr_custom_callback = additional_callback_actions[i]
        else:
            curr_custom_callback = None
            
        curr_widget_actor = PhoWidgetHelper.perform_add_custom_button_widget(p, curr_callback, value=False,
                position=curr_widget_position, size=widget_size,
                border_size=widget_border_size,
                color_on=colors[:,i],
                color_off='grey',
                background_color=colors[:,i], # background_color is used for the border
                render=True
        )
        
        if labels is None:
            curr_widget_label = '{}'.format(i)
        else:
            curr_widget_label = labels[i]
            
        curr_widget_label_actor = PhoWidgetHelper.perform_add_button_text_label(p, curr_widget_label, curr_widget_position, font_size=6, color=[1, 1, 1], shadow=False, name='lblPlacemapCheckboxLabel[{}]'.format(i), viewport=False)        
        curr_checkbox_checked_callback = SetUICheckboxValueCallback(curr_widget_actor)
        if isinstance(curr_callback, CallbackSequence):
            curr_combined_callback = curr_callback
            curr_combined_callback.callbacks_list.append(curr_checkbox_checked_callback)
        else:
            curr_combined_callback = CallbackSequence([curr_callback, curr_checkbox_checked_callback]) # otherwise if it isn't a CallbackSequence, we just have to wrap it in one
            
        if curr_custom_callback is not None:
            curr_combined_callback.callbacks_list.append(curr_custom_callback) 
        # append the callbacks to the lists:
        
        final_combined_callbacks.append(curr_combined_callback)
        # append actors to lists:
        labelWidgetActors.append(curr_widget_label_actor)
        checkboxWidgetActors.append(curr_widget_actor)

    
    # build the mutually exclusive group:
    mutually_exclusive_radiobutton_group = MutuallyExclusiveRadioButtonGroup(len(final_combined_callbacks), active_element_idx=active_element_idx, on_element_state_changed_callbacks=final_combined_callbacks, require_active_selection=require_active_selection, is_debug=is_debug)
    
    def _update_mutually_exclusive_callback(widget_index, state):
        if debug_console_widget is not None:
            debug_console_widget.add_line_to_buffer('_update_mutually_exclusive_callback(widget[{}]): updated value {})'.format(widget_index, state))
        mutually_exclusive_radiobutton_group[widget_index] = state # set the mutually exclusive active element using the widget changed callback

    # add the function that responds to user initiated changes by clicking on the value
    # checkbox_changed_callbacks = list()
    for i, a_checkbox_widget_actor in enumerate(checkboxWidgetActors):
        curr_checkbox_changed_callback = OnUICheckboxChangedCallback(a_checkbox_widget_actor, i, _update_mutually_exclusive_callback, is_debug=is_debug)
        a_checkbox_widget_actor.AddObserver(pv._vtk.vtkCommand.StateChangedEvent, curr_checkbox_changed_callback)
        # checkbox_changed_callbacks.append(curr_checkbox_changed_callback)
        
    return checkboxWidgetActors, final_combined_callbacks, mutually_exclusive_radiobutton_group