#!/usr/pkg/bin/python3.13
"""
Syncthing-GTK - Configuration

Configuration object implementation; Uses JSON.
Config file is by default in ~/.config/syncthing-gtk/config.json
or other ~/.config equivalent
"""


import json
import logging
import os
import sys
from datetime import datetime

import dateutil.parser

from syncthing_gtk.tools import IS_WINDOWS, get_config_dir, is_portable

log = logging.getLogger("Configuration")

LONG_AGO = datetime.fromtimestamp(1)


class _Configuration(object):
    """
    Configuration object implementation.
    Use like dict to save / access values
    """

    # Dict with keys that are required in configuration file
    # and default values for those keys.
    # Format: key : (type, default)
    REQUIRED_KEYS = {
        "autostart_daemon": (int, 2),  # 0 - wait for daemon, 1 - autostart, 2 - ask
        "autokill_daemon": (int, 2),  # 0 - never kill, 1 - always kill, 2 - ask
        "daemon_priority": (int, 0),  # uses nice values
        "max_cpus": (int, 0),  # 0 for all cpus
        "syncthing_binary": (str, "/usr/pkg/bin/syncthing"),
        "syncthing_arguments": (str, ""),
        "minimize_on_start": (bool, False),
        "folder_as_path": (bool, True),
        "use_old_header": (bool, False),
        "animate_icon": (bool, True),
        "notification_for_update": (bool, True),
        "notification_for_folder": (bool, False),
        "notification_for_error": (bool, True),
        "st_autoupdate": (bool, False),
        "last_updatecheck": (datetime, LONG_AGO),
        "window_position": (tuple, None),
        "infobox_style": (str, 'font_weight="bold" font_size="large"'),
        "icon_theme": (str, "syncthing"),
        "force_dark_theme": (bool, False),  # Windows-only
        "language": (str, ""),  # Windows-only
        "file_browser": (str, "explore"),  # Windows-only
    }

    # Overrides some default values on Windows
    WINDOWS_OVERRIDE = {
        "syncthing_binary": (str, "C:\\Program Files\\Syncthing\\syncthing.exe"),
        "autokill_daemon": (int, 1),
        "use_old_header": (bool, False),
        "st_autoupdate": (bool, True),
    }

    def __init__(self):
        try:
            self.load()
        except Exception as e:
            log.warning("Failed to load configuration; Creating new one.")
            log.warning("Reason: %s", (e,))
            self.create()

        # Convert objects serialized as string back to object
        self.convert_values()
        # Check if everything is in place, add default value
        # where value is missing
        if self.check_values():
            # check_values returns True if any default value is added
            log.info("Saving configuration...")
            self.save()

    def load(self):
        # Check & create directory
        if not os.path.exists(self.get_config_dir()):
            try:
                os.makedirs(self.get_config_dir())
            except Exception as e:
                log.error("Cannot create configuration directory")
                log.exception(e)
                sys.exit(1)
        # Load json
        with open(self.get_config_file(), "r") as conf:
            self.values = json.loads(conf.read())

    def get_default_value(self, key):
        if IS_WINDOWS:
            return self.WINDOWS_OVERRIDE.get(key, self.REQUIRED_KEYS[key])[-1]
        return self.REQUIRED_KEYS[key][-1]

    def get_config_dir(self):
        return os.path.join(get_config_dir(), "syncthing-gtk")

    def get_config_file(self):
        return os.path.join(self.get_config_dir(), "config.json")

    def create(self):
        """Creates new, empty configuration"""
        self.values = {}
        self.check_values()
        self.save()

    def check_values(self):
        """
        Check if all required values are in place and fill by default
        whatever is missing.

        Returns True if anything gets changed.
        """
        needs_to_save = False
        for key in Configuration.REQUIRED_KEYS:
            tp, default = Configuration.REQUIRED_KEYS[key]
            if not self.check_type(key, tp):
                log.verbose("Configuration key %s is missing. Using default", key)
                if IS_WINDOWS and key in Configuration.WINDOWS_OVERRIDE:
                    tp, default = Configuration.WINDOWS_OVERRIDE[key]
                self.values[key] = default
                needs_to_save = True
        return needs_to_save

    def convert_values(self):
        """
        Converts all objects serialized as string back to object
        """
        for key in Configuration.REQUIRED_KEYS:
            if key in self.values:
                tp, trash = Configuration.REQUIRED_KEYS[key]
                try:
                    if tp == datetime and isinstance(self.values[key], str):
                        # Parse datetime
                        self.values[key] = dateutil.parser.parse(self.values[key])
                    elif tp == tuple and isinstance(self.values[key], list):
                        # Convert list to tuple
                        self.values[key] = tuple(self.values[key])
                    elif tp == bool and isinstance(self.values[key], int):
                        # Convert bools
                        self.values[key] = bool(self.values[key])
                except Exception as e:
                    log.warning("Failed to parse configuration value '%s'. Using default.", key)
                    log.warning(e)
                    # Value will be re-created by check_values method
                    del self.values[key]

    def check_type(self, key, tp):
        """
        Returns True if value is set and type match.
        Auto-converts objects serialized as string back to objects
        """
        if key not in self.values:
            return False
        # Handle special cases
        if isinstance(self.values[key], str) and tp == str:
            return True
        if tp in (tuple,) and self.values[key] is None:
            return True
        # Return value
        return isinstance(self.values[key], tp)

    def save(self):
        """Saves configuration file"""
        with open(self.get_config_file(), "w") as conf:
            conf.write(json.dumps(self.values, sort_keys=True, indent=4, separators=(",", ": "), default=serializer))

    def __iter__(self):
        for k in self.values:
            yield k

    def get(self, key):
        return self.values[key]

    def set(self, key, value):
        self.values[key] = value
        self.save()

    __getitem__ = get
    __setitem__ = set

    def __delitem__(self, key):
        del self.values[key]

    def __contains__(self, key):
        """Returns true if there is such value"""
        return key in self.values


def serializer(obj):
    """Handles serialization where json can't do it by itself"""
    if hasattr(obj, "isoformat"):
        # datetime object
        return obj.isoformat()
    raise TypeError("Can't serialize object of type %s" % (type(obj),))


def Configuration(*a, **b):
    if IS_WINDOWS and not is_portable():
        from syncthing_gtk.windows import WinConfiguration

        return WinConfiguration(*a, **b)
    return _Configuration(*a, **b)


Configuration.REQUIRED_KEYS = _Configuration.REQUIRED_KEYS
Configuration.WINDOWS_OVERRIDE = _Configuration.WINDOWS_OVERRIDE


def migrate_fs_watch(stgtk_config, st_config):
    """
    Migrates filesystem watch config from ST-GTK configuration
    to Syncthing configuration and posts it to daemon.

    Returns True if anything was changed.

    Called automatically if old fs watch setting is found in ST-GTK config.
    """
    # TODO: This can be removed later
    if "use_inotify" not in stgtk_config:
        return False
    changed = False
    folder_by_id = {x["id"]: x for x in st_config["folders"]}
    for rid in stgtk_config["use_inotify"]:
        if rid in folder_by_id:
            folder_by_id[rid]["fsWatcherDelayS"] = 10
            folder_by_id[rid]["fsWatcherEnabled"] = True
            changed = True
    del stgtk_config["use_inotify"]
    return changed
