import FreeCAD
import FreeCADGui as Gui
import subprocess
import PySide
import re
from PySide import QtGui
from PySide import QtCore

import XternalAppsList
from MyX11Utils import *

#class MyMdiSubWindow(QMdiSubWindow):
#    def closeEvent

class EmbeddedWindow(QtCore.QObject):
    def __init__(self, app, xternalAppInstance, processId, windowId):
        super(EmbeddedWindow, self).__init__()
        self.app = app
        self.xternalAppInstance = xternalAppInstance
        self.processId = processId
        self.windowId = windowId
        self.mdi = Gui.getMainWindow().findChild(QtGui.QMdiArea)
        self.xw = QtGui.QWindow.fromWinId(self.windowId)
        self.xw.setFlags(QtGui.Qt.FramelessWindowHint)
        self.xwd = QtGui.QWidget.createWindowContainer(self.xw)
        self.mwx = QtGui.QMainWindow()
        #self.mwx.layout().addWidget(self.xwd)
        self.mwx.setCentralWidget(self.xwd)
        self.mdiSub = self.mdi.addSubWindow(self.xwd)
        self.xwd.setBaseSize(640,480)
        self.mwx.setBaseSize(640,480)
        self.mdiSub.setBaseSize(640,480)
        self.mdiSub.setWindowTitle(app.name)
        self.mdiSub.show()
        self.xwd.installEventFilter(self)
        self.mwx.installEventFilter(self)
        self.mdiSub.installEventFilter(self)
        self.timer = QtCore.QTimer(self)
        self.timer.timeout.connect(self.pollWindowClosed)
        self.timer.start(1000)

    def eventFilter(self, obj, event):
        # This doesn't seem to work, some events occur but no the close one.
        if event.type() == QtCore.QEvent.Close and x11stillAlive(self.windowId):
            #xdotool closes the window without asking for confirmation
            #subprocess.Popen(['xdotool', 'windowclose', str(self.windowId)])

            # use decode('utf-8', 'ignore') to use strings instead of
            # byte strings and discard ill-formed unicode in case this
            # tool doesn't sanitize their output
            xwininfo_output = subprocess.check_output(['xwininfo', '-root', '-int']).decode('utf-8', 'ignore').split('\n')
            root_id = None
            for line in xwininfo_output:
                match = re.compile(r'^xwininfo: Window id: ([0-9]+)').match(line)
                if match:
                    root_id = match.group(1)
                    break
            if root_id is not None:
                # detach
                subprocess.Popen(['xdotool', 'windowreparent', str(self.windowId), root_id])
                # send friendly close signal
                subprocess.Popen(['wmctrl', '-i', '-c', str(self.windowId)])
                # Cleanup
                # TODO: destroy self.xw and .xwd if possible to avoid a leak
                self.xw.setParent(None)
                self.xwd.setParent(None)
                self.timer.stop()
                # remove from dictionary of found windows
                self.xternalAppInstance.foundWindows.pop(self.windowId, None)
                # avoid GC
                self.xternalAppInstance.closedWindows[self.windowId] = self
                # re-attach in case it didn't close (confirmation dialog etc.)
                print('waitForWindow')
                self.xternalAppInstance.waitForWindow()
#                try:
#                    self.xw = QtGui.QWindow.fromWinId(self.windowId)
#                    self.xwd = QtGui.QWidget.createWindowContainer(self.xw)
#                    self.mwx.setCentralWidget(self.xwd)
#                except Exception as e:
#                    print(repr(e))
#                    pass
            else:
                event.ignore()
            return True
        else:
            return False

    @QtCore.Slot()
    def pollWindowClosed(self):
        # TODO: find an event instead of polling
        if not x11stillAlive(self.windowId) and not deleted(self.mdiSub):
            self.mdiSub.close()
            self.timer.stop()

    # TODO: also kill or at least detach on application exit

# <optional spaces> <digits (captured in group 1)> <optional spaces> "<quoted string>"  <optional spaces> : <anything>
xwininfo_re = re.compile(r'^\s*([0-9]+)\s*"[^"]*"\s*:.*$')

def try_pipe_lines(commandAndArguments):
    try:
        # use decode('utf-8', 'ignore') to use strings instead of
        # byte strings and discard ill-formed unicode in case this
        # tool doesn't sanitize their output
        return subprocess.check_output(commandAndArguments).decode('utf-8', 'ignore').split('\n')
    except:
        return []

# TODO: this is just a quick & dirty way to attach a field to the FreeCad object
class XternalApps():
    def __init__(self):
        setattr(FreeCAD, 'XternalApps', self)

def deleted(widget):
    """Detect RuntimeError: Internal C++ object (PySide2.QtGui.QWindow) already deleted."""
    try:
        str(widget) # str fails on already-deleted Qt wrappers.
        return False
    except:
        return True

class XternalAppInstance(QtCore.QObject):
    def __init__(self, appName):
        super(XternalAppInstance, self).__init__()
        self.app = XternalAppsList.apps[appName]
        # Start the application
        # TODO: popen_process shouldn't be exposed to in-document scripts, it would allow them to redirect output etc.
        print('Starting ' + ' '.join(self.app.start_command_and_args))
        self.popen_process = subprocess.Popen(self.app.start_command_and_args)
        self.appProcessIds = [self.popen_process.pid]
        self.initWaitForWindow()
        self.foundWindows = dict()
        self.closedWindows = dict()
        setattr(FreeCAD.XternalApps, self.app.name, self)

    def initWaitForWindow(self):
        self.TimeoutHasOccurred  = False # for other scritps to know the status
        self.startupTimeout = 10000
        self.elapsed = QtCore.QElapsedTimer()
        self.elapsed.start()
        self.timer = QtCore.QTimer(self)
        self.timer.timeout.connect(self.attemptToFindWindow)

    def waitForWindow(self):
        self.timer.start(50)

    @QtCore.Slot()
    def attemptToFindWindow(self):
        try:
            self.attemptToFindWindowWrapped()
        except:
            self.timer.stop()
            raise

    def attemptToFindWindowWrapped(self):
        for line in try_pipe_lines(['xwininfo', '-root', '-tree', '-int']):
            self.attemptWithLine(line)

        if self.elapsed.elapsed() > self.startupTimeout:
            self.timer.stop()
            self.TimeoutHasOccurred = True

    def attemptWithLine(self, line):
        if not self.app.xwininfo_filter_re.search(line):
            return
        xwininfo_re_match_line = xwininfo_re.match(line)
        if not xwininfo_re_match_line:
            return
        windowId = int(xwininfo_re_match_line.group(1))
        xprop_try_process_id = x11prop(windowId, '_NET_WM_PID', 'CARDINAL')
        if not xprop_try_process_id:
            return
        processId = int(xprop_try_process_id) # TODO try parse int and catch failure
        if processId not in self.appProcessIds:
            return
        if not self.app.extra_xprop_filter(processId, windowId, len(self.foundWindows)):
            return
        self.foundWindow(processId, windowId)

    def foundWindow(self, processId, windowId):
        print('found ' + str(windowId))
        if windowId not in self.foundWindows.keys():
            self.foundWindows[windowId] = EmbeddedWindow(self.app, self, processId, windowId)
#            for w in self.foundWindows.values():
#                #if not deleted(xw) and not xw.isActive():
#                if not x11stillAlive(w.windowId):
#                    w.mdiSub.close()