import threading
import shlex

from guppyproxy.util import max_len_str, query_to_str, display_error_box, display_info_box, display_req_context, display_multi_req_context, hostport, method_color, sc_color, DisableUpdates, host_color
from guppyproxy.proxy import HTTPRequest, RequestContext, InvalidQuery, SocketClosed, time_to_nsecs, ProxyThread
from guppyproxy.reqview import ReqViewWidget
from guppyproxy.reqtree import ReqTreeView
from PyQt5.QtWidgets import QWidget, QTableWidget, QTableWidgetItem, QGridLayout, QHeaderView, QAbstractItemView, QVBoxLayout, QHBoxLayout, QComboBox, QTabWidget, QPushButton, QLineEdit, QStackedLayout, QToolButton, QCheckBox, QLabel, QTableView, QMenu
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QVariant, Qt, QAbstractTableModel, QModelIndex, QItemSelection, QSortFilterProxyModel
from itertools import groupby, count

def get_field_entry():
    dropdown = QComboBox()
    dropdown.addItem("Anywhere", "all")
    dropdown.addItem("Req. Body", "reqbody")
    dropdown.addItem("Rsp. Body", "rspbody")
    dropdown.addItem("Any Body", "body")
    # dropdown.addItem("WSMessage", "wsmessage")

    dropdown.addItem("Req. Header", "reqheader")
    dropdown.addItem("Rsp. Header", "rspheader")
    dropdown.addItem("Any Header", "header")

    dropdown.addItem("Method", "method")
    dropdown.addItem("Host", "host")
    dropdown.addItem("Path", "path")
    dropdown.addItem("URL", "url")
    dropdown.addItem("Status", "statuscode")
    dropdown.addItem("Tag", "tag")

    dropdown.addItem("Any Param", "param")
    dropdown.addItem("URL Param", "urlparam")
    dropdown.addItem("Post Param", "postparam")
    dropdown.addItem("Rsp. Cookie", "rspcookie")
    dropdown.addItem("Req. Cookie", "reqcookie")
    dropdown.addItem("Any Cookie", "cookie")

    # dropdown.addItem("After", "")
    # dropdown.addItem("Before", "")
    # dropdown.addItem("TimeRange", "")
    # dropdown.addItem("Id", "")
    return dropdown


def get_string_cmp_entry():
    dropdown = QComboBox()
    dropdown.addItem("cnt.", "contains")
    dropdown.addItem("cnt. (rgx)", "containsregexp")
    dropdown.addItem("is", "is")
    dropdown.addItem("len. >", "lengt")
    dropdown.addItem("len. <", "lenlt")
    dropdown.addItem("len. =", "leneq")
    return dropdown


class StringCmpWidget(QWidget):
    returnPressed = pyqtSignal()

    def __init__(self, *args, **kwargs):
        QWidget.__init__(self, *args, **kwargs)
        layout = QHBoxLayout()
        self.cmp_entry = get_string_cmp_entry()
        self.text_entry = QLineEdit()
        self.text_entry.returnPressed.connect(self.returnPressed)
        layout.addWidget(self.cmp_entry)
        layout.addWidget(self.text_entry)
        self.setLayout(layout)
        self.layout().setContentsMargins(0, 0, 0, 0)

    def get_value(self):
        str_cmp = self.cmp_entry.itemData(self.cmp_entry.currentIndex())
        str_val = self.text_entry.text()
        return [str_cmp, str_val]

    def reset(self):
        self.cmp_entry.setCurrentIndex(0)
        self.text_entry.setText("")


def dt_sort_key(r):
    if r.time_start:
        return time_to_nsecs(r.time_start)
    return 0


class StringKVWidget(QWidget):
    returnPressed = pyqtSignal()

    def __init__(self, *args, **kwargs):
        QWidget.__init__(self, *args, **kwargs)
        self.str2_shown = False
        self.str1 = StringCmpWidget()
        self.str2 = StringCmpWidget()
        self.str1.returnPressed.connect(self.returnPressed)
        self.str2.returnPressed.connect(self.returnPressed)
        self.toggle_button = QToolButton()
        self.toggle_button.setText("+")

        self.toggle_button.clicked.connect(self._show_hide_str2)

        layout = QHBoxLayout()
        layout.addWidget(self.str1)
        layout.addWidget(self.str2)
        layout.addWidget(self.toggle_button)

        self.str2.setVisible(self.str2_shown)
        self.setLayout(layout)
        self.layout().setContentsMargins(0, 0, 0, 0)

    @pyqtSlot()
    def _show_hide_str2(self):
        if self.str2_shown:
            self.toggle_button.setText("+")
            self.str2_shown = False
        else:
            self.toggle_button.setText("-")
            self.str2_shown = True
        self.str2.setVisible(self.str2_shown)

    def get_value(self):
        retval = self.str1.get_value()
        if self.str2_shown:
            retval += self.str2.get_value()
        return retval

    def reset(self):
        self.str1.reset()
        self.str2.reset()


class DropdownFilterEntry(QWidget):
    # a widget that lets you enter filters using ezpz dropdowns/text boxes
    filterEntered = pyqtSignal(list)

    def __init__(self, *args, **kwargs):
        QWidget.__init__(self, *args, **kwargs)
        layout = QHBoxLayout()
        confirm = QToolButton()
        confirm.setText("OK")
        confirm.setToolTip("Apply the entered filter")
        self.field_entry = get_field_entry()

        # stack containing widgets for string, k/v, date, daterange
        self.str_cmp_entry = StringCmpWidget()
        self.kv_cmp_entry = StringKVWidget()
        self.inv_entry = QCheckBox("inv")
        # date
        # daterange

        self.entry_layout = QStackedLayout()
        self.entry_layout.setContentsMargins(0, 0, 0, 0)
        self.current_entry = 0
        self.entry_layout.addWidget(self.str_cmp_entry)
        self.entry_layout.addWidget(self.kv_cmp_entry)
        # add date # 2
        # add daterange # 3

        confirm.clicked.connect(self.confirm_entry)
        self.str_cmp_entry.returnPressed.connect(self.confirm_entry)
        self.kv_cmp_entry.returnPressed.connect(self.confirm_entry)
        self.field_entry.currentIndexChanged.connect(self._display_value_widget)

        layout.addWidget(confirm)
        layout.addWidget(self.inv_entry)
        layout.addWidget(self.field_entry)
        layout.addLayout(self.entry_layout)

        self.setLayout(layout)
        self.setContentsMargins(0, 0, 0, 0)
        self._display_value_widget()

    @pyqtSlot()
    def _display_value_widget(self):
        # show the correct value widget in the value stack layout
        field = self.field_entry.itemData(self.field_entry.currentIndex())
        self.current_entry = 0
        if field in ("all", "reqbody", "rspbody", "body", "wsmessage", "method",
                     "host", "path", "url", "statuscode", "tag"):
            self.current_entry = 0
        elif field in ("reqheader", "rspheader", "header", "param", "urlparam"
                       "postparam", "rspcookie", "reqcookie", "cookie"):
            self.current_entry = 1
        # elif for date
        # elif for daterange
        self.entry_layout.setCurrentIndex(self.current_entry)

    def get_value(self):
        val = []
        if self.inv_entry.isChecked():
            val.append("inv")
        field = self.field_entry.itemData(self.field_entry.currentIndex())
        val.append(field)
        if self.current_entry == 0:
            val += self.str_cmp_entry.get_value()
        elif self.current_entry == 1:
            val += self.kv_cmp_entry.get_value()
        # elif for date
        # elif for daterange
        return [val]  # no support for OR

    @pyqtSlot()
    def confirm_entry(self):
        phrases = self.get_value()
        self.filterEntered.emit(phrases)
        self.str_cmp_entry.reset()
        self.kv_cmp_entry.reset()
        # reset date
        # reset date range


class TextFilterEntry(QWidget):
    # a text box that can be used to enter filters
    filterEntered = pyqtSignal(list)

    def __init__(self, *args, **kwargs):
        QWidget.__init__(self, *args, **kwargs)
        layout = QHBoxLayout()
        self.textEntry = QLineEdit()
        self.textEntry.returnPressed.connect(self.confirm_entry)
        self.textEntry.setToolTip("Enter the filter here and press return to apply it")
        layout.addWidget(self.textEntry)
        self.setLayout(layout)
        self.layout().setContentsMargins(0, 0, 0, 0)

    @pyqtSlot()
    def confirm_entry(self):
        args = shlex.split(self.textEntry.text())
        phrases = [list(group) for k, group in groupby(args, lambda x: x == "OR") if not k]
        self.filterEntered.emit(phrases)
        self.textEntry.setText("")


class FilterEntry(QWidget):
    # a widget that lets you switch between filter entries
    filterEntered = pyqtSignal(list)

    def __init__(self, *args, **kwargs):
        QWidget.__init__(self, *args, **kwargs)
        self.current_entry = 0
        self.max_entries = 2
        self.text_entry = TextFilterEntry()
        dropdown_entry = DropdownFilterEntry()

        self.text_entry.filterEntered.connect(self.filterEntered)
        dropdown_entry.filterEntered.connect(self.filterEntered)

        self.entry_layout = QStackedLayout()
        self.entry_layout.addWidget(dropdown_entry)
        self.entry_layout.addWidget(self.text_entry)

        swap_button = QToolButton()
        swap_button.setText(">")
        swap_button.setToolTip("Switch between dropdown and text entry")
        swap_button.clicked.connect(self.next_entry)

        hlayout = QHBoxLayout()
        hlayout.addWidget(swap_button)
        hlayout.addLayout(self.entry_layout)
        self.setLayout(hlayout)
        self.layout().setContentsMargins(0, 0, 0, 0)
        self.layout().setSpacing(0)

    @pyqtSlot()
    def next_entry(self):
        self.current_entry += 1
        self.current_entry = self.current_entry % self.max_entries
        self.entry_layout.setCurrentIndex(self.current_entry)

    def set_entry(self, entry):
        self.current_entry = entry
        self.current_entry = self.current_entry % self.max_entries
        self.entry_layout.setCurrentIndex(self.current_entry)


class FilterListWidget(QTableWidget):
    # list part of the filter tab
    def __init__(self, *args, **kwargs):
        self.client = kwargs.pop("client")
        QTableWidget.__init__(self, *args, **kwargs)
        self.context = RequestContext(self.client)

        # Set up table
        self.setColumnCount(1)
        self.horizontalHeader().hide()
        self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        self.verticalHeader().hide()
        self.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
        #self.setSelectionMode(QAbstractItemView.NoSelection)
        #self.setEditTriggers(QAbstractItemView.NoEditTriggers)

    def append_fstr(self, fstr):
        args = shlex.split(fstr)
        phrase = [list(group) for k, group in groupby(args, lambda x: x == "OR") if not k]
        self.context.apply_phrase(phrase)
        self._append_fstr_row(fstr)

    def set_query(self, query):
        self.context.set_query(query)
        self.redraw_table()

    def pop_phrase(self):
        self.context.pop_phrase()
        self.redraw_table()

    def clear_phrases(self):
        self.context.set_query([])
        self.redraw_table()

    def _append_fstr_row(self, fstr):
        row = self.rowCount()
        self.insertRow(row)
        self.setItem(row, 0, QTableWidgetItem(fstr))

    def redraw_table(self):
        self.setRowCount(0)
        query = self.context.query
        for p in query:
            condstrs = [' '.join(l) for l in p]
            fstr = ' OR '.join(condstrs)
            self._append_fstr_row(fstr)

    def get_query(self):
        return self.context.query


class FilterEditor(QWidget):
    # a widget containing a list of filters and the ability to edit the filters in the list
    filtersEdited = pyqtSignal(list)

    builtin_filters = (
        ('No Images', ['inv', 'path', 'containsregexp', r'(\.png$|\.jpg$|\.jpeg$|\.gif$|\.ico$|\.bmp$|\.svg$)']),
        ('No JavaScript/CSS/Fonts', ['inv', 'path', 'containsregexp', r'(\.js$|\.css$|\.woff$)']),
    )

    def __init__(self, *args, **kwargs):
        self.client = kwargs.pop("client")
        QWidget.__init__(self, *args, **kwargs)
        layout = QVBoxLayout()

        # Manage bar
        manage_bar = QHBoxLayout()
        pop_button = QPushButton("Pop")
        pop_button.setToolTip("Remove the most recently applied filter")
        clear_button = QPushButton("Clear")
        clear_button.setToolTip("Remove all active filters")
        scope_reset_button = QPushButton("Scope")
        scope_reset_button.setToolTip("Set the active filters to the current scope")
        scope_save_button = QPushButton("Save Scope")
        scope_save_button.setToolTip("Set the scope to the current filters. Any messages that don't match the active filters will be ignored by the proxy.")

        self.builtin_combo = QComboBox()
        self.builtin_combo.addItem("Apply a built-in filter", None)
        for desc, filt in FilterEditor.builtin_filters:
            self.builtin_combo.addItem(desc, filt)
        self.builtin_combo.currentIndexChanged.connect(self._apply_builtin_filter)

        manage_bar.addWidget(clear_button)
        manage_bar.addWidget(pop_button)
        manage_bar.addWidget(scope_reset_button)
        manage_bar.addWidget(scope_save_button)
        manage_bar.addWidget(self.builtin_combo)
        manage_bar.addStretch()
        mbar_widget = QWidget()
        mbar_widget.setLayout(manage_bar)
        pop_button.clicked.connect(self.pop_phrase)
        clear_button.clicked.connect(self.clear_phrases)
        scope_reset_button.clicked.connect(self.reset_to_scope)
        scope_save_button.clicked.connect(self.save_scope)

        # Filter list
        self.filter_list = FilterListWidget(client=self.client)

        # Filter entry
        self.entry = FilterEntry()
        self.entry.setMaximumHeight(self.entry.sizeHint().height())
        self.entry.filterEntered.connect(self.apply_phrase)

        layout.addWidget(mbar_widget)
        layout.addWidget(self.filter_list)
        layout.addWidget(self.entry)
        self.setLayout(layout)
        self.layout().setSpacing(0)
        self.layout().setContentsMargins(0, 0, 0, 0)

    @pyqtSlot()
    def save_scope(self):
        query = self.filter_list.get_query()
        self.client.set_scope(query)
        display_info_box("Scope updated")

    @pyqtSlot()
    def reset_to_scope(self):
        query = self.client.get_scope().filter
        self.filter_list.set_query(query)
        self.filtersEdited.emit(self.filter_list.get_query())

    @pyqtSlot()
    def clear_phrases(self):
        self.filter_list.clear_phrases()
        self.filtersEdited.emit(self.filter_list.get_query())

    @pyqtSlot()
    def pop_phrase(self):
        self.filter_list.pop_phrase()
        self.filtersEdited.emit(self.filter_list.get_query())

    @pyqtSlot(list)
    def apply_phrase(self, phrase):
        fstr = query_to_str([phrase])
        try:
            self.filter_list.append_fstr(fstr)
        except InvalidQuery as e:
            display_error_box("Could not add filter:\n\n%s" % e)
            return
        self.filtersEdited.emit(self.filter_list.get_query())

    @pyqtSlot(int)
    def _apply_builtin_filter(self, ind):
        phrase = self.builtin_combo.itemData(ind)
        if phrase:
            self.apply_phrase([phrase])
        self.builtin_combo.setCurrentIndex(0)
        
    def set_is_text(self, is_text):
        if is_text:
            self.entry.set_entry(1)
        else:
            self.entry.set_entry(0)
        

class ReqListModel(QAbstractTableModel):
    requestsLoading = pyqtSignal()
    requestsLoaded = pyqtSignal()
    
    HD_ID = 0
    HD_VERB = 1
    HD_HOST = 2
    HD_PATH = 3
    HD_SCODE = 4
    HD_REQLEN = 5
    HD_RSPLEN = 6
    HD_TIME = 7
    HD_TAGS = 8
    HD_MNGL = 9

    def __init__(self, client, *args, **kwargs):
        QAbstractTableModel.__init__(self, *args, **kwargs)
        self.client = client
        self.header_order = [
            self.HD_ID,
            self.HD_VERB,
            self.HD_HOST,
            self.HD_PATH,
            self.HD_SCODE,
            self.HD_REQLEN,
            self.HD_RSPLEN,
            self.HD_TIME,
            self.HD_TAGS,
            self.HD_MNGL,
        ]
        self.table_headers = {
            self.HD_ID: "ID",
            self.HD_VERB: "Method",
            self.HD_HOST: "Host",
            self.HD_PATH: "Path",
            self.HD_SCODE: "S-Code",
            self.HD_REQLEN: "Req Len",
            self.HD_RSPLEN: "Rsp Len",
            self.HD_TIME: "Time",
            self.HD_TAGS: "Tags",
            self.HD_MNGL: "Mngl",
        }
        self.reqs = []
        self.sort_enabled = False
        self.header_count = len(self.header_order)
        self.reqs_loaded = 0
            
    def headerData(self, section, orientation, role):
        if role == Qt.DisplayRole and orientation == Qt.Horizontal:
            hd = self.header_order[section]
            return self.table_headers[hd]
        return QVariant()
            
    def rowCount(self, parent):
        return self.reqs_loaded
    
    def columnCount(self, parent):
        return self.header_count
    
    def _gen_req_row(self, req):
        MAX_PATH_LEN = 60
        MAX_TAG_LEN = 40
        reqid = self.client.get_reqid(req)
        method = req.method
        host = hostport(req)
        path = max_len_str(req.url.path, MAX_PATH_LEN)
        reqlen = str(req.content_length)
        tags = max_len_str(', '.join(sorted(req.tags)), MAX_TAG_LEN)
        
        if req.response:
            scode = str(req.response.status_code) + ' ' + req.response.reason
            rsplen = str(req.response.content_length)
        else:
            scode = "--"
            rsplen = "--"

        if req.time_start and req.time_end:
            time_delt = req.time_end - req.time_start
            reqtime = ("%.2f" % time_delt.total_seconds())
        else:
            reqtime = "--"
        if req.unmangled and req.response and req.response.unmangled:
            manglestr = "q/s"
        elif req.unmangled:
            manglestr = "q"
        elif req.response and req.response.unmangled:
            manglestr = "s"
        else:
            manglestr = "N/A"
        return (req, reqid, method, host, path, scode, reqlen, rsplen, reqtime, tags, manglestr)
        
    
    def data(self, index, role):
        if role == Qt.BackgroundColorRole:
           req = self.reqs[index.row()][0]
           if index.column() == 2:
               return host_color(hostport(req))
           elif index.column() == 4:
               if req.response:
                   return sc_color(str(req.response.status_code))
           elif index.column() == 1:
               return method_color(req.method)
           return QVariant()
        elif role == Qt.DisplayRole:
           rowdata = self.reqs[index.row()]
           return rowdata[index.column()+1]
        return QVariant()
    
    def canFetchMore(self, parent):
        if parent.isValid():
            return False
        return (self.reqs_loaded < len(self.reqs))
    
    def fetchMore(self, parent):
        if parent.isValid():
            return
        if self.reqs_loaded == len(self.reqs):
            return
        n_to_fetch = 50
        if self.reqs_loaded + n_to_fetch > len(self.reqs):
            n_to_fetch = len(self.reqs) - self.reqs_loaded
        self.beginInsertRows(QModelIndex(), self.reqs_loaded, self.reqs_loaded + n_to_fetch)
        self.reqs_loaded += n_to_fetch
        self.endInsertRows()

    def _sort_reqs(self):
        def skey(rowdata):
            return dt_sort_key(rowdata[0])
        if self.sort_enabled:
            self.reqs = sorted(self.reqs, key=skey, reverse=True)
        
    def _req_ind(self, req=None, reqid=None):
        if not reqid:
            reqid = self.client.get_reqid(req)
        for ind, rowdata in zip(count(), self.reqs):
            req = rowdata[0]
            if self.client.get_reqid(req) == reqid:
                return ind
        return -1
    
    def _emit_all_data(self):
        self.dataChanged.emit(self.createIndex(0, 0), self.createIndex(self.rowCount(None), self.columnCount(None)))
        
    def _set_requests(self, reqs):
        self.reqs = [self._gen_req_row(req) for req in reqs]
        self.reqs_loaded = 0
    
    def set_requests(self, reqs):
        self.beginResetModel()
        self._set_requests(reqs)
        self._sort_reqs()
        self._emit_all_data()
        self.endResetModel()
    
    def clear(self):
        self.beginResetModel()
        self.reqs = []
        self.reqs_loaded = 0
        self._emit_all_data()
        self.endResetModel()

    def add_request_head(self, req):
        self.beginInsertRows(QModelIndex(), 0, 0)
        self.reqs = [self._gen_req_row(req)] + self.reqs
        self.reqs_loaded += 1
        self.endInsertRows()
    
    def add_request(self, req):
        self.beginResetModel()
        self.reqs.append(self._gen_req_row(req))
        self.reqs_loaded = 0
        self._sort_reqs()
        self._emit_all_data()
        self.endResetModel()
        
    def add_requests(self, reqs):
        self.beginResetModel()
        for req in reqs:
            self.reqs.append(self._gen_req_row(req))
        self.reqs_loaded = 0
        self._sort_reqs()
        self._emit_all_data()
        self.endResetModel()
    
    def update_request(self, req):
        ind = self._req_ind(req)
        if ind < 0:
            return
        self.reqs[ind] = self._gen_req_row(req)
        self.dataChanged.emit(self.createIndex(ind, 0), self.createIndex(ind, self.rowCount(None)))

    def delete_request(self, req=None, reqid=None):
        ind = self._req_ind(req, reqid)
        if ind < 0:
            return
        self.beginRemoveRows(QModelIndex(), ind, ind)
        self.reqs_loaded -= 1
        self.reqs = self.reqs[:ind] + self.reqs[(ind+1):]
        self.endRemoveRows()
        
    def has_request(self, req=None, reqid=None):
        if self._req_ind(req, reqid) < 0:
            return False
        return True
    
    def get_requests(self):
        return [row[0] for row in self.reqs]
    
    def disable_sort(self):
        self.sort_enabled = False

    def enable_sort(self):
        self.sort_enabled = True
        self._sort_reqs()
        
    def req_by_ind(self, ind):
        return self.reqs[ind][0]

    
class ReqBrowser(QWidget):
    # Widget containing request viewer, tabs to view list of reqs, filters, and (evevntually) site map
    # automatically updated with requests as they're saved
    def __init__(self, client, repeater_widget=None, macro_widget=None, reload_reqs=True, update=False, filter_tab=True, is_client_context=False):
        QWidget.__init__(self)
        self.client = client
        self.filters = []
        self.reload_reqs = reload_reqs

        self.mylayout = QGridLayout()
        self.mylayout.setSpacing(0)
        self.mylayout.setContentsMargins(0, 0, 0, 0)

        # reqtable updater
        if update:
            self.updater = ReqListUpdater(self.client)
        else:
            self.updater = None

        # reqtable/search
        self.listWidg = ReqTableWidget(client, repeater_widget=repeater_widget, macro_widget=macro_widget)
        if self.updater:
            self.updater.add_reqlist_widget(self.listWidg)
        self.listWidg.requestsSelected.connect(self.update_viewer)
        self.listLayout = QVBoxLayout()
        self.listLayout.setContentsMargins(0, 0, 0, 0)
        self.listLayout.setSpacing(0)
        self.listButtonLayout = QHBoxLayout()
        self.listButtonLayout.setContentsMargins(0, 0, 0, 0)
        clearSelectionBut = QPushButton("Clear Selection")
        clearSelectionBut.clicked.connect(self.listWidg.clear_selection)
        self.listButtonLayout.addWidget(clearSelectionBut)
        self.listButtonLayout.addStretch()
        self.listLayout.addWidget(self.listWidg)
        self.listLayout.addLayout(self.listButtonLayout)

        # Filter widget
        self.filterWidg = FilterEditor(client=self.client)
        self.filterWidg.filtersEdited.connect(self.listWidg.set_filter)
        if is_client_context:
            self.filterWidg.filtersEdited.connect(self.set_client_context)
        self.filterWidg.reset_to_scope()

        # Tree widget
        self.treeWidg = ReqTreeView()

        # add tabs
        self.listTabs = QTabWidget()
        lwidg = QWidget()
        lwidg.setLayout(self.listLayout)
        self.listTabs.addTab(lwidg, "List")
        self.tree_ind = self.listTabs.count()
        self.listTabs.addTab(self.treeWidg, "Tree")
        if filter_tab:
            self.listTabs.addTab(self.filterWidg, "Filters")
        self.listTabs.currentChanged.connect(self._tab_changed)

        # reqview
        self.reqview = ReqViewWidget(info_tab=True, param_tab=True, tag_tab=True)
        self.reqview.set_tags_read_only(False)
        self.reqview.tag_widg.tagsUpdated.connect(self._tags_updated)
        self.listWidg.req_view_widget = self.reqview

        self.mylayout.addWidget(self.reqview, 0, 0, 3, 1)
        self.mylayout.addWidget(self.listTabs, 4, 0, 2, 1)

        self.setLayout(self.mylayout)
        
    def show_filters(self):
        self.listTabs.setCurrentIndex(2)

    def show_history(self):
        self.listTabs.setCurrentIndex(0)

    def show_tree(self):
        self.listTabs.setCurrentIndex(1)

    @pyqtSlot(list)
    def set_client_context(self, query):
        self.client.context.set_query(query)
        
    @pyqtSlot()
    def reset_to_scope(self):
        self.filterWidg.reset_to_scope()

    @pyqtSlot(list)
    def update_viewer(self, reqs):
        self.reqview.set_request(None)
        if len(reqs) > 0:
            if self.reload_reqs:
                reqh = reqs[0]
                req = self.client.req_by_id(reqh.db_id)
            else:
                req = reqs[0]
            self.reqview.set_request(req)

    @pyqtSlot(list)
    def update_filters(self, query):
        self.filters = query

    @pyqtSlot(HTTPRequest)
    def add_request_item(self, req):
        self.listWidg.add_request_item(req)
        self.treeWidg.add_request_item(req)

    @pyqtSlot(list)
    def set_requests(self, reqs):
        self.listWidg.set_requests(reqs)
        self.treeWidg.set_requests(reqs)

    @pyqtSlot(int)
    def _tab_changed(self, i):
        if i == self.tree_ind:
            self.treeWidg.set_requests(self.listWidg.get_requests())

    @pyqtSlot(set)
    def _tags_updated(self, tags):
        req = self.reqview.req
        req.tags = tags
        if req.db_id:
            reqid = self.client.get_reqid(req)
            self.client.clear_tag(reqid)
            for tag in tags:
                self.client.add_tag(reqid, tag)
        
    def set_filter_is_text(self, is_text):
        self.filterWidg.set_is_text(is_text)
                

class ReqListUpdater(QObject):

    newRequest = pyqtSignal(HTTPRequest)
    requestUpdated = pyqtSignal(HTTPRequest)
    requestDeleted = pyqtSignal(str)

    def __init__(self, client):
        QObject.__init__(self)
        self.mtx = threading.Lock()
        self.client = client
        self.reqlist_widgets = []
        self.t = ProxyThread(target=self.run_updater)
        self.t.start()

    def add_reqlist_widget(self, widget):
        self.mtx.acquire()
        try:
            self.newRequest.connect(widget.add_request)
            self.requestUpdated.connect(widget.update_request)
            self.requestDeleted.connect(widget.delete_request)
            self.reqlist_widgets.append(widget)
        finally:
            self.mtx.release()

    def run_updater(self):
        conn = self.client.new_conn()
        try:
            try:
                for msg in conn.watch_storage():
                    self.mtx.acquire()
                    try:
                        if msg["Action"] == "NewRequest":
                            self.newRequest.emit(msg["Request"])
                        elif msg["Action"] == "RequestUpdated":
                            self.requestUpdated.emit(msg["Request"])
                        elif msg["Action"] == "RequestDeleted":
                            self.requestDeleted.emit(msg["MessageId"])
                    finally:
                        self.mtx.release()
            except SocketClosed:
                return
        finally:
            conn.close()

    def stop(self):
        self.conn.close()


class ReqTableWidget(QWidget):
    requestsChanged = pyqtSignal(list)
    requestsSelected = pyqtSignal(list)

    def __init__(self, client, repeater_widget=None, macro_widget=None, *args, **kwargs):
        QWidget.__init__(self, *args, **kwargs)
        self.allow_save = False

        self.client = client
        self.repeater_widget = repeater_widget
        self.macro_widget = macro_widget
        self.query = []
        self.req_view_widget = None

        self.setLayout(QStackedLayout())
        self.layout().setContentsMargins(0, 0, 0, 0)
        
        self.tableModel = ReqListModel(self.client)
        self.tableView = QTableView()
        self.tableView.setModel(self.tableModel)

        self.tableView.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
        self.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
        self.tableView.verticalHeader().hide()
        self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
        #self.tableView.setSelectionMode(QAbstractItemView.SingleSelection)
        self.tableView.horizontalHeader().setStretchLastSection(True)
        
        self.tableView.selectionModel().selectionChanged.connect(self.on_select_change)
        self.tableModel.dataChanged.connect(self._paint_view)
        self.tableModel.rowsInserted.connect(self._on_rows_inserted)
        self.requestsChanged.connect(self.set_requests)
        self.requestsSelected.connect(self._updated_selected_request)
        
        self.selected_reqs = []
        
        self.layout().addWidget(self.tableView)
        self.layout().addWidget(QLabel("<b>Loading requests from data file...</b>"))
        
    @pyqtSlot(HTTPRequest)
    def add_request(self, req):
        with DisableUpdates(self.tableView):
            if req.db_id != "":
                reqid = self.client.get_reqid(req)
                if self.client.check_request(self.query, reqid=reqid):
                    self.tableModel.add_request_head(req)
                if req.unmangled and req.unmangled.db_id != "" and self.tableModel.has_request(req.unmangled):
                    self.tableModel.delete_request(req.unmangled)
            else:
                if self.client.check_request(self.query, req=req):
                    self.tableModel.add_request_head(req)
                    
    @pyqtSlot()
    def clear(self):
        self.tableModel.clear()
        
    def get_requests(self):
        return self.tableModel.get_requests()

    @pyqtSlot(list)
    def set_requests(self, reqs, check_filter=False):
        to_add = []
        if not check_filter:
            to_add = reqs
        else:
            for req in reqs:
                if req.db_id != "":
                    reqid = self.client.get_reqid(req)
                    if self.client.check_request(self.query, reqid=reqid):
                        to_add.append(req)
                else:
                    if self.client.check_request(self.query, req=req):
                        to_add.append(req)
        with DisableUpdates(self.tableView):
            self.clear()
            self.tableModel.disable_sort()
            self.tableModel.add_requests(to_add)
            self.tableModel.enable_sort()
            self.set_is_not_loading()

    @pyqtSlot(HTTPRequest)
    def update_request(self, req):
        with DisableUpdates(self.tableView):
            self.tableModel.update_request(req)
            if req.db_id != "":
                if req.unmangled and req.unmangled.db_id != "":
                    self.tableModel.delete_request(reqid=self.client.get_reqid(req.unmangled))

    @pyqtSlot(str)
    def delete_request(self, reqid):
        with DisableUpdates(self.tableView):
            self.tableModel.delete_request(reqid=reqid)

    @pyqtSlot(list)
    def set_filter(self, query):
        self.query = query
        self.set_is_loading()
        self.client.query_storage_async(self.requestsChanged, self.query, headers_only=True)

    @pyqtSlot(list)
    def _updated_selected_request(self, reqs):
        if len(reqs) > 0:
            self.selected_reqs = reqs
        else:
            self.selected_reqs = []
            
    @pyqtSlot(QModelIndex, int, int)
    def _on_rows_inserted(self, parent, first, last):
        rows = self.tableView.selectionModel().selectedRows()
        if len(rows) > 0:
            row = rows[0].row()
            idx = self.tableModel.index(row, 0, QModelIndex())
            self.tableView.scrollTo(idx)

    @pyqtSlot(QItemSelection, QItemSelection)
    def on_select_change(self, newSelection, oldSelection):
        reqs = []
        added = set()
        for rowidx in self.tableView.selectionModel().selectedRows():
            row = rowidx.row()
            if row not in added:
                reqs.append(self.tableModel.req_by_ind(row))
                added.add(row)
        self.requestsSelected.emit(reqs)

    @pyqtSlot()
    def clear_selection(self):
        self.tableView.clearSelection()
        
    def get_selected_request(self):
        # load the full request
        if len(self.selected_reqs) > 0:
            return self.client.load_by_reqheaders(self.selected_reqs[0])
        else:
            return None

    def get_selected_requests(self):
        ret = []
        for hreq in self.selected_reqs:
            ret.append(self.client.load_by_reqheaders(hreq))
        return ret

    def get_all_requests(self):
        return [self.client.req_by_id(self.client.get_reqid(req)) for req in self.tableModel.get_requests()]

    def contextMenuEvent(self, event):
        if len(self.selected_reqs) > 1:
            reqs = self.get_selected_requests()
            display_multi_req_context(self, self.client, reqs, event,
                                      macro_widget=self.macro_widget,
                                      save_option=self.allow_save)
        elif len(self.selected_reqs) == 1:
            req = self.get_selected_request()
            display_req_context(self, self.client, req, event,
                                repeater_widget=self.repeater_widget,
                                req_view_widget=self.req_view_widget,
                                macro_widget=self.macro_widget,
                                save_option=self.allow_save)

    def set_is_loading(self):
        self.set_loading(True)

    def set_is_not_loading(self):
        self.set_loading(False)

    def set_loading(self, is_loading):
        with DisableUpdates(self.tableView):
            if is_loading:
                self.layout().setCurrentIndex(1)
            else:
                self.layout().setCurrentIndex(0)
            
    @pyqtSlot(QModelIndex, QModelIndex)
    def _paint_view(self, indA, indB):
        self.tableView.repaint()
        
    @pyqtSlot()
    def delete_selected(self):
        with DisableUpdates(self.tableView):
            for req in self.selected_reqs:
                self.tableModel.delete_request(req=req)