# SPDX-License-Identifier: Apache-2.0
# -----------------------------------------------------------------------------
# Copyright 2019-2020 Arm Limited
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy
# of the License at:
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# -----------------------------------------------------------------------------
"""
This module contains code for loading image metadata from a file path on disk.

The directory path is structured:

    TestSetName/TestFormat/FileName

... and the file name is structured:

    colorProfile-colorFormat-name[-flags].extension
"""

from collections.abc import Iterable
import os
import re
import subprocess as sp

from PIL import Image as PILImage

import testlib.misc as misc


CONVERT_BINARY = ["convert"]


g_ConvertVersion = None


def get_convert_version():
    """
    Get the major/minor version of ImageMagick on the system.
    """
    global g_ConvertVersion

    if g_ConvertVersion is None:
        command = list(CONVERT_BINARY)
        command += ["--version"]
        result = sp.run(command, stdout=sp.PIPE, stderr=sp.PIPE,
                        check=True, encoding="utf-8")

        # Version is top row
        version = result.stdout.splitlines()[0]
        # ... third token
        version = re.split(" ", version)[2]
        # ... major/minor/patch/subpatch
        version = re.split("\\.|-", version)

        numericVersion = float(version[0])
        numericVersion += float(version[1]) / 10.0

        g_ConvertVersion = numericVersion

    return g_ConvertVersion


class ImageException(Exception):
    """
    Exception thrown for bad image specification.
    """


class TestImage():
    """
    Objects of this type contain metadata for a single test image on disk.

    Attributes:
        filePath: The path of the file on disk.
        outFilePath: The path of the output file on disk.
        testSet: The name of the test set.
        testFormat: The test format group.
        testFile: The test file name.
        colorProfile: The image compression color profile.
        colorFormat: The image color format.
        name: The image human name.
        is3D: True if the image is 3D, else False.
        isMask: True if the image is a non-correlated mask texture, else False.
        isAlphaScaled: True if the image wants alpha scaling, else False.
        TEST_EXTS: Expected test image extensions.
        PROFILES: Tuple of valid color profile values.
        FORMATS: Tuple of valid color format values.
        FLAGS: Map of valid flags (key) and their meaning (value).
    """
    TEST_EXTS = (".jpg", ".png", ".tga", ".dds", ".hdr")

    PROFILES = ("ldr", "ldrs", "hdr")

    FORMATS = ("l", "la", "xy", "rgb", "rgba")

    FLAGS = {
        # Flags for image compression control
        "3": "3D image",
        "m": "Mask image",
        "a": "Alpha scaled image"
    }

    def __init__(self, filePath):
        """
        Create a new image definition, based on a structured file path.

        Args:
            filePath (str): The path of the image on disk.

        Raises:
            ImageException: The image couldn't be found or is unstructured.
        """
        self.filePath = os.path.abspath(filePath)
        if not os.path.exists(self.filePath):
            raise ImageException("Image doesn't exist (%s)" % filePath)

        # Decode the path
        scriptDir = os.path.dirname(__file__)
        rootInDir = os.path.join(scriptDir, "..", "Images")
        partialPath = os.path.relpath(self.filePath, rootInDir)
        parts = misc.path_splitall(partialPath)
        if len(parts) != 3:
            raise ImageException("Image path not path triplet (%s)" % parts)
        self.testSet = parts[0]
        self.testFormat = parts[1]
        self.testFile = parts[2]

        # Decode the file name
        self.decode_file_name(self.testFile)

        # Output file path (store base without extension)
        rootOutDir = os.path.join(scriptDir, "..", "..", "TestOutput")
        outFilePath = os.path.join(rootOutDir, partialPath)
        outFilePath = os.path.abspath(outFilePath)
        outFilePath = os.path.splitext(outFilePath)[0]
        self.outFilePath = outFilePath

    def decode_file_name(self, fileName):
        """
        Utility function to decode metadata from an encoded file name.

        Args:
            fileName (str): The file name to tokenize.

        Raises:
            ImageException: The image file path is badly structured.
        """
        # Strip off the extension
        rootName = os.path.splitext(fileName)[0]

        parts = rootName.split("-")

        # Decode the mandatory fields
        if len(parts) >= 3:
            self.colorProfile = parts[0]
            if self.colorProfile not in self.PROFILES:
                raise ImageException("Unknown color profile (%s)" % parts[0])

            self.colorFormat = parts[1]
            if self.colorFormat not in self.FORMATS:
                raise ImageException("Unknown color format (%s)" % parts[1])

            # Consistency check between directory and file names
            reencode = "%s-%s" % (self.colorProfile, self.colorFormat)
            compare = self.testFormat.lower()
            if reencode != compare:
                dat = (self.testFormat, reencode)
                raise ImageException("Mismatched test and image (%s:%s)" % dat)

            self.name = parts[2]

        # Set default values for the optional fields
        self.is3D = False
        self.isMask = False
        self.isAlphaScaled = False

        # Decode the flags field if present
        if len(parts) >= 4:
            flags = parts[3]
            seenFlags = set()
            for flag in flags:
                if flag in seenFlags:
                    raise ImageException("Duplicate flag (%s)" % flag)
                if flag not in self.FLAGS:
                    raise ImageException("Unknown flag (%s)" % flag)
                seenFlags.add(flag)

            self.is3D = "3" in seenFlags
            self.isMask = "m" in seenFlags
            self.isAlphaScaled = "a" in seenFlags

    def get_size(self):
        """
        Get the dimensions of this test image, if format is known.

        Known cases today where the format is not known:

        * 3D .dds files.
        * Any .ktx, .hdr, .exr, or .astc file.

        Returns:
            tuple(int, int): The dimensions of a 2D image, or ``None`` if PIL
            could not open the file.
        """
        try:
            img = PILImage.open(self.filePath)
        except IOError:
            # HDR files
            return None
        except NotImplementedError:
            # DDS files
            return None

        return (img.size[0], img.size[1])


class Image():
    """
    Wrapper around an image on the file system.
    """

    # TODO: We don't support KTX yet, as ImageMagick doesn't.
    SUPPORTED_LDR = ["bmp", "jpg", "png", "tga"]
    SUPPORTED_HDR = ["exr", "hdr"]

    @classmethod
    def is_format_supported(cls, fileFormat, profile=None):
        """
        Test if a given file format is supported by the library.

        Args:
            fileFormat (str): The file extension (excluding the ".").
            profile (str or None): The profile (ldr or hdr) of the image.

        Returns:
            bool: `True` if the image is supported, `False` otherwise.
        """
        assert profile in [None, "ldr", "hdr"]

        if profile == "ldr":
            return fileFormat in cls.SUPPORTED_LDR

        if profile == "hdr":
            return fileFormat in cls.SUPPORTED_HDR

        return fileFormat in cls.SUPPORTED_LDR or \
            fileFormat in cls.SUPPORTED_HDR

    def __init__(self, filePath):
        """
        Construct a new Image.

        Args:
            filePath (str): The path to the image on disk.
        """
        convert = get_convert_version()

        # ImageMagick 7 started to use .tga file origin information. By default
        # TGA files store data from bottom up, and define the origin as bottom
        # left. We want our color samples to always use a top left origin, even
        # if the data is stored in alternative layout.
        self.invertYCoords = (convert >= 7.0) and filePath.endswith(".tga")

        self.filePath = filePath
        self.proxyPath = None

    def get_colors(self, coords):
        """
        Get the image colors at the given coordinate.

        Args:
            coords (tuple or list): A single coordinate, or a list of
                coordinates to sample.

        Returns:
            tuple: A single sample color (if `coords` was a coordinate).
            list: A list of sample colors (if `coords` was a list).

            Colors are returned as float values between 0.0 and 1.0 for LDR,
            and float values which may exceed 1.0 for HDR.
        """
        colors = []

        # We accept both a list of positions and a single position;
        # canonicalize here so the main processing only handles lists
        isList = len(coords) != 0 and isinstance(coords[0], Iterable)

        if not isList:
            coords = [coords]

        for (x, y) in coords:
            command = list(CONVERT_BINARY)
            command += [self.filePath]

            # Invert coordinates if the format needs it
            if self.invertYCoords:
                command += ["-flip"]

            command += [
                "-format", "%%[pixel:p{%u,%u}]" % (x, y),
                "info:"
            ]

            if os.name == 'nt':
                command.insert(0, "magick")

            result = sp.run(command, stdout=sp.PIPE, stderr=sp.PIPE,
                            check=True, universal_newlines=True)

            rawcolor = result.stdout.strip()

            # Decode ImageMagick's annoying named color outputs. Note that this
            # only handles "known" cases triggered by our test images, we don't
            # support the entire ImageMagick named color table.
            if rawcolor == "black":
                colors.append([0.0, 0.0, 0.0, 1.0])
            elif rawcolor == "white":
                colors.append([1.0, 1.0, 1.0, 1.0])
            elif rawcolor == "red":
                colors.append([1.0, 0.0, 0.0, 1.0])
            elif rawcolor == "blue":
                colors.append([0.0, 0.0, 1.0, 1.0])

            # Decode ImageMagick's format tuples
            elif rawcolor.startswith("srgba"):
                rawcolor = rawcolor[6:]
                rawcolor = rawcolor[:-1]
                channels = rawcolor.split(",")
                for i, channel in enumerate(channels):
                    if (i < 3) and channel.endswith("%"):
                        channels[i] = float(channel[:-1]) / 100.0
                    elif (i < 3) and not channel.endswith("%"):
                        channels[i] = float(channel) / 255.0
                    else:
                        channels[i] = float(channel)
                colors.append(channels)
            elif rawcolor.startswith("srgb"):
                rawcolor = rawcolor[5:]
                rawcolor = rawcolor[:-1]
                channels = rawcolor.split(",")
                for i, channel in enumerate(channels):
                    if (i < 3) and channel.endswith("%"):
                        channels[i] = float(channel[:-1]) / 100.0
                    if (i < 3) and not channel.endswith("%"):
                        channels[i] = float(channel) / 255.0
                channels.append(1.0)
                colors.append(channels)
            elif rawcolor.startswith("rgba"):
                rawcolor = rawcolor[5:]
                rawcolor = rawcolor[:-1]
                channels = rawcolor.split(",")
                for i, channel in enumerate(channels):
                    if (i < 3) and channel.endswith("%"):
                        channels[i] = float(channel[:-1]) / 100.0
                    elif (i < 3) and not channel.endswith("%"):
                        channels[i] = float(channel) / 255.0
                    else:
                        channels[i] = float(channel)
                colors.append(channels)
            elif rawcolor.startswith("rgb"):
                rawcolor = rawcolor[4:]
                rawcolor = rawcolor[:-1]
                channels = rawcolor.split(",")
                for i, channel in enumerate(channels):
                    if (i < 3) and channel.endswith("%"):
                        channels[i] = float(channel[:-1]) / 100.0
                    if (i < 3) and not channel.endswith("%"):
                        channels[i] = float(channel) / 255.0
                channels.append(1.0)
                colors.append(channels)
            else:
                print(x, y)
                print(rawcolor)
                assert False

        # ImageMagick decodes DDS files as BGRA not RGBA; manually correct
        if self.filePath.endswith("dds"):
            for color in colors:
                tmp = color[0]
                color[0] = color[2]
                color[2] = tmp

        # ImageMagick decodes EXR files with premult alpha; manually correct
        if self.filePath.endswith("exr"):
            for color in colors:
                color[0] /= color[3]
                color[1] /= color[3]
                color[2] /= color[3]

        # Undo list canonicalization if we were given a single scalar coord
        if not isList:
            return colors[0]

        return colors
