# -*- coding: utf-8 -*-
# noinspection PyPep8Naming
"""
***************************************************************************
    qps/resources.py

    A module to manage Qt/PyQt resources
    ---------------------
    Beginning            : 2019-02-18
    Copyright            : (C) 2020 by Benjamin Jakimow
    Email                : benjamin.jakimow@geo.hu-berlin.de
***************************************************************************
    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 software. If not, see <https://www.gnu.org/licenses/>.
***************************************************************************
"""

import os
import re
import sys
from pathlib import Path
from typing import Any, Generator, List, Optional, Union

from qgis.PyQt.QtCore import QAbstractTableModel, QDirIterator, QFile, QModelIndex, QSortFilterProxyModel, Qt, \
    QTextStream, QT_VERSION_STR
from qgis.PyQt.QtGui import QContextMenuEvent, QIcon, QPixmap

try:
    from PyQt6.QtSvgWidgets import QGraphicsSvgItem  # noqa: QGS103
except ImportError:
    from qgis.PyQt.QtSvg import QGraphicsSvgItem
from qgis.PyQt.QtWidgets import QAction, QApplication, QGraphicsPixmapItem, QGraphicsScene, QGraphicsView, QLabel, \
    QLineEdit, QMenu, QTableView, QTextBrowser, QToolButton, QWidget
from qgis.PyQt.QtXml import QDomDocument, QDomElement
from .utils import file_search, findUpwardPath

if QT_VERSION_STR[0] == '5':
    from qgis.PyQt.QtCore import QRegExp as QRegularExpression  # noqa: QGS406
else:
    from qgis.PyQt.QtCore import QRegularExpression

REGEX_FILEXTENSION_IMAGE = re.compile(r'\.([^.]+)$')
REGEX_QGIS_IMAGES_QRC = re.compile(r'.*QGIS[^\/]*[\/]images[\/]images\.qrc$')


def getDOMAttributes(elem):
    if not (isinstance(elem, QDomElement)):
        raise AssertionError
    values = dict()
    attributes = elem.attributes()
    for a in range(attributes.count()):
        attr = attributes.item(a)
        values[str(attr.nodeName())] = attr.nodeValue()
    return values


def compileResourceFiles(dirRoot: Union[str, Path],
                         targetDir: Optional[Union[str, Path]] = None,
                         suffix: str = '_rc.py',
                         skip_qgis_images: bool = True,
                         compressLevel=19,
                         compressThreshold=100
                         ):
    """
    Searches for *.ui files and compiles the *.qrc files they use.
    :param compressLevel:
    :param compressThreshold:
    :param suffix:
    :param skip_qgis_images:
    :type skip_qgis_images: bool, if True (default), qrc paths to the qgis images.qrc will be skipped
    :param dirRoot: str, root directory, in which to search for *.qrc files or a list of *.ui file paths.
    :param targetDir: str, output directory to write the compiled *.py files to.
           Defaults to the *.qrc's directory
    """
    # find ui files
    dirRoot = Path(dirRoot)
    if not (dirRoot.is_dir()):
        raise AssertionError('"dirRoot" is not a directory: {}'.format(dirRoot))
    dirRoot = dirRoot.resolve()

    ui_files = list(file_search(dirRoot, '*.ui', recursive=True))

    qrc_files = []
    qrc_files_skipped = []
    doc = QDomDocument()

    for ui_file in ui_files:
        ui_dir = Path(ui_file).parent
        doc.setContent(QFile(ui_file))
        includeNodes = doc.elementsByTagName('include')
        for i in range(includeNodes.count()):
            attr = getDOMAttributes(includeNodes.item(i).toElement())
            if 'location' in attr.keys():
                location = attr['location']
                qrc_path = (ui_dir / Path(location)).resolve()
                if not qrc_path.exists():
                    if REGEX_QGIS_IMAGES_QRC.match(qrc_path.as_posix()) and skip_qgis_images:
                        continue
                    info = ['Broken *.qrc location in {}'.format(ui_file),
                            ' `location="{}"`'.format(location)]
                    print('\n'.join(info), file=sys.stderr)
                    continue

                elif not qrc_path.as_posix().startswith(dirRoot.as_posix()):
                    # skip resource files out of the root directory
                    if qrc_path not in qrc_files_skipped:
                        qrc_files_skipped.append(qrc_path)

                    continue
                elif qrc_path not in qrc_files:
                    qrc_files.append(qrc_path)

    for file in file_search(dirRoot, '*.qrc', recursive=True):
        file = Path(file)
        if file not in qrc_files:
            qrc_files.append(file)

    if len(qrc_files) == 0:
        print('Did not find any *.qrc files in {}'.format(dirRoot), file=sys.stderr)
        return

    print('Compile {} *.qrc files:'.format(len(qrc_files)))
    targetDirOutputNames = []
    for qrcFile in qrc_files:
        if not (isinstance(qrcFile, Path)):
            raise AssertionError
        # in case of similar base names, use different output names
        # e.g. make
        #  src/images.qrc
        #  src/sub/images.qrc
        # to
        #  targetDir/images_rc.py
        #  targetDir/images2_rc.py
        bn = os.path.splitext(qrcFile.name)[0]
        s = suffix
        i = 1
        outName = '{}{}'.format(bn, s)
        while outName in targetDirOutputNames:
            i += 1
            s = '{}{}'.format(i, suffix)
            outName = '{}{}'.format(bn, s)

        compileResourceFile(qrcFile,
                            targetDir=targetDir,
                            suffix=s,
                            compressLevel=compressLevel,
                            compressThreshold=compressThreshold)
        targetDirOutputNames.append(outName)

    if len(qrc_files_skipped) > 0:
        print('Skipped *.qrc files (out of root directory):')
        for qrcFile in qrc_files_skipped:
            print(qrcFile.as_posix())


def compileResourceFile(pathQrc, targetDir=None, suffix: str = '_rc.py', compressLevel=7, compressThreshold=100):
    """
    Compiles a *.qrc file
    :param pathQrc:
    :return:
    """
    import PyQt5.pyrcc_main  # noqa: QGS104 # cannot be imported from qgis.PyQt.pyrcc_main
    if not isinstance(pathQrc, Path):
        pathQrc = Path(pathQrc)

    if not (isinstance(pathQrc, Path)):
        raise AssertionError
    if not (pathQrc.name.endswith('.qrc')):
        raise AssertionError
    print('Compile {}...'.format(pathQrc))
    if targetDir is None:
        targetDir = pathQrc.parent
    elif not isinstance(targetDir, Path):
        targetDir = Path(targetDir)

    if not (isinstance(targetDir, Path)):
        raise AssertionError
    targetDir = targetDir.resolve()

    cwd = Path(pathQrc).parent

    pathPy = targetDir / (os.path.splitext(pathQrc.name)[0] + suffix)

    last_cwd = os.getcwd()
    os.chdir(cwd)

    last_level = PyQt5.pyrcc_main.compressLevel
    last_threshold = PyQt5.pyrcc_main.compressThreshold

    # increase compression level and move to *.qrc's directory
    PyQt5.pyrcc_main.compressLevel = compressLevel
    PyQt5.pyrcc_main.compressThreshold = compressThreshold

    if not (PyQt5.pyrcc_main.processResourceFile([pathQrc.name], pathPy.as_posix(), False)):
        raise AssertionError

    with open(pathPy, 'r') as f:
        content = f.read()

    content = re.sub(r'from PyQt[56] import QtCore', r'from qgis.PyQt import QtCore', content)
    content = re.sub(r'\ndef (q.*:)', r'\n\ndef \g<1>', content)
    content = re.sub(r'\nqInitResources\(\)', '\n\nqInitResources()', content)

    with open(pathPy, 'w') as f:
        f.write(content)

    # restore previous settings
    PyQt5.pyrcc_main.compressLevel = last_level
    PyQt5.pyrcc_main.compressThreshold = last_threshold

    os.chdir(last_cwd)


def compileQGISResourceFiles(qgis_repo: Union[str, Path, None], target: str = None):
    """
    Searches for *.qrc files in the QGIS repository and compiles them to <target>

    :param qgis_repo: str, path to local QGIS repository.
    :param target: str, path to directory that contains the compiled QGIS resources. By default, it will be
            `<REPOSITORY_ROOT>/qgisresources`.
    """

    if qgis_repo is None:
        for k in ['QGIS_REPO', 'QGIS_REPOSITORY']:
            if k in os.environ.keys():
                qgis_repo = Path(os.environ[k])
                break
    else:
        qgis_repo = Path(qgis_repo)
    if qgis_repo is None:
        print('QGIS_REPO location undefined', file=sys.stderr)
        return

    if not isinstance(qgis_repo, Path):
        qgis_repo = Path(qgis_repo)
    if not (isinstance(qgis_repo, Path)):
        raise AssertionError
    if not (qgis_repo.is_dir()):
        raise AssertionError
    if not ((qgis_repo / 'images' / 'images.qrc').is_file()):
        raise AssertionError('{} is not the QGIS repository root'.format(
            qgis_repo.as_posix()))

    if target is None:
        DIR_REPO = findUpwardPath(__file__, '.git')
        target = DIR_REPO / 'qgisresources'

    if not isinstance(target, Path):
        target = Path(target)

    os.makedirs(target, exist_ok=True)
    compileResourceFiles(qgis_repo / 'src', targetDir=target, skip_qgis_images=False)
    compileResourceFiles(qgis_repo / 'images', targetDir=target, skip_qgis_images=False)


def initQtResources(roots: Union[None, str, Path, list] = None):
    """
    Searches recursively for `*_rc.py` files and loads them into the QApplications resources system
    :param roots: list of root folders to search within
    :type roots:
    :return:
    :rtype:
    """
    if roots is None:
        roots = []
    elif not isinstance(roots, list):
        roots = [roots]

    if len(roots) == 0:
        p = Path(__file__).parent
        roots.append(p.parent)

    rc_files = []
    for rootDir in roots:
        for r, dirs, files in os.walk(rootDir):
            root = Path(r)
            for f in files:
                if f.endswith('_rc.py'):
                    path = root / f
                    if path not in rc_files:
                        rc_files.append(path)

    for path in rc_files:
        print('load {}'.format(path))
        initResourceFile(path)


def initResourceFile(path: Union[str, Path]):
    """
    Loads a '*_rc.py' file into the QApplication's resource system
    """
    if not isinstance(path, Path):
        path = Path(path)
    f = path.name
    name = f[:-3]
    add_path = path.parent.as_posix() not in sys.path
    if add_path:
        sys.path.append(path.parent.as_posix())
    try:
        rcModule = __import__(name)
        # spec = importlib.util.spec_from_file_location(name, path)
        # rcModule = importlib.util.module_from_spec(spec)
        # spec.loader.exec_module(rcModule)
        # rcModule.qInitResources()
        rcModule.qInitResources()

    except Exception as ex:
        print(ex, file=sys.stderr)

    if add_path:
        sys.path.remove(path.parent.as_posix())


def findQGISResourceFiles() -> List[Path]:
    """
    Tries to find a folder 'qgisresources'.
    See snippets/create_qgisresourcefilearchive.py to create the 'qgisresources' folder.
    """
    results = []
    root = None
    if 'QPS_QGIS_RESOURCES' in os.environ.keys():
        root = os.environ.keys()
    else:
        d = Path(__file__)

        while d != Path('.'):
            if (d / 'qgisresources').is_dir():
                root = (d / 'qgisresources')
                break
            else:
                d = d.parent
            if len(d.parts) == 1:
                break

    if isinstance(root, Path):
        for root, dirs, files in os.walk(root):
            for rc_file_name in [f for f in files if f.endswith('_rc.py')]:
                path = Path(root) / rc_file_name
                if path.is_file():
                    results.append(path)
    return results


def scanResources(path=':') -> Generator[str, None, None]:
    """Recursively returns file paths in directory"""
    D = QDirIterator(path)
    while D.hasNext():
        entry = D.next()
        if D.fileInfo().isDir():
            yield from scanResources(path=entry)
        elif D.fileInfo().isFile():
            yield D.filePath()


def printResources():
    print('Available resources:')
    res = sorted(list(scanResources()))
    for r in res:
        print(r)


class ResourceTableModel(QAbstractTableModel):

    def __init__(self, *args, **kwds):

        super().__init__(*args, **kwds)

        self.cnUri = 'Path'
        self.cnIcon = 'Resource'
        self.RESOURCES = []
        self.reloadResources()

    def reloadResources(self):

        self.beginResetModel()
        self.RESOURCES.clear()
        self.RESOURCES.extend(list(scanResources()))
        self.endResetModel()

    def columnCount(self, parent: QModelIndex) -> int:
        return 2

    def rowCount(self, parent: QModelIndex = ...) -> int:
        return len(self.RESOURCES)

    def columnNames(self) -> List[str]:
        return [self.cnUri, self.cnIcon]

    def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...) -> Any:
        if role == Qt.ItemDataRole.DisplayRole:
            if orientation == Qt.Orientation.Horizontal:
                return self.columnNames()[section]

        if role == Qt.ItemDataRole.TextAlignmentRole and orientation == Qt.Orientation.Vertical:
            return Qt.AlignmentFlag.AlignRight

        return super().headerData(section, orientation, role)

    def data(self, index: QModelIndex, role: int = ...) -> Any:
        if not index.isValid():
            return None

        uri = self.RESOURCES[index.row()]
        cn = self.columnNames()[index.column()]

        if role == Qt.ItemDataRole.DisplayRole:
            if cn == self.cnUri:
                return uri
            else:
                return os.path.basename(uri)
        if role == Qt.ItemDataRole.DecorationRole:
            if cn == self.cnIcon:
                return QIcon(uri)

        if role == Qt.ItemDataRole.ToolTipRole:
            if cn == self.cnUri:
                return uri

        if role == Qt.ItemDataRole.UserRole:
            return uri

        return None


class ResourceTableView(QTableView):

    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)

    def contextMenuEvent(self, event: QContextMenuEvent) -> None:
        idx = self.indexAt(event.pos())
        if isinstance(idx, QModelIndex) and idx.isValid():
            uri = idx.data(Qt.ItemDataRole.UserRole)
            m = QMenu()
            a = m.addAction('Copy Name')
            a.triggered.connect(lambda *args, n=os.path.basename(uri): QApplication.clipboard().setText(n))
            a = m.addAction('Copy Path')
            a.triggered.connect(lambda *args, n=uri: QApplication.clipboard().setText(n))
            a = m.addAction('Copy Icon')
            a.triggered.connect(lambda *args, n=uri: QApplication.clipboard().setPixmap(QPixmap(n)))

            m.exec(event.globalPos())

        pass


class ResourceBrowser(QWidget):

    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)

        from .utils import loadUi
        pathUi = Path(__file__).parent / 'ui' / 'qpsresourcebrowser.ui'
        loadUi(pathUi, self)
        self.setWindowTitle('QPS Resource Browser')
        self.actionReload: QAction
        self.optionUseRegex: QAction
        self.tbFilter: QLineEdit
        self.tableView: ResourceTableView
        self.btnUseRegex: QToolButton
        self.btnCaseSensitive: QToolButton
        self.btnReload: QToolButton
        self.preview: QLabel

        self.graphicsView: QGraphicsView
        self.graphicsScene = QGraphicsScene()
        self.graphicsView.setScene(self.graphicsScene)

        self.textBrowser: QTextBrowser

        self.resourceModel: ResourceTableModel = ResourceTableModel()
        self.resourceProxyModel = QSortFilterProxyModel()
        self.resourceProxyModel.setFilterKeyColumn(0)
        self.resourceProxyModel.setFilterRole(Qt.ItemDataRole.UserRole)
        self.resourceProxyModel.setSourceModel(self.resourceModel)

        self.tableView.setSortingEnabled(True)
        self.tableView.setModel(self.resourceProxyModel)
        self.tableView.selectionModel().selectionChanged.connect(self.onSelectionChanged)

        self.btnReload.setDefaultAction(self.actionReload)
        self.btnUseRegex.setDefaultAction(self.optionUseRegex)
        self.btnCaseSensitive.setDefaultAction(self.optionCaseSensitive)
        self.actionReload.triggered.connect(self.resourceModel.reloadResources)

        self.optionCaseSensitive.toggled.connect(self.updateFilter)
        self.optionUseRegex.toggled.connect(self.updateFilter)
        self.tbFilter.textChanged.connect(self.updateFilter)

    def updateFilter(self):

        txt = self.tbFilter.text()

        expr = QRegularExpression(txt)

        if self.optionUseRegex.isChecked():
            expr.setPatternSyntax(QRegularExpression.RegExp)
        else:
            expr.setPatternSyntax(QRegularExpression.Wildcard)

        if self.optionCaseSensitive.isChecked():
            expr.setCaseSensitivity(Qt.CaseSensitivity.CaseSensitive)
        else:
            expr.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)

        if expr.isValid():
            self.resourceProxyModel.setFilterRegExp(expr)
            self.info.setText('')
        else:
            self.resourceProxyModel.setFilterRegExp(None)
            self.info.setText(expr.errorString())

    def onSelectionChanged(self, selected, deselected):

        selectedIdx = selected.indexes()
        if len(selectedIdx) == 0:
            self.updatePreview(None)
        else:
            idx1 = selectedIdx[0]
            if not (isinstance(idx1, QModelIndex)):
                raise AssertionError

            uri = idx1.data(Qt.ItemDataRole.UserRole)
            self.updatePreview(uri)

    def updatePreview(self, uri: str):

        hasImage = False
        hasText = False
        self.textBrowser.clear()
        self.graphicsScene.clear()

        if isinstance(uri, str) and '.' in uri:
            ext = os.path.splitext(uri)[1]

            item = None
            if ext == '.svg':
                item = QGraphicsSvgItem(uri)
            else:
                pm = QPixmap(uri)
                if not pm.isNull():
                    item = QGraphicsPixmapItem(pm)

            if item:
                hasImage = True
                self.graphicsScene.addItem(item)
                self.graphicsView.fitInView(item, Qt.AspectRatioMode.KeepAspectRatio)

            if re.search(r'\.(svg|html|xml|txt)$', uri, re.I) is not None:
                file = QFile(uri)
                if file.open(QFile.OpenModeFlag.ReadOnly | QFile.OpenModeFlag.Text):
                    stream = QTextStream(file)
                    stream.setAutoDetectUnicode(True)
                    txt = stream.readAll()
                    self.textBrowser.setPlainText(txt)
                    hasText = True
                    file.close()

        self.tabWidget.setTabEnabled(self.tabWidget.indexOf(self.pageImage), hasImage)
        self.tabWidget.setTabEnabled(self.tabWidget.indexOf(self.pageText), hasText)

    def useFilterRegex(self) -> bool:
        return self.optionUseRegex.isChecked()


def showResources() -> ResourceBrowser:
    """
    A simple way to list available Qt resources
    :return:
    :rtype:
    """
    needQApp = not isinstance(QApplication.instance(), QApplication)
    if needQApp:
        _ = QApplication([])
    browser = ResourceBrowser()
    browser.show()
    if needQApp:
        QApplication.instance().exec()
    return browser
