# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsPalLabeling: base suite setup

From build dir, run: ctest -R PyQgsPalLabelingBase -V

See <qgis-src-dir>/tests/testdata/labeling/README.rst for description.

.. note:: 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 2 of the License, or
(at your option) any later version.
"""

from future import standard_library
import collections
standard_library.install_aliases()

__author__ = 'Larry Shaffer'
__date__ = '07/09/2013'
__copyright__ = 'Copyright 2013, The QGIS Project'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'

import qgis  # NOQA

import os
import sys
import datetime
import glob
import shutil

from qgis.PyQt.QtCore import QSize, qDebug, Qt
from qgis.PyQt.QtGui import QFont, QColor

from qgis.core import (
    QgsCoordinateReferenceSystem,
    QgsDataSourceUri,
    QgsProject,
    QgsMapSettings,
    QgsPalLabeling,
    QgsPalLayerSettings,
    QgsProviderRegistry,
    QgsVectorLayer,
    QgsMultiRenderChecker,
    QgsUnitTypes
)

from qgis.testing import start_app, unittest
from qgis.testing.mocked import get_iface

from utilities import (
    unitTestDataPath,
    getTempfilePath,
    renderMapToImage,
    loadTestFonts,
    getTestFont,
    openInBrowserTab
)


start_app(sys.platform != 'darwin')  # No cleanup on mac os x, it crashes the pallabelingcanvas test on exit
FONTSLOADED = loadTestFonts()

PALREPORT = 'PAL_REPORT' in os.environ
PALREPORTS = {}


# noinspection PyPep8Naming,PyShadowingNames
class TestQgsPalLabeling(unittest.TestCase):

    _TestDataDir = unitTestDataPath()
    _PalDataDir = os.path.join(_TestDataDir, 'labeling')
    _PalFeaturesDb = os.path.join(_PalDataDir, 'pal_features_v3.sqlite')
    _TestFont = getTestFont()  # Roman at 12 pt
    """:type: QFont"""
    _MapRegistry = None
    """:type: QgsProject"""
    _MapSettings = None
    """:type: QgsMapSettings"""
    _Canvas = None
    """:type: QgsMapCanvas"""
    _Pal = None
    """:type: QgsPalLabeling"""
    _BaseSetup = False

    @classmethod
    def setUpClass(cls):
        """Run before all tests"""

        # qgis iface
        cls._Iface = get_iface()
        cls._Canvas = cls._Iface.mapCanvas()

        # verify that spatialite provider is available
        msg = '\nSpatialite provider not found, SKIPPING TEST SUITE'
        # noinspection PyArgumentList
        res = 'spatialite' in QgsProviderRegistry.instance().providerList()
        assert res, msg

        cls._TestFunction = ''
        cls._TestGroup = ''
        cls._TestGroupPrefix = ''
        cls._TestGroupAbbr = ''
        cls._TestGroupCanvasAbbr = ''
        cls._TestImage = ''
        cls._TestMapSettings = None
        cls._Mismatch = 0
        cls._Mismatches = dict()
        cls._ColorTol = 0
        cls._ColorTols = dict()

        # initialize class MapRegistry, Canvas, MapRenderer, Map and PAL
        # noinspection PyArgumentList
        cls._MapRegistry = QgsProject.instance()

        cls._MapSettings = cls.getBaseMapSettings()
        osize = cls._MapSettings.outputSize()
        cls._Canvas.resize(QSize(osize.width(), osize.height()))  # necessary?
        # set color to match render test comparisons background
        cls._Canvas.setCanvasColor(cls._MapSettings.backgroundColor())

        cls.setDefaultEngineSettings()
        msg = ('\nCould not initialize PAL labeling engine, '
               'SKIPPING TEST SUITE')
        assert cls._Pal, msg

        cls._BaseSetup = True

    @classmethod
    def tearDownClass(cls):
        """Run after all tests"""

    def setUp(self):
        """Run before each test."""
        TestQgsPalLabeling.setDefaultEngineSettings()
        self.lyr = self.defaultLayerSettings()

    @classmethod
    def setDefaultEngineSettings(cls):
        """Restore default settings for pal labeling"""
        cls._Pal = QgsPalLabeling()

    @classmethod
    def removeAllLayers(cls):
        cls._MapSettings.setLayers([])
        cls._MapRegistry.removeAllMapLayers()

    @classmethod
    def removeMapLayer(cls, layer):
        if layer is None:
            return
        lyr_id = layer.id()
        cls._MapRegistry.removeMapLayer(lyr_id)
        ms_layers = cls._MapSettings.layers()
        if layer in ms_layers:
            ms_layers.remove(layer)
            cls._MapSettings.setLayers(ms_layers)

    @classmethod
    def getTestFont(cls):
        return QFont(cls._TestFont)

    @classmethod
    def loadFeatureLayer(cls, table, chk=False):
        if chk and cls._MapRegistry.mapLayersByName(table):
            return
        uri = QgsDataSourceUri()
        uri.setDatabase(cls._PalFeaturesDb)
        uri.setDataSource('', table, 'geometry')
        vlayer = QgsVectorLayer(uri.uri(), table, 'spatialite')
        # .qml should contain only style for symbology
        vlayer.loadNamedStyle(os.path.join(cls._PalDataDir,
                                           '{0}.qml'.format(table)))
        # qDebug('render_lyr = {0}'.format(repr(vlayer)))
        cls._MapRegistry.addMapLayer(vlayer)
        # place new layer on top of render stack
        render_lyrs = [vlayer]
        render_lyrs.extend(cls._MapSettings.layers())
        # qDebug('render_lyrs = {0}'.format(repr(render_lyrs)))
        cls._MapSettings.setLayers(render_lyrs)

        # zoom to aoi
        cls._MapSettings.setExtent(cls.aoiExtent())
        cls._Canvas.zoomToFullExtent()
        return vlayer

    @classmethod
    def aoiExtent(cls):
        """Area of interest extent, which matches output aspect ratio"""
        uri = QgsDataSourceUri()
        uri.setDatabase(cls._PalFeaturesDb)
        uri.setDataSource('', 'aoi', 'geometry')
        aoilayer = QgsVectorLayer(uri.uri(), 'aoi', 'spatialite')
        return aoilayer.extent()

    @classmethod
    def getBaseMapSettings(cls):
        """
        :rtype: QgsMapSettings
        """
        ms = QgsMapSettings()
        crs = QgsCoordinateReferenceSystem()
        """:type: QgsCoordinateReferenceSystem"""
        # default for labeling test data: WGS 84 / UTM zone 13N
        crs.createFromSrid(32613)
        ms.setBackgroundColor(QColor(152, 219, 249))
        ms.setOutputSize(QSize(420, 280))
        ms.setOutputDpi(72)
        ms.setFlag(QgsMapSettings.Antialiasing, True)
        ms.setFlag(QgsMapSettings.UseAdvancedEffects, False)
        ms.setFlag(QgsMapSettings.ForceVectorOutput, False)  # no caching?
        ms.setDestinationCrs(crs)
        ms.setExtent(cls.aoiExtent())
        return ms

    def cloneMapSettings(self, oms):
        """
        :param QgsMapSettings oms: Other QgsMapSettings
        :rtype: QgsMapSettings
        """
        ms = QgsMapSettings()
        ms.setBackgroundColor(oms.backgroundColor())
        ms.setOutputSize(oms.outputSize())
        ms.setOutputDpi(oms.outputDpi())
        ms.setFlags(oms.flags())
        ms.setDestinationCrs(oms.destinationCrs())
        ms.setExtent(oms.extent())
        ms.setOutputImageFormat(oms.outputImageFormat())

        ms.setLayers(oms.layers())
        return ms

    def configTest(self, prefix, abbr):
        """Call in setUp() function of test subclass"""
        self._TestGroupPrefix = prefix
        self._TestGroupAbbr = abbr

        # insert test's Class.function marker into debug output stream
        # this helps visually track down the start of a test's debug output
        testid = self.id().split('.')
        self._TestGroup = testid[1]
        self._TestFunction = testid[2]
        testheader = '\n#####_____ {0}.{1} _____#####\n'.\
            format(self._TestGroup, self._TestFunction)
        qDebug(testheader)

        # define the shorthand name of the test (to minimize file name length)
        self._Test = '{0}_{1}'.format(self._TestGroupAbbr,
                                      self._TestFunction.replace('test_', ''))

    def defaultLayerSettings(self):
        lyr = QgsPalLayerSettings()
        lyr.enabled = True
        lyr.fieldName = 'text'  # default in test data sources
        font = self.getTestFont()
        font.setPointSize(32)
        format = lyr.format()
        format.setFont(font)
        format.setNamedStyle('Roman')
        format.setSize(32)
        format.setSizeUnit(QgsUnitTypes.RenderPoints)
        format.buffer().setJoinStyle(Qt.BevelJoin)
        lyr.setFormat(format)
        return lyr

    @staticmethod
    def settingsDict(lyr):
        """Return a dict of layer-level labeling settings

        .. note:: QgsPalLayerSettings is not a QObject, so we can not collect
        current object properties, and the public properties of the C++ obj
        can't be listed with __dict__ or vars(). So, we sniff them out relative
        to their naming convention (camelCase), as reported by dir().
        """
        res = {}
        for attr in dir(lyr):
            if attr[0].islower() and not attr.startswith("__"):
                value = getattr(lyr, attr)
                if not isinstance(value, collections.Callable):
                    res[attr] = value
        return res

    def controlImagePath(self, grpprefix=''):
        if not grpprefix:
            grpprefix = self._TestGroupPrefix
        return os.path.join(self._TestDataDir, 'control_images',
                            'expected_' + grpprefix,
                            self._Test, self._Test + '.png')

    def saveControlImage(self, tmpimg=''):
        # don't save control images for RenderVsOtherOutput (Vs) tests, since
        # those control images belong to a different test result
        if ('PAL_CONTROL_IMAGE' not in os.environ or
                'Vs' in self._TestGroup):
            return
        imgpath = self.controlImagePath()
        testdir = os.path.dirname(imgpath)
        if not os.path.exists(testdir):
            os.makedirs(testdir)
        imgbasepath = \
            os.path.join(testdir,
                         os.path.splitext(os.path.basename(imgpath))[0])
        # remove any existing control images
        for f in glob.glob(imgbasepath + '.*'):
            if os.path.exists(f):
                os.remove(f)
        qDebug('Control image for {0}.{1}'.format(self._TestGroup,
                                                  self._TestFunction))

        if not tmpimg:
            # TODO: this can be deprecated, when per-base-test-class rendering
            #       in checkTest() is verified OK for all classes
            qDebug('Rendering control to: {0}'.format(imgpath))
            ms = self._MapSettings  # class settings
            """:type: QgsMapSettings"""
            settings_type = 'Class'
            if self._TestMapSettings is not None:
                ms = self._TestMapSettings  # per test settings
                settings_type = 'Test'
            qDebug('MapSettings type: {0}'.format(settings_type))

            img = renderMapToImage(ms, parallel=False)
            """:type: QImage"""
            tmpimg = getTempfilePath('png')
            if not img.save(tmpimg, 'png'):
                os.unlink(tmpimg)
                raise OSError('Control not created for: {0}'.format(imgpath))

        if tmpimg and os.path.exists(tmpimg):
            qDebug('Copying control to: {0}'.format(imgpath))
            shutil.copyfile(tmpimg, imgpath)
        else:
            raise OSError('Control not copied to: {0}'.format(imgpath))

    def renderCheck(self, mismatch=0, colortol=0, imgpath='', grpprefix=''):
        """Check rendered map canvas or existing image against control image

        :mismatch: number of pixels different from control, and still valid
        :colortol: maximum difference for each color component including alpha
        :imgpath: existing image; if present, skips rendering canvas
        :grpprefix: compare test image/rendering against different test group
        """
        if not grpprefix:
            grpprefix = self._TestGroupPrefix
        chk = QgsMultiRenderChecker()

        chk.setControlPathPrefix('expected_' + grpprefix)

        chk.setControlName(self._Test)

        if imgpath:
            chk.setRenderedImage(imgpath)

        ms = self._MapSettings  # class settings
        if self._TestMapSettings is not None:
            ms = self._TestMapSettings  # per test settings
        chk.setMapSettings(ms)

        chk.setColorTolerance(colortol)
        # noinspection PyUnusedLocal
        res = chk.runTest(self._Test, mismatch)
        if PALREPORT and not res:  # don't report ok checks
            testname = self._TestGroup + ' . ' + self._Test
            PALREPORTS[testname] = chk.report()
        msg = '\nRender check failed for "{0}"'.format(self._Test)
        return res, msg

    def checkTest(self, **kwargs):
        """Intended to be overridden in subclasses"""
        pass


class TestPALConfig(TestQgsPalLabeling):

    @classmethod
    def setUpClass(cls):
        TestQgsPalLabeling.setUpClass()
        cls.layer = TestQgsPalLabeling.loadFeatureLayer('point')

    @classmethod
    def tearDownClass(cls):
        cls.removeMapLayer(cls.layer)

    def setUp(self):
        """Run before each test."""
        self.configTest('pal_base', 'base')

    def tearDown(self):
        """Run after each test."""
        pass

    def test_default_pal_disabled(self):
        # Verify PAL labeling is disabled for layer by default
        palset = self.layer.customProperty('labeling', '')
        msg = '\nExpected: Empty string\nGot: {0}'.format(palset)
        self.assertEqual(palset, '', msg)

    def test_settings_enable_pal(self):
        # Verify default PAL settings enable PAL labeling for layer
        lyr = QgsPalLayerSettings()
        lyr.writeToLayer(self.layer)
        palset = self.layer.customProperty('labeling', '')
        msg = '\nExpected: Empty string\nGot: {0}'.format(palset)
        self.assertEqual(palset, 'pal', msg)

    def test_layer_pal_activated(self):
        # Verify, via engine, that PAL labeling can be activated for layer
        lyr = self.defaultLayerSettings()
        lyr.writeToLayer(self.layer)
        msg = '\nLayer labeling not activated, as reported by labelingEngine'
        self.assertTrue(QgsPalLabeling.staticWillUseLayer(self.layer), msg)

    def test_write_read_settings(self):
        # Verify written PAL settings are same when read from layer
        # load and write default test settings
        lyr1 = self.defaultLayerSettings()
        lyr1dict = self.settingsDict(lyr1)
        # print lyr1dict
        lyr1.writeToLayer(self.layer)

        # read settings
        lyr2 = QgsPalLayerSettings()
        lyr2.readFromLayer(self.layer)
        lyr2dict = self.settingsDict(lyr1)
        # print lyr2dict

        msg = '\nLayer settings read not same as settings written'
        self.assertDictEqual(lyr1dict, lyr2dict, msg)

    def test_default_partials_labels_enabled(self):
        # Verify ShowingPartialsLabels is enabled for PAL by default
        pal = QgsPalLabeling()
        self.assertTrue(pal.isShowingPartialsLabels())

    def test_partials_labels_activate(self):
        pal = QgsPalLabeling()
        # Enable partials labels
        pal.setShowingPartialsLabels(True)
        self.assertTrue(pal.isShowingPartialsLabels())

    def test_partials_labels_deactivate(self):
        pal = QgsPalLabeling()
        # Disable partials labels
        pal.setShowingPartialsLabels(False)
        self.assertFalse(pal.isShowingPartialsLabels())


# noinspection PyPep8Naming,PyShadowingNames
def runSuite(module, tests):
    """This allows for a list of test names to be selectively run.
    Also, ensures unittest verbose output comes at end, after debug output"""
    loader = unittest.defaultTestLoader
    if 'PAL_SUITE' in os.environ:
        if tests:
            suite = loader.loadTestsFromNames(tests, module)
        else:
            raise Exception(
                "\n\n####__ 'PAL_SUITE' set, but no tests specified __####\n")
    else:
        suite = loader.loadTestsFromModule(module)
    verb = 2 if 'PAL_VERBOSE' in os.environ else 0

    res = unittest.TextTestRunner(verbosity=verb).run(suite)

    if PALREPORTS:
        teststamp = 'PAL Test Report: ' + \
                    datetime.datetime.now().strftime('%Y-%m-%d %X')
        report = '<html><head><title>{0}</title></head><body>'.format(teststamp)
        report += '\n<h2>Failed Tests: {0}</h2>'.format(len(PALREPORTS))
        for k, v in list(PALREPORTS.items()):
            report += '\n<h3>{0}</h3>\n{1}'.format(k, v)
        report += '</body></html>'

        tmp_name = getTempfilePath('html')
        with open(tmp_name, 'wt') as report_file:
            report_file.write(report)
        openInBrowserTab('file://' + tmp_name)

    return res


if __name__ == '__main__':
    # NOTE: unless PAL_SUITE env var is set all test class methods will be run
    # ex: 'TestGroup(Point|Line|Curved|Polygon|Feature).test_method'
    suite = [
        'TestPALConfig.test_write_read_settings'
    ]
    res = runSuite(sys.modules[__name__], suite)
    sys.exit(not res.wasSuccessful())
