#-*- coding:utf-8 -*-

#  Copyright © 2009, 2011-2015  B. Clausius <barcc@gmx.de>
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program 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 General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.


import os
from ast import literal_eval
from io import StringIO

from PyQt5.QtCore import QObject, QTimer
from PyQt5.QtCore import pyqtSignal as Signal

try:
    from .debug import debug
except ImportError:
    debug = print
from . import config


current_settings_version = 2

class KeyStore (QObject):
    changed = Signal(str)
    error = Signal(str)
    
    def __init__(self):
        super().__init__()
        
        self.schema = None
        self.filename = None
        self.keystore = {}
        
    def clone_key(self, keyfrom, keyto):
        self.schema[keyto] = self.schema[keyfrom]
        if keyfrom in self.keystore and keyto not in self.keystore:
            self.keystore[keyto] = self.keystore[keyfrom]
            
    def get_default(self, key):
        return self.schema[key][0]
    def get_validator(self, key):
        return self.schema[key][1]
        
    def get_range(self, key):
        validator = self.get_validator(key)
        if not isinstance(validator, (tuple, list)):
            raise ValueError('{} is not a range'.format(key))
        return validator
    
    def get_value(self, key):
        try:
            return self.keystore[key]
        except KeyError:
            return self.get_default(key)
            
    def get_nick(self, key):
        value = self.get_value(key)
        validator = self.get_validator(key)
        if not isinstance(validator, list):
            raise ValueError('{} is not an enum'.format(key))
        return validator[value]
        
    def set_value(self, key, value):
        self.keystore[key] = value
        self.changed.emit(key)
        
    def set_nick(self, key, nick):
        validator = self.get_validator(key)
        if not isinstance(validator, list):
            raise ValueError('{} is not an enum'.format(key))
        value = validator.index(nick)
        return self.set_value(key, value)
        
    def del_value(self, key):
        try:
            del self.keystore[key]
        except KeyError:
            pass # already the default value
        self.changed.emit(key)
        
    def find_match(self, key):
        def match(key, pattern):
            key = key.split('.')
            pattern = pattern.split('.')
            for k, p in zip(key, pattern):
                if k != p != '*':
                    return False
            return True
        for pattern in self.schema.keys():
            if match(key, pattern):
                return pattern
        return None
        
    def read_settings(self, filename):
        from .schema import schema, deprecated
        self.schema = schema
        self.filename = filename
        if not self.filename:
            return
        dirname = os.path.dirname(self.filename)
        if dirname and not os.path.exists(dirname):
            os.makedirs(dirname)
            
        # read settings
        try:
            with open(self.filename, 'rt', encoding='utf-8') as settings_file:
                lines = settings_file.readlines()
        except FileNotFoundError:
            lines = []
        for line in lines:
            # parse the line, discard invalid keys
            try:
                key, strvalue = line.split('=', 1)
            except ValueError:
                continue
            key = key.strip()
            strvalue = strvalue.strip()
            pattern = self.find_match(key)
            if pattern is None:
                continue
            self.schema[key] = self.schema[pattern]
            try:
                value = literal_eval(strvalue)
            except (ValueError, SyntaxError):
                continue
                
            # translate enums and validate values
            validator = self.get_validator(key)
            if validator is deprecated:
                pass
            elif isinstance(validator, list):
                try:
                    value = validator.index(value)
                except ValueError:
                    continue
            elif isinstance(validator, tuple):
                if not validator[0] <= value <= validator[1]:
                    continue
            elif validator is not None:
                if not validator(value):
                    continue
                    
            self.keystore[key] = value
            
    def dump(self, file, all=False):    # pylint: disable=W0622
        keydict = self.schema if all else self.keystore
        for key in sorted(keydict.keys()):
            if '*' in key:
                continue
            try:
                # translate enums
                value = self.get_nick(key)
            except ValueError:
                value = self.get_value(key)
            print(key, '=', repr(value), file=file)
        
    def write_settings(self):
        if not self.filename:
            return
        buf = StringIO()
        self.dump(buf)
        try:
            with open(self.filename, 'wt', encoding='utf-8') as settings_file:
                settings_file.write(buf.getvalue())
        except OSError as e:
            error_message = _('Settings can not be written to file: '
                            '{error_message}').format(error_message=e)
            debug(error_message)
            self.error.emit(error_message)
            
        
class Values:
    def __init__(self, key):
        object.__setattr__(self, '_key', key)
        object.__setattr__(self, '_subkeys_nodes', [])
        object.__setattr__(self, '_subkeys_leaves', [])
        
    def clone(self, key):
        values = Values(key)
        for subkey in self._subkeys_nodes:
            values._add_attr(subkey, self[subkey].clone(key+subkey+'.'))
        for subkey in self._subkeys_leaves:
            settings.keystore.clone_key(self._key + subkey, key + subkey)
            values._add_key(subkey)
        return values
        
    def sync(self, glob=None):
        glob2 = self['*'] if '*' in self._subkeys_nodes else None
        for subkey in self._subkeys_nodes:
            if subkey == '*':   # glob2 == self[subkey]
                glob2.sync()
            else:
                self[subkey].sync(glob2)
            if glob is not None and subkey in glob._subkeys_nodes:
                self[subkey].sync(glob[subkey])
        if glob is not None:
            for subkey in glob._subkeys_nodes:
                if subkey not in self._subkeys_nodes:
                    self._add_attr(subkey, glob[subkey].clone(self._key+subkey+'.'))
            for subkey in glob._subkeys_leaves:
                if subkey not in self._subkeys_leaves:
                    settings.keystore.clone_key(glob._key + subkey, self._key + subkey)
                    self._add_key(subkey)
                    
    def __getattr__(self, key):
        if '.' in key:
            value = self
            subkeys = key.split('.')
            for subkey in subkeys:
                value = value[subkey]
            return value
        if key and key[0] == '_':
            return super().__getattribute__(key)
        if key.endswith('_nick'):
            fullkey = self._key + key[:-5]
            return settings.keystore.get_nick(fullkey)
        if key.endswith('_range'):
            fullkey = self._key + key[:-6]
            return settings.keystore.get_range(fullkey)
        if key in self._subkeys_leaves:
            fullkey = self._key + key
            return settings.keystore.get_value(fullkey)
        if key in self._subkeys_nodes:
            return super().__getattribute__(key)
        if '*' in self._subkeys_leaves:
            fullkey = self._key + key
            settings.keystore.clone_key(self._key+'*', fullkey)
            self._add_key(key)
            return settings.keystore.get_value(fullkey)
        if '*' in self._subkeys_nodes:
            value = super().__getattribute__('*')
            value = value.clone(self._key+key+'.')
            self._add_attr(key, value)
            return value
        raise AttributeError('Unknown attribute "%s" in %s' % (key, self._key))
            
    def __setattr__(self, key, value):
        if '.' in key:
            val = self
            *subkeys, key = key.split('.')
            for subkey in subkeys:
                val = val[subkey]
            return val.__setattr__(key, value)
        if key.endswith('_nick'):
            fullkey = self._key + key[:-5]
            func = settings.keystore.set_nick
        elif key in self._subkeys_leaves:
            fullkey = self._key + key
            func = settings.keystore.set_value
        elif '*' in self._subkeys_leaves:
            fullkey = self._key + key
            settings.keystore.clone_key(self._key+'*', fullkey)
            self._add_key(key)
            func = settings.keystore.set_value
        else:
            raise AttributeError('unknown attribute {!r} in {!r}'.format(key, self._key))
        func(fullkey, value)
        if not settings._write_timer.isActive():
            settings._write_timer.start()
            
    def __delattr__(self, key):
        if '.' in key:
            value = self
            *subkeys, key = key.split('.')
            for subkey in subkeys:
                value = value[subkey]
            return value.__delattr__(key)
        if key in self._subkeys_leaves:
            fullkey = self._key + key
            settings.keystore.del_value(fullkey)
            if not settings._write_timer.isActive():
                settings._write_timer.start()
        else:
            raise AttributeError('unknown attribute "%s"' % key)
            
    def __getitem__(self, key):
        if type(key) is int:
            key = str(key)
        elif type(key) is tuple:
            key = '_'.join(str(v) for v in key)
        return getattr(self, key)
        
    def __setitem__(self, key, value):
        return setattr(self, str(key), value)
        
    def _add_attr(self, key, value):
        assert key not in self._subkeys_nodes
        assert key not in self._subkeys_leaves
        self._subkeys_nodes.append(key)
        super().__setattr__(key, value)
        
    def _add_key(self, key):
        assert key not in self._subkeys_nodes
        assert key not in self._subkeys_leaves
        self._subkeys_leaves.append(key)
        
        
class Settings:
    keystore = KeyStore()
    
    def __init__(self, key=''):
        object.__setattr__(self, '_key', key)
        object.__setattr__(self, '_array_len', 0)
        object.__setattr__(self, '_values', Values(''))
        object.__setattr__(self, '_write_timer', QTimer())
        self._write_timer.setSingleShot(True)
        self._write_timer.setInterval(5000)
        self._write_timer.timeout.connect(self.on_write_timer_timeout)
        
    def reset(self):
        self._write_timer.stop()
        self._write_timer.timeout.disconnect(self.on_write_timer_timeout)
        self.keystore.keystore.clear()
        self.__init__()
        
    def load(self, filename):
        self.keystore.read_settings(filename)
        keys = list(self.keystore.schema.keys())
        
        for key in keys:
            subkeys = key.split('.')
            settings_parent = self._values
            for subkey in subkeys[:-1]:
                if subkey.isdigit():
                    subkey = str(int(subkey))
                try:
                    settings_child = getattr(settings_parent, subkey)
                except (KeyError, AttributeError):
                    settings_child = Values(settings_parent._key + subkey + '.')
                    settings_parent._add_attr(subkey, settings_child)
                assert isinstance(settings_child, Values), settings_child
                settings_parent = settings_child
            else:
                if subkeys[-1] not in settings_parent._subkeys_leaves:
                    settings_parent._add_key(subkeys[-1])
        self._values.sync()
        version = settings.version
        if version != current_settings_version:
            settings.version = current_settings_version
        return version
            
    def on_write_timer_timeout(self):
        self.keystore.write_settings()
        
    def disconnect(self):
        self.keystore.changed.disconnect()
        self.keystore.error.disconnect()
        self._write_timer.stop()
        
    def flush(self):
        self.keystore.write_settings()
        
    def __getattr__(self, key):
        return getattr(self._values, key)
        
    def __setattr__(self, key, value):
        setattr(self._values, key, value)
        
    def __delattr__(self, key):
        delattr(self._values, key)
        
    def __getitem__(self, key):
        return self._values[key]
        

settings = Settings()

