import numbers
import sys
import warnings
from configparser import NoOptionError
from os import getenv, makedirs, mkdir, pardir
from os.path import dirname, expanduser, isdir, join, normpath
from typing import TYPE_CHECKING, Optional, Union

from config import applongname, appname, update_interval
from EDMCLogging import get_main_logger

logger = get_main_logger()

if sys.platform == 'darwin':
    from Foundation import (  # type: ignore
        NSApplicationSupportDirectory, NSBundle, NSDocumentDirectory, NSSearchPathForDirectoriesInDomains,
        NSUserDefaults, NSUserDomainMask
    )

elif sys.platform == 'win32':
    import ctypes
    import uuid
    from ctypes.wintypes import DWORD, HANDLE, HKEY, LONG, LPCVOID, LPCWSTR
    if TYPE_CHECKING:
        import ctypes.windll  # type: ignore

    FOLDERID_Documents = uuid.UUID('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}')
    FOLDERID_LocalAppData = uuid.UUID('{F1B32785-6FBA-4FCF-9D55-7B8E7F157091}')
    FOLDERID_Profile = uuid.UUID('{5E6C858F-0E22-4760-9AFE-EA3317B67173}')
    FOLDERID_SavedGames = uuid.UUID('{4C5C32FF-BB9D-43b0-B5B4-2D72E54EAAA4}')

    SHGetKnownFolderPath = ctypes.windll.shell32.SHGetKnownFolderPath
    SHGetKnownFolderPath.argtypes = [ctypes.c_char_p, DWORD, HANDLE, ctypes.POINTER(ctypes.c_wchar_p)]

    CoTaskMemFree = ctypes.windll.ole32.CoTaskMemFree
    CoTaskMemFree.argtypes = [ctypes.c_void_p]

    # winreg in Python <= 3.7.4 handles REG_MULTI_SZ incorrectly, so do this instead. https://bugs.python.org/issue32587
    HKEY_CURRENT_USER = 0x80000001
    KEY_ALL_ACCESS = 0x000F003F
    REG_CREATED_NEW_KEY = 0x00000001
    REG_OPENED_EXISTING_KEY = 0x00000002
    REG_SZ = 1
    REG_DWORD = 4
    REG_MULTI_SZ = 7

    REG_RESERVED_ALWAYS_ZERO = 0

    RegCreateKeyEx = ctypes.windll.advapi32.RegCreateKeyExW
    RegCreateKeyEx.restype = LONG
    RegCreateKeyEx.argtypes = [
        HKEY, LPCWSTR, DWORD, LPCVOID, DWORD, DWORD, LPCVOID, ctypes.POINTER(HKEY), ctypes.POINTER(DWORD)
    ]

    RegOpenKeyEx = ctypes.windll.advapi32.RegOpenKeyExW
    RegOpenKeyEx.restype = LONG
    RegOpenKeyEx.argtypes = [HKEY, LPCWSTR, DWORD, DWORD, ctypes.POINTER(HKEY)]

    RegCloseKey = ctypes.windll.advapi32.RegCloseKey
    RegCloseKey.restype = LONG
    RegCloseKey.argtypes = [HKEY]

    RegQueryValueEx = ctypes.windll.advapi32.RegQueryValueExW
    RegQueryValueEx.restype = LONG
    RegQueryValueEx.argtypes = [HKEY, LPCWSTR, LPCVOID, ctypes.POINTER(DWORD), LPCVOID, ctypes.POINTER(DWORD)]

    RegSetValueEx = ctypes.windll.advapi32.RegSetValueExW
    RegSetValueEx.restype = LONG
    RegSetValueEx.argtypes = [HKEY, LPCWSTR, LPCVOID, DWORD, LPCVOID, DWORD]

    RegCopyTree = ctypes.windll.advapi32.RegCopyTreeW
    RegCopyTree.restype = LONG
    RegCopyTree.argtypes = [HKEY, LPCWSTR, HKEY]

    RegDeleteKey = ctypes.windll.advapi32.RegDeleteTreeW
    RegDeleteKey.restype = LONG
    RegDeleteKey.argtypes = [HKEY, LPCWSTR]

    RegDeleteValue = ctypes.windll.advapi32.RegDeleteValueW
    RegDeleteValue.restype = LONG
    RegDeleteValue.argtypes = [HKEY, LPCWSTR]

    def known_folder_path(guid: uuid.UUID) -> Optional[str]:
        """Look up a Windows GUID to actual folder path name."""
        buf = ctypes.c_wchar_p()
        if SHGetKnownFolderPath(ctypes.create_string_buffer(guid.bytes_le), 0, 0, ctypes.byref(buf)):
            return None
        retval = buf.value  # copy data
        CoTaskMemFree(buf)  # and free original
        return retval

elif sys.platform == 'linux':
    import codecs
    from configparser import RawConfigParser


class OldConfig():
    """Object that holds all configuration data."""

    OUT_MKT_EDDN = 1
    # OUT_MKT_BPC = 2	# No longer supported
    OUT_MKT_TD = 4
    OUT_MKT_CSV = 8
    OUT_SHIP = 16
    # OUT_SHIP_EDS = 16	# Replaced by OUT_SHIP
    # OUT_SYS_FILE = 32	# No longer supported
    # OUT_STAT = 64	# No longer available
    # OUT_SHIP_CORIOLIS = 128	# Replaced by OUT_SHIP
    OUT_STATION_ANY = OUT_MKT_EDDN | OUT_MKT_TD | OUT_MKT_CSV
    # OUT_SYS_EDSM = 256  # Now a plugin
    # OUT_SYS_AUTO = 512  # Now always automatic
    OUT_MKT_MANUAL = 1024
    OUT_SYS_EDDN = 2048
    OUT_SYS_DELAY = 4096

    if sys.platform == 'darwin':  # noqa: C901 # It's gating *all* the functions

        def __init__(self):
            self.app_dir = join(
                NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0], appname
            )
            if not isdir(self.app_dir):
                mkdir(self.app_dir)

            self.plugin_dir = join(self.app_dir, 'plugins')
            if not isdir(self.plugin_dir):
                mkdir(self.plugin_dir)

            if getattr(sys, 'frozen', False):
                self.internal_plugin_dir = normpath(join(dirname(sys.executable), pardir, 'Library', 'plugins'))
                self.respath = normpath(join(dirname(sys.executable), pardir, 'Resources'))
                self.identifier = NSBundle.mainBundle().bundleIdentifier()

            else:
                self.internal_plugin_dir = join(dirname(__file__), 'plugins')
                self.respath = dirname(__file__)
                # Don't use Python's settings if interactive
                self.identifier = f'uk.org.marginal.{appname.lower()}'
                NSBundle.mainBundle().infoDictionary()['CFBundleIdentifier'] = self.identifier

            self.default_journal_dir = join(
                NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0],
                'Frontier Developments',
                'Elite Dangerous'
            )
            self.home = expanduser('~')

            self.defaults = NSUserDefaults.standardUserDefaults()
            self.settings = dict(self.defaults.persistentDomainForName_(self.identifier) or {})  # make writeable

            # Check out_dir exists
            if not self.get('outdir') or not isdir(str(self.get('outdir'))):
                self.set('outdir', NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, True)[0])

        def get(self, key: str, default: Union[None, list, str] = None) -> Union[None, list, str]:
            """Look up a string configuration value."""
            val = self.settings.get(key)
            if val is None:
                return default

            elif isinstance(val, str):
                return str(val)

            elif isinstance(val, list):
                return list(val)  # make writeable

            else:
                return default

        def getint(self, key: str, default: int = 0) -> int:
            """Look up an integer configuration value."""
            try:
                return int(self.settings.get(key, default))  # should already be int, but check by casting

            except ValueError as e:
                logger.error(f"Failed to int({key=})", exc_info=e)
                return default

            except Exception as e:
                logger.debug('The exception type is ...', exc_info=e)
                return default

        def set(self, key: str, val: Union[int, str, list]) -> None:
            """Set value on the specified configuration key."""
            self.settings[key] = val

        def delete(self, key: str) -> None:
            """Delete the specified configuration key."""
            self.settings.pop(key, None)

        def save(self) -> None:
            """Save current configuration to disk."""
            self.defaults.setPersistentDomain_forName_(self.settings, self.identifier)
            self.defaults.synchronize()

        def close(self) -> None:
            """Close the configuration."""
            self.save()
            self.defaults = None

    elif sys.platform == 'win32':

        def __init__(self):
            self.app_dir = join(known_folder_path(FOLDERID_LocalAppData), appname)  # type: ignore # Not going to change
            if not isdir(self.app_dir):
                mkdir(self.app_dir)

            self.plugin_dir = join(self.app_dir, 'plugins')
            if not isdir(self.plugin_dir):
                mkdir(self.plugin_dir)

            if getattr(sys, 'frozen', False):
                self.internal_plugin_dir = join(dirname(sys.executable), 'plugins')
                self.respath = dirname(sys.executable)

            else:
                self.internal_plugin_dir = join(dirname(__file__), 'plugins')
                self.respath = dirname(__file__)

            # expanduser in Python 2 on Windows doesn't handle non-ASCII - http://bugs.python.org/issue13207
            self.home = known_folder_path(FOLDERID_Profile) or r'\\'

            journaldir = known_folder_path(FOLDERID_SavedGames)
            if journaldir:
                self.default_journal_dir = join(journaldir, 'Frontier Developments', 'Elite Dangerous')

            else:
                self.default_journal_dir = None

            self.identifier = applongname
            self.hkey = HKEY()
            disposition = DWORD()
            if RegCreateKeyEx(
                    HKEY_CURRENT_USER,
                    r'Software\Marginal\EDMarketConnector',
                    0,
                    None,
                    0,
                    KEY_ALL_ACCESS,
                    None,
                    ctypes.byref(self.hkey),
                    ctypes.byref(disposition)
            ):
                raise Exception()

            # set WinSparkle defaults - https://github.com/vslavik/winsparkle/wiki/Registry-Settings
            edcdhkey = HKEY()
            if RegCreateKeyEx(
                    HKEY_CURRENT_USER,
                    r'Software\EDCD\EDMarketConnector',
                    0,
                    None,
                    0,
                    KEY_ALL_ACCESS,
                    None,
                    ctypes.byref(edcdhkey),
                    ctypes.byref(disposition)
            ):
                raise Exception()

            sparklekey = HKEY()
            if not RegCreateKeyEx(
                    edcdhkey,
                    'WinSparkle',
                    0,
                    None,
                    0,
                    KEY_ALL_ACCESS,
                    None,
                    ctypes.byref(sparklekey),
                    ctypes.byref(disposition)
            ):
                if disposition.value == REG_CREATED_NEW_KEY:
                    buf = ctypes.create_unicode_buffer('1')
                    RegSetValueEx(sparklekey, 'CheckForUpdates', 0, 1, buf, len(buf) * 2)

                buf = ctypes.create_unicode_buffer(str(update_interval))
                RegSetValueEx(sparklekey, 'UpdateInterval', 0, 1, buf, len(buf) * 2)
                RegCloseKey(sparklekey)

            if not self.get('outdir') or not isdir(self.get('outdir')):  # type: ignore # Not going to change
                self.set('outdir', known_folder_path(FOLDERID_Documents) or self.home)

        def get(self, key: str, default: Union[None, list, str] = None) -> Union[None, list, str]:
            """Look up a string configuration value."""
            key_type = DWORD()
            key_size = DWORD()
            # Only strings are handled here.
            if (
                    RegQueryValueEx(
                        self.hkey,
                        key,
                        0,
                        ctypes.byref(key_type),
                        None,
                        ctypes.byref(key_size)
                    )
                    or key_type.value not in [REG_SZ, REG_MULTI_SZ]
            ):
                return default

            buf = ctypes.create_unicode_buffer(int(key_size.value / 2))
            if RegQueryValueEx(self.hkey, key, 0, ctypes.byref(key_type), buf, ctypes.byref(key_size)):
                return default

            elif key_type.value == REG_MULTI_SZ:
                return list(ctypes.wstring_at(buf, len(buf)-2).split('\x00'))

            else:
                return str(buf.value)

        def getint(self, key: str, default: int = 0) -> int:
            """Look up an integer configuration value."""
            key_type = DWORD()
            key_size = DWORD(4)
            key_val = DWORD()
            if (
                    RegQueryValueEx(
                        self.hkey,
                        key,
                        0,
                        ctypes.byref(key_type),
                        ctypes.byref(key_val),
                        ctypes.byref(key_size)
                    )
                    or key_type.value != REG_DWORD
            ):
                return default

            else:
                return key_val.value

        def set(self, key: str, val: Union[int, str, list]) -> None:
            """Set value on the specified configuration key."""
            if isinstance(val, str):
                buf = ctypes.create_unicode_buffer(val)
                RegSetValueEx(self.hkey, key, 0, REG_SZ, buf, len(buf)*2)

            elif isinstance(val, numbers.Integral):
                RegSetValueEx(self.hkey, key, 0, REG_DWORD, ctypes.byref(DWORD(val)), 4)

            elif isinstance(val, list):
                # null terminated non-empty strings
                string_val = '\x00'.join([str(x) or ' ' for x in val] + [''])
                buf = ctypes.create_unicode_buffer(string_val)
                RegSetValueEx(self.hkey, key, 0, REG_MULTI_SZ, buf, len(buf)*2)

            else:
                raise NotImplementedError()

        def delete(self, key: str) -> None:
            """Delete the specified configuration key."""
            RegDeleteValue(self.hkey, key)

        def save(self) -> None:
            """Save current configuration to disk."""
            pass  # Redundant since registry keys are written immediately

        def close(self) -> None:
            """Close the configuration."""
            RegCloseKey(self.hkey)
            self.hkey = None

    elif sys.platform == 'linux':
        SECTION = 'config'

        def __init__(self):

            # http://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
            self.app_dir = join(getenv('XDG_DATA_HOME', expanduser('~/.local/share')), appname)
            if not isdir(self.app_dir):
                makedirs(self.app_dir)

            self.plugin_dir = join(self.app_dir, 'plugins')
            if not isdir(self.plugin_dir):
                mkdir(self.plugin_dir)

            self.internal_plugin_dir = join(dirname(__file__), 'plugins')
            self.default_journal_dir = None
            self.home = expanduser('~')
            self.respath = dirname(__file__)
            self.identifier = f'uk.org.marginal.{appname.lower()}'

            self.filename = join(getenv('XDG_CONFIG_HOME', expanduser('~/.config')), appname, f'{appname}.ini')
            if not isdir(dirname(self.filename)):
                makedirs(dirname(self.filename))

            self.config = RawConfigParser(comment_prefixes=('#',))
            try:
                with codecs.open(self.filename, 'r') as h:
                    self.config.read_file(h)

            except Exception as e:
                logger.debug('Reading config failed, assuming we\'re making a new one...', exc_info=e)
                self.config.add_section(self.SECTION)

            if not self.get('outdir') or not isdir(self.get('outdir')):  # type: ignore # Not going to change
                self.set('outdir', expanduser('~'))

        def get(self, key: str, default: Union[None, list, str] = None) -> Union[None, list, str]:
            """Look up a string configuration value."""
            try:
                val = self.config.get(self.SECTION, key)
                if '\n' in val:  # list
                    # ConfigParser drops the last entry if blank,
                    # so we add a spurious ';' entry in set() and remove it here
                    assert val.split('\n')[-1] == ';', val.split('\n')
                    return [self._unescape(x) for x in val.split('\n')[:-1]]
                else:
                    return self._unescape(val)

            except NoOptionError:
                logger.debug(f'attempted to get key {key} that does not exist')
                return default

            except Exception as e:
                logger.debug('And the exception type is...', exc_info=e)
                return default

        def getint(self, key: str, default: int = 0) -> int:
            """Look up an integer configuration value."""
            try:
                return self.config.getint(self.SECTION, key)

            except ValueError as e:
                logger.error(f"Failed to int({key=})", exc_info=e)

            except NoOptionError:
                logger.debug(f'attempted to get key {key} that does not exist')

            except Exception:
                logger.exception(f'unexpected exception while attempting to access {key}')

            return default

        def set(self, key: str, val: Union[int, str, list]) -> None:
            """Set value on the specified configuration key."""
            if isinstance(val, bool):
                self.config.set(self.SECTION, key, val and '1' or '0')  # type: ignore # Not going to change

            elif isinstance(val, str) or isinstance(val, numbers.Integral):
                self.config.set(self.SECTION, key, self._escape(val))  # type: ignore # Not going to change

            elif isinstance(val, list):
                self.config.set(self.SECTION, key, '\n'.join([self._escape(x) for x in val] + [';']))

            else:
                raise NotImplementedError()

        def delete(self, key: str) -> None:
            """Delete the specified configuration key."""
            self.config.remove_option(self.SECTION, key)

        def save(self) -> None:
            """Save current configuration to disk."""
            with codecs.open(self.filename, 'w', 'utf-8') as h:
                self.config.write(h)

        def close(self) -> None:
            """Close the configuration."""
            self.save()
            self.config = None

        def _escape(self, val: str) -> str:
            """Escape a string for storage."""
            return str(val).replace('\\', '\\\\').replace('\n', '\\n').replace(';', '\\;')

        def _unescape(self, val: str) -> str:
            """Un-escape a string from storage."""
            chars = list(val)
            i = 0
            while i < len(chars):
                if chars[i] == '\\':
                    chars.pop(i)
                    if chars[i] == 'n':
                        chars[i] = '\n'
                i += 1
            return ''.join(chars)

    else:
        def __init__(self):
            raise NotImplementedError('Implement me')

    # Common

    def get_password(self, account: str) -> None:
        """Legacy password retrieval."""
        warnings.warn("password subsystem is no longer supported", DeprecationWarning)

    def set_password(self, account: str, password: str) -> None:
        """Legacy password setting."""
        warnings.warn("password subsystem is no longer supported", DeprecationWarning)

    def delete_password(self, account: str) -> None:
        """Legacy password deletion."""
        warnings.warn("password subsystem is no longer supported", DeprecationWarning)


old_config = OldConfig()
