#! /usr/bin/env python2
#coding=UTF-8
from __future__ import print_function

# Run uninstalled webvi as
# PYTHONPATH=src/libwebvi python2 src/webvicli/webvi --templatepath=templates

# Dependencies:
# python2
# python2-qt4
# python2-webvi
# QT or Freedesktop icon theme?

# TODO
# Investigate timeouts (keeps retrying when gateway removed)

fromlist = ("traced", "trace", "tracer")
ftrace = __import__("ftrace", fromlist=fromlist)
for name in fromlist:
    setattr(__builtins__, name, getattr(ftrace, name))

import webvi.api as webvi
from webvi.constants import (
    WebviRequestType, WebviOpt, WebviConfig, WebviSelectBitmask, WebviSelect
)
from PyQt4.QtGui import (
    QApplication,
    QTreeWidget, QTreeWidgetItem,
    QMainWindow, QAction, QLabel, QIcon, QFont, QHeaderView, QStyle,
    QLineEdit, QSplitter, QWidget, QFormLayout, QScrollArea, QDialog,
    QDialogButtonBox, QComboBox, QProgressDialog
)
from PyQt4.QtCore import (Qt, QTimer, QSocketNotifier)
import qtwrap
import event
from contextlib import closing
import xml.etree.cElementTree as etree
from lib import Record
from sys import (stderr, argv)
from functools import partial
from sys import (excepthook, exc_info)
from ftrace import print_call

def trace_coro(coro, *args, **kw):
    stderr.write("(yield ")
    print_call(coro.__name__, args, kw)
    stderr.write(")")
    stderr.flush()
    ret = (yield coro(*args, **kw))
    print(" ->", repr(ret), file=stderr)
    raise StopIteration(ret)

def main():
    (opt, _) = webvi.load_config()
    webvi.apply_config(opt)
    
    app = QApplication([])
    
    w = Window(
        windowTitle="Webvi",
    )
    
    with closing(event.Group()) as w.coros:
        event.Routine(w.main(*argv[1:]), w.coros)
        app.exec_()

class Window(QMainWindow):
    def __init__(self, *args, **kw):
        QMainWindow.__init__(self, *args, **kw)
        
        m = self.menuBar().addMenu("Dessert&s")
        #~ m.addAction(QAction("Refresh menu", self))
        #~ m.addAction(QAction("Go to . . .", self))
        #~ m.addAction(QAction("Copy menu URI", self))
        #~ m.addAction(QAction("Copy stream URL", self))
        #~ m.addAction(QAction("Play", self))
        #~ m.addAction(QAction("Download", self))
        m.addAction(QAction("E&xit", self,
            menuRole=QAction.QuitRole,
            triggered=self.close,
        ))
        
        self.tree = QTreeWidget(
            selectionMode=QTreeWidget.ExtendedSelection,
            contextMenuPolicy=Qt.ActionsContextMenu,
            itemExpanded=self.size_tree,
            itemCollapsed=self.size_tree,
            expandsOnDoubleClick=False,
            verticalScrollMode=QTreeWidget.ScrollPerPixel,
            headerHidden=True,
            currentItemChanged=self.item_changed,
            itemActivated=self.item_activated,
        )
        self.tree.itemExpanded.connect(self.item_expanded)
        
        self.location = QLineEdit()
        
        splitter = QSplitter(Qt.Vertical)
        splitter.addWidget(self.tree)
        tray = WindowTray(self.tree)
        lay = QFormLayout()
        lay.addRow("&Location:", self.location)
        self.info = QScrollArea()
        self.info.setWidget(QLabel("'Ello I am an 'eading\n\nI\nlove\nto\n'ead?!\n\n\nblausadfoijaofij awoefj aowefj awofj aowejf owejfoaweji foawej foiawje foiaj weofj aweofij wej"))
        #~ infolay = QVBoxLayout()
        #~ infolay.addWidget(QLabel("'Ello I am an 'eading\n\nI love to 'ead?!\n\n\nblausadfoijaofij awoefj aowefj awofj aowejf owejfoaweji foawej foiawje foiaj weofj aweofij wej"))
        #~ self.info.setLayout(infolay)
        lay.addRow(self.info)
        lay.addRow("&Ref:", QLineEdit())
        lay.addRow("&Stream:", QLineEdit())
        tray.setLayout(lay)
        splitter.addWidget(tray)
        self.setCentralWidget(splitter)
        
        self.show()
        
        self.wv_finished = dict()
        self.wv_timer = QTimer(
            singleShot=True,
            timeout=partial(self.wv_action,
                WebviSelect.TIMEOUT, WebviSelectBitmask.CHECK),
        )
        webvi.set_config(WebviConfig.TIMEOUT_CALLBACK, self.set_wv_timeout)
        self.wv_notes = list()
    
    def item_expanded(self, item):
        data = item.data(0, Qt.UserRole).toPyObject()
        if data.opened:
            return
        data.opened = True
        
        if hasattr(data, "ref"):
            event.Routine(self.menu(item), self.coros)
        if hasattr(data, "submission"):
            dialog = item.parent().data(0, Qt.UserRole).toPyObject().dialog
            dialog.pyqtConfigure(accepted=partial(self.query_submit, item))
            dialog.show()
    
    def query_submit(self, item):
        QTreeWidgetItem(item, (". . .",)).setDisabled(True)
        item.setChildIndicatorPolicy(
            QTreeWidgetItem.DontShowIndicatorWhenChildless)
        lay = item.parent().data(0, Qt.UserRole).toPyObject().dialog.layout()
        data = item.data(0, Qt.UserRole).toPyObject()
        
        params = list()
        for i in range(lay.rowCount() - 1):
            widget = lay.itemAt(i, QFormLayout.FieldRole).widget()
            if isinstance(widget, QLineEdit):
                params.append("subst={0},{1}".format(widget.name,
                    widget.text()))
            if isinstance(widget, QComboBox):
                params.append("subst={0},{1}".format(widget.name,
                    widget.itemData(widget.currentIndex()).toPyObject()))
        data.ref = data.submission + "&" + "&".join(params)
        event.Routine(self.menu(item), self.coros)
    
    def item_changed(self, current, _):
        self.location.setText(
            (current.parent() or self.tree.invisibleRootItem()).
            data(0, Qt.UserRole).toPyObject().ref)
    
    def main(self, ref="wvt:///?srcurl=mainmenu"):
        root = self.tree.invisibleRootItem()
        root.setData(0, Qt.UserRole, Record(ref=ref))
        return self.menu(root)
    
    def menu(self, parent):
        parent_data = parent.data(0, Qt.UserRole).toPyObject()
        handle = trace(webvi.new_request,
            parent_data.ref, WebviRequestType.MENU)
        if handle == -1:
            raise Exception("Failed to open handle")
        
        webvi.set_opt(handle, WebviOpt.WRITEFUNC, buffer_write)
        
        buffer = bytearray()
        webvi.set_opt(handle, WebviOpt.WRITEDATA, buffer)
        
        finished = event.Queue()
        self.wv_finished[handle] = finished
        
        webvi.start_handle(handle)
        self.reschedule_wv()
        
        if not finished:
            yield finished
        next(finished)
        
        webvi.delete_handle(handle)
        
        parent.takeChildren()
        
        root = etree.XML(str(buffer))
        if root.tag != "wvmenu":
            raise Exception("Expected <wvmenu> root element")
        
        parent_data.dialog = None
        for node in root:
            if node.tag == "title":
                title = node.text
                if title:
                    item = QTreeWidgetItem(parent, (title,))
                    item.setIcon(0, QApplication.style().standardIcon(
                        QStyle.SP_ArrowForward))
                    font = item.font(0)
                    font.setBold(True)
                    item.setFont(0, font)
            
            elif node.tag == "link":
                label = node.find("label")
                if label is None:
                    label = ""
                else:
                    label = label.text
                item = QTreeWidgetItem(parent, (label,))
                
                data = Record(opened=False)
                item.setData(0, Qt.UserRole, data)
                
                item_ref = node.find("ref")
                if item_ref is not None:
                    data.ref = item_ref.text
                    QTreeWidgetItem(item, (". . .",)).setDisabled(True)
                
                stream = node.find("stream")
                if stream is not None:
                    data.stream = stream.text
                    item.setIcon(0, QApplication.style().standardIcon(
                        QStyle.SP_FileIcon))
                    font = item.font(0)
                    font.setFamily("serif")
                    font.setItalic(True)
                    item.setFont(0, font)
            
            elif node.tag == "textfield":
                query_dialog(parent_data, self)
                lay = parent_data.dialog.layout()
                edit = QLineEdit()
                lay.insertRow(lay.rowCount() - 1,
                    node.find("label").text, edit)
                edit.name = node.get("name")
            
            elif node.tag == "itemlist":
                query_dialog(parent_data, self)
                lay = parent_data.dialog.layout()
                box = QComboBox()
                for item in node.findall("item"):
                    box.addItem(item.text, userData=item.get("value"))
                lay.insertRow(lay.rowCount() - 1,
                    node.find("label").text, box)
                box.name = node.get("name")
            
            elif node.tag == 'textarea':
                # TODO: word wrap
                QTreeWidgetItem(parent, (node.find("label").text,))
            
            elif node.tag == "button":
                query_dialog(parent_data, self)
                item = QTreeWidgetItem(parent, (node.find("label").text,))
                item.setIcon(0, QApplication.style().standardIcon(
                    QStyle.SP_DialogOkButton))
                item.setData(0, Qt.UserRole, Record(
                    opened=False,
                    submission=node.find("submission").text,
                ))
                item.setChildIndicatorPolicy(QTreeWidgetItem.ShowIndicator)
            
            else:
                raise TypeError(
                    "Unexpected <wvmenu> node: <{0}>".format(node.tag))
        self.size_tree()
        
        if parent_data.dialog is not None:
            if title:
                parent_data.dialog.setWindowTitle(title)
            lay.itemAt(0, QFormLayout.FieldRole).widget().setFocus()
        
        return
    
    def item_activated(self, item, _):
        data = item.data(0, Qt.UserRole).toPyObject()
        try:
            stream = data.stream
        except AttributeError:
            return
        event.Routine(self.download(stream), self.coros)
    
    def download(self, stream):
        handle = trace(webvi.new_request, stream, WebviRequestType.FILE)
        if handle == -1:
            raise Exception("Failed to open handle")
        
        progress = QProgressDialog(self,
            labelText="Waiting for information",
            autoReset=False,
            value=0,
        )
        canceled = qtwrap.Event(progress.canceled)
        
        finished = event.Queue()
        self.wv_finished[handle] = finished
        
        writefunc = event.Queue()
        webvi.set_opt(handle, WebviOpt.WRITEFUNC, traced(writefunc, name="writefunc", abbrev=(0,)))
        
        trace(webvi.start_handle, handle)
        trace(self.reschedule_wv)
        
        try:
            if not writefunc and not finished:
                yield trace_coro(event.Any, (writefunc, finished, canceled))
                if progress.wasCanceled():
                    return
            
            (_, contentlength) = trace(webvi.get_info, handle,
                WebviInfo.CONTENT_LENGTH)
            progress.setMaximum(contentlength)
            
            (_, url) = webvi.get_info(handle, WebviInfo.URL)
            progress.setLabelText(url)
            
            (_, title) = webvi.get_info(handle, WebviInfo.STREAM_TITLE)
            dialog = QFileDialog(self)
            dialog.selectFile(title)
            dialog.show()
            if (yield qtwrap.Event(dialog.finished)) != QDialog.Accepted:
                return
            
            with open(dialog.selectedFile(), "wb") as file:
                while True:
                    for (data, _, _) in writefunc:
                        file.write(data)
                        progress.setValue(file.tell())
                    
                    if finished:
                        break
                    
                    yield event.Any((writefunc, finished, canceled))
                    if progress.wasCanceled():
                        return
        
        finally:
            trace(webvi.stop_handle, handle)
            if not finished:
                yield finished
            try:
                next(finished)
            except EnvironmentError:  # Ignore "Aborted" exception
                excepthook(*exc_info())
            webvi.delete_handle(handle)
            progress.reset()
    
    def set_wv_timeout(self, timeout, _):
        if timeout < 0:
            self.wv_timer.stop()
        else:
            self.wv_timer.start(timeout)
    
    def reschedule_wv(self):
        while self.wv_notes:
            self.wv_notes.pop().setEnabled(False)
        
        while True:
            (finished, status, errmsg, remaining) = webvi.pop_message()
            if finished != -1:
                finished = self.wv_finished.pop(finished)
                if status:
                    finished.throw(EnvironmentError(status, errmsg))
                else:
                    finished.send()
            elif status not in (-1, 0):
                raise EnvironmentError(status, errmsg)
            if not remaining:
                break
        
        (_, read, write, exc, _) = webvi.fdset()
        for (name, type, bitmask) in (
            ("read", QSocketNotifier.Read, WebviSelectBitmask.READ),
            ("write", QSocketNotifier.Write, WebviSelectBitmask.WRITE),
            ("exc", QSocketNotifier.Exception, WebviSelectBitmask.EXCEPTION),
        ):
            action = partial(self.wv_action, bitmask=bitmask)
            for fd in locals()[name]:
                note = QSocketNotifier(fd, type, activated=action)
                note.setEnabled(True)
                self.wv_notes.append(note)
        
        if not self.wv_timer.isActive():
            self.wv_timer.start(10 * 1000)
    
    def wv_action(self, fd, bitmask):
        webvi.perform(fd, bitmask)
        self.reschedule_wv()
    
    def size_tree(self):
        self.tree.resizeColumnToContents(0)

def query_dialog(data, parent):
    if data.dialog:
        return
    
    data.dialog = QDialog(parent=parent)
    lay = qtwrap.FormLayout()
    lay.addRow(QDialogButtonBox(
        standardButtons=QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
        accepted=data.dialog.accept,
        rejected=data.dialog.reject,
    ))
    data.dialog.setLayout(lay)

class WindowTray(QWidget):
    def __init__(self, main):
        QWidget.__init__(self)
        self.main = main
    
    def keyPressEvent(self, press):
        if press.key() == Qt.Key_Escape:
            self.main.setFocus(Qt.ShortcutFocusReason)
        else:
            return QWidget.keyPressEvent(self, press)

#~ @partial(traced, abbrev=set((0, "data")))
def buffer_write(data, len, buffer):
    buffer.extend(data)
    return len

if "__main__" == __name__:
    from gc import (set_debug, DEBUG_LEAK, collect, garbage)
    set_debug(DEBUG_LEAK)
    
    try:
        main()
    finally:
        collect()
        # Cannot help that class definitions create garbage cycles
        ignore = [g for g in garbage if isinstance(g, type)]
        del garbage[:]
        collect()
        if garbage:
            try:
                import cycles
                cycles.show()
            except Exception:
                excepthook(*exc_info())
            
            print(len(garbage), file=stderr)
            for g in garbage:
                print(repr(g), file=stderr)
