import asyncio
import qrcode
from qrcode.exceptions import DataOverflowError

import math
import urllib

from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QRect
from PyQt6.QtGui import QImage, QColor
from PyQt6.QtQuick import QQuickImageProvider
try:
    from PyQt6.QtMultimedia import QVideoSink
except ImportError:
    # stub QVideoSink when not found, as it's not essential on android
    # and requires many dependencies when unit testing.
    # Note: missing QtMultimedia will lead to errors when using QR scanner on desktop
    from PyQt6.QtCore import QObject as QVideoSink

from electrum.logging import get_logger
from electrum.qrreader import get_qr_reader
from electrum.i18n import _
from electrum.util import profiler, get_asyncio_loop
from electrum.gui.common_qt.util import draw_qr


class QEQRParser(QObject):
    _logger = get_logger(__name__)

    busyChanged = pyqtSignal()
    dataChanged = pyqtSignal()
    sizeChanged = pyqtSignal()
    videoSinkChanged = pyqtSignal()

    def __init__(self, text=None, parent=None):
        super().__init__(parent)

        self._busy = False
        self._data = None
        self._video_sink = None

        self._text = text
        self.qrreader = get_qr_reader()
        if not self.qrreader:
            raise Exception(_("The platform QR detection library is not available."))

    @pyqtProperty(QVideoSink, notify=videoSinkChanged)
    def videoSink(self):
        return self._video_sink

    @videoSink.setter
    def videoSink(self, sink: QVideoSink):
        if self._video_sink != sink:
            self._video_sink = sink
            self._video_sink.videoFrameChanged.connect(self.onVideoFrame)

    def onVideoFrame(self, videoframe):
        if self._busy or self._data:
            return

        self._busy = True
        self.busyChanged.emit()

        if not videoframe.isValid():
            self._logger.debug('invalid frame')
            return

        async def co_parse_qr(frame):
            image = frame.toImage()
            self._parseQR(image)

        asyncio.run_coroutine_threadsafe(co_parse_qr(videoframe), get_asyncio_loop())

    def _parseQR(self, image: QImage):
        self._size = min(image.width(), image.height())
        self.sizeChanged.emit()
        img_crop_rect = self._get_crop(image, self._size)
        frame_cropped = image.copy(img_crop_rect)

        # Convert to Y800 / GREY FourCC (single 8-bit channel)
        frame_y800 = frame_cropped.convertToFormat(QImage.Format.Format_Grayscale8)
        self.frame_id = 0
        # Read the QR codes from the frame
        self.qrreader_res = self.qrreader.read_qr_code(
            frame_y800.constBits().__int__(),
            frame_y800.sizeInBytes(),
            frame_y800.bytesPerLine(),
            frame_y800.width(),
            frame_y800.height(),
            self.frame_id
            )

        if len(self.qrreader_res) > 0:
            result = self.qrreader_res[0]
            self._data = result
            self.dataChanged.emit()

        self._busy = False
        self.busyChanged.emit()

    def _get_crop(self, image: QImage, scan_size: int) -> QRect:
        """Returns a QRect that is scan_size x scan_size in the middle of the resolution"""
        scan_pos_x = (image.width() - scan_size) // 2
        scan_pos_y = (image.height() - scan_size) // 2
        return QRect(scan_pos_x, scan_pos_y, scan_size, scan_size)

    @pyqtProperty(bool, notify=busyChanged)
    def busy(self):
        return self._busy

    @pyqtProperty(int, notify=sizeChanged)
    def size(self):
        return self._size

    @pyqtProperty(str, notify=dataChanged)
    def data(self):
        if not self._data:
            return ''
        return self._data.data

    @pyqtSlot()
    def reset(self):
        self._data = None
        self.dataChanged.emit()


class QEQRImageProvider(QQuickImageProvider):
    MAX_QR_PIXELSIZE = 400
    ERROR_CORRECT_LEVEL = qrcode.constants.ERROR_CORRECT_M
    # ^ note: this is higher than for desktop. but on desktop we don't put a logo in the middle.
    QR_BORDER = 2

    def __init__(self, max_size, parent=None):
        super().__init__(QQuickImageProvider.ImageType.Image)
        self._max_size = max_size
        self.qimg = None

    _logger = get_logger(__name__)

    @profiler
    def requestImage(self, qstr, size):
        # Qt does a urldecode before passing the string here
        # but BIP21 (and likely other uri based specs) requires urlencoding,
        # so we re-encode percent-quoted if a known 'scheme' is found in the string
        # (unknown schemes might be found when a colon is in a serialized TX, which
        # leads to mangling of the tx, so we check for supported schemes.)
        uri = urllib.parse.urlparse(qstr)
        if uri.scheme and uri.scheme in ['bitcoin', 'lightning']:
            # urlencode request parameters
            query = urllib.parse.parse_qs(uri.query)
            query = urllib.parse.urlencode(query, doseq=True, quote_via=urllib.parse.quote)
            uri = uri._replace(query=query)
            qstr = urllib.parse.urlunparse(uri)

        qr = qrcode.main.QRCode(border=self.QR_BORDER, error_correction=self.ERROR_CORRECT_LEVEL)

        # calculate best box_size
        pixelsize = min(self._max_size, self.MAX_QR_PIXELSIZE)
        try:
            qr.add_data(qstr)
            modules = len(qr.get_matrix())
            qr.box_size = math.floor(pixelsize/modules)
            qr.make(fit=True)
            self.qimg = QImage(modules * qr.box_size, modules * qr.box_size, QImage.Format.Format_RGB32)
            draw_qr(qr=qr, paint_device=self.qimg)
        except (ValueError, qrcode.exceptions.DataOverflowError):
            # fake it
            modules = 17 + qr.border * 2
            box_size = math.floor(pixelsize/modules)
            self.qimg = QImage(box_size * modules, box_size * modules, QImage.Format.Format_RGB32)
            self.qimg.fill(QColor('gray'))
        return self.qimg, self.qimg.size()


# helper for placing icon exactly where it should go on the QR code
# pyqt5 is unwilling to accept slots on QEQRImageProvider, so we need to define
# a separate class (sigh)
class QEQRImageProviderHelper(QObject):
    def __init__(self, max_size, parent=None):
        super().__init__(parent)
        self._max_size = max_size

    @pyqtSlot(str, result='QVariantMap')
    def getDimensions(self, qstr):
        qr = qrcode.QRCode(
            border=QEQRImageProvider.QR_BORDER,
            error_correction=QEQRImageProvider.ERROR_CORRECT_LEVEL,
        )

        # calculate best box_size
        pixelsize = min(self._max_size, QEQRImageProvider.MAX_QR_PIXELSIZE)
        try:
            qr.add_data(qstr)
            modules = len(qr.get_matrix())
            valid = True
        except (ValueError, qrcode.exceptions.DataOverflowError):
            # fake it
            modules = 17 + qr.border * 2
            valid = False

        qr.box_size = math.floor(pixelsize/modules)
        # calculate icon width in modules
        icon_modules = int(modules / 5)
        icon_modules += (icon_modules+1) % 2  # force odd

        return {
            'qr_pixelsize': modules * qr.box_size,
            'icon_pixelsize': icon_modules * qr.box_size,
            'valid': valid
        }
