from __future__ import division

from PyQt5 import QtGui, QtCore, QtWidgets
from PyQt5.uic import loadUi

from androguard.gui.Banners import Banners, FileAddrBanner, TopBanner, BottomBanner
from androguard.gui.cemu import Cursor
from androguard.gui.TextDecorators import HighlightASCII, TextDecorator
from androguard.gui import TextSelection

import pyperclip
import os

import logging
log = logging.getLogger("androguard.gui")

class SearchWindow(QtWidgets.QDialog):
    def __init__(self, parent, plugin, searchable):
        super(SearchWindow, self).__init__(parent)
        self.searchable = searchable
        self.parent = parent
        self.plugin = plugin
        self.oshow = super(SearchWindow, self).show

        root = os.path.dirname(os.path.realpath(__file__))
        self.ui = loadUi(os.path.join(root, 'search.ui'), baseinstance=self)
        self.ui.setWindowTitle('Search')
        self._lastText = ''

        self.initUI()

    def show(self):
        # TODO: remember position? resize plugin windows when parent resize?

        width = self.ui.size().width() + 15
        height = self.ui.size().height() + 15

        self.move((self.parent.width() - width) // 2, (self.parent.height() - height) // 2)
        self.ui.lineEdit.setText(self._lastText)
        self.ui.lineEdit.selectAll()
        self.oshow()

    def initUI(self):

        self.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)

        shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("/"), self, self.close, self.close)

        self.ui.pushButton.clicked.connect(self.onClicked)

        width = self.ui.size().width() + 15
        height = self.ui.size().height() + 15
        self.setFixedSize(width, height)

    def onClicked(self):
        text = self.ui.lineEdit.text()
        text = str(text)

        hexstr = '0123456789abcdefABCDEF'
        if self.ui.checkHex.isChecked():
            T = text.split(' ')
            oldtext = text
            text = ''
            for t in T:
                if len(t) != 2:
                    reply = QtWidgets.QMessageBox.warning(self, 'Qiew', "Hex string with errors.",
                                                          QtWidgets.QMessageBox.Ok)
                    self.close()
                    return

                if t[0] in hexstr and t[1] in hexstr:
                    o = int(t, 16)
                    text += chr(o)
                else:
                    reply = QtWidgets.QMessageBox.warning(self, 'Qiew', "Hex string with errors.",
                                                          QtWidgets.QMessageBox.Ok)
                    self.close()
                    return

            self._lastText = oldtext

        else:
            self._lastText = text

        if not self.ui.checkHex.isChecked():
            text = text.encode('utf-8')

        idx = self.searchable.search(text)
        if idx == -1:
            reply = QtWidgets.QMessageBox.warning(self, 'Qiew', "Nothing found.", QtWidgets.QMessageBox.Ok)

        self.parent.viewMode.draw(refresh=True)
        self.close()


class Observable(object):
    def __init__(self):
        self.Callbacks = []

    def addHandler(self, h):
        if h not in self.Callbacks:
            self.Callbacks.append(h)

    def notify(self, viewMode):
        for cbk in self.Callbacks:
            cbk.changeViewMode(viewMode)


class Observer(object):
    def changeViewMode(self, viewMode):
        self._viewMode = viewMode


class Searchable(Observer):
    def __init__(self, dataModel, viewMode):
        self._viewMode = viewMode
        self._dataModel = dataModel
        self._lastIdx = -1
        self._lastText = ''

    def next(self, start=None):
        data = self._dataModel.getData()
        text = self._lastText

        if not start:
            idx = self._lastIdx + 1
        else:
            idx = start

        if idx > -1:
            self._search(data, text, idx)

    @property
    def lastText(self):
        return self._lastText

    def previous(self, start=None):
        data = self._dataModel.getData()
        text = self._lastText

        if not start:
            idx = self._lastIdx
        else:
            idx = start

        if idx > -1:
            self._search(data, text, idx, previous=True)

    def _search(self, data, text, start, previous=False):

        self._lastText = text
        if text == '':
            return -1

        if not previous:
            idx1 = data.find(text, start)
            text1 = '\0'.join(text)

            idx2 = data.find(text1, start)

            idx = idx1
            if idx1 == -1:
                idx = idx2
            else:
                if idx2 < idx1 and idx2 != -1:
                    idx = idx2

        else:
            idx1 = data.rfind(text, 0, start)
            text1 = '\0'.join(text)

            idx2 = data.rfind(text1, 0, start)

            idx = idx1

            if idx1 == -1:
                idx = idx2
            else:
                if idx2 > idx1 and idx2 != -1:
                    idx = idx2

        if idx > -1:
            self._lastIdx = idx

        if idx > -1:
            self._viewMode.selector.addSelection((idx, idx + len(text), QtGui.QBrush(QtGui.QColor(125, 0, 100)), 0.8),
                                                 type=TextSelection.SelectionType.NORMAL)
            self._viewMode.goTo(idx)

        return idx

    def search(self, text):
        data = self._dataModel.getData()
        return self._search(data, text, 0)


class binWidget(QtWidgets.QWidget, Observable):
    scrolled = QtCore.pyqtSignal(int, name='scroll')

    def __init__(self, parent, source, title):
        super(binWidget, self).__init__()
        Observable.__init__(self)
        self.parent = parent

        self.title = title
        self.active = False
        # offset for text window
        # self.data = mapped
        self.dataOffset = 0

        self.dataModel = source
        self.cursor = Cursor(0, 0)

        self.themes = {
            'font': QtGui.QFont('Monaco', 9, QtGui.QFont.Light),
            'background': QtGui.QColor(0x00, 0x2b, 0x36),
            'background_cursor': QtGui.QColor(255, 255, 0),
            'selection': QtGui.QColor(125, 255, 0),
            'pen': QtGui.QColor(0xb5, 0x89, 0x00)
        }

        self.multipleViewModes = []
        for view_mode in self.dataModel.GetViews():
            v = view_mode(self.themes, self.size().width(), self.size().height(), self.dataModel, self.cursor, self)
            textDecorator = HighlightASCII(TextDecorator(v))

            v.setTransformationEngine(textDecorator)

            self.multipleViewModes.append(v)

        self.viewMode = self.multipleViewModes[0]

        self.Banners = Banners()

        self.Banners.add(FileAddrBanner(self.themes, self.dataModel, self.viewMode))
        self.Banners.add(TopBanner(self.themes, self.dataModel, self.viewMode))
        self.Banners.add(BottomBanner(self.themes, self.dataModel, self.viewMode))

        self.offsetWindow_h = 0
        self.offsetWindow_v = 0
        self.searchable = Searchable(self.dataModel, self.viewMode)

        self.initUI()

        self.searchWindow = SearchWindow(self, None, self.searchable)

        self.addHandler(self.searchable)
        self.addHandler(self.Banners)

        self.notify(self.viewMode)

    def enable(self):
        self.active = True

    def disable(self):
        self.active = False

    def scroll_from_outside(self, i):
        self.update()

    def initUI(self):
        self.setFocusPolicy(QtCore.Qt.StrongFocus)

        self.setMinimumSize(1, 30)
        self.activateWindow()
        self.setFocus()

    def switchViewMode(self):
        self.multipleViewModes = self.multipleViewModes[1:] + [self.multipleViewModes[0]]
        self.viewMode = self.multipleViewModes[0]

        # notify obervers
        self.notify(self.viewMode)

    def _resize(self):

        self.Banners.resize(self.size().width() - self.offsetWindow_h, self.size().height() - self.offsetWindow_v)

        # compute space ocupated by banners        
        offsetLeft = self.offsetWindow_h + self.Banners.getLeftOffset()
        offsetBottom = self.offsetWindow_v + self.Banners.getBottomOffset() + self.Banners.getTopOffset()

        # resize window, substract space ocupated by banners
        self.viewMode.resize(self.size().width() - offsetLeft, self.size().height() - offsetBottom)

    # event handlers
    def resizeEvent(self, e):
        self._resize()

    def paintEvent(self, e):
        qp = QtGui.QPainter()
        qp.begin(self)
        qp.setOpacity(1)

        offsetLeft = self.offsetWindow_h + self.Banners.getLeftOffset()
        offsetBottom = self.offsetWindow_v + self.Banners.getTopOffset()

        qp.drawPixmap(offsetLeft, offsetBottom, self.viewMode.getPixmap())

        self.Banners.draw(qp, self.offsetWindow_h, self.offsetWindow_v, self.size().height())

        qp.end()

    def eventFilter(self, watched, event):
        if not self.active:
            return False

        if event.type() == QtCore.QEvent.KeyRelease:
            key = event.key()
            modifiers = event.modifiers()
            if self.viewMode.handleKeyEvent(modifiers, key, event=event):
                self.update()

        if event.type() == QtCore.QEvent.KeyPress:
            # TODO: should we accept only certain keys ?
            key = event.key()
            modifiers = event.modifiers()
            if key == QtCore.Qt.Key_F2:
                if self.viewMode.isEditable():
                    if self.viewMode.isInEditMode():
                        self.viewMode.setEditMode(False)
                    else:
                        self.viewMode.setEditMode(True)

                    self.viewMode.draw(refresh=False)
            # switch view mode
            if key == QtCore.Qt.Key_V:
                log.debug('SWITCH VIEW')
                offs = self.viewMode.getCursorOffsetInPage()
                base = self.viewMode.getDataModel().getOffset()
                self.switchViewMode()
                self._resize()
                self.viewMode.goTo(base + offs)
                self.update()

            if key == QtCore.Qt.Key_S:
                log.debug('OPEN SOURCE')
                self.parent.openSourceWindow(self.dataModel.current_class)

            if event.modifiers() & QtCore.Qt.ControlModifier:
                if key == QtCore.Qt.Key_Insert:
                    if self.viewMode.selector.getCurrentSelection():
                        a, b = self.viewMode.selector.getCurrentSelection()

                        hx = ''
                        for s in self.dataModel.getStream(a, b):
                            hx += '{:02x}'.format(s)

                        pyperclip.copy(hx)
                        del pyperclip

            if event.modifiers() & QtCore.Qt.ShiftModifier:
                if key == QtCore.Qt.Key_Insert:
                    import re
                    hx = pyperclip.paste()
                    L = re.findall(r'.{1,2}', hx, re.DOTALL)

                    array = ''
                    for s in L:
                        array += chr(int(s, 16))

                    self.dataModel.write(0, array)
                    self.viewMode.draw(True)
                    del pyperclip

            if not self.viewMode.isInEditMode():
                if key == QtCore.Qt.Key_Slash:
                    self.searchWindow.show()

                if key == QtCore.Qt.Key_N:
                    self.searchable.next(self.viewMode.getCursorAbsolutePosition() + 1)

                if key == QtCore.Qt.Key_B:
                    self.searchable.previous(self.viewMode.getCursorAbsolutePosition())

            # handle keys to view plugin
            if self.viewMode.handleKeyEvent(modifiers, key, event=event):
                event.accept()
                self.update()
                return True

        return False

    def setTextViewport(self, qp):
        qp.setViewport(self.offsetWindow_h, self.offsetWindow_v, self.size().width(), self.size().height())
        qp.setWindow(0, 0, self.size().width(), self.size().height())

    def needsSave(self):
        return self.dataModel.isDirty()

    def save(self):
        return self.dataModel.flush()
