"""Manipulate Wine registry files"""

import os
import re
from collections import OrderedDict
from datetime import datetime

from lutris.util import system
from lutris.util.log import logger
from lutris.util.wine.wine import WINE_DEFAULT_ARCH

(
    REG_NONE,
    REG_SZ,
    REG_EXPAND_SZ,
    REG_BINARY,
    REG_DWORD,
    REG_DWORD_BIG_ENDIAN,
    REG_LINK,
    REG_MULTI_SZ,
) = range(8)

DATA_TYPES = {
    '"': REG_SZ,
    'str:"': REG_SZ,
    'str(2):"': REG_EXPAND_SZ,
    'str(7):"': REG_MULTI_SZ,
    "hex": REG_BINARY,
    "dword": REG_DWORD,
}


class WindowsFileTime:
    """Utility class to deal with Windows FILETIME structures.

    See: https://msdn.microsoft.com/en-us/library/ms724284(v=vs.85).aspx
    """

    ticks_per_seconds = 10000000  # 1 tick every 100 nanoseconds
    epoch_delta = 11644473600  # 3600 * 24 * ((1970 - 1601) * 365 + 89)

    def __init__(self, timestamp=None):
        self.timestamp = timestamp

    def __repr__(self):
        return "<{}>: {}".format(self.__class__.__name__, self.timestamp)

    @classmethod
    def from_hex(cls, hexvalue):
        timestamp = int(hexvalue, 16)
        return WindowsFileTime(timestamp)

    def to_hex(self):
        return "{:x}".format(self.timestamp)

    @classmethod
    def from_unix_timestamp(cls, timestamp):
        timestamp = timestamp + cls.epoch_delta
        timestamp = int(timestamp * cls.ticks_per_seconds)
        return WindowsFileTime(timestamp)

    def to_unix_timestamp(self):
        if not self.timestamp:
            raise ValueError("No timestamp set")
        unix_ts = self.timestamp / self.ticks_per_seconds
        unix_ts = unix_ts - self.epoch_delta
        return unix_ts

    def to_date_time(self):
        return datetime.fromtimestamp(self.to_unix_timestamp())


class WineRegistry:
    version_header = "WINE REGISTRY Version "
    relative_to_header = ";; All keys relative to "

    def __init__(self, reg_filename=None):
        self.arch = WINE_DEFAULT_ARCH
        self.version = 2
        self.relative_to = "\\\\User\\\\S-1-5-21-0-0-0-1000"
        self.keys = OrderedDict()
        self.reg_filename = reg_filename
        if reg_filename:
            if not system.path_exists(reg_filename):
                logger.error("No registry file at %s", reg_filename)
            self.parse_reg_file(reg_filename)

    def __str__(self):
        return "Windows Registry @ %s" % self.reg_filename

    @property
    def prefix_path(self):
        """Return the Wine prefix path (where the .reg files are located)"""
        if self.reg_filename:
            return os.path.dirname(self.reg_filename)
        return None

    @staticmethod
    def get_raw_registry(reg_filename):
        """Return an array of the unprocessed contents of a registry file"""
        if not system.path_exists(reg_filename):
            return []
        with open(reg_filename, "r", encoding="utf-8") as reg_file:
            try:
                registry_content = reg_file.readlines()
            except Exception:  # pylint: disable=broad-except
                logger.exception("Failed to registry read %s", reg_filename)
                registry_content = []
        return registry_content

    def parse_reg_file(self, reg_filename):
        registry_lines = self.get_raw_registry(reg_filename)
        current_key = None
        add_next_to_value = False
        additional_values = []
        for line in registry_lines:
            line = line.rstrip("\n")

            if line.startswith("["):
                current_key = WineRegistryKey(key_def=line)
                self.keys[current_key.name] = current_key
            elif current_key:
                if add_next_to_value:
                    additional_values.append(line)
                elif not add_next_to_value:
                    if additional_values:
                        additional_values = "\n".join(additional_values)
                        current_key.add_to_last(additional_values)
                        additional_values = []
                    current_key.parse(line)
                add_next_to_value = line.endswith("\\")
            elif line.startswith(self.version_header):
                self.version = int(line[len(self.version_header) :])
            elif line.startswith(self.relative_to_header):
                self.relative_to = line[len(self.relative_to_header) :]
            elif line.startswith("#arch"):
                self.arch = line.split("=")[1]

    def render(self):
        content = "{}{}\n".format(self.version_header, self.version)
        content += "{}{}\n\n".format(self.relative_to_header, self.relative_to)
        content += "#arch={}\n".format(self.arch)
        for key in self.keys:
            content += "\n"
            content += self.keys[key].render()
        return content

    def save(self, path=None):
        """Write the registry to a file"""
        if not path:
            path = self.reg_filename
        if not path:
            raise OSError("No filename provided")
        prefix_path = os.path.dirname(path)
        if not os.path.isdir(prefix_path):
            raise OSError(
                "Invalid Wine prefix path %s, make sure to create the prefix before saving to a registry" % prefix_path
            )
        with open(path, "w", encoding="utf-8") as registry_file:
            registry_file.write(self.render())

    def query(self, path, subkey):
        key = self.keys.get(path)
        if key:
            return key.get_subkey(subkey)
        return

    def set_value(self, path, subkey, value):
        key = self.keys.get(path)
        if not key:
            key = WineRegistryKey(path=path)
            self.keys[key.name] = key
        key.set_subkey(subkey, value)

    def clear_key(self, path):
        """Removes all subkeys from a key"""
        key = self.keys.get(path)
        if not key:
            return
        key.subkeys.clear()

    def clear_subkeys(self, path, keys):
        """Remove some subkeys from a key"""
        key = self.keys.get(path)
        if not key:
            return
        for subkey in list(key.subkeys.keys()):
            if subkey not in keys:
                continue
            key.subkeys.pop(subkey)

    def get_unix_path(self, windows_path):
        windows_path = windows_path.replace("\\", "/")
        if not self.prefix_path:
            return
        drives_path = os.path.join(self.prefix_path, "dosdevices")
        if not system.path_exists(drives_path):
            return
        letter, relpath = windows_path.split(":", 1)
        relpath = relpath.strip("/")
        drive_link = os.path.join(drives_path, letter.lower() + ":")
        try:
            drive_path = os.readlink(drive_link)
        except FileNotFoundError:
            logger.error("Unable to read link for %s", drive_link)
            return

        if not os.path.isabs(drive_path):
            drive_path = os.path.join(drives_path, drive_path)
        return os.path.join(drive_path, relpath)


class WineRegistryKey:
    def __init__(self, key_def=None, path=None):
        self.subkeys = OrderedDict()
        self.metas = OrderedDict()

        if path:
            # Key is created by path, it's a new key
            timestamp = datetime.now().timestamp()
            self.name = path
            self.raw_name = "[{}]".format(path.replace("/", "\\\\"))
            self.raw_timestamp = " ".join(str(timestamp).split("."))

            windows_timestamp = WindowsFileTime.from_unix_timestamp(timestamp)
            self.metas["time"] = windows_timestamp.to_hex()
        else:
            # Existing key loaded from file
            self.raw_name, self.raw_timestamp = re.split(re.compile(r"(?<=[^\\]\]) "), key_def, maxsplit=1)
            self.name = self.raw_name.replace("\\\\", "/").strip("[]")

        # Parse timestamp either as int or float
        ts_parts = self.raw_timestamp.strip().split()
        if len(ts_parts) == 1:
            self.timestamp = int(ts_parts[0])
        else:
            self.timestamp = float("{}.{}".format(ts_parts[0], ts_parts[1]))

    def __str__(self):
        return "{0} {1}".format(self.raw_name, self.raw_timestamp)

    def parse(self, line):
        """Parse a registry line, populating meta and subkeys"""
        if len(line) < 4:
            # Line is too short, nothing to parse
            return

        if line.startswith("#"):
            self.add_meta(line)
        elif line.startswith('"'):
            try:
                key, value = re.split(re.compile(r"(?<![^\\]\\\")="), line, maxsplit=1)
            except ValueError as ex:
                logger.error("Unable to parse line %s", line)
                logger.exception(ex)
                return
            key = key[1:-1]
            self.subkeys[key] = value
        elif line.startswith("@"):
            key, value = line.split("=", 1)
            self.subkeys["default"] = value

    def add_to_last(self, line):
        try:
            last_subkey = next(reversed(self.subkeys))
        except StopIteration:
            logger.warning("Should this be happening?")
            return
        self.subkeys[last_subkey] += "\n{}".format(line)

    def render(self):
        """Return the content of the key in the wine .reg format"""
        content = self.raw_name + " " + self.raw_timestamp + "\n"
        for key, value in self.metas.items():
            if value is None:
                content += "#{}\n".format(key)
            else:
                content += "#{}={}\n".format(key, value)
        for key, value in self.subkeys.items():
            if key == "default":
                key = "@"
            else:
                key = '"{}"'.format(key)
            content += "{}={}\n".format(key, value)
        return content

    def render_value(self, value):
        if isinstance(value, int):
            return "dword:{:08x}".format(value)
        if isinstance(value, str):
            return '"{}"'.format(value)
        raise NotImplementedError("TODO")

    @staticmethod
    def decode_unicode(string):
        # There may be a r"\\" in front of r"\x", so replace the r"\\" to r"\x005c"
        # to avoid missing matches. Example: r"C:\\users\\x1234\\\x0041\x0042CD".
        # Note the difference between r"\\x1234", r"\\\x0041" and r"\x0042".
        # It should be r"C:\users\x1234\ABCD" after decoding.
        chunks = re.split(r"\\x", string.replace(r"\\", r"\x005c"))
        out = chunks.pop(0).encode().decode("unicode_escape")
        for chunk in chunks:
            # We have seen file with unicode characters escaped on 1 byte (\xfa),
            # 1.5 bytes (\x444) and 2 bytes (\x00ed). So we try 0 padding, 1 and 2
            # (python wants its escaped sequence to be exactly on 4 characters).
            # The exception let us know if it worked or not
            for i in [0, 1, 2]:
                try:
                    out += "\\u{}{}".format("0" * i, chunk).encode().decode("unicode_escape")
                    break
                except UnicodeDecodeError:
                    pass
        return out

    def add_meta(self, meta_line):
        if not meta_line.startswith("#"):
            raise ValueError("Key metas should start with '#'")
        meta_line = meta_line[1:]
        parts = meta_line.split("=")
        if len(parts) == 2:
            key = parts[0]
            value = parts[1]
        elif len(parts) == 1:
            key = parts[0]
            value = None
        else:
            raise ValueError("Invalid meta line '{}'".format(meta_line))
        self.metas[key] = value

    def get_meta(self, name):
        return self.metas.get(name)

    def set_subkey(self, name, value):
        self.subkeys[name] = self.render_value(value)

    def get_subkey(self, name):
        if name not in self.subkeys:
            return None
        value = self.subkeys[name]
        if value.startswith('"') and value.endswith('"'):
            return self.decode_unicode(value[1:-1])
        if value.startswith("dword:"):
            return int(value[6:], 16)
        raise ValueError("Handle %s" % value)
