import sys
import time
import logging
import threading

from .shim import *
from ..misc import is_mainthread
from ..python import *
from ..disassembler import disassembler

logger = logging.getLogger("Lighthouse.Qt.Util")

#------------------------------------------------------------------------------
# Qt Fonts
#------------------------------------------------------------------------------

def MonospaceFont():
    """
    Convenience alias for creating a monospace Qt font object.
    """
    font = QtGui.QFont("Courier New")
    font.setStyleHint(QtGui.QFont.Monospace)
    return font

#------------------------------------------------------------------------------
# Qt Util
#------------------------------------------------------------------------------

def color_text(text, color):
    """
    Return a colorized (HTML) version of the given string.
    """
    return "<font color=\"%s\">%s</font>" % (color.name(), text)

def copy_to_clipboard(data):
    """
    Copy the given data (a string) to the system clipboard.
    """
    cb = QtWidgets.QApplication.clipboard()
    cb.clear(mode=cb.Clipboard)
    cb.setText(data, mode=cb.Clipboard)

def flush_qt_events():
    """
    Flush the Qt event pipeline.
    """
    app = QtCore.QCoreApplication.instance()
    app.processEvents()

def get_qt_icon(name):
    """
    Get a standard Qt icon by name.
    """
    icon_type = getattr(QtWidgets.QStyle, name)
    return QtWidgets.QApplication.style().standardIcon(icon_type)

def get_default_font_size():
    """
    Get the default font size for this QApplication.
    """
    return QtGui.QFont().pointSizeF()

def get_dpi_scale():
    """
    Get a DPI-afflicted value useful for consistent UI scaling.
    """
    font = MonospaceFont()
    font.setPointSize(normalize_to_dpi(120))
    fm = QtGui.QFontMetricsF(font)

    # xHeight is expected to be 40.0 at normal DPI
    return fm.height() / 173.0

def compute_color_on_gradiant(percent, color1, color2):
    """
    Compute the color specified by a percent between two colors.
    """
    r1, g1, b1, _ = color1.getRgb()
    r2, g2, b2, _ = color2.getRgb()

    # compute the new color across the gradiant of color1 -> color 2
    r = r1 + percent * (r2 - r1)
    g = g1 + percent * (g2 - g1)
    b = b1 + percent * (b2 - b1)

    # return the new color
    return QtGui.QColor(r,g,b)

def move_mouse_event(mouse_event, position):
    """
    Move the given mouse event to a different position.
    """
    new_event = QtGui.QMouseEvent(
        mouse_event.type(),
        position,
        mouse_event.button(),
        mouse_event.buttons(),
        mouse_event.modifiers()
    )
    return new_event

def normalize_to_dpi(font_size):
    """
    Normalize the given font size based on the system DPI.
    """
    if sys.platform == "darwin": # macos is lame
        return font_size + 2
    return font_size

def prompt_string(label, title, default=""):
    """
    Prompt the user with a dialog to enter a string.

    This does not block the IDA main thread (unlike idaapi.askstr)
    """
    dpi_scale = get_dpi_scale()
    dlg = QtWidgets.QInputDialog(None)
    dlg.setWindowFlags(dlg.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
    dlg.setInputMode(QtWidgets.QInputDialog.TextInput)
    dlg.setLabelText(label)
    dlg.setWindowTitle(title)
    dlg.setTextValue(default)
    dlg.resize(
        dpi_scale*400,
        dpi_scale*50
    )
    dlg.setModal(True)
    dlg.show()
    dlg.setFocus(QtCore.Qt.PopupFocusReason)
    ok = dlg.exec_()
    text = str(dlg.textValue())
    return (ok, text)

def predict_bg_color(image):
    """
    Predict the 'background color' of a given image.

    This function takes an image, and analyzes its first row of pixels. It
    will return the color that it believes to be the 'background color' based
    on the longest sequence of identical pixels.
    """
    assert image.width() and image.height()

    # the details for the longest known color streak will be saved in these
    longest = 1
    speculative_bg = image.pixel(0, 0)

    # this will be the computed length of the current color streak
    sequence = 1

    # find the longest streak of color in a single pixel slice
    for x in xrange(1, image.width()):

        # the color of this pixel matches the last pixel, extend the streak count
        if image.pixel(x, 0) == image.pixel(x-1,0):
            sequence += 1

            #
            # this catches the case where the longest color streak is in fact
            # the last one. this ensures the streak color will get saved.
            #

            if x != image.width():
                continue

        # color change, determine if this was the longest continuous color streak
        if sequence > longest:

            # save the last pixel as the longest sequence / most likely BG color
            longest = sequence
            speculative_bg = image.pixel(x-1, 0)

            # reset the sequence counter
            sequence = 1

    # return the color we speculate to be the background color
    return speculative_bg

def remap_key_event(event, new_key):
    """
    Change a given KeyPress QEvent to a different key.
    """
    return QtGui.QKeyEvent(
        QtCore.QEvent.KeyPress,
        new_key,
        event.modifiers(),
        event.text(),
        event.isAutoRepeat(),
        event.count()
    )

def singleshot(ms, function=None):
    """
    A Qt Singleshot timer that can be stopped.
    """
    timer = QtCore.QTimer()
    timer.setInterval(ms)
    timer.setSingleShot(True)
    timer.timeout.connect(function)
    return timer

#------------------------------------------------------------------------------
# Async Util
#------------------------------------------------------------------------------

def await_future(future):
    """
    Wait for a queue (future) message without blocking the main (Qt) thread.

    This is effectively a technique I use to get around completely blocking
    IDA's mainthread while waiting for a threaded result that may need to make
    use of the execute_sync operators.

    Waiting for a 'future' thread result to come through via this function
    lets other execute_sync actions to slip through (at least Read, Fast).
    """
    interval = 0.02    # the interval which we wait for a response

    # run until the message arrives through the future (a queue)
    while True:

        # block for a brief period to see if the future completes
        try:
            return future.get(timeout=interval)

        #
        # the future timed out, so perhaps it is blocked on a request
        # to the mainthread. flush the requests now and try again
        #

        except queue.Empty as e:
            pass

        logger.debug("Awaiting future...")

        #
        # if we are executing (well, blocking) as the main thread, we need
        # to flush the event loop so IDA does not hang
        #

        if QT_AVAILABLE and is_mainthread():
            flush_qt_events()

def await_lock(lock):
    """
    Wait for a lock without blocking the main (Qt) thread.

    See await_future() for more details.
    """

    elapsed  = 0       # total time elapsed waiting for the lock
    interval = 0.02    # the interval (in seconds) between acquire attempts
    timeout  = 60.0    # the total time allotted to acquiring the lock
    end_time = time.time() + timeout

    # wait until the lock is available
    while time.time() < end_time:

        #
        # attempt to acquire the given lock without blocking (via 'False').
        # if we successfully acquire the lock, then we can return (success)
        #

        if lock.acquire(False):
            logger.debug("Acquired lock!")
            return

        #
        # the lock is not available yet. we need to sleep so we don't choke
        # the cpu, and try to acquire the lock again next time through...
        #

        logger.debug("Awaiting lock...")
        time.sleep(interval)

        #
        # if we are executing (well, blocking) as the main thread, we need
        # to flush the event loop so IDA does not hang
        #

        if QT_AVAILABLE and is_mainthread():
            flush_qt_events()

    #
    # we spent 60 seconds trying to acquire the lock, but never got it...
    # to avoid hanging IDA indefinitely (or worse), we abort via signal
    #

    raise RuntimeError("Failed to acquire lock after %f seconds!" % timeout)

class QMainthread(QtCore.QObject):
    """
    A Qt object whose sole purpose is to execute code on the mainthread.
    """
    toMainthread = QtCore.pyqtSignal(object)
    toMainthreadFast = QtCore.pyqtSignal(object)

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

        # helpers used to ensure thread safety
        self._lock = threading.Lock()
        self._fast_refs = []
        self._result_queue = queue.Queue()

        # signals used to communicate with the Qt mainthread
        self.toMainthread.connect(self._execute_with_result)
        self.toMainthreadFast.connect(self._execute_fast)

    #--------------------------------------------------------------------------
    # Public
    #--------------------------------------------------------------------------

    def execute(self, function):
        """
        Execute a function on the mainthread and wait for its return value.

        This function is safe to call from any thread, at any time.
        """

        # if we are already on the mainthread, execute the callable inline
        if is_mainthread():
            return function()

        # execute the callable on the mainthread and wait for it to complete
        with self._lock:
            self.toMainthread.emit(function)
            result = self._result_queue.get()

        # return the result of executing on the mainthread
        return result

    def execute_fast(self, function):
        """
        Execute a function on the mainthread without waiting for completion.
        """

        #
        # append the given function to a reference list.
        #
        # I do this because I am not confident python / qt will guarantee the
        # lifetime of the callable (function) as we cross threads and the
        # callee scope/callstack dissolves away from beneath us
        #
        # this callable will be deleted from the ref list in _excute_fast()
        #

        self._fast_refs.append(function)

        # signal to the mainthread that a new function is ready to execute
        self.toMainthreadFast.emit(function)

    #--------------------------------------------------------------------------
    # Internal
    #--------------------------------------------------------------------------

    def _execute_with_result(self, function):
        try:
            self._result_queue.put(function())
        except Exception as e:
            logger.exception("QMainthread Exception")
            self._result_queue.put(None)

    def _execute_fast(self, function):
        function()
        self._fast_refs.remove(function)

qt_mainthread = QMainthread()
