# Copyright (C) 2017 Thomas Rinsma / Riscure
# 
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import ctypes
import time

# Qt related
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import pyqtSlot, QThread, QEvent
import sip

# IDA related
import idaapi, idautils, idc
from idaapi import PluginForm
import sark

# Other non-builtin packages
import networkx

# Package-local imports
from workers import *

WINDOW_NAME = 'Drop'

# TODO: remove this
MANUAL_CONCRETE_ARGS = True

class DropGUI(PluginForm):

    def __init__(self, plugin):
        PluginForm.__init__(self)
        self.plugin = plugin
        self.current_function = None
        self.refresh_output = False
        self.current_worker_thread = None

    def show(self):
        PluginForm.Show(self, WINDOW_NAME)

        # Set up the refresh timer
        self.timer = QtCore.QTimer(self.parent)
        self.timer.timeout.connect(self.timer_update)
        self.timer.start(250) # every quarter second
        self.current_function = None

    def OnCreate(self, form):
        # Get parent widget
        self.parent = self.FormToPyQtWidget(form)

        # Set location and width (TODO: hacky)
        idaapi.set_dock_pos(WINDOW_NAME, 'IDA View-A', idaapi.DP_RIGHT)
        self.parent.parent().setMaximumWidth(500)

        # Populate
        self.populate_form()


    def _make_hline(self):
        hline = QtWidgets.QFrame()
        hline.setFrameShape(QtWidgets.QFrame.HLine)
        hline.setFrameShadow(QtWidgets.QFrame.Sunken)
        return hline


    def populate_form(self):
        # Create layout
        hbox = QtWidgets.QHBoxLayout()
        vbox = QtWidgets.QVBoxLayout()
        #layout.addWidget(QtWidgets.QLabel("Bla"))

        # Basic-block color marking buttons
        #mark_color_hbox = QtWidgets.QHBoxLayout()
        self.btn_mark_neutral = QtWidgets.QPushButton("NEUTRAL");
        self.btn_mark_neutral.setStyleSheet("background-color: rgb({c[0]:d},{c[1]:d},{c[2]:d});".format(c=self.plugin.col_neutral));
        self.btn_mark_neutral.clicked.connect(self.btn_mark_color)
        self.btn_mark_visited = QtWidgets.QPushButton("TRACE");
        self.btn_mark_visited.setStyleSheet("background-color: rgb({c[0]:d},{c[1]:d},{c[2]:d});".format(c=self.plugin.col_visited));
        self.btn_mark_visited.clicked.connect(self.btn_mark_color)
        self.btn_mark_unreachable = QtWidgets.QPushButton("UNREACHABLE");
        self.btn_mark_unreachable.setStyleSheet("background-color: rgb({c[0]:d},{c[1]:d},{c[2]:d});".format(c=self.plugin.col_unreachable));
        self.btn_mark_unreachable.clicked.connect(self.btn_mark_color)
        #mark_color_hbox.addWidget(QtWidgets.QLabel("Mark current CB as:"))
        #mark_color_hbox.addWidget(self.btn_mark_neutral)
        #mark_color_hbox.addWidget(self.btn_mark_visited)
        #mark_color_hbox.addWidget(self.btn_mark_unreachable)

        self.btn_reset_graph = QtWidgets.QPushButton('Reset all block colors of current function')
        self.btn_reset_graph.clicked.connect(self.btn_reset_graph_clicked)

        self.btn_config_settings = QtWidgets.QPushButton('Settings...')
        self.btn_config_settings.clicked.connect(self.btn_config_settings_clicked)

        
        # OP Widgets
        #self.btn_init_angr = QtWidgets.QPushButton('Initialize angr')
        #self.btn_init_angr.clicked.connect(self.btn_init_angr_clicked)

        self.btn_stop_thread = QtWidgets.QPushButton('Try to interrupt analysis (might crash IDA)')
        self.btn_stop_thread.clicked.connect(self.btn_stop_thread_clicked)
        self.btn_show_angr_graph = QtWidgets.QPushButton('angr CFG')
        self.btn_show_angr_graph.clicked.connect(self.btn_show_angr_graph_clicked)



        self.btn_concrete = QtWidgets.QPushButton('concrete trace...')
        self.btn_concrete.clicked.connect(self.btn_concrete_clicked)

        self.btn_opaque = QtWidgets.QPushButton('along trace')
        self.btn_opaque.clicked.connect(self.btn_opaque_clicked)
        self.btn_branch_opaque = QtWidgets.QPushButton('in current block')
        self.btn_branch_opaque.clicked.connect(self.btn_branch_opaque_clicked)


        self.btn_propagate = QtWidgets.QPushButton('Propagate unreachability')
        self.btn_propagate.clicked.connect(self.btn_propagate_clicked)
        self.btn_nop = QtWidgets.QPushButton('NOP unreachable code')
        self.btn_nop.clicked.connect(self.btn_nop_clicked)


        self.list_constraints = QtWidgets.QListWidget()
        self.list_symbolic = QtWidgets.QListWidget()
        self.list_hooks = QtWidgets.QListWidget()

        self.btn_add_constraint = QtWidgets.QPushButton('Add...')
        self.btn_add_constraint.clicked.connect(self.btn_add_constraint_clicked)

        self.btn_del_constraint = QtWidgets.QPushButton('Delete')
        self.btn_del_constraint.clicked.connect(self.btn_del_constraint_clicked)
        self.btn_del_constraint.setEnabled(False)

        self.btn_add_symbolic = QtWidgets.QPushButton('Add...')
        self.btn_add_symbolic.clicked.connect(self.btn_add_symbolic_clicked)

        self.btn_del_symbolic = QtWidgets.QPushButton('Delete')
        self.btn_del_symbolic.clicked.connect(self.btn_del_symbolic_clicked)
        self.btn_del_symbolic.setEnabled(False)

        self.btn_add_hook = QtWidgets.QPushButton('Add...')
        self.btn_add_hook.clicked.connect(self.btn_add_hook_clicked)

        self.btn_del_hook = QtWidgets.QPushButton('Delete')
        self.btn_del_hook.clicked.connect(self.btn_del_hook_clicked)
        self.btn_del_hook.setEnabled(False)


        self.table_ops_found = QtWidgets.QTableWidget(0,3)
        self.table_ops_found.setHorizontalHeaderLabels(['block addr', 'type', 'condition'])
        self.table_ops_found.setSelectionBehavior(QtWidgets.QTableWidget.SelectRows);
        self.table_ops_found.doubleClicked.connect(self.table_ops_doubleclicked)
        self.table_ops_found.setEditTriggers(QtWidgets.QTableWidget.NoEditTriggers)

        self.btn_clear_ops = QtWidgets.QPushButton('Clear table')
        self.btn_clear_ops.clicked.connect(self.btn_clear_ops_clicked)


        # Log stuff
        self.output = QtWidgets.QTextEdit("")
        self.output.setReadOnly(True)


        # Step 1
        gbox_step1 = QtWidgets.QGroupBox("Step 1: Find candidate branches")
        hb1 = make_hbox(
            "manually mark current block as: ",
            self.btn_mark_visited,
            self.btn_mark_neutral
        )
        hb2 = make_hbox(
            "or automatically mark blocks along a: ",
            self.btn_concrete
        )
        self.btn_concrete.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        vbox_gbox_step1 = make_vbox(
            hb1,
            hb2
        )
        gbox_step1.setLayout(vbox_gbox_step1)


        # Step 2
        gbox_step2 = QtWidgets.QGroupBox("Step 2: Detect opaque predicates")
        self.checkbox_all_symbolic = QtWidgets.QCheckBox("all globals symbolic")
        self.checkbox_all_symbolic.setChecked(False)
        self.checkbox_all_symbolic.toggled.connect(self.checkbox_all_symbolic_toggled)
        # Additional inputs
        vbox_extra_input = make_hbox(
            make_vbox(
                "Extra constraints:",
                self.list_constraints,
                make_hbox(self.btn_add_constraint, self.btn_del_constraint)
            ),
            make_vbox(
                "Symbolic variables:",
                self.list_symbolic,
                self.checkbox_all_symbolic,
                make_hbox(self.btn_add_symbolic, self.btn_del_symbolic)
            ),
            make_vbox(
                "Hooked functions:",
                self.list_hooks,
                make_hbox(self.btn_add_hook, self.btn_del_hook)
            )
        )
        vbox_gbox_step2 = make_vbox(
            vbox_extra_input,
            self._make_hline(),
            make_hbox(
                "Find opaque predicates: ",
                self.btn_opaque,
                " or ",
                self.btn_branch_opaque
            )
        )
        self.btn_opaque.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        self.btn_branch_opaque.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        gbox_step2.setLayout(vbox_gbox_step2)


        # Step 3
        gbox_step3 = QtWidgets.QGroupBox("Step 3: Dead code removal")
        vbox_gbox_step3 = make_vbox(
            make_hbox(
                "manually mark current block as: ",
                self.btn_mark_unreachable
            ),
            make_hbox(
                "or",
                self.btn_propagate,
                "and then",
                self.btn_nop
            )
        )
        self.btn_mark_unreachable.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        self.btn_propagate.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        self.btn_nop.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        gbox_step3.setLayout(vbox_gbox_step3)


        # Add groupboxes to super vbox
        vbox.addWidget(gbox_step1)
        vbox.addWidget(gbox_step2)
        vbox.addWidget(gbox_step3)

        # Add other widgets
        vbox.addLayout(make_hbox(
            "Opaque predicates found (doubleclick to jump to condition):",
            self.btn_clear_ops
        ))
        vbox.addWidget(self.table_ops_found)

        # Add some extraneous buttons
        vbox.addLayout(make_vbox(
            make_hbox(
                self.btn_stop_thread,
                self.btn_show_angr_graph,
                self.btn_config_settings
            ),
            self.btn_reset_graph
        ))


        #vbox.addStretch(1)
        #tab_op.addWidget(self._make_hline())

        # Final hbox to stretch everything out horizontally
        hbox.addLayout(vbox)
        #hbox.addStretch(1)
        self.parent.setLayout(hbox)


    def timer_update(self):
        """
        Called everytime the timer times out. Refresh displayed info.
        """

        try:
            f = sark.Function()
            fun_addr = f.startEA
        except:
            # Probably not in a function
            return

        # Do nothing if the current function didn't change
        # and a refresh was not requested
        if self.current_function == fun_addr and not self.refresh_output:
            return

        fun_name = f.name
        if self.plugin.angr_proj and self.plugin.angr_proj.kb.functions:
            cfg_made = self.plugin.angr_proj.kb.functions.function(fun_addr) is not None
        else:
            cfg_made = False
        extra_cons = None
        ops_found = None

        # Clear the constraint list in GUI
        while self.list_constraints.count() > 0:
            self.list_constraints.takeItem(0)
        self.btn_del_constraint.setEnabled(False)

        # If constraint set exists and is not empty
        cons_to_str = lambda (opr1, op, opr2): "{} {} {}".format(opr2str(opr1), op, opr2str(opr2))
        if fun_addr in self.plugin.extra_constraints and self.plugin.extra_constraints[fun_addr]:
            # Add new items back to GUI list
            for cons in self.plugin.extra_constraints[fun_addr]:
                item = QtWidgets.QListWidgetItem(cons_to_str(cons), self.list_constraints)
                item.setData(QtCore.Qt.UserRole, cons)
                self.list_constraints.addItem(item)
                self.btn_del_constraint.setEnabled(True)

        # Clear the symbolic var list in GUI
        while self.list_symbolic.count() > 0:
            self.list_symbolic.takeItem(0)
        self.btn_del_symbolic.setEnabled(False)

        # If symbolic var set exists and is not empty
        symb_var_to_str = lambda symb_var: "0x{:X}".format(symb_var) # TODO get IDA symbol name?
        if fun_addr in self.plugin.symbolic_vars and self.plugin.symbolic_vars[fun_addr]:
            # Add new items back to GUI list
            for symb_var in self.plugin.symbolic_vars[fun_addr]:
                item = QtWidgets.QListWidgetItem(symb_var_to_str(symb_var), self.list_symbolic)
                item.setData(QtCore.Qt.UserRole, symb_var)
                self.list_symbolic.addItem(item)
                self.btn_del_symbolic.setEnabled(True)


        # Clear the hook list in GUI
        while self.list_hooks.count() > 0:
            self.list_hooks.takeItem(0)
        self.btn_del_hook.setEnabled(False)

        # If hook set exists and is not empty
        hook_to_str = lambda hook: "0x{:X} -> {}{}".format(hook[0], hook[1], (" ("+hook[2]+")") if hook[2] else "")
        if fun_addr in self.plugin.function_hooks and self.plugin.function_hooks[fun_addr]:
            # Add new items back to GUI list
            for hook in self.plugin.function_hooks[fun_addr]:
                item = QtWidgets.QListWidgetItem(hook_to_str(hook), self.list_hooks)
                item.setData(QtCore.Qt.UserRole, hook)
                self.list_hooks.addItem(item)
                self.btn_del_hook.setEnabled(True)
        

        # Clear OP table
        self.table_ops_found.setRowCount(0)

        # If OP set exists and is not empty
        if fun_addr in self.plugin.opaque_predicates and self.plugin.opaque_predicates[fun_addr]:
            # (addr, type, str)
            #ops_found = "\n" + "\n".join(map(
            #    lambda (a, t, s): "  0x{:x}: {}: {}".format(a, "invariant" if t == 0 else "contextual",
            #        s if s is not "True" else "True (simplified away by angr)"),
            #    self.plugin.opaque_predicates[fun_addr]
            #))

            # Add new items back to GUI table
            for (op_addr, op_type, op_str) in self.plugin.opaque_predicates[fun_addr]:
                row_count = self.table_ops_found.rowCount()
                self.table_ops_found.insertRow(row_count)
                self.table_ops_found.setItem(row_count, 0, QtWidgets.QTableWidgetItem("{:X}".format(op_addr)))
                self.table_ops_found.setItem(row_count, 1, QtWidgets.QTableWidgetItem("invariant" if op_type == 0 else "contextual"))

                if op_str == "True":
                    op_str = "True (simplified away)"

                item_constraint = QtWidgets.QTableWidgetItem(op_str)
                item_constraint.setToolTip(op_str)
                self.table_ops_found.setItem(row_count, 2, item_constraint)

                # Background color
                col_tuple = self.plugin.col_invariant if op_type == 0 else self.plugin.col_contextual
                qcolor = QtGui.QColor(col_tuple[0], col_tuple[1], col_tuple[2])
                self.table_ops_found.item(row_count, 0).setBackground(qcolor)
                self.table_ops_found.item(row_count, 1).setBackground(qcolor)
                self.table_ops_found.item(row_count, 2).setBackground(qcolor)


        new_text = (
            "Current function: {} (0x{:x})\n".format(fun_name, fun_addr) +
            "angr CFG generated: {}\n".format("Yes" if cfg_made else "No") +
            "Opaque predicates found: {}\n".format("-" if not ops_found else ops_found)
        )
        
        self.output.setText(new_text)
        self.current_function = fun_addr
        self.refresh_output = False


    def spawn_worker_thread(self, worker, **signals):
        # Setup worker and its thread
        thread = QThread(self.parent)
        thread.setTerminationEnabled(True)
        worker.moveToThread(thread)
        thread.started.connect(worker.run)
        thread.worker = worker # TODO: needed?

        # Connect log and any extra signals
        worker.log_signal.connect(self.log_append)
        for signal_name in signals:
            getattr(worker, signal_name).connect(signals[signal_name])

        thread.start()

        self.current_worker_thread = thread

    def log_append(self, s):
        idaapi.msg(s + "\n")

    def concrete_trace_results(self, addr_trace):
        # Color all blocks along the trace as 'visited'
        for addr in addr_trace:
            set_block_color(addr, col_t2ida(self.plugin.col_visited))
        self.btn_concrete.setEnabled(True)

    def opaque_results(self, f_start_ea, opaque_preds, opaque_invariant, opaque_contextual, done=False):
        # Save the results
        if opaque_preds:
            if f_start_ea not in self.plugin.opaque_predicates:
                self.plugin.opaque_predicates[f_start_ea] = set()
            self.plugin.opaque_predicates[f_start_ea] |= set(opaque_preds)

        # Color the appropriate blocks
        for (branch_addr, succ_sat, succ_unsat) in opaque_invariant:
            set_block_color(succ_unsat, col_t2ida(self.plugin.col_invariant))

        for (branch_addr, succ_sat, succ_unsat) in opaque_contextual:
            set_block_color(succ_unsat, col_t2ida(self.plugin.col_contextual))

        if done:
            self.btn_opaque.setEnabled(True)
            self.btn_branch_opaque.setEnabled(True)
            
        self.refresh_output = True

    def unreachable_results(self, unreachable_blocks):
        for addr in unreachable_blocks:
            set_block_color(addr, col_t2ida(self.plugin.col_unreachable))

        self.btn_propagate.setEnabled(True)
        self.refresh_output = True

 
    def btn_mark_color(self):
        sender = self.parent.sender()
        cb = sark.CodeBlock()

        if sender is self.btn_mark_neutral:
            cb.color = col_t2ida(self.plugin.col_neutral)
        elif sender is self.btn_mark_visited:
            cb.color = col_t2ida(self.plugin.col_visited)
        elif sender is self.btn_mark_unreachable:
            cb.color = col_t2ida(self.plugin.col_unreachable)


    def checkbox_all_symbolic_toggled(self):
        if self.checkbox_all_symbolic.isChecked():
            # Deactivate individual global variable input
            self.list_symbolic.setEnabled(False)
            self.btn_add_symbolic.setEnabled(False)
            self.btn_del_symbolic.setEnabled(False)
        else:
            # Activate individual global variable input
            self.list_symbolic.setEnabled(True)
            self.btn_add_symbolic.setEnabled(True)
            self.btn_del_symbolic.setEnabled(True)


    def btn_stop_thread_clicked(self):
        if self.current_worker_thread and self.current_worker_thread.isRunning():
            self.plugin.stop_analysis = True
            # woow hacky
            try:
                import z3
                frames = sys._current_frames().values()
                for f in frames:
                    if 'a0' in f.f_locals:
                        #print("Found Z3 frame: {}".format(f.f_code))
                        c = f.f_locals['a0']
                        ctx = z3.main_ctx()
                        ctx.ctx = c
                        ctx.interrupt()
                        break
                self.current_worker_thread.terminate()
            except:
                print("Ex: {}, {}".format(sys.exc_info(), frame.f_code))
            #print("Bla 2")
            if self.current_worker_thread.isRunning():
                print("Termination failed")
            else:
                print("Terminated thread")


    def btn_show_angr_graph_clicked(self):
        # Assume angr is loaded
        def mapping(node):
            b = self.plugin.angr_proj.factory.block(node.addr)
            return str(b.capstone)

        try:
            graph = self.plugin.cfg.functions[self.current_function].graph
            graph2 = networkx.relabel_nodes(graph, mapping)
            viewer = sark.ui.NXGraph(graph2, title="angr graph", padding=0)
            viewer.Show()
        except:
            print("ERROR: {}".format(sys.exc_info()))

    def btn_reset_graph_clicked(self):
        reset_block_colors(sark.Function())

    def btn_clear_ops_clicked(self):
        self.plugin.opaque_predicates[self.current_function] = set()
        self.refresh_output = True

    def btn_concrete_clicked(self):
        self.btn_concrete.setEnabled(False)

        if not MANUAL_CONCRETE_ARGS:
            # Get current function's arguments and ask for concrete values
            f_type = get_func_type(self.current_function)
            if not f_type:
                self.btn_concrete.setEnabled(True)
                return
        else:
            # Ask for the number of arguments
            val, ok = QtWidgets.QInputDialog.getInt(None,"param input","How many arguments should we pass?", 1)
            if not ok or val < 0:
                self.log_append("Cancelled.")
                self.btn_concrete.setEnabled(True)
                return
            # Set types to all int, TODO: fix
            f_type = ("void", ["int"] * val)

        self.log_append("Function type: {}".format(f_type))
        params = []
        for idx, arg_type in enumerate(f_type[1]):
            #val = asklong(42, "Give a value for argument {} of type '{}'".format(idx+1, arg_type))
            val, ok = QtWidgets.QInputDialog.getText(None,"param input","Give a value for argument {}. Enclose strings with double quotes.".format(idx+1, arg_type))
            if not ok:
                self.log_append("Cancelled.")
                self.btn_concrete.setEnabled(True)
                return
            params.append(val)

        # Spawn the worker thread
        f = IDAFunctionCodeBlocks(sark.Function())
        self.spawn_worker_thread(
            ConcreteRunner(self.plugin, f, arg_vals=params),
            result_signal=self.concrete_trace_results
        )

    def btn_opaque_clicked(self):
        self.btn_opaque.setEnabled(False)

        # Retrieve the addr_trace from colored code-blocks
        f = IDAFunctionCodeBlocks(sark.Function())
        cbs = get_all_blocks_of_color(f, col_t2ida(self.plugin.col_visited))
        addr_trace = [cb.startEA for cb in cbs]

        # Spawn the worker thread
        self.spawn_worker_thread(
            OpaquePredicateFinder(self.plugin, f, addr_trace, self.checkbox_all_symbolic.isChecked()),
            result_signal=self.opaque_results
        )

    def btn_branch_opaque_clicked(self):
        self.btn_branch_opaque.setEnabled(False)
        
        # Only have it look at the currently selected code-block
        addr_trace = [get_current_codeblock().startEA]

        f = IDAFunctionCodeBlocks(sark.Function())
        self.spawn_worker_thread(
            OpaquePredicateFinder(self.plugin, f, addr_trace, self.checkbox_all_symbolic.isChecked()),
            result_signal=self.opaque_results
        )

    def btn_propagate_clicked(self):
        self.btn_propagate.setEnabled(False)

        self.spawn_worker_thread(
            UnreachabilityPropagator(self.plugin),
            result_signal=self.unreachable_results
        )

    def btn_nop_clicked(self):
        self.spawn_worker_thread(UnreachableCodePatcher(self.plugin))


    def btn_config_settings_clicked(self):
        global CONCRETE_MAX_ITERATIONS, SYMBOLIC_MAX_ITERATIONS
        dia = SettingsDialog(CONCRETE_MAX_ITERATIONS, SYMBOLIC_MAX_ITERATIONS)
        if dia.exec_() == QtWidgets.QDialog.Accepted:
            (CONCRETE_MAX_ITERATIONS, SYMBOLIC_MAX_ITERATIONS) = dia.get_value()

    def btn_add_constraint_clicked(self):
        dia = AddConstraintDialog()
        if dia.exec_() == QtWidgets.QDialog.Accepted:
            (exp1, op, exp2) = dia.get_value()
            if not op:
                self.log_append("ERROR: Invalid operator")
                return

            # Try to parse input
            exp1_parsed = try_parse_ida_ident(exp1)
            exp2_parsed = try_parse_ida_ident(exp2)

            if not exp1_parsed or not exp2_parsed:
                self.log_append("ERROR: Couldn't parse an operand as a valid expression")
                return

            #self.log_append("{} {} {}".format(exp1_parsed, op, exp2_parsed))

            # Add it to the list for this function
            if self.current_function not in self.plugin.extra_constraints:
                self.plugin.extra_constraints[self.current_function] = []
            self.plugin.extra_constraints[self.current_function].append((exp1_parsed, op, exp2_parsed))

            # Refresh the output display
            self.refresh_output = True


    def btn_del_constraint_clicked(self):
        # Delete the currently selected constraint
        for item in self.list_constraints.selectedItems():
            # Delete from set
            self.plugin.extra_constraints[self.current_function].remove(item.data(QtCore.Qt.UserRole))
            # Delete from GUI
            self.list_constraints.takeItem(self.list_constraints.row(item))
        self.refresh_output = True


    def btn_add_symbolic_clicked(self):
        dia = AddSymbolicVarDialog() # TODO: also ask for var type / size
        if dia.exec_() == QtWidgets.QDialog.Accepted:
            symb_var_str = dia.get_value()

            # Try to parse input
            parse_res = try_parse_ida_ident(symb_var_str)
            if parse_res == False:
                self.log_append("ERROR: Couldn't parse input as a symbol name or address")
                return

            (symb_type, symb_var_addr) = parse_res
            if symb_type != 3 or not symb_var_addr:
                self.log_append("ERROR: Couldn't parse input as a symbol name or address")
                return

            if symb_var_addr not in dict(self.plugin.global_vars):
                self.log_append("ERROR: Address {} is not a detected variable: {}".format(symb_var_addr, self.plugin.global_vars.keys()))
                return

            # Add it to the list for this function
            if self.current_function not in self.plugin.symbolic_vars:
                self.plugin.symbolic_vars[self.current_function] = []
            self.plugin.symbolic_vars[self.current_function].append(symb_var_addr)

            # Refresh the output display
            self.refresh_output = True

    def btn_del_symbolic_clicked(self):
        # Delete the currently selected symbolic variable
        for item in self.list_symbolic.selectedItems():
            # Delete from set
            self.plugin.symbolic_vars[self.current_function].remove(item.data(QtCore.Qt.UserRole))
            # Delete from GUI
            self.list_symbolic.takeItem(self.list_symbolic.row(item))
        self.refresh_output = True


    def btn_add_hook_clicked(self):
        dia = AddHookDialog()
        if dia.exec_() == QtWidgets.QDialog.Accepted:
            (fun_addr, hook_text, custom_text) = dia.get_value()

            # Add it to the list for this function
            if self.current_function not in self.plugin.function_hooks:
                self.plugin.function_hooks[self.current_function] = []
            self.plugin.function_hooks[self.current_function].append((fun_addr, hook_text, custom_text))

            # Refresh the output display
            self.refresh_output = True


    def btn_del_hook_clicked(self):
        # Delete the currently selected hook
        for item in self.list_hooks.selectedItems():
            hook = item.data(QtCore.Qt.UserRole)

            # Unhook
            hook_addr = hook[0]
            self.plugin.angr_proj.unhook(hook_addr)

            # Delete from set
            self.plugin.function_hooks[self.current_function].remove(hook)
            # Delete from GUI
            self.list_hooks.takeItem(self.list_hooks.row(item))
        self.refresh_output = True


    def table_ops_doubleclicked(self, index):
        row = index.row()
        print("row = {}".format(row))
        addr = int(self.table_ops_found.item(row, 0).text(), 16)
        print("addr = {}".format(addr))
        idaapi.jumpto(addr)

    def OnClose(self, form):
        if self.current_worker_thread and self.current_worker_thread.isRunning():
            print("A worker is still running...")
            return False
        #self.plugin.Close()
        self.timer.stop()
        # TODO: kill any threads?
        return True


class SettingsDialog(QtWidgets.QDialog):
    def __init__(self, num_c, num_s):
        QtWidgets.QDialog.__init__(self)
        self.result = None

        self.max_concrete_iter = QtWidgets.QLineEdit(str(num_c))
        self.max_symbolic_iter = QtWidgets.QLineEdit(str(num_s))
        btn_ok = QtWidgets.QPushButton("OK")
        btn_ok.clicked.connect(self.btn_ok_clicked)

        vbox = make_vbox(
            make_hbox(
                "Maximum iterations (basic-blocks) during concrete execution",
                self.max_concrete_iter
            ),
            make_hbox(
                "Maximum iterations (basic-blocks) during symbolic execution",
                self.max_symbolic_iter
            ),
            make_hbox(
                btn_ok
            )
        )
        self.setLayout(vbox)

    def btn_ok_clicked(self):
        num_c = self.max_concrete_iter.text()
        num_s = self.max_symbolic_iter.text()
        try:
            num_c = int(num_c)
            num_s = int(num_s)
            if num_c < 1 or num_s < 1:
                raise ValueError()
        except ValueError:
            QtWidgets.QMessageBox.information(None, 'Error','Inputs can only be positive numbers')
            return
        self.result = (num_c, num_s)
        self.accept()

    def get_value(self):
        return self.result


class AddHookDialog(QtWidgets.QDialog):
    list_simprocedures = {
        "No-op": "nop",
        "Return unconstrained": "unconstrained",
        "Return constant word...": "constant",
        #"Redirect control-flow to...": "redirect",
        "Return printable character": "printable_char"
    }

    def __init__(self):
        QtWidgets.QDialog.__init__(self)
        self.result = None
        self.picked_function = None

        buttons_hbox = QtWidgets.QHBoxLayout()

        # Operands
        self.edit = QtWidgets.QLineEdit()

        btn_ok = QtWidgets.QPushButton("OK")
        btn_ok.clicked.connect(self.btn_ok_clicked)
        btn_cancel = QtWidgets.QPushButton("Cancel")
        btn_cancel.clicked.connect(self.btn_cancel_clicked)

        buttons_hbox.addWidget(btn_ok)
        buttons_hbox.addWidget(btn_cancel)

        self.edit_picked_fun = QtWidgets.QLineEdit("<none>")
        self.edit_picked_fun.setDisabled(True)
        btn_pick = QtWidgets.QPushButton("Choose...")
        btn_pick.clicked.connect(self.btn_pick_clicked)
        self.chooser_sim_procedure = QtWidgets.QComboBox()
        self.chooser_sim_procedure.addItems(AddHookDialog.list_simprocedures.keys())
        self.chooser_sim_procedure.currentIndexChanged.connect(self.chooser_selection_changed)

        self.custom_label = QtWidgets.QLabel("")
        self.custom_edit = QtWidgets.QLineEdit("")
        self.custom_label.setVisible(False)
        self.custom_edit.setVisible(False)

        vbox = make_vbox(
            make_hbox(
                "Pick a function to hook: ",
                self.edit_picked_fun,
                btn_pick
            ),
            make_hbox(
                "Choose a predefined hook:",
                self.chooser_sim_procedure
            ),
            make_hbox(
                self.custom_label,
                self.custom_edit
            ),
            buttons_hbox
        )

        self.setLayout(vbox)

    def chooser_selection_changed(self):
        self.custom_edit.setText("")
        sel_sp = AddHookDialog.list_simprocedures[self.chooser_sim_procedure.currentText()]
        if sel_sp == "constant":
            # Show editbox for the value
            self.custom_label.setText("Specify the integer value to return: ")
            self.custom_edit.setText("42")
            self.custom_label.setVisible(True)
            self.custom_edit.setVisible(True)
        elif sel_sp == "redirect":
            # Show editbox for address
            self.custom_label.setText("Specify the address to redirect to: ")
            self.custom_edit.setText("0xDEADBEEF")
            self.custom_label.setVisible(True)
            self.custom_edit.setVisible(True)
        else:
            # Hide editbox
            self.custom_label.setVisible(False)
            self.custom_edit.setVisible(False)



    def btn_pick_clicked(self):
        ida_f = idaapi.choose_func("Pick a function to hook", idaapi.BADADDR)
        func_addr = ida_f.startEA
        func_name = idaapi.get_func_name(func_addr)

        self.picked_function = func_addr
        self.edit_picked_fun.setText("0x{:X} ({})".format(func_addr, func_name))


    def btn_ok_clicked(self):
        if not self.picked_function:
            QtWidgets.QMessageBox.information(None, 'Error','Choose a function to hook')
            return

        if self.custom_edit.isVisible() and len(self.custom_edit.text()) == 0:
            QtWidgets.QMessageBox.information(None, 'Error','Please specify additional information for the hook')
            return

        sp = AddHookDialog.list_simprocedures[self.chooser_sim_procedure.currentText()]
        self.result = (self.picked_function, sp, self.custom_edit.text())

        self.accept()
        return self.result

    def btn_cancel_clicked(self):
        self.reject()

    def get_value(self):
        return self.result


class AddSymbolicVarDialog(QtWidgets.QDialog):
    def __init__(self):
        QtWidgets.QDialog.__init__(self)
        self.result = None

        vbox = QtWidgets.QVBoxLayout()
        buttons_hbox = QtWidgets.QHBoxLayout()

        # Operands
        self.edit = QtWidgets.QLineEdit()

        btn_ok = QtWidgets.QPushButton("OK")
        btn_ok.clicked.connect(self.btn_ok_clicked)
        btn_cancel = QtWidgets.QPushButton("Cancel")
        btn_cancel.clicked.connect(self.btn_cancel_clicked)

        buttons_hbox.addWidget(btn_ok)
        buttons_hbox.addWidget(btn_cancel)

        vbox.addWidget(QtWidgets.QLabel("Enter the address or name of a .bss/.data/.rodata variable."))
        vbox.addWidget(self.edit)
        vbox.addLayout(buttons_hbox)

        self.setLayout(vbox)

    def btn_ok_clicked(self):

        self.result = self.edit.text()
        self.accept()
        return self.result

    def btn_cancel_clicked(self):
        self.reject()

    def get_value(self):
        return self.result


class AddConstraintDialog(QtWidgets.QDialog):
    def __init__(self):
        #super(AddConstraintDialog, self).__init__(parent)
        QtWidgets.QDialog.__init__(self)
        self.result = (None,None,None)

        vbox = QtWidgets.QVBoxLayout()
        buttons_hbox = QtWidgets.QHBoxLayout()

        # Operands
        self.edit1 = QtWidgets.QLineEdit()
        self.edit2 = QtWidgets.QLineEdit()

        # Operator selection box
        operators = ["<", ">", "==", "!=", "<=", ">="]
        self.operator = QtWidgets.QComboBox()
        self.operator.addItems(operators)

        


        btn_ok = QtWidgets.QPushButton("OK")
        btn_ok.clicked.connect(self.btn_ok_clicked)
        btn_cancel = QtWidgets.QPushButton("Cancel")
        btn_cancel.clicked.connect(self.btn_cancel_clicked)

        buttons_hbox.addWidget(btn_ok)
        buttons_hbox.addWidget(btn_cancel)

        vbox.addWidget(QtWidgets.QLabel("Enter a new constraint. Example operands:\neax, arg_0, [globalVar], 0xFEEDBEEF, 42"))
        vbox.addWidget(self.edit1)
        vbox.addWidget(self.operator)
        vbox.addWidget(self.edit2)
        vbox.addLayout(buttons_hbox)

        self.setLayout(vbox)
        #self.setGeometry(300, 200, 460, 350)

    def btn_ok_clicked(self):

        self.result = (
            self.edit1.text(),
            self.operator.currentText(),
            self.edit2.text()
        )

        #if self.result == None:
        #    QtWidgets.QMessageBox.information(self, "Error",
        #    "Please enter a valid constraint")
        #    self.reject()
        #    return
        self.accept()
        return self.result

    def btn_cancel_clicked(self):
        self.reject()

    def get_value(self):
        return self.result