import sys
import time
import threading
import queue
import wx
from dlg import G2MultiChoiceDialog

asyncResults = {}
dialog_queue = queue.Queue()

def getAllChildren(parent):
    '''Recursively finds all children of a given parent window.

    :Returns: A list containing all descendant wx.Window objects.
    '''
    all_children = []
    for child in parent.GetChildren():
        all_children.append(child)
        all_children.extend(getAllChildren(child))
    return all_children

def getButtons(winname, btnList):
    '''Finds a window with a label matching `winname` and
    then the button objects matching the names in `btnlist`

    If run in a thread, this should be called by CallAfter
    '''    
    asyncResults.clear()
    # find window
    for w in wx.GetTopLevelWindows():
        if w.GetTitle() == winname:
            #print(f"Window {winname!r} found")
            win = w
            asyncResults['frame'] = win
            break
    else:
        asyncResults['error'] = f"Window {winname!r} not found"        
        return
    # find each button
    btns = []
    for btnname in btnList:
        for b in getAllChildren(win):
            if not hasattr(b,'GetLabelText'): continue # not button
            if not issubclass(b.__class__,wx.Button): continue # not button
            if b.GetLabelText() == btnname:
                btns.append(b)
                break
        else:
            asyncResults['error'] = f'Button {btnname} not found'
            return
    asyncResults['buttons'] = btns

def invokeButton(frame, btn):
    '''"press" button (`btn`) in a supplied window (`frame`) by 
    moving the mouse to the center of the button and "left-clicking"
    via UIActionSimulator.

    If run in a thread, this should be called by CallAfter
    '''
    pos = frame.ClientToScreen(btn.GetPosition() + btn.GetSize()/2)
    sim = wx.UIActionSimulator()
    sim.MouseMove(pos.x,pos.y)
    time.sleep(0.2)
    sim.MouseClick(wx.MOUSE_BTN_LEFT)
    print('invoke done @',pos)
    time.sleep(0.2)
    asyncResults['done'] = True

def waitForDone(asyncResults):
    time.sleep(0.1)
    count = 0
    while len(asyncResults) == 0:
        count += 1
        time.sleep(0.05)
        if count > 1000: raise Exception('too many waits')
    if 'error' in asyncResults:
        raise Exception(f"async failure: {asyncResults['error']}")
    print('done',count,asyncResults)
    return count
    
def doInWindow(winname,bnlList):
    '''Runs in a thread to press two buttons in a modal dialog
    that is opened just after this is started. Note that 
    second button closes the dialog.
    '''
    time.sleep(0.2) # wait longer for window to open
    wx.CallAfter(getButtons,winname,bnlList)
    count = waitForDone(asyncResults)
    #print('getButtons',asyncResults, count)
    buttons,frame = asyncResults['buttons'],asyncResults['frame']
    
    for i, b in enumerate(buttons):
        wx.CallAfter(invokeButton,frame,b)
        if i < len(buttons) - 1:  # If not the last button
            time.sleep(0.3)  # Wait between button clicks
        print(f'button {i} pressed')

dlgResults = {}
def OpenDialog(choices, callback=None, on_shown=None):
    '''Opens a GSAS-II dialog non-modally.
    '''
    dlgResults.clear()
    dlg = G2MultiChoiceDialog(frm,'Select atoms',
            'Select atoms for action',choices)
    
    def handle_dialog_end(ok=False):
        if ok:
            dlgResults['selected'] = dlg.GetSelections()
            print('Selected:', dlgResults['selected'])
        else:
            dlgResults['selected'] = None
        print('Closedialog')
        dlg.Destroy()
        frm.Enable()
        if callback:
            wx.CallLater(500, callback)
    
    def on_ok(event):
        event.Skip()
        handle_dialog_end(ok=True)
    
    def on_cancel(event):
        event.Skip()
        handle_dialog_end(ok=False)
    
    def on_close(event):
        event.Skip()
        handle_dialog_end(ok=False)
    
    def on_show(event):
        event.Skip()
        # Only trigger when dialog is being shown, not hidden
        if event.IsShown() and on_shown:
            on_shown(dlg)
    
    # Bind to both button events and window close
    dlg.Bind(wx.EVT_BUTTON, on_ok, id=wx.ID_OK)
    dlg.Bind(wx.EVT_BUTTON, on_cancel, id=wx.ID_CANCEL)
    dlg.Bind(wx.EVT_CLOSE, on_close)
    dlg.Bind(wx.EVT_SHOW, on_show)
    
    frm.Disable()  # Disable main window while dialog is open
    dlg.Show()

class DialogAutomation:
    def __init__(self, frame):
        self.frame = frame
        self.choices = ['Bi1', 'O1', 'Cl1']
        self.timer = wx.Timer(self.frame)
        self.frame.Bind(wx.EVT_TIMER, self.on_timer, self.timer)
        self.stage = 0
        
    def start(self):
        self.stage = 1
        self.timer.Start(100, oneShot=True)
        
    def on_timer(self, event):
        if self.stage == 1:
            # Start first automation
            threading.Thread(target=doInWindow,
                args=('Select atoms for action',['Set All','OK'])).start()
            self.stage = 2
            self.timer.Start(200, oneShot=True)
            
        elif self.stage == 2:
            # Show first dialog
            OpenDialog(self.choices)
            print('First dialog complete')
            self.stage = 3
            self.timer.Start(1000, oneShot=True)
            
        elif self.stage == 3:
            # Start second automation
            threading.Thread(target=doInWindow,
                args=('Select atoms for action',['Set All','OK'])).start()
            self.stage = 4
            self.timer.Start(200, oneShot=True)
            
        elif self.stage == 4:
            # Show second dialog
            OpenDialog(self.choices)
            print('Second dialog complete')
            self.stage = 0
            
def openSelect(event):
    '''launches `doInWindow` in a thread then opens a modal window
    with `OpenDialog`. `doInWindow` simulates pressing two buttons
    in the dialog, where the second closes the dialog.

    Does this twice, just to prove I can.
    '''
    choices = ['Bi1', 'O1', 'Cl1']
    
    def on_dialog_shown(dlg):
        # Start automation after dialog is shown and had time to render
        wx.CallLater(200, lambda: threading.Thread(target=doInWindow,
            args=('Select atoms for action',['Set All','OK'])).start())
    
    def start_second_dialog():
        # Show second dialog and set up its automation
        OpenDialog(choices, on_shown=on_dialog_shown)
    
    def start_first_dialog():
        # Show first dialog and set up its automation
        OpenDialog(choices, callback=start_second_dialog, on_shown=on_dialog_shown)
    
    start_first_dialog()

app = wx.App()
frm = wx.Frame(None)
ms = wx.BoxSizer(wx.VERTICAL)
btn = wx.Button(frm,wx.ID_ANY,'Open modal window')
btn.Bind(wx.EVT_BUTTON,openSelect)
ms.Add(btn)
frm.SetSizer(ms)
frm.Show(True)
app.MainLoop()
