"""
Drop-in replacement for termuxgui using Kivy with Trio async support.

Usage:
    # Instead of: import termuxgui as tg
    # Use:        import kivy_termux_wrapper as tg
    
    # Then in your Termux.run_() method:
    # await async_runTouchApp(self.root.activity.root, async_lib='trio')
"""
from kivy.base           import EventLoop
from kivy.uix.button     import Button as KivyButton
from kivy.uix.label      import Label
from kivy.uix.boxlayout  import BoxLayout
from kivy.uix.scrollview import ScrollView
from kivy.uix.spinner    import Spinner as KivySpinner
from kivy.uix.popup      import Popup, ModalView
from kivy.clock          import Clock
from collections         import namedtuple
from kivy.metrics        import dp as dp0
from kivy.core.window    import Window
from kivy.app            import App

from TrioUtils import Queue

DEBUG = False

def dbg(ob, s):
    print(f"{type(ob).__name__:20}/{id(ob)//8} {s}")

def dp(x):
    return dp0(x*(100/113))

# Constants to match termuxgui
class View:
    WRAP_CONTENT = 'wrap'
    MATCH_PARENT = 'match'

# Event structure to match termuxgui events
Event = namedtuple('Event', ['type', 'value'])

class Connection:
    """Manages the Kivy app lifecycle and event queue"""
    
    def __init__(self):
        self.event_queue = Queue()
    
    def events(self):
        """Generator that yields events (blocking) - for compatibility"""
        # This shouldn't be used in async code - use async iteration instead
        raise NotImplementedError("Use 'async for event in connection.event_queue' in async trio code")
    
    def _push_event(self, event_type, value):
        """Internal method to push events (synchronous, called from Kivy callbacks)"""
        self.event_queue.push(Event(type=event_type, value=value))
    
    def close(self):
        """Close the connection"""
        # Your Queue might have a close method, or just let it be GC'd
        pass

def fs(s):
    if isinstance(s, (tuple, list)) and len(s) == 2:
        return f"[{'n/a' if s[0] is None else str(int(s[0])):3},{'n/a' if s[1] is None else str(int(s[1])):3}]"
    else:
        return str(s)

class Sizeable(object):

    def __init__(self, activity, parent):
        self.activity = activity    # Activities have self.activity = self
        self.parent   = parent      # Activities have parent=None
        self.children = []          # Non-containers simply leave this empty.
        self.size     = (0, 0)      # Current actual size
        self.dirty    = False

        # Constraints:
        self.min_width  = None
        self.min_height = None
        self.max_width  = None
        self.max_height = None

        if parent:
            parent.children.append(self)

        self.mark_dirty()

    def mark_dirty(self):
        if self.dirty:
            return
        self.dirty = True
        if self.parent:
            self.parent.mark_dirty()
        self.now_dirty()

    def now_dirty(self):
        """Subclass can override to take action when first marked dirty.
        """
        pass

    def constrain(self, min_width=None, min_height=None, max_width=None, max_height=None):

        if self.dirty or min_width != self.min_width or min_height != self.min_height or max_width != self.max_width or max_height != self.max_height:

            if self.debug:
                dbg(self, f" min {fs((self.min_width,self.min_height))} -> {fs((min_width,min_height))} max {fs((self.max_width,self.max_height))} -> {fs((max_width,max_height))}")

            self.min_width  = min_width
            self.max_width  = max_width
            self.min_height = min_height
            self.max_height = max_height

            width, height = self.figure_size()

            if self.min_width  is not None and width  < self.min_width:
                width = self.min_width

            if self.min_height is not None and height < self.min_height:
                height = self.min_height

            if self.max_width  is not None and width  > self.max_width:
                width = self.max_width

            if self.max_height is not None and height > self.max_height:
                height = self.max_height

            self.set_size((width, height))

        self.dirty = False

    def figure_size(self):
        "Subclass should override where this is (often) not suitable"
        if self.debug:
            dbg(self, "Figure Size")
        if self.children:
            for child in self.children:
                child.constrain(self.min_width, self.min_height, self.max_width, self.max_height)
            width  = max(child.size[0] for child in self.children)
            height = max(child.size[1] for child in self.children)
            return (width, height)
        else:
            return self.size


    def set_size(self, size):
        """Called by constrain once our size is figured.
        """
        if size != self.size:
            if self.debug:
                dbg(self, f" size {self.size} -> {size}")
            self.size = size
            self.size_changed()

    def size_changed(self):
        """Called when our size is changed externally.
        Subclass should implement this to enact the actual change.
        """
        pass

class AppWrap(App):
    def __init__(self, activity):
        App.__init__(self)
        self.activity = activity
    def build(self):
        return self.activity.root
    def on_pause(self, *args, **kargs):
        print("ON PAUSE")
        self.activity.bridge.disable()  #HACK
        return True
    def on_resume(self, *args, **kargs):
        print("ON RESUME")
        self.activity.bridge.enable()   #HACK

class Activity(Sizeable):
    """Represents a Kivy window/screen"""
    
    debug = DEBUG

    def __init__(self, connection, dialog=False):
        Sizeable.__init__(self, self, None)
        self.connection = connection
        self.aid        = id(self)
        self.dialog     = dialog
        self.widgets    = {}  # Track all widgets by ID
        self.popup      = None
        
        if dialog:
            # Create a popup
            self.root  = BoxLayout(orientation='vertical', size_hint=(None, 1))
            self.popup = ModalView(
                size_hint    = (None, None),
                auto_dismiss = True
            )
            self.popup.add_widget(self.root)
            def on_dismiss(instance):
                self._send_destroy()
            self.popup.bind(on_dismiss=on_dismiss)

            def defered_open(dt):
                self.constrain(max_width=Window.size[0]*0.8, max_height=Window.size[1]*0.8)
                self.popup.size = self.size
                self.popup.open()

            Clock.schedule_once(defered_open, 0.1)
        else:
            # Main activity - just create the root layout
            # Root should fill the entire window
            self.size = Window.size
            self.root = BoxLayout(orientation='vertical', size=self.size)
            # Bind to window size changes
            Window.bind(size=self.window_size_callback)
            
            # Bind to window close event
            def on_close(*args, **kwargs):
                self._send_destroy()
                return False  # Allow close to proceed
            Window.bind(on_request_close=on_close)

            self.app = AppWrap(self)
        
        # Send create event
        self.connection._push_event('create', {'aid': self.aid})
        
        if not dialog:
            # For main activity, we'll send start event when app actually starts
            Clock.schedule_once(lambda dt: self._send_start(), 0.1)
    
    async def run(self, bridge):
        self.bridge = bridge    # THIS IS A TOTAL HACK of expediency
        self.configure()
        # await async_runTouchApp(self.root, async_lib='trio')
        await self.app.async_run(async_lib='trio')

    def now_dirty(self):
        Clock.schedule_once(self.re_configure, 0.01)

    def re_configure(self, dt):
        #print("DIRTY -> Reconfigure.")
        self.configure()

    def configure(self):
        self.window_size_callback(None, self.size)
        if self.debug:
            self.dump_tree()

    def window_size_callback(self, instance, size):
        if self.debug and instance:
            print(f"Root window size change: {self.size} -> {size}")
        if not min(size):
            if self.debug:
                print(f"(Ignoring--Zero!)")
            return
        self.constrain(size[0], size[1], size[0], size[1])

    def size_changed(self):
        self.root.size = self.size

    def _send_start(self):
        self.connection._push_event('start', {'aid': self.aid})
    
    def _send_destroy(self):
        self.connection._push_event('destroy', {'aid': self.aid})
    
    def finish(self):
        """Close this activity"""
        if self.popup:
            Clock.schedule_once(lambda dt: self.popup.dismiss(), 0)
        else:
            # For main activity, close the window
            Window.close()
        self.connection._push_event('destroy', {'aid': self.aid})

    def dump_tree(self, indent=''):
        print(f"{indent}{type(self).__name__}: size={fs(self.size)}")
        assert len(self.children) == 1
        self.children[0].dump_tree(indent + '  ')

class _KivyWidget(Sizeable):
    """Base wrapper for Kivy widgets to match termuxgui API"""
    
    is_container = False

    # If True, this container should shrink to fit its contents in that direction; otherwise use layout or specified size.
    fit_width    = True
    fit_height   = True

    debug = DEBUG

    def __init__(self, activity, widget, parent=None):
        Sizeable.__init__(self, activity, parent or activity)

        self.widget = widget
        self.id     = id(self)

        # Add to parent if specified, or to activity root if parent is None
        if parent is None:
            activity.root.add_widget(widget)
            widget.size_hint = (1, 1)
        else:
            target = parent._container if hasattr(parent, '_container') else parent.widget
            target.add_widget(widget)
            if target.orientation == 'horizontal':
                widget.size_hint = (None, 1)
            else:
                widget.size_hint = (1, None)
        
        # Register with activity
        self.activity.widgets[self.id] = self
    
    def setheight(self, height):
        """Set height in dp or WRAP_CONTENT"""
        if self.debug:
            dbg(self, f"Setheight({height}) where Hint={self.widget.size_hint} ; Size={self.widget.size} ; Fit={self.fit_width},{self.fit_height}")
        if height == View.WRAP_CONTENT:
            self.min_height = self.max_height = None
            self.widget.size_hint_y = None
            self.fit_height         = True
        elif isinstance(height, (int, float)):
            height = dp(height)
            self.min_height = self.max_height = height
            self.widget.size_hint_y = None
            self.widget.height      = height
            self.fit_height         = False
        else:
            raise
        self.mark_dirty()
        if self.debug:
            dbg(self, f"                  -> Hint={self.widget.size_hint} ; Size={self.widget.size} ; Fit={self.fit_width},{self.fit_height}")
    
    def setwidth(self, width):
        """Set width in dp or WRAP_CONTENT"""
        if width == View.WRAP_CONTENT:
            self.min_width = self.max_width = None
            self.widget.size_hint_x = None
            self.fit_width          = True
        elif isinstance(width, (int, float)):
            width = dp(width)
            self.min_width = self.max_width = width
            self.widget.size_hint_x = None
            self.widget.width       = width
            self.fit_width          = False
        else:
            raise
        self.mark_dirty()
        if self.debug:
            dbg(self, f"width={width} -> Hint={self.widget.size_hint} ; Size={self.widget.size}")
    
    def setlinearlayoutparams(self, weight=None):
        """Set layout weight (maps to size_hint x or y)
        Note we have to infer whether it's x or y based on the parent's orientation...
        """
        if self.debug:
            dbg(self, f"setweight({weight}) where  Hint={self.widget.size_hint} ; Size={self.widget.size} ; Fit={self.fit_width},{self.fit_height}")
        xaxis = self.widget.parent.orientation == 'horizontal'
        if weight == 0:
            #
            # weight=0 means use minimum size
            # But don't override if widget is a root-level layout that should fill
            if not (isinstance(self.widget, BoxLayout) and self.widget.parent == self.activity.root):
                if xaxis:
                    self.widget.size_hint_x = None
                    #self.fit_width = True      # Termuxgui doesn't actually do this
                else:
                    self.widget.size_hint_y = None
                    #self.fit_height = True     # Termuxgui doesn't actually do this
        else:
            if xaxis:
                self.widget.size_hint_x = weight
                self.fit_width          = False
            else:
                self.widget.size_hint_y = weight
                self.fit_height         = False
        self.mark_dirty()
        if self.debug:
            dbg(self, f"                 -> Hint={self.widget.size_hint} ; Size={self.widget.size} ; Fit={self.fit_width},{self.fit_height}")
    
    def setbackgroundcolor(self, color):
        """Set background color from ARGB integer"""
        # Convert ARGB int to RGBA float tuple
        a = ((color >> 24) & 0xFF) / 255.0
        r = ((color >> 16) & 0xFF) / 255.0
        g = ((color >>  8) & 0xFF) / 255.0
        b = ( color        & 0xFF) / 255.0
        
        # Set color based on widget type
        if hasattr(self.widget, 'background_color'):
            self.widget.background_color = (r, g, b, a)
        elif False and hasattr(self.widget, 'color'):
            # For labels, set text color
            self.widget.color = (r, g, b, a)
    
    def setvisibility(self, visibility):
        """Set visibility (0=hidden, 2=visible in termuxgui)"""
        visible = (visibility == 2)
        
        if visible:
            # Make visible: restore to parent and reset properties
            if hasattr(self, '_hidden_from_parent') and self._hidden_from_parent:
                parent = self._hidden_from_parent
                parent.add_widget(self.widget)
                self._hidden_from_parent = None
            self.widget.opacity = 1
            self.widget.disabled = False
            if hasattr(self, '_saved_size_hint_y'):
                self.widget.size_hint_y = self._saved_size_hint_y
                delattr(self, '_saved_size_hint_y')
            if hasattr(self, '_saved_height'):
                self.widget.height = self._saved_height
                delattr(self, '_saved_height')
        else:
            # Make invisible: remove from parent to avoid blocking touch events
            if self.widget.parent:
                self._hidden_from_parent = self.widget.parent
                self._saved_size_hint_y = self.widget.size_hint_y
                self._saved_height = self.widget.height
                self._hidden_from_parent.remove_widget(self.widget)
            self.widget.opacity = 0
            self.widget.disabled = True

    def size_changed(self):
        self.widget.size = self.size

    def dump_tree(self, indent=''):
        print(f"{indent}{type(self).__name__}: size={fs(self.size)} widget={fs(self.widget.size)} hint={self.widget.size_hint}"
              f" min={fs(self.widget.minimum_size) if hasattr(self.widget, 'minimum_size') else 'N/A'}"
              f" tex={fs(self.widget.texture_size) if hasattr(self.widget, 'texture_size') else 'N/A'}"
              )
        for child in self.children:
            child.dump_tree(indent+'  ')

class TextWidget(_KivyWidget):

    def figure_size(self):
        # This is painful.  We have to disable wrapping to see how narrow it can be:
        self.widget.text_size = (None, None)
        self.widget.texture_update()

        # But if it's too wide, we need to enable wrapping now in order to see how tall it will be:
        if self.max_width and self.widget.texture_size[0] > self.max_width:
            self.widget.text_size = (self.max_width, None)
            self.widget.texture_update()

        if self.debug:
            dbg(self, f"Figure Size: {self.size} -> {self.widget.texture_size} ; {self.widget.text!r}")
        return self.widget.texture_size

    def settext(self, text):
        """Update text"""
        self.widget.text = str(text)
        self.mark_dirty()

class TextView(TextWidget):
    """Text display widget"""
    
    def __init__(self, activity, text='', parent=None):
        label = Label(text=text)
        super().__init__(activity, label, parent)
    
class Button(TextWidget):
    """Button widget with click events"""
    
    def __init__(self, activity, text='', parent=None):
        button = KivyButton(text=text, background_normal='', halign='center')
        button.padding = (dp(5), dp(5))
        super().__init__(activity, button, parent)

        self.min_width  = dp(90)
        self.min_height = dp(45)

        # Bind click event
        def on_press(instance):
            activity.connection._push_event('click', {
                'aid': activity.aid,
                'id' : self.id
            })
        self.widget.bind(on_press=on_press)
    
class LinearLayout(_KivyWidget):
    """Layout container (vertical or horizontal)"""
    
    is_container = True

    def __init__(self, activity, parent=None, vertical=True):
        orientation = 'vertical' if vertical else 'horizontal'
        layout = BoxLayout(orientation=orientation)
        super().__init__(activity, layout, parent)

        # BoxLayout should expand to fill parent by default
        layout.size_hint = (1.0, 1.0)
        layout.spacing   = dp(8)

        self.vertical = vertical

    def figure_size(self):
        if not self.children:
            return self.size

        if self.vertical:
            for child in self.children:
                child.constrain(min_width=self.min_width, max_width=self.max_width, min_height=child.min_height, max_height=child.max_height)
            width = max(child.size[0] for child in self.children)
            # Kivy computes the height:
            self.widget.width = width
            self.widget.do_layout()
            return (width, self.widget.minimum_size[1])
        else:
            for child in self.children:
                child.constrain(min_height=self.min_height, max_height=self.max_height, min_width=child.min_width, max_width=child.max_width)
            height = max(child.size[1] for child in self.children)
            # Kivy computes the width:
            self.widget.height = height
            self.widget.do_layout()
            return (self.widget.minimum_size[0], height)

class NestedScrollView(_KivyWidget):
    """Scrollable container"""
    is_container = True
    
    def __init__(self, activity, parent=None):
        
        scroll = ScrollView(do_scroll_x=False, do_scroll_y=True)
        
        # ScrollView requires an inner container for content
        container = BoxLayout(orientation='vertical', size_hint_y=None)
        container.bind(minimum_height=container.setter('height'))
        scroll.add_widget(container)
        
        # Bind container width to scroll width
        scroll.bind(width=lambda instance, value: setattr(container, 'width', value))
        
        # Children should be added to container, not scroll
        self._container = container
        
        # Pass scroll to parent
        super().__init__(activity, scroll, parent)
        
        # Limit ScrollView height when container size updates
        def update_scroll_height(instance, value):
            #dbg(self, f"UpdateScrollHeight({value}) -- self.size={self.size} ; inst.size={instance.size}")
            scroll.height      = min(value, self.max_height) if self.max_height else value
            scroll.size_hint_y = None
        
        container.bind(minimum_height=update_scroll_height)

    def figure_size(self):
        if not self.children:
            return self.size

        for child in self.children:
            child.constrain(min_width=self.min_width, max_width=self.max_width, min_height=child.min_height, max_height=child.max_height)
        width  = max(child.size[0] for child in self.children)
        height = max(child.size[1] for child in self.children)
        return (width, height)

class TabLayout(_KivyWidget):
    """Tabbed interface - manages tab buttons and controls page visibility"""
    
    is_container = True

    def __init__(self, activity, parent=None):
        tab_box = BoxLayout(orientation='horizontal')
        super().__init__(activity, tab_box, parent)

        tab_box.size_hint = (1, None)
        tab_box.height    = dp(44)

        self.tabs = []
        self.current_tab = 0
        self.pages = []  # Will be set by Pager
    
    def setlist(self, tab_names):
        """Set tab names"""
        self.widget.clear_widgets()
        self.tabs = []
        for idx, name in enumerate(tab_names):
            btn = KivyButton(text=name)
            # Highlight the first tab by default
            if idx == 0:
                btn.background_color = (0.5, 0.5, 1, 1)
            btn.bind(on_press=lambda instance, i=idx: self._on_tab_select(i))
            self.widget.add_widget(btn)
            self.tabs.append(btn)
    
    def figure_size(self):
        return (self.widget.width, self.widget.height)

    def _on_tab_select(self, index):
        """Handle tab selection"""
        if index == self.current_tab:
            return  # Already on this tab
        
        old_tab = self.current_tab
        self.current_tab = index
        
        # Update button colors to show active tab
        for i, btn in enumerate(self.tabs):
            if i == index:
                btn.background_color = (0.5, 0.5, 1, 1)  # Highlighted
            else:
                btn.background_color = (1, 1, 1, 1)  # Normal
        
        # Notify activity
        self.activity.connection._push_event('itemselected', {
            'aid'     : self.activity.aid,
            'id'      : self.id,
            'selected': index
        })


# Export all classes at module level for easy import
__all__ = [
    'View', 'Connection', 'Activity', 'TextView', 'Button',
    'LinearLayout', 'NestedScrollView', 'Spinner', 'TabLayout'
]

