# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.

import re  # Regular expressions for parsing escape characters in the settings.
import json
from typing import Optional

from UM.Settings.ContainerFormatError import ContainerFormatError
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Logger import Logger
from UM.i18n import i18nCatalog
from cura.ReaderWriters.ProfileReader import ProfileReader, NoProfileException

catalog = i18nCatalog("cura")


class GCodeProfileReader(ProfileReader):
    """A class that reads profile data from g-code files.

    It reads the profile data from g-code files and stores it in a new profile.
    This class currently does not process the rest of the g-code in any way.
    """

    version = 3
    """The file format version of the serialized g-code.

    It can only read settings with the same version as the version it was
    written with. If the file format is changed in a way that breaks reverse
    compatibility, increment this version number!
    """

    escape_characters = {
        re.escape("\\\\"): "\\",  # The escape character.
        re.escape("\\n"): "\n",   # Newlines. They break off the comment.
        re.escape("\\r"): "\r"    # Carriage return. Windows users may need this for visualisation in their editors.
    }
    """Dictionary that defines how characters are escaped when embedded in

    g-code.

    Note that the keys of this dictionary are regex strings. The values are
    not.
    """

    def read(self, file_name):
        """Reads a g-code file, loading the profile from it.

        :param file_name: The name of the file to read the profile from.
        :return: The profile that was in the specified file, if any. If the
            specified file was no g-code or contained no parsable profile,
            None  is returned.
        """
        Logger.log("i", "Attempting to read a profile from the g-code")

        if file_name.split(".")[-1] != "gcode":
            return None

        prefix = ";SETTING_" + str(GCodeProfileReader.version) + " "
        prefix_length = len(prefix)

        # Loading all settings from the file.
        # They are all at the end, but Python has no reverse seek any more since Python3.
        # TODO: Consider moving settings to the start?
        serialized = ""  # Will be filled with the serialized profile.
        try:
            with open(file_name, "r", encoding = "utf-8") as f:
                for line in f:
                    if line.startswith(prefix):
                        # Remove the prefix and the newline from the line and add it to the rest.
                        serialized += line[prefix_length: -1]
        except IOError as e:
            Logger.log("e", "Unable to open file %s for reading: %s", file_name, str(e))
            return None

        serialized = unescapeGcodeComment(serialized)
        serialized = serialized.strip()

        if not serialized:
            Logger.log("w", "No custom profile to import from this g-code: %s", file_name)
            raise NoProfileException()

        # Serialized data can be invalid JSON
        try:
            json_data = json.loads(serialized)
        except Exception as e:
            Logger.log("e", "Could not parse serialized JSON data from g-code %s, error: %s", file_name, e)
            return None

        profiles = []
        global_profile = readQualityProfileFromString(json_data["global_quality"])

        # This is a fix for profiles created with 2.3.0 For some reason it added the "extruder" property to the
        # global profile.
        # The fix is simple and safe, as a global profile should never have the extruder entry.
        if global_profile.getMetaDataEntry("extruder", None) is not None:
            global_profile.setMetaDataEntry("extruder", None)
        profiles.append(global_profile)

        for profile_string in json_data.get("extruder_quality", []):
            profiles.append(readQualityProfileFromString(profile_string))
        return profiles


def unescapeGcodeComment(string: str) -> str:
    """Unescape a string which has been escaped for use in a gcode comment.

    :param string: The string to unescape.
    :return: The unescaped string.
    """

    # Un-escape the serialized profile.
    pattern = re.compile("|".join(GCodeProfileReader.escape_characters.keys()))

    # Perform the replacement with a regular expression.
    return pattern.sub(lambda m: GCodeProfileReader.escape_characters[re.escape(m.group(0))], string)


def readQualityProfileFromString(profile_string) -> Optional[InstanceContainer]:
    """Read in a profile from a serialized string.

    :param profile_string: The profile data in serialized form.
    :return: The resulting Profile object or None if it could not be read.
    """

    # Create an empty profile - the id and name will be changed by the ContainerRegistry
    profile = InstanceContainer("")
    try:
        profile.deserialize(profile_string)
    except ContainerFormatError as e:
        Logger.log("e", "Corrupt profile in this g-code file: %s", str(e))
        return None
    except Exception as e:  # Not a valid g-code file.
        Logger.log("e", "Unable to serialise the profile: %s", str(e))
        return None
    return profile
