
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys, struct, socket, time, os

import disassemble


class Message:
    DSI = 0
    ISI = 1
    Program = 2
    GetStat = 3
    OpenFile = 4
    ReadFile = 5
    CloseFile = 6
    SetPosFile = 7
    GetStatFile = 8

    Continue = 0
    Step = 1
    StepOver = 2

    def __init__(self, type, data, arg):
        self.type = type
        self.data = data
        self.arg = arg


class EventHolder(QObject):
    Exception = pyqtSignal()
    Connected = pyqtSignal()
    Closed = pyqtSignal()
    BreakPointChanged = pyqtSignal()
    Continue = pyqtSignal()
events = EventHolder()


#I don't want to deal with the whole threading trouble to complete big
#file transfers without the UI becoming unresponsive. There probably is
#a better way to code this, but this is what I came up with.
class TaskMgr:
    def __init__(self):
        self.taskQueue = []

    def add(self, task):
        if not self.taskQueue:
            window.mainWidget.statusWidget.disconnectButton.setEnabled(False)
        self.taskQueue.append(task)

    def pop(self, task):
        assert task == self.taskQueue.pop()

        if not self.taskQueue:
            window.mainWidget.tabWidget.setEnabled(True)
            window.mainWidget.statusWidget.cancelButton.setEnabled(False)
            window.mainWidget.statusWidget.disconnectButton.setEnabled(True)
            window.mainWidget.statusWidget.progressBar.setValue(0)
            window.mainWidget.statusWidget.progressInfo.setText("Connected")
        else:
            self.taskQueue[-1].resume()

    def isBlocking(self):
        if not self.taskQueue:
            return False
        return self.taskQueue[-1].blocking

    def cancel(self):
        self.taskQueue[-1].canceled = True
taskMgr = TaskMgr()


class Task:
    def __init__(self, blocking, cancelable):
        taskMgr.add(self)

        self.canceled = False
        self.blocking = blocking
        self.cancelable = cancelable
        window.mainWidget.tabWidget.setEnabled(not blocking)
        window.mainWidget.statusWidget.cancelButton.setEnabled(cancelable)

    def setInfo(self, info, maxValue):
        self.info = info
        self.maxValue = maxValue
        window.mainWidget.statusWidget.progressInfo.setText(info)
        window.mainWidget.statusWidget.progressBar.setRange(0, maxValue)

    def update(self, progress):
        self.progress = progress
        window.mainWidget.statusWidget.progressBar.setValue(progress)
        app.processEvents()

    def resume(self):
        window.mainWidget.tabWidget.setEnabled(not self.blocking)
        window.mainWidget.statusWidget.cancelButton.setEnabled(self.cancelable)
        window.mainWidget.statusWidget.progressInfo.setText(self.info)
        window.mainWidget.statusWidget.progressBar.setRange(0, self.maxValue)
        window.mainWidget.statusWidget.progressBar.setValue(self.progress)

    def end(self):
        taskMgr.pop(self)


class Thread:

    cores = {
        1: "Core 0",
        2: "Core 1",
        4: "Core 2"
    }

    def __init__(self, data, offs=0):
        self.core = self.cores[struct.unpack_from(">I", data, offs)[0]]
        self.priority = struct.unpack_from(">I", data, offs + 4)[0]
        self.stackBase = struct.unpack_from(">I", data, offs + 8)[0]
        self.stackEnd = struct.unpack_from(">I", data, offs + 12)[0]
        self.entryPoint = struct.unpack_from(">I", data, offs + 16)[0]

        namelen = struct.unpack_from(">I", data, offs + 20)[0]
        self.name = data[offs + 24 : offs + 24 + namelen].decode("ascii")


class DirEntry:
    def __init__(self, flags, size, name):
        self.flags = flags
        self.size = size
        self.name = name

    def isDir(self):
        return self.flags & 0x80000000


class PyBugger:
    def __init__(self):
        super().__init__()
        self.connected = False
        self.breakPoints = []

        self.basePath = b""
        self.currentHandle = 0x12345678
        self.files = {}

        self.messageHandlers = {
            Message.DSI: self.handleException,
            Message.ISI: self.handleException,
            Message.Program: self.handleException,
            Message.GetStat: self.handleGetStat,
            Message.OpenFile: self.handleOpenFile,
            Message.ReadFile: self.handleReadFile,
            Message.CloseFile: self.handleCloseFile,
            Message.SetPosFile: self.handleSetPosFile,
            Message.GetStatFile: self.handleGetStatFile
        }

    def handleException(self, msg):
        exceptionState.load(msg.data, msg.type)
        events.Exception.emit()

    def handleGetStat(self, msg):
        gamePath = msg.data.decode("ascii")
        path = os.path.join(self.basePath, gamePath.strip("/vol"))
        print("GetStat: %s" %gamePath)
        self.sendFileMessage(os.path.getsize(path))

    def handleOpenFile(self, msg):
        mode = struct.pack(">I", msg.arg).decode("ascii").strip("\x00") + "b"
        path = msg.data.decode("ascii")
        print("Open: %s" %path)

        f = open(os.path.join(self.basePath, path.strip("/vol")), mode)
        self.files[self.currentHandle] = f
        self.sendFileMessage(self.currentHandle)
        self.currentHandle += 1

    def handleReadFile(self, msg):
        print("Read")
        task = Task(blocking=False, cancelable=False)
        bufferAddr, size, count, handle = struct.unpack(">IIII", msg.data)

        data = self.files[handle].read(size * count)
        task.setInfo("Sending file", len(data))

        bytesSent = 0
        while bytesSent < len(data):
            length = min(len(data) - bytesSent, 0x8000)
            self.sendall(b"\x03")
            self.sendall(struct.pack(">II", bufferAddr, length))
            self.sendall(data[bytesSent : bytesSent + length])
            bufferAddr += length
            bytesSent += length
            task.update(bytesSent)
        self.sendFileMessage(bytesSent // size)
        task.end()

    def handleCloseFile(self, msg):
        print("Close")
        self.files.pop(msg.arg).close()
        self.sendFileMessage()

    def handleSetPosFile(self, msg):
        print("SetPos")
        handle, pos = struct.unpack(">II", msg.data)
        self.files[handle].seek(pos)
        self.sendFileMessage()

    def handleGetStatFile(self, msg):
        print("GetStatFile")
        f = self.files[msg.arg]
        pos = f.tell()
        f.seek(0, 2)
        size = f.tell()
        f.seek(pos)
        self.sendFileMessage(size)

    def connect(self, host):
        self.s = socket.socket()
        self.s.connect((host, 1559))
        self.connected = True
        self.closeRequest = False
        events.Connected.emit()

    def close(self):
        self.sendall(b"\x01")
        self.s.close()
        self.connected = False
        self.breakPoints = []
        events.Closed.emit()

    def updateMessages(self):
        self.sendall(b"\x07")
        count = struct.unpack(">I", self.recvall(4))[0]
        for i in range(count):
            type, ptr, length, arg = struct.unpack(">IIII", self.recvall(16))
            data = None
            if length:
                data = self.recvall(length)
            self.messageHandlers[type](Message(type, data, arg))

    def read(self, addr, num):
        self.sendall(b"\x02")
        self.sendall(struct.pack(">II", addr, num))
        data = self.recvall(num)
        return data

    def write(self, addr, data):
        self.sendall(b"\x03")
        self.sendall(struct.pack(">II", addr, len(data)))
        self.sendall(data)

    def writeCode(self, addr, instr):
        self.sendall(b"\x04")
        self.sendall(struct.pack(">II", addr, instr))

    def getThreadList(self):
        self.sendall(b"\x05")
        length = struct.unpack(">I", self.recvall(4))[0]
        data = self.recvall(length)

        offset = 0
        threads = []
        while offset < length:
            thread = Thread(data, offset)
            threads.append(thread)
            offset += 24 + len(thread.name)
        return threads

    def toggleBreakPoint(self, addr):
        if addr in self.breakPoints: self.breakPoints.remove(addr)
        else:
            if len(self.breakPoints) >= 10:
                return
            self.breakPoints.append(addr)

        self.sendall(b"\x0A")
        self.sendall(struct.pack(">I", addr))
        events.BreakPointChanged.emit()

    def continueBreak(self): self.sendCrashMessage(Message.Continue)
    def stepBreak(self): self.sendCrashMessage(Message.Step)
    def stepOver(self): self.sendCrashMessage(Message.StepOver)

    def sendCrashMessage(self, message):
        self.sendMessage(message)
        events.Continue.emit()

    def sendMessage(self, message, data0=0, data1=0, data2=0):
        self.sendall(b"\x06")
        self.sendall(struct.pack(">IIII", message, data0, data1, data2))

    def sendFileMessage(self, data0=0, data1=0, data2=0):
        self.sendall(b"\x0F")
        self.sendall(struct.pack(">IIII", 0, data0, data1, data2))

    def getStackTrace(self):
        self.sendall(b"\x08")
        count = struct.unpack(">I", self.recvall(4))[0]
        trace = struct.unpack(">%iI" %count, self.recvall(4 * count))
        return trace

    def pokeExceptionRegisters(self):
        self.sendall(b"\x09")
        data = struct.pack(">32I32d", *exceptionState.gpr, *exceptionState.fpr)
        self.sendall(data)

    def readDirectory(self, path):
        self.sendall(b"\x0B")
        self.sendall(struct.pack(">I", len(path)))
        self.sendall(path.encode("ascii"))

        entries = []
        namelen = struct.unpack(">I", self.recvall(4))[0]
        while namelen != 0:
            flags = struct.unpack(">I", self.recvall(4))[0]

            size = -1
            if not flags & 0x80000000:
                size = struct.unpack(">I", self.recvall(4))[0]

            name = self.recvall(namelen).decode("ascii")
            entries.append(DirEntry(flags, size, name))

            namelen = struct.unpack(">I", self.recvall(4))[0]
        return entries

    def dumpFile(self, gamePath, outPath, task):
        if task.canceled:
            return

        self.sendall(b"\x0C")
        self.sendall(struct.pack(">I", len(gamePath)))
        self.sendall(gamePath.encode("ascii"))

        length = struct.unpack(">I", self.recvall(4))[0]
        task.setInfo("Dumping %s" %gamePath, length)

        with open(outPath, "wb") as f:
            bytesDumped = 0
            while bytesDumped < length:
                data = self.s.recv(length - bytesDumped)
                f.write(data)
                bytesDumped += len(data)
                task.update(bytesDumped)

    def getModuleName(self):
        self.sendall(b"\x0D")
        length = struct.unpack(">I", self.recvall(4))[0]
        return self.recvall(length).decode("ascii") + ".rpx"

    def setPatchFiles(self, fileList, basePath):
        self.basePath = basePath
        self.sendall(b"\x0E")

        fileBuffer = struct.pack(">I", len(fileList))
        for path in fileList:
            fileBuffer += struct.pack(">H", len(path))
            fileBuffer += path.encode("ascii")

        self.sendall(struct.pack(">I", len(fileBuffer)))
        self.sendall(fileBuffer)

    def clearPatchFiles(self):
        self.sendall(b"\x10")

    def sendall(self, data):
        try:
            self.s.sendall(data)
        except socket.error:
            self.connected = False
            events.Closed.emit()

    def recvall(self, num):
        try:
            data = b""
            while len(data) < num:
                data += self.s.recv(num - len(data))
        except socket.error:
            self.connected = False
            events.Closed.emit()
            return b"\x00" * num

        return data


class HexSpinBox(QAbstractSpinBox):
    def __init__(self, parent, stepSize = 1):
        super().__init__(parent)
        self._value = 0
        self.stepSize = stepSize

    def validate(self, text, pos):
        if all([char in "0123456789abcdefABCDEF" for char in text]):
            if not text:
                return QValidator.Intermediate, text.upper(), pos

            value = int(text, 16)
            if value <= 0xFFFFFFFF:
                self._value = value
                if value % self.stepSize:
                    self._value -= value % self.stepSize
                    return QValidator.Acceptable, text.upper(), pos
                return QValidator.Acceptable, text.upper(), pos

        return QValidator.Invalid, text.upper(), pos

    def stepBy(self, steps):
        self._value = min(max(self._value + steps * self.stepSize, 0), 0x100000000 - self.stepSize)
        self.lineEdit().setText("%X" %self._value)

    def stepEnabled(self):
        return QAbstractSpinBox.StepUpEnabled | QAbstractSpinBox.StepDownEnabled

    def setValue(self, value):
        self._value = value
        self.lineEdit().setText("%X" %self._value)

    def value(self):
        return self._value


class ExceptionState:

    exceptionNames = ["DSI", "ISI", "Program"]

    def load(self, context, type):
        #Convert tuple to list to make it mutable
        self.gpr = list(struct.unpack_from(">32I", context, 8))
        self.cr, self.lr, self.ctr, self.xer = struct.unpack_from(">4I", context, 0x88)
        self.srr0, self.srr1, self.ex0, self.ex1 = struct.unpack_from(">4I", context, 0x98)
        self.fpr = list(struct.unpack_from(">32d", context, 0xB8))
        self.gqr = list(struct.unpack_from(">8I", context, 0x1BC))
        self.psf = list(struct.unpack_from(">32d", context, 0x1E0))

        self.exceptionName = self.exceptionNames[type]

    def isBreakPoint(self):
        return self.exceptionName == "Program" and self.srr1 & 0x20000


def format_hex(blob, offs):
    return "%02X" %blob[offs]

def format_ascii(blob, offs):
    if 0x30 <= blob[offs] <= 0x39 or 0x41 <= blob[offs] <= 0x5A or 0x61 <= blob[offs] <= 0x7A:
        return chr(blob[offs])
    return "?"

def format_float(blob, offs):
    value = struct.unpack_from(">f", blob, offs)[0]
    if abs(value) >= 1000000 or 0 < abs(value) < 0.000001:
        return "%e" %value
    return ("%.8f" %value).rstrip("0")


class MemoryViewer(QWidget):

    class Format:
        Hex = 0
        Ascii = 1
        Float = 2

    Width = 1, 1, 4
    Funcs = format_hex, format_ascii, format_float

    def __init__(self, parent):
        super().__init__(parent)

        self.layout = QGridLayout()

        for i in range(16):
            self.layout.addWidget(QLabel("%X" %i, self), 0, i + 1)
        self.addrLabels = []
        for i in range(16):
            label = QLabel("%X" %(i * 0x10), self)
            self.layout.addWidget(label, i + 1, 0)
            self.addrLabels.append(label)
        self.dataCells = []

        self.base = 0
        self.format = self.Format.Hex
        self.updateData()

        self.setLayout(self.layout)

        events.Connected.connect(self.connected)

    def connected(self):
        self.setBase(0x10000000)

    def setFormat(self, format):
        self.format = format
        self.updateData()

    def setBase(self, base):
        window.mainWidget.tabWidget.memoryTab.memoryInfo.baseBox.setValue(base)
        self.base = base
        for i in range(16):
            self.addrLabels[i].setText("%X" %(self.base + i * 0x10))
        self.updateData()

    def updateData(self):
        for cell in self.dataCells:
            self.layout.removeWidget(cell)
            cell.setParent(None)

        if bugger.connected:
            blob = bugger.read(self.base, 0x100)
        else:
            blob = b"\x00" * 0x100

        width = self.Width[self.format]
        func = self.Funcs[self.format]
        for i in range(16 // width):
            for j in range(16):
                label = QLabel(func(blob, j * 0x10 + i * width), self)
                self.layout.addWidget(label, j + 1, i * width + 1, 1, width)
                self.dataCells.append(label)


class MemoryInfo(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.dataTypeLabel = QLabel("Data type:")
        self.dataTypeBox = QComboBox(self)
        self.dataTypeBox.addItems(["Hex", "Ascii", "Float"])
        self.dataTypeBox.currentIndexChanged.connect(self.updateDataType)

        self.baseLabel = QLabel("Address:")
        self.baseBox = HexSpinBox(self, 0x10)
        self.baseButton = QPushButton("Update", self)
        self.baseButton.clicked.connect(self.updateMemoryBase)

        self.pokeAddr = HexSpinBox(self, 4)
        self.pokeValue = HexSpinBox(self)
        self.pokeButton = QPushButton("Poke", self)
        self.pokeButton.clicked.connect(self.pokeMemory)

        self.layout = QGridLayout()
        self.layout.addWidget(self.baseLabel, 0, 0)
        self.layout.addWidget(self.baseBox, 0, 1)
        self.layout.addWidget(self.baseButton, 0, 2)
        self.layout.addWidget(self.pokeAddr, 1, 0)
        self.layout.addWidget(self.pokeValue, 1, 1)
        self.layout.addWidget(self.pokeButton, 1, 2)
        self.layout.addWidget(self.dataTypeLabel, 2, 0)
        self.layout.addWidget(self.dataTypeBox, 2, 1, 1, 2)
        self.setLayout(self.layout)

    def updateDataType(self, index):
        window.mainWidget.tabWidget.memoryTab.memoryViewer.setFormat(index)

    def updateMemoryBase(self):
        window.mainWidget.tabWidget.memoryTab.memoryViewer.setBase(self.baseBox.value())

    def pokeMemory(self):
        bugger.write(self.pokeAddr.value(), struct.pack(">I", self.pokeValue.value()))
        window.mainWidget.tabWidget.memoryTab.memoryViewer.updateData()


class MemoryTab(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.memoryInfo = MemoryInfo(self)
        self.memoryViewer = MemoryViewer(self)
        self.layout = QHBoxLayout()
        self.layout.addWidget(self.memoryInfo)
        self.layout.addWidget(self.memoryViewer)
        self.setLayout(self.layout)


class DisassemblyWidget(QTextEdit):
    def __init__(self, parent):
        super().__init__(parent)
        self.setTextInteractionFlags(Qt.NoTextInteraction)

        self.currentInstruction = None
        self.selectedAddress = 0
        self.setBase(0)

        events.BreakPointChanged.connect(self.updateHighlight)
        events.Continue.connect(self.handleContinue)

    def handleContinue(self):
        self.currentInstruction = None
        self.updateHighlight()

    def setCurrentInstruction(self, instr):
        self.currentInstruction = instr
        self.setBase(instr - 0x20)

    def setBase(self, base):
        self.base = base
        self.updateText()
        self.updateHighlight()

    def updateText(self):
        if bugger.connected:
            blob = bugger.read(self.base, 0x60)
        else:
            blob = b"\x00" * 0x60

        text = ""
        for i in range(24):
            address = self.base + i * 4
            value = struct.unpack_from(">I", blob, i * 4)[0]
            instr = disassemble.disassemble(value, address)
            text += "%08X:  %08X  %s\n" %(address, value, instr)
        self.setPlainText(text)

    def updateHighlight(self):
        selections = []
        for i in range(24):
            address = self.base + i * 4

            color = self.getColor(address)
            if color:
                cursor = self.textCursor()
                cursor.movePosition(QTextCursor.Down, n=i)
                cursor.select(QTextCursor.LineUnderCursor)
                format = QTextCharFormat()
                format.setBackground(QBrush(QColor(color)))
                selection = QTextEdit.ExtraSelection()
                selection.cursor = cursor
                selection.format = format
                selections.append(selection)
        self.setExtraSelections(selections)

    def getColor(self, addr):
        colors = []
        if addr in bugger.breakPoints:
            colors.append((255, 0, 0))
        if addr == self.currentInstruction:
            colors.append((0, 255, 0))
        if addr == self.selectedAddress:
            colors.append((0, 0, 255))

        if not colors:
            return None

        color = [sum(l)//len(colors) for l in zip(*colors)]
        return "#%02X%02X%02X" %tuple(color)

    def mousePressEvent(self, e):
        super().mousePressEvent(e)
        line = self.cursorForPosition(e.pos()).blockNumber()
        self.selectedAddress = self.base + line * 4
        if e.button() == Qt.MidButton:
            bugger.toggleBreakPoint(self.selectedAddress)
        self.updateHighlight()


class DisassemblyInfo(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.baseLabel = QLabel("Address:")
        self.baseBox = HexSpinBox(self, 4)
        self.baseButton = QPushButton("Update", self)
        self.baseButton.clicked.connect(self.updateDisassemblyBase)

        self.pokeBox = HexSpinBox(self)
        self.pokeButton = QPushButton("Poke", self)
        self.pokeButton.clicked.connect(self.poke)

        self.layout = QGridLayout()
        self.layout.addWidget(self.baseLabel, 0, 0)
        self.layout.addWidget(self.baseBox, 0, 1)
        self.layout.addWidget(self.baseButton, 0, 2)
        self.layout.addWidget(self.pokeBox, 1, 0)
        self.layout.addWidget(self.pokeButton, 1, 1, 1, 2)
        self.setLayout(self.layout)
        self.setMinimumWidth(300)

    def updateDisassemblyBase(self):
        window.mainWidget.tabWidget.disassemblyTab.disassemblyWidget.setBase(self.baseBox.value())

    def poke(self):
        disassembly = window.mainWidget.tabWidget.disassemblyTab.disassemblyWidget
        if disassembly.selectedAddress:
            bugger.writeCode(disassembly.selectedAddress, self.pokeBox.value())
            disassembly.updateText()


class DisassemblyTab(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.disassemblyInfo = DisassemblyInfo(self)
        self.disassemblyWidget = DisassemblyWidget(self)
        self.layout = QHBoxLayout()
        self.layout.addWidget(self.disassemblyInfo)
        self.layout.addWidget(self.disassemblyWidget)
        self.setLayout(self.layout)

        events.Connected.connect(self.connected)

    def connected(self):
        self.disassemblyWidget.setBase(0x10000000)


class ThreadList(QTableWidget):
    def __init__(self, parent):
        super().__init__(0, 5, parent)
        self.setHorizontalHeaderLabels(["Name", "Priority", "Core", "Stack", "Entry Point"])
        self.setEditTriggers(self.NoEditTriggers)
        self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)

        events.Connected.connect(self.updateThreads)

    def updateThreads(self):
        threads = bugger.getThreadList()
        self.setRowCount(len(threads))
        for i in range(len(threads)):
            thread = threads[i]
            self.setItem(i, 0, QTableWidgetItem(thread.name))
            self.setItem(i, 1, QTableWidgetItem(str(thread.priority)))
            self.setItem(i, 2, QTableWidgetItem(thread.core))
            self.setItem(i, 3, QTableWidgetItem("0x%x - 0x%x" %(thread.stackEnd, thread.stackBase)))
            self.setItem(i, 4, QTableWidgetItem(hex(thread.entryPoint)))


class ThreadingTab(QTableWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.threadList = ThreadList(self)
        self.updateButton = QPushButton("Update", self)
        self.updateButton.clicked.connect(self.threadList.updateThreads)
        self.layout = QVBoxLayout()
        self.layout.addWidget(self.threadList)
        self.layout.addWidget(self.updateButton)
        self.setLayout(self.layout)


class BreakPointList(QListWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.itemDoubleClicked.connect(self.goToDisassembly)
        events.BreakPointChanged.connect(self.updateList)

    def updateList(self):
        self.clear()
        for bp in bugger.breakPoints:
            self.addItem("0x%08X" %bp)

    def goToDisassembly(self, item):
        address = bugger.breakPoints[self.row(item)]
        window.mainWidget.tabWidget.disassemblyTab.disassemblyWidget.setBase(address)
        window.mainWidget.tabWidget.setCurrentIndex(1)


class BreakPointTab(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.list = BreakPointList(self)
        self.button = QPushButton("Remove", self)
        self.button.clicked.connect(self.removeBreakPoint)
        self.layout = QVBoxLayout()
        self.layout.addWidget(self.list)
        self.layout.addWidget(self.button)
        self.setLayout(self.layout)

    def removeBreakPoint(self):
        if self.list.currentRow() != -1:
            bugger.toggleBreakPoint(bugger.breakPoints[self.list.currentRow()])


class RegisterTab(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.gprLabels = []
        self.gprBoxes = []
        self.fprLabels = []
        self.fprBoxes = []
        for i in range(32):
            self.gprLabels.append(QLabel("r%i" %i, self))
            self.fprLabels.append(QLabel("f%i" % i, self))
            gprBox = HexSpinBox(self)
            fprBox = QDoubleSpinBox(self)
            fprBox.setRange(float("-inf"), float("inf"))
            self.gprBoxes.append(gprBox)
            self.fprBoxes.append(fprBox)

        self.layout = QGridLayout()
        for i in range(32):
            self.layout.addWidget(self.gprLabels[i], i % 16, i // 16 * 2)
            self.layout.addWidget(self.gprBoxes[i], i % 16, i // 16 * 2 + 1)
            self.layout.addWidget(self.fprLabels[i], i % 16, i // 16 * 2 + 4)
            self.layout.addWidget(self.fprBoxes[i], i % 16, i // 16 * 2 + 5)
        self.setLayout(self.layout)

        self.pokeButton = QPushButton("Poke", self)
        self.resetButton = QPushButton("Reset", self)
        self.pokeButton.clicked.connect(self.pokeRegisters)
        self.resetButton.clicked.connect(self.updateRegisters)
        self.layout.addWidget(self.pokeButton, 16, 0, 1, 4)
        self.layout.addWidget(self.resetButton, 16, 4, 1, 4)

        self.setEditEnabled(False)

        events.Exception.connect(self.exceptionOccurred)
        events.Continue.connect(lambda: self.setEditEnabled(False))

    def setEditEnabled(self, enabled):
        for i in range(32):
            self.gprBoxes[i].setEnabled(enabled)
            self.fprBoxes[i].setEnabled(enabled)
        self.pokeButton.setEnabled(enabled)
        self.resetButton.setEnabled(enabled)

    def exceptionOccurred(self):
        self.updateRegisters()
        self.setEditEnabled(exceptionState.isBreakPoint())

    def updateRegisters(self):
        for i in range(32):
            self.gprBoxes[i].setValue(exceptionState.gpr[i])
            self.fprBoxes[i].setValue(exceptionState.fpr[i])

    def pokeRegisters(self):
        for i in range(32):
            exceptionState.gpr[i] = self.gprBoxes[i].value()
            exceptionState.fpr[i] = self.fprBoxes[i].value()
        bugger.pokeExceptionRegisters()


class ExceptionInfo(QGroupBox):
    def __init__(self, parent):
        super().__init__("Info", parent)
        self.typeLabel = QLabel(self)
        self.layout = QVBoxLayout()
        self.layout.addWidget(self.typeLabel)
        self.setLayout(self.layout)

        events.Exception.connect(self.updateInfo)

    def updateInfo(self):
        self.typeLabel.setText("Type: %s" %exceptionState.exceptionName)


class SpecialRegisters(QGroupBox):
    def __init__(self, parent):
        super().__init__("Special registers", parent)
        self.cr = QLabel(self)
        self.lr = QLabel(self)
        self.ctr = QLabel(self)
        self.xer = QLabel(self)
        self.srr0 = QLabel(self)
        self.srr1 = QLabel(self)
        self.ex0 = QLabel(self)
        self.ex1 = QLabel(self)

        self.layout = QHBoxLayout()
        self.userLayout = QFormLayout()
        self.kernelLayout = QFormLayout()

        self.userLayout.addRow("CR:", self.cr)
        self.userLayout.addRow("LR:", self.lr)
        self.userLayout.addRow("CTR:", self.ctr)
        self.userLayout.addRow("XER:", self.xer)

        self.kernelLayout = QFormLayout()
        self.kernelLayout.addRow("SRR0:", self.srr0)
        self.kernelLayout.addRow("SRR1:", self.srr1)
        self.kernelLayout.addRow("EX0:", self.ex0)
        self.kernelLayout.addRow("EX1:", self.ex1)

        self.layout.addLayout(self.userLayout)
        self.layout.addLayout(self.kernelLayout)
        self.setLayout(self.layout)

        events.Exception.connect(self.updateRegisters)

    def updateRegisters(self):
        self.cr.setText("%X" %exceptionState.cr)
        self.lr.setText("%X" %exceptionState.lr)
        self.ctr.setText("%X" %exceptionState.ctr)
        self.xer.setText("%X" %exceptionState.xer)
        self.srr0.setText("%X" %exceptionState.srr0)
        self.srr1.setText("%X" %exceptionState.srr1)
        self.ex0.setText("%X" %exceptionState.ex0)
        self.ex1.setText("%X" %exceptionState.ex1)


class ExceptionInfoTab(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.exceptionInfo = ExceptionInfo(self)
        self.specialRegisters = SpecialRegisters(self)
        self.layout = QGridLayout()
        self.layout.addWidget(self.exceptionInfo, 0, 0)
        self.layout.addWidget(self.specialRegisters, 0, 1)
        self.setLayout(self.layout)


class StackTrace(QListWidget):
    def __init__(self, parent):
        super().__init__(parent)
        events.Exception.connect(self.updateTrace)

    def updateTrace(self):
        self.clear()
        stackTrace = bugger.getStackTrace()
        for address in (exceptionState.srr0, exceptionState.lr) + stackTrace:
            self.addItem("%X" %address)


class BreakPointActions(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.continueButton = QPushButton("Continue", self)
        self.stepButton = QPushButton("Step", self)
        self.stepOverButton = QPushButton("Step over", self)
        self.continueButton.clicked.connect(bugger.continueBreak)
        self.stepButton.clicked.connect(bugger.stepBreak)
        self.stepOverButton.clicked.connect(bugger.stepOver)

        self.layout = QHBoxLayout()
        self.layout.addWidget(self.continueButton)
        self.layout.addWidget(self.stepButton)
        self.layout.addWidget(self.stepOverButton)
        self.setLayout(self.layout)

        events.Exception.connect(self.updateButtons)
        events.Continue.connect(self.disableButtons)

    def disableButtons(self):
        self.setButtonsEnabled(False)

    def updateButtons(self):
        self.setButtonsEnabled(exceptionState.isBreakPoint())

    def setButtonsEnabled(self, enabled):
        self.continueButton.setEnabled(enabled)
        self.stepButton.setEnabled(enabled)
        self.stepOverButton.setEnabled(enabled)


class StackTraceTab(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.stackTrace = StackTrace(self)
        self.disassembly = DisassemblyWidget(self)
        self.breakPointActions = BreakPointActions(self)
        self.layout = QVBoxLayout()
        hlayout = QHBoxLayout()
        hlayout.addWidget(self.stackTrace)
        hlayout.addWidget(self.disassembly)
        self.layout.addLayout(hlayout)
        self.layout.addWidget(self.breakPointActions)
        self.setLayout(self.layout)

        self.stackTrace.itemDoubleClicked.connect(self.jumpDisassembly)
        events.Exception.connect(self.exceptionOccurred)

    def exceptionOccurred(self):
        self.disassembly.setCurrentInstruction(exceptionState.srr0)

    def jumpDisassembly(self, item):
        self.disassembly.setBase(int(item.text(), 16) - 0x20)


class ExceptionTab(QTabWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.infoTab = ExceptionInfoTab(self)
        self.registerTab = RegisterTab(self)
        self.stackTab = StackTraceTab(self)
        self.addTab(self.infoTab, "General")
        self.addTab(self.registerTab, "Registers")
        self.addTab(self.stackTab, "Stack trace")

        events.Exception.connect(self.exceptionOccurred)

    def exceptionOccurred(self):
        self.setCurrentIndex(2)  #Stack trace


def formatFileSize(size):
    if size >= 1024 ** 3:
        return "%.1f GiB" %(size / (1024 ** 3))
    if size >= 1024 ** 2:
        return "%.1f MiB" %(size / (1024 ** 2))
    if size >= 1024:
        return "%.1f KiB" %(size / 1024)
    return "%i B" %size

class FileTreeNode(QTreeWidgetItem):
    def __init__(self, parent, name, size, path):
        super().__init__(parent)
        self.name = name
        self.size = size
        self.path = path

        self.setText(0, name)
        if size == -1: #It's a folder
            self.loaded = False
        else: #It's a file
            self.setText(1, formatFileSize(size))
            self.loaded = True

    def loadChildren(self):
        if not self.loaded:
            for i in range(self.childCount()):
                child = self.child(i)
                if not child.loaded:
                    self.child(i).loadContent()
            self.loaded = True

    def loadContent(self):
        entries = bugger.readDirectory(self.path)
        for entry in entries:
            FileTreeNode(self, entry.name, entry.size, self.path + "/" + entry.name)

    def dump(self, outdir, task):
        if task.canceled:
            return

        outpath = os.path.join(outdir, self.name)
        if self.size == -1:
            if os.path.isfile(outpath):
                os.remove(outpath)
            if not os.path.exists(outpath):
                os.mkdir(outpath)

            self.loadChildren()
            for i in range(self.childCount()):
                self.child(i).dump(outpath, task)
        else:
            bugger.dumpFile(self.path, outpath, task)


class FileTreeWidget(QTreeWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.setHeaderLabels(["Name", "Size"])
        self.itemExpanded.connect(self.handleItemExpanded)
        events.Connected.connect(self.initFileTree)

    def initFileTree(self):
        self.clear()
        rootItem = FileTreeNode(self, "content", -1, "/vol/content")
        rootItem.loadContent()
        self.resizeColumnToContents(0)

    def handleItemExpanded(self, item):
        item.loadChildren()
        self.resizeColumnToContents(0)


class FileSystemTab(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.fileTree = FileTreeWidget(self)
        self.dumpButton = QPushButton("Dump", self)
        self.dumpButton.clicked.connect(self.dump)
        self.patchButton = QPushButton("Load patch", self)
        self.patchButton.clicked.connect(self.loadPatch)
        self.clearButton = QPushButton("Clear patch", self)
        self.clearButton.clicked.connect(self.clearPatch)
        self.clearButton.setEnabled(False)

        self.layout = QVBoxLayout()
        hlayout = QHBoxLayout()
        hlayout.addWidget(self.dumpButton)
        hlayout.addWidget(self.patchButton)
        hlayout.addWidget(self.clearButton)
        self.layout.addWidget(self.fileTree)
        self.layout.addLayout(hlayout)
        self.setLayout(self.layout)

    def dump(self):
        item = self.fileTree.currentItem()
        if item:
            outdir = QFileDialog.getExistingDirectory(self, "Dump")
            if outdir:
                task = Task(blocking=True, cancelable=True)
                item.dump(outdir, task)
                task.end()

    def loadPatch(self):
        patchDir = QFileDialog.getExistingDirectory(self, "Load patch")
        if patchDir:
            baseLength = len(patchDir)
            fileList = []
            for dirname, subdirs, files in os.walk(patchDir):
                for filename in files:
                    gamePath = "/vol" + dirname[baseLength:].replace("\\", "/") + "/" + filename
                    fileList.append(gamePath)

            bugger.setPatchFiles(fileList, patchDir)
            self.clearButton.setEnabled(True)

    def clearPatch(self):
        bugger.clearPatchFiles()
        self.clearButton.setEnabled(False)


class DebuggerTabs(QTabWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.memoryTab = MemoryTab(self)
        self.disassemblyTab = DisassemblyTab(self)
        self.threadingTab = ThreadingTab(self)
        self.breakPointTab = BreakPointTab(self)
        self.exceptionTab = ExceptionTab(self)
        self.fileSystemTab = FileSystemTab(self)
        self.addTab(self.memoryTab, "Memory")
        self.addTab(self.disassemblyTab, "Disassembly")
        self.addTab(self.threadingTab, "Threads")
        self.addTab(self.breakPointTab, "Breakpoints")
        self.addTab(self.exceptionTab, "Exceptions")
        self.addTab(self.fileSystemTab, "File System")
        self.setTabEnabled(4, False)

        events.Exception.connect(self.exceptionOccurred)
        events.Connected.connect(self.connected)
        events.Closed.connect(self.disconnected)

    def exceptionOccurred(self):
        self.setTabEnabled(4, True)
        self.setCurrentIndex(4) #Exceptions

    def connected(self):
        self.setEnabled(True)

    def disconnected(self):
        self.setEnabled(False)
        self.setTabEnabled(4, False)


class StatusWidget(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.serverLabel = QLabel("Wii U IP:")
        self.serverBox = QLineEdit(self)
        self.serverBox.returnPressed.connect(self.connect)
        self.connectButton = QPushButton("Connect", self)
        self.connectButton.clicked.connect(self.connect)
        self.disconnectButton = QPushButton("Disconnect", self)
        self.disconnectButton.clicked.connect(bugger.close)
        self.disconnectButton.setEnabled(False)

        self.progressBar = QProgressBar(self)
        self.progressInfo = QLabel("Disconnected", self)
        self.cancelButton = QPushButton("Cancel", self)
        self.cancelButton.clicked.connect(taskMgr.cancel)
        self.cancelButton.setEnabled(False)

        self.layout = QGridLayout()
        self.layout.addWidget(self.serverLabel, 0, 0)
        self.layout.addWidget(self.serverBox, 1, 0)
        self.layout.addWidget(self.connectButton, 0, 1)
        self.layout.addWidget(self.disconnectButton, 1, 1)
        self.layout.addWidget(self.progressBar, 2, 0)
        self.layout.addWidget(self.cancelButton, 2, 1)
        self.layout.addWidget(self.progressInfo, 3, 0, 1, 2)
        self.setLayout(self.layout)

        events.Connected.connect(self.connected)
        events.Closed.connect(self.disconnected)

    def connect(self):
        try: bugger.connect(str(self.serverBox.text()))
        except: pass

    def connected(self):
        self.progressInfo.setText("Connected")
        self.connectButton.setEnabled(False)
        self.serverBox.setEnabled(False)
        self.disconnectButton.setEnabled(True)

    def disconnected(self):
        self.progressInfo.setText("Disconnected")
        self.connectButton.setEnabled(True)
        self.serverBox.setEnabled(True)
        self.disconnectButton.setEnabled(False)


class MainWidget(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.tabWidget = DebuggerTabs(self)
        self.statusWidget = StatusWidget(self)
        self.layout = QVBoxLayout()
        self.layout.addWidget(self.tabWidget)
        self.layout.addWidget(self.statusWidget)
        self.tabWidget.setEnabled(False)
        self.setLayout(self.layout)


class MainWindow(QMainWindow):
    def init(self):
        self.mainWidget = MainWidget(self)
        self.setCentralWidget(self.mainWidget)

        self.setWindowTitle("DiiBugger")
        self.resize(1080, 720)

        self.timer = QTimer(self)
        self.timer.setInterval(100)
        self.timer.timeout.connect(self.updateBugger)
        self.timer.start()

        events.Connected.connect(self.updateTitle)
        events.Closed.connect(self.updateTitle)

    def updateTitle(self):
        if bugger.connected:
            name = bugger.getModuleName()
            self.setWindowTitle("DiiBugger - %s" %name)
        else:
            self.setWindowTitle("DiiBugger")

    def updateBugger(self):
        if bugger.connected and not taskMgr.isBlocking():
            bugger.updateMessages()

    def closeEvent(self, e):
        if taskMgr.taskQueue:
            e.ignore()
        else:
            e.accept()


exceptionState = ExceptionState()
bugger = PyBugger()
app = QApplication(sys.argv)
app.setFont(QFontDatabase.systemFont(QFontDatabase.FixedFont))
window = MainWindow()
window.init()
window.show()
app.exec()
if bugger.connected:
    bugger.close()
