# Copyright (c) 2023 elParaguayo
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import re
import subprocess
import tempfile
from pathlib import Path

import cairocffi
import pytest
from libqtile.bar import Bar
from libqtile.command.base import expose_command
from libqtile.config import Screen

from test.helpers import BareConfig

# Regex pattern for extracting difference value from compare output
# Matches a float including using scientific notation
COMPARE_RESULT = re.compile(r"\((-?[\d.]+(?:e-?\d+)?)\)")

RESOURCES = Path(__file__).parent / ".." / ".." / "resources" / "test_images"
TEST_FOLDER = Path(__file__).parent.resolve().as_posix()

# Required accuracy. Difference should be less than 0.5% (CI fonts render slightly differently)
TOLERANCE = 0.005


@pytest.fixture(scope="session")
def temp_images():
    # with tempfile.TemporaryDirectory() as temp:
    # yield temp
    tempdir = tempfile.mkdtemp()
    yield tempdir


@pytest.fixture(scope="function")
def camera(manager_nospawn, request, temp_images):
    """
    This fixture provides a means to take a picture of the current bar.

    The snapshot is then compared against a reference image to check that the
    content is still being rendered correctly.

    Reference images can be generated by passing the --generate option when
    calling pytest. The images will be saved in `test/widgets/decorations` and,
    if they are deemed correct (by manual verification), they should be moved to
    test/resources/test_images.
    """
    # Check if we're generating reference images
    generate = request.config.getoption("--generate")
    image_dir = request.config.getoption("--generate-dir")
    ci = request.config.getoption("--generate-ci")

    # Get the configuration
    config = getattr(request, "param", False)
    if not config:
        assert False, "No configuration provided."

    # Get name of the decoration scenario
    name = config["name"]

    # Generate output filepath
    make_images = generate or ci
    if make_images:
        if ci:
            output_dir = (
                (Path(__file__).parent / ".." / ".." / ".." / "decoration_images")
                .resolve()
                .as_posix()
            )
        else:
            output_dir = image_dir or TEST_FOLDER
    else:
        output_dir = temp_images

    output_file = f"{output_dir}/{name}.png"

    class ScreenshotBar(Bar):
        """
        Subclass of Qtile's bar with additional methods to dump the
        Drawer to a png file.

        Both X11 and Wayland backends are supported.
        """

        def x11_screenshot(self, name):
            """Dumps X11 Drawer to png file."""
            # Create a temporary image surface
            isurf = cairocffi.ImageSurface(
                cairocffi.FORMAT_ARGB32, self.drawer.width, self.drawer.height
            )

            # Create context for image surface
            with cairocffi.Context(isurf) as ctx:
                # Loop over widgets and copy widget's drawer to image surface
                # The context is translated to position contents correctly
                for w in self.widgets:
                    ctx.save()
                    ctx.set_operator(cairocffi.OPERATOR_SOURCE)
                    ctx.translate(w.offsetx, w.offsety)
                    ctx.rectangle(0, 0, w.width, w.height)
                    ctx.set_source_surface(w.drawer._xcb_surface)
                    ctx.fill()
                    ctx.restore()

                # Check if we need to fill the end of the bar
                # (this happens if the bar doesn't have a SPACER widget)
                last_widget = self.widgets[-1]
                end = last_widget.offsetx + last_widget.width
                if end < self.width:
                    ctx.save()
                    ctx.translate(end, 0)
                    ctx.set_source_surface(self.drawer._xcb_surface)
                    ctx.rectangle(0, 0, self.drawer.width - end, self.drawer.height)
                    ctx.fill()
                    ctx.restore()

            # Dump surface to PNG file
            isurf.write_to_png(name)

        def wayland_screenshot(self, name):
            """Dumps Wayland Drawer to png file."""
            # Much easier: Wayland has an ImageSurface which we can access directly
            self.drawer._win.surface.write_to_png(name)

        @expose_command
        def take_screenshot(self, name):
            """Exposed command to save bar contents to png file."""
            if self.qtile.core.name == "x11":
                self.x11_screenshot(name)
            else:
                self.wayland_screenshot(name)

    class DecorationConfig(BareConfig):
        """Simple configuration to add widgets to ScreenshotBar."""

        fake_screens = [Screen(top=ScreenshotBar(config["widgets"], 50), width=400, height=400)]

    def screenshot():
        """Convenience function to call screenshot."""
        manager_nospawn.c.bar["top"].take_screenshot(output_file)

    def assert_similar():
        """Check if generated images are similar to reference images."""
        # If we're generating images then we don't need to compare them
        if generate:
            return

        # Build filepath to reference image
        reference = RESOURCES / f"{name}.png"

        # Command line
        cmd = ["compare", "-metric", "MSE", output_file, reference.resolve().as_posix(), "null:"]
        proc = subprocess.run(cmd, capture_output=True)

        # Check if our regex pattern can find a match in the output text
        search = COMPARE_RESULT.search(proc.stderr.decode())
        if not search:
            assert False, proc.stderr.decode()

        # There's a match so let's convert to a float
        diff = float(search.group(1))

        # Verify that difference is within acceptable tolerance
        assert diff < TOLERANCE

    # Start the manager and map convenience functions
    manager_nospawn.start(DecorationConfig)
    manager_nospawn.take_screenshot = screenshot
    manager_nospawn.assert_similar = assert_similar

    yield manager_nospawn
