"""UI Element base classes for Winston dialogs.

This module provides the Element base class and non-form element types used
throughout the Winston dialog system.

Key Classes:
    Element: Base class for all UI elements with keyboard navigation
    ValuesElement: Element with multiple selectable values (row-style)
    AppletElement: Element that launches an applet

All elements inherit from KeyHandler to support @KeyHandler.hotkey() decorators.
"""

from __future__ import annotations
from typing import Any, Callable, TYPE_CHECKING
import re
from .key_handler import KeyHandler
from . import punctuator
from . import unspeakables
from .framework import beep

if TYPE_CHECKING:
    from .dialog import Dialog

class Element(KeyHandler):
    def __init__(self, owner: Dialog, name: str, value: str) -> None:
        self._dialog = owner
        self._name = name 
        self._value = value
        self._caret = 0
        if not hasattr(self.dialog,'empty_value'):
            self.empty_value = 'blank line'
        else:
            self.empty_value = self.dialog.empty_value  # type: ignore[has-type] 
        super().__init__()
    def __str__(self) -> str:
        if not unspeakables.substitute(self.value,unspeakables.unspeakables).strip():
            representation = self.empty_value
        #speak punc if that is the only non-blank text in value, or if say_punctuation is enabled  
        elif self.dialog.say_punctuation or punctuator.is_punctuation(self.value.strip()):
            representation = punctuator.substitute(self.value,punctuator.all)
        else:
            representation = self.__value_representation__() 
        #speaking of label is done in eg tab driven move rather than being intrinsic to the repr of the element 
        return ''.join([self.prefix(),str(representation)])         
    def __value_representation__(self) -> str:
        """
        override this method to modify the value before speaking - e.g. to inject tags
        """
        return self.value 
    @staticmethod 
    def __inspection_class__() -> type[Element]:
        """
        unless this is overridden in a subclass then the subclass  will be inspected by the inspect_element custom inspection method rather than needing a separate inspect_xxx_element for every subclass xxx
        """
        return Element 
    def prefix(self) -> str:
        pref = f'line {self.dialog.index(self)+1} ' if self.dialog.say_line_numbers else ''
        if self.dialog.say_indents:  # type: ignore[has-type] 
            indents = self.indents()
            if (len(self.value.strip())==0) or not ((abs(self.dialog.cursor-self.dialog.previous_cursor)==1) and (indents==self.dialog.current_indents)):  # type: ignore[has-type] 
                pref += indents   
            self.dialog.current_indents = indents 
        return pref
    def indents(self) -> str:
        spaces = len(self.value)-len(self.value.lstrip())
        return f'tab {int(spaces/4)} ' if  not spaces%4 else f'{spaces} spaces '
    def get_hotkey_handler(self, hotkey: str) -> Callable[[str], Any] | None:
        return self._key_methods.get(hotkey,None)
    def say_current_char(self, action: str = "") -> None:
        if self.caret==len(self.value):
            utterance = 'blank'
        else: 
            char = self.value[self.caret]
            utterance = ('cap\u200c' if char.isupper() else '')+char
        self.dialog.say_strings(''.join([utterance,action]),substitutions=punctuator.all|{' ':' space '})
    @property
    def caret(self) -> int:
        return self._caret
    @caret.setter
    def caret(self, val: int) -> None:
        #self._caret=val
        self._caret = min(val,len(self.value))        
    @property
    def dialog(self) -> Dialog:
        return self._dialog
    #@dialog.setter
    #def dialog(self,val):
        #self._dialog=val
    @property
    def value(self) -> str:
        return self._value
    @value.setter
    def value(self, val: str) -> None:
        self._value=val
    @property
    def name(self) -> str:
        return self._name
    @name.setter
    def name(self, val: str) -> None:
        self._name=val
    def beep(self, modified_name: str) -> None:
        beep(1200,500)
    @KeyHandler.hotkey("Left")
    def left(self, modified_name: str) -> None:
        #below will wrap to prev line if allowed and caret = -1  and not on first line
        self.dialog.move(self.dialog.cursor,self.caret-1,modified_name.startswith('Shift'),caret_only=True)  # type: ignore[has-type]
    @KeyHandler.hotkey("Right")
    def right(self, modified_name: str) -> None:
        #below will wrap to next  line if allowed and caret = len+1   and not on first line
        self.dialog.move(self.dialog.cursor,self.caret+1,modified_name.startswith('Shift'),caret_only=True)  # type: ignore[has-type]
    @KeyHandler.hotkey("Home")
    def home(self, modified_name: str, silent: bool = False) -> None:
        shift = modified_name.startswith('Shift')
        self.dialog.move(self.dialog.cursor,0,shift,silent=silent)  # type: ignore[has-type]
        if not (shift or silent):
            self.dialog.say_strings(self.next_word(),interrupt=True)
    @KeyHandler.hotkey("End")
    def end(self, modified_name: str, silent: bool = False) -> None:
        self.dialog.move(self.dialog.cursor,len(self.value),modified_name.startswith('Shift'),silent=silent)  # type: ignore[has-type]
    def previous_char(self) -> str:
        return '' if (self.caret==0) else self.value[self.caret-1] 
    def previous_word(self) -> str:
        if (not len(self.value)) or (self.caret==0):
            return 'start of line' 
        #boundaries is a list of at least 2 match objects with start and end equal  (i.e. zero len) representing the word boundaries in the value string up to the caret unless there are no tokens to the left of caret   
        boundaries = [match.start() for match in re.finditer(r'\b', self.value[:self.caret])]
        return self.value[boundaries[-2]:boundaries[-1]] if len(boundaries)>=2  else self.value[self.caret-1]         
    def next_word(self) -> str:
        m  =re.match(r'(?:\s*)([^\s^\w]+|\w+)',self.value[self.caret:])
        spaces= (len(self.value)-len(self.value.lstrip()))
        if (self.caret==0) and (spaces!=0):             
            return self.indents() 
        elif m is None:
            return 'start of line'  
        else:
            return m.group()
    @KeyHandler.hotkey("Control+Left")
    def move_previous(self, modified_name: str) -> None:
        l=[m.start() for m in re.finditer(r'([^\s^\w]+|\w+)\s*$',self.value[:self.caret])] 
        if not len(l):
            self.home(modified_name) #force past leading blanks
        else:
            self.dialog.move(self.dialog.cursor,l[0],modified_name.startswith('Shift'))  # type: ignore[has-type]
            #get rid of test for Shifted once checked new simplified behaviour is acceptable
            #if not shifted override the default speaking behaviour in move and instead speak the word which we are now at the start of   
            if True: #not modified_name.startswith('Shift'):
                self.dialog.say_strings(self.next_word(),substitutions=punctuator.all)
    @KeyHandler.hotkey("Control+Right")
    def move_next(self, modified_name: str) -> None:
        shift = modified_name.startswith('Shift')        
        #print(f'{modified_name} shifted state is {shift}')
        l=[(m.start(),m.group()) for m in re.finditer(r'([^\s^\w]+|\w+)\s*',self.value[self.caret:])] 
        i  = int(not((self.caret==len(self.value)) or (self.value[self.caret]==' ')))  #if at end or at a preceding space then look for  first token else move to second (index 0  and 1 respectively  
        if len(l)<=i: #no token to  move to 
            self.end(modified_name)
            say_string = 'blank'
        else:
            increment = l[i][0] #offset into string starting at self.caret - i.e. exactly the increment we need to move
            say_string = l[i][1]
            self.dialog.move(self.dialog.cursor,self.caret+increment,shift)  # type: ignore[has-type]
        #get rid of special case for selecting once tested the new simplified behaviour is acceptable
        if True: #not shift:
            self.dialog.say_strings(say_string,substitutions=punctuator.all)
        else:
            #print('moving with shift and selection = ',self.dialog.selection.get()) 
            self.dialog.say_short_action(self.dialog.selection.get(),"selected",substitutions=punctuator.all)
class ValuesElement(Element):
    """
    Row style element with movement between cells using Control+Left/Right and Home/End
    To put a list in a FormDialog, use ListElement derived from this class which uses Up/Down to navigate between list items 
    """
    def __init__(self, owner: Dialog, name: str, value: str, values: list[Any] = [], content_cookie: Any = None) -> None:
        self.index=0
        self._values: list[Any] = []
        self.content_cookie = content_cookie
        super().__init__(owner,name,value)
        self.refill(values,value,content_cookie,silent=True)
        if self.value is None:
            self.value=''        
    def __str__(self) -> str:
        if not self.value:
            return self.empty_message()
        else:
            return super().__str__()    
    def add(self, value: Any) -> None:
        self.values.append(value)
        new_index=self.values.index(value)
        self.rotate(new_index-self.index)        
    @property
    def values(self) -> list[Any]:
        return self._values    
    #no setter for values as always set by the refill method below which is required in the interface 
    def refill(self, values: list[Any], value: str, content_cookie: Any, silent: bool = False) -> None:
        self.value=''
        #direct assignment to _values as no setter for values because should always be done with this refill method  
        self._values = values
        if value in self.values: 
            new_index=self.values.index(value)
        else:
            new_index = 0
        self.rotate(new_index-self.index,silent=silent)
    @KeyHandler.hotkey("Control+Left")
    def previous_value(self, modified_name: str) -> None:
        self.dialog.rotate_elements(-1)  # type: ignore[attr-defined]
    @KeyHandler.hotkey("Control+Right")
    def next_value(self, modified_name: str) -> None:
        self.dialog.rotate_elements(1)  # type: ignore[attr-defined]
    @KeyHandler.hotkey("Home")
    def home(self, modified_name: str) -> None:
        self.dialog.rotate_elements(-self.dialog.column,wrap=False)  # type: ignore[attr-defined]
    @KeyHandler.hotkey("End")
    def end(self, modified_name: str) -> None:
        self.dialog.rotate_elements(len(self.values)-1-self.dialog.column,wrap=False)  # type: ignore[attr-defined]
    def update_column(self, column: int, silent: bool = False) -> None:
        self.rotate(column-self.index,silent=silent)
    def set_index(self, index: int) -> None:
        self.index=index
        self.value = self.values[self.index]
        self.caret=0 #return caret to start of value  when move to a new value 
    def rotate(self, increment: int, wrap: bool = False, interrupt: bool = True, say_header: bool = False, silent: bool = False) -> None:
        """
        to implement a carousel simply override this method and call super().rotate(*args,wrap=True)
        This method has been retired from use in ValuesElement  within a TableDialog but left here until FileOrFolderElement, ListElement and it's subclasses have been updated to safely use set_index with bounds checking 
        """
        if len(self.values):
            if wrap:
                self.index = (self.index+increment)%len(self.values)
            else:
                self.index = max(0,min(self.index+increment,len(self.values)-1)) 
            self.value = self.values[self.index]
            self.dialog.column = self.index  # type: ignore[attr-defined]
            self.caret=0 #return caret to start of value  when move to a new value 
            if not silent:
                if not say_header:
                    self.dialog.say_current(interrupt=interrupt)
                else:
                    what = str(self.current())
                    if hasattr(self.dialog,'headers'):
                        what = ', '.join([self.dialog.headers[self.index], what]) 
                    self.dialog.say_strings(what,interrupt=interrupt)            
        elif not silent:
            self.dialog.say_strings(self.empty_message())
    def current(self) -> Any | None:
        if not len(self.values):
            return None
        else:
            return self.values[self.index]    
    def empty_message(self) -> str:
        return f"empty {self.__class__.__name__.replace('Element','')}"


class AppletElement(Element):
    """Element that launches an applet when activated.
    
    Pressing Return launches the applet. Shift+Return launches the safe_ version.
    Control+Return opens the applet's source file in pycoder for editing.
    """
    
    @KeyHandler.hotkey("Return")
    def open(self, modified_name: str) -> None:
        """Launch the applet.
        
        Args:
            modified_name: The key combination pressed. Shift+Return launches safe_ version.
        """
        module_name = self.value
        if 'Shift' in modified_name:
            module_name = 'safe_' + module_name
        self.dialog.app.launch_applet(module_name)  # type: ignore[has-type]
    
    @KeyHandler.hotkey("Control+Return")
    def command_edit(self, modified_name: str) -> None:
        """Edit the applet's source file in pycoder.
        
        Args:
            modified_name: The key combination pressed.
        """
        import os
        file_path = os.path.join(os.getcwd(), 'applets', self.value + '.py')
        self.dialog.app.launch_applet('pycoder', filler=file_path, revert_to=self.dialog)  # type: ignore[has-type]

        