#!/usr/bin/env python3
#
# Copyright (c) 2024 Stormux
# Copyright (c) 2010-2012 The Orca Team
# Copyright (c) 2012 Igalia, S.L.
# Copyright (c) 2005-2010 Sun Microsystems Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA  02110-1301 USA.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu

"""TOML backend for Cthulhu settings"""

__id__        = "$Id$"
__version__   = "$Revision$"
__date__      = "$Date$"
__copyright__ = "Copyright (c) 2010-2011 Consorcio Fernando de los Rios."
__license__   = "LGPL"

import os

from tomlkit import parse, dumps, document

from cthulhu import settings, acss

class Backend:

    def __init__(self, prefsDir):
        """ Initialize the TOML Backend.
        """
        self.general = {}
        self.pronunciations = {}
        self.keybindings = {}
        self.profiles = {}
        self.settingsFile = os.path.join(prefsDir, "user-settings.toml")
        self.appPrefsDir = os.path.join(prefsDir, "app-settings")

        self._defaultProfiles = {'default': { 'profile':  settings.profile,
                                                          'pronunciations': {},
                                                          'keybindings': {}
                                            }
                                }

    def _stripNone(self, value):
        if isinstance(value, dict):
            cleaned = {}
            for key, item in value.items():
                if item is None:
                    continue
                cleanedItem = self._stripNone(item)
                if cleanedItem is None:
                    continue
                cleaned[key] = cleanedItem
            return cleaned
        if isinstance(value, list):
            cleanedList = []
            for item in value:
                if item is None:
                    continue
                cleanedList.append(self._stripNone(item))
            return cleanedList
        return value

    def _readDocument(self, fileName):
        if os.path.exists(fileName):
            with open(fileName, 'r', encoding='utf-8') as settingsFile:
                return parse(settingsFile.read())
        return document()

    def _writeDocument(self, fileName, prefsDoc):
        with open(fileName, 'w', encoding='utf-8') as settingsFile:
            settingsFile.write(dumps(prefsDoc))

    def _updateTable(self, targetTable, newValues, preserveMissingKeys=None):
        if not isinstance(newValues, dict):
            return

        preserveMissingKeys = set(preserveMissingKeys or [])

        for key in list(targetTable.keys()):
            if key not in newValues:
                if key in preserveMissingKeys:
                    continue
                del targetTable[key]
                continue
            newValue = newValues[key]
            existingValue = targetTable.get(key)
            if isinstance(newValue, dict) and isinstance(existingValue, dict):
                self._updateTable(existingValue, newValue)
                continue
            if existingValue != newValue:
                targetTable[key] = newValue

        for key, newValue in newValues.items():
            if key not in targetTable:
                targetTable[key] = newValue

    def _legacyProfileValue(self, profileTable, sectionName, legacyKey):
        section = profileTable.get(sectionName)
        if not isinstance(section, dict):
            return None
        return section.get(legacyKey)

    def _normalizeLegacyValue(self, currentKey, legacyValue):
        if currentKey == 'keyboardLayout' and isinstance(legacyValue, str):
            layout = legacyValue.strip().lower()
            if layout == 'desktop':
                return settings.GENERAL_KEYBOARD_LAYOUT_DESKTOP
            if layout == 'laptop':
                return settings.GENERAL_KEYBOARD_LAYOUT_LAPTOP
        return legacyValue

    def _normalizeLegacyProfile(self, profileName, profileTable):
        if not isinstance(profileTable, dict):
            return profileTable

        profileSettings = {}
        legacySections = {
            'metadata',
            'plugins',
            'ai-assistant',
            'ocr',
        }

        for key, value in profileTable.items():
            if key in legacySections:
                continue
            if key == 'keybindings':
                if isinstance(value, dict):
                    keybindings = dict(value)
                    keybindings.pop('keyboard-layout', None)
                    keybindings.pop('desktop-modifier-keys', None)
                    keybindings.pop('laptop-modifier-keys', None)
                    if keybindings:
                        profileSettings[key] = keybindings
                    continue
            profileSettings[key] = value

        displayName = self._legacyProfileValue(profileTable, 'metadata', 'display-name')
        internalName = self._legacyProfileValue(profileTable, 'metadata', 'internal-name')
        if 'profile' not in profileSettings and (displayName or internalName):
            profileSettings['profile'] = [
                displayName or str(profileName).title(),
                internalName or profileName,
            ]

        legacyKeyMap = {
            ('keybindings', 'keyboard-layout'): 'keyboardLayout',
            ('keybindings', 'desktop-modifier-keys'): 'cthulhuModifierKeys',
            ('keybindings', 'laptop-modifier-keys'): 'cthulhuModifierKeys',
            ('plugins', 'active-plugins'): 'activePlugins',
            ('plugins', 'plugin-sources'): 'pluginSources',
            ('ai-assistant', 'enabled'): 'aiAssistantEnabled',
            ('ai-assistant', 'provider'): 'aiProvider',
            ('ai-assistant', 'api-key-file'): 'aiApiKeyFile',
            ('ai-assistant', 'ollama-model'): 'aiOllamaModel',
            ('ai-assistant', 'ollama-endpoint'): 'aiOllamaEndpoint',
            ('ai-assistant', 'confirmation-required'): 'aiConfirmationRequired',
            ('ai-assistant', 'action-timeout'): 'aiActionTimeout',
            ('ai-assistant', 'screenshot-quality'): 'aiScreenshotQuality',
            ('ai-assistant', 'max-context-length'): 'aiMaxContextLength',
            ('ocr', 'language-code'): 'ocrLanguageCode',
            ('ocr', 'scale-factor'): 'ocrScaleFactor',
            ('ocr', 'grayscale-image'): 'ocrGrayscaleImg',
            ('ocr', 'invert-image'): 'ocrInvertImg',
            ('ocr', 'black-white-image'): 'ocrBlackWhiteImg',
            ('ocr', 'black-white-threshold'): 'ocrBlackWhiteImgValue',
            ('ocr', 'color-calculation'): 'ocrColorCalculation',
            ('ocr', 'color-calculation-max'): 'ocrColorCalculationMax',
            ('ocr', 'copy-to-clipboard'): 'ocrCopyToClipboard',
        }

        for (sectionName, legacyKey), currentKey in legacyKeyMap.items():
            if currentKey in profileSettings:
                continue
            legacyValue = self._legacyProfileValue(profileTable, sectionName, legacyKey)
            if legacyValue is not None:
                profileSettings[currentKey] = self._normalizeLegacyValue(currentKey, legacyValue)

        return profileSettings

    def _normalizeProfiles(self, profiles):
        if not isinstance(profiles, dict):
            return {}
        return {
            profileName: self._normalizeLegacyProfile(profileName, profileTable)
            for profileName, profileTable in profiles.items()
        }

    def _normalizeProfilesDocument(self, prefsDoc):
        profiles = prefsDoc.get('profiles')
        if not isinstance(profiles, dict):
            return

        for profileName in list(profiles.keys()):
            profileTable = profiles[profileName]
            normalizedProfile = self._normalizeLegacyProfile(profileName, profileTable)
            profiles[profileName] = self._stripNone(normalizedProfile)

        if 'format-version' in prefsDoc:
            del prefsDoc['format-version']

    def saveDefaultSettings(self, general, pronunciations, keybindings):
        """ Save default settings for all the properties from
            cthulhu.settings. """
        prefs = {'general': self._stripNone(general),
                 'profiles': self._stripNone(self._defaultProfiles),
                 'pronunciations': self._stripNone(pronunciations),
                 'keybindings': self._stripNone(keybindings)}

        self.general = general
        self.profiles = self._defaultProfiles
        self.pronunciations = pronunciations
        self.keybindings = keybindings

        prefsDoc = document()
        prefsDoc['general'] = prefs['general']
        prefsDoc['profiles'] = prefs['profiles']
        prefsDoc['pronunciations'] = prefs['pronunciations']
        prefsDoc['keybindings'] = prefs['keybindings']

        self._writeDocument(self.settingsFile, prefsDoc)

    def getAppSettings(self, appName):
        fileName = os.path.join(self.appPrefsDir, f"{appName}.toml")
        return self._readDocument(fileName)

    def saveAppSettings(self, appName, profile, general, pronunciations, keybindings):
        prefsDoc = self.getAppSettings(appName)
        profiles = prefsDoc.get('profiles')
        if profiles is None or not isinstance(profiles, dict):
            prefsDoc['profiles'] = {}
            profiles = prefsDoc['profiles']

        profileTable = profiles.get(profile)
        if profileTable is None or not isinstance(profileTable, dict):
            profiles[profile] = {}
            profileTable = profiles[profile]

        self._updateTable(profileTable, {
            'general': self._stripNone(general),
            'pronunciations': self._stripNone(pronunciations),
            'keybindings': self._stripNone(keybindings),
        })

        fileName = os.path.join(self.appPrefsDir, f"{appName}.toml")
        self._writeDocument(fileName, prefsDoc)

    def saveProfileSettings(self, profile, general,
                                  pronunciations, keybindings):
        """ Save minimal subset defined in the profile against current
            defaults. """
        if profile is None:
            profile = 'default'

        general['pronunciations'] = pronunciations
        general['keybindings'] = keybindings
        general = self._stripNone(general)

        prefsDoc = self._readDocument(self.settingsFile)
        self._normalizeProfilesDocument(prefsDoc)
        profiles = prefsDoc.get('profiles')
        if profiles is None or not isinstance(profiles, dict):
            prefsDoc['profiles'] = {}
            profiles = prefsDoc['profiles']

        profileTable = profiles.get(profile)
        if profileTable is None or not isinstance(profileTable, dict):
            profiles[profile] = {}
            profileTable = profiles[profile]

        # Keep plugin persistence keys when callers provide partial updates.
        self._updateTable(
            profileTable,
            general,
            preserveMissingKeys={"activePlugins", "pluginSources"},
        )
        self._writeDocument(self.settingsFile, prefsDoc)

    def _getSettings(self):
        """ Load from config file all settings """
        prefsDoc = self._readDocument(self.settingsFile)
        try:
            self.general = dict(prefsDoc.get('general', {}))
            self.pronunciations = dict(prefsDoc.get('pronunciations', {}))
            self.keybindings = dict(prefsDoc.get('keybindings', {}))
            self.profiles = self._normalizeProfiles(dict(prefsDoc.get('profiles', {})))
        except Exception:
            return

    def _migrateSettings(self, settingsDict):
        """Migrate old setting names to new ones."""
        # Migration: enableSpeechIndentation -> enableIndentation
        if 'enableSpeechIndentation' in settingsDict and 'enableIndentation' not in settingsDict:
            settingsDict['enableIndentation'] = settingsDict['enableSpeechIndentation']

        # Remove obsolete indentation settings that are now hardcoded
        obsoleteKeys = [
            'enableSpeechIndentation',
            'indentationChangeMode',
            'speakIndentationOnlyIfChanged',
            'indentationSpeechStyle',
            'indentationAudioUnit',
            'indentationSpacesPerLevel',
            'indentationTabWidth',
            'indentationAudioBaseFrequency',
            'indentationAudioStepFrequency',
            'indentationAudioMaxFrequency',
            'indentationAudioDuration',
            'indentationAudioVolume',
        ]
        for key in obsoleteKeys:
            settingsDict.pop(key, None)

        return settingsDict

    def _hasLikelyCustomEchoVoice(self, echoVoice, defaultVoice):
        """Return True when echoVoice appears intentionally customized."""

        if not echoVoice:
            return False

        echoEstablished = bool(echoVoice.get('established', False))
        if not defaultVoice:
            return echoEstablished or bool(echoVoice.get(acss.ACSS.FAMILY))

        for key in [acss.ACSS.RATE, acss.ACSS.AVERAGE_PITCH, acss.ACSS.GAIN, acss.ACSS.FAMILY]:
            echoValue = echoVoice.get(key)
            defaultValue = defaultVoice.get(key)
            if echoValue is not None and echoValue != defaultValue:
                return True

        return echoEstablished and bool(echoVoice.get(acss.ACSS.FAMILY))

    def getGeneral(self, profile=None):
        """ Get general settings from default settings and
            override with profile values. """
        self._getSettings()
        generalSettings = self.general.copy()
        generalSettings = self._migrateSettings(generalSettings)
        # Plugin state is profile-scoped; ignore legacy/global values.
        generalSettings.pop('activePlugins', None)
        generalSettings.pop('pluginSources', None)
        defaultProfile = generalSettings.get('startingProfile',
                                             ['Default', 'default'])
        if profile is None:
            profile = defaultProfile[1]
        profileSettings = self.profiles[profile].copy()
        profileSettings = self._migrateSettings(profileSettings)
        for key, value in profileSettings.items():
            if key == 'voices':
                for voiceType, voiceDef in value.items():
                    value[voiceType] = acss.ACSS(voiceDef)
            if key == 'echoVoice' and isinstance(value, dict):
                value = acss.ACSS(value)
            if key not in ['startingProfile', 'activeProfile']:
                generalSettings[key] = value

        # Backward compatibility: recover custom echo behavior when legacy
        # profiles have echoVoice but are missing custom-echo toggle keys.
        if 'useCustomEchoVoice' not in profileSettings:
            voices = generalSettings.get('voices') or {}
            defaultVoice = voices.get(settings.DEFAULT_VOICE)
            echoVoice = generalSettings.get('echoVoice')
            if self._hasLikelyCustomEchoVoice(echoVoice, defaultVoice):
                generalSettings['useCustomEchoVoice'] = True
                if 'useCustomEchoForKey' not in profileSettings:
                    generalSettings['useCustomEchoForKey'] = True
                if 'useCustomEchoForCharacter' not in profileSettings:
                    generalSettings['useCustomEchoForCharacter'] = True

        try:
            generalSettings['activeProfile'] = profileSettings['profile']
        except KeyError:
            generalSettings['activeProfile'] = defaultProfile
        return generalSettings

    def getPronunciations(self, profile='default'):
        """ Get pronunciation settings from default settings and
            override with profile values. """
        self._getSettings()
        pronunciations = self.pronunciations.copy()
        profileSettings = self.profiles[profile].copy()
        if 'pronunciations' in profileSettings:
            pronunciations = profileSettings['pronunciations']
        return pronunciations

    def getKeybindings(self, profile='default'):
        """ Get keybindings settings from default settings and
            override with profile values. """
        self._getSettings()
        keybindings = self.keybindings.copy()
        profileSettings = self.profiles[profile].copy()
        if 'keybindings' in profileSettings:
            keybindings = profileSettings['keybindings']
        return keybindings

    def isFirstStart(self):
        """ Check if we're in first start. """

        return not os.path.exists(self.settingsFile)

    def _setProfileKey(self, key, value):
        self.general[key] = value

        prefsDoc = self._readDocument(self.settingsFile)
        general = prefsDoc.get('general')
        if general is None or not isinstance(general, dict):
            prefsDoc['general'] = {}
            general = prefsDoc['general']

        if value is None:
            if key in general:
                del general[key]
        else:
            general[key] = value
        self._writeDocument(self.settingsFile, prefsDoc)

    def setFirstStart(self, value=False):
        """Set firstStart. This user-configurable setting is primarily
        intended to serve as an indication as to whether or not initial
        configuration is needed."""
        self.general['firstStart'] = value
        self._setProfileKey('firstStart', value)

    def availableProfiles(self):
        """ List available profiles. """
        self._getSettings()
        profiles = []

        for profileName in self.profiles.keys():
            profileDict = self.profiles[profileName].copy()
            profiles.append(profileDict.get('profile'))

        return profiles

    def removeProfile(self, profile):
        """Remove an existing profile"""
        def removeProfileFrom(dict):
            del dict[profile]
            # if we removed the last profile, restore the default ones
            if len(dict) == 0:
                for profileName in self._defaultProfiles:
                    dict[profileName] = self._defaultProfiles[profileName].copy()

        if profile in self.profiles:
            removeProfileFrom(self.profiles)

        prefsDoc = self._readDocument(self.settingsFile)
        profiles = prefsDoc.get('profiles')
        if isinstance(profiles, dict) and profile in profiles:
            removeProfileFrom(profiles)
            self._writeDocument(self.settingsFile, prefsDoc)
