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

#  Pybik -- A 3 dimensional magic cube game.
#  Copyright © 2009, 2011-2013  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 sys, os
from ast import literal_eval
import errno

from PyQt4.QtCore import * # pylint: disable=W0614,W0401
from PyQt4.QtCore import pyqtSignal as Signal

from .debug import debug
from . import config
from . import model


N_ = lambda s: s

def tuple_validator(lenght, itemtype, value, valuerange=None):
    if type(value) is not tuple:
        return False
    if len(value) != lenght:
        return False
    for v in value:
        if type(v) is not itemtype:
            return False
        if valuerange is not None and not valuerange[0] <= v <= valuerange[1]:
            return False
    return True
    
class KeyStore (QObject):
    schema = {
        # key:              (default,   range/enum/validator)
        #                               None: value without restriction
        #                               tuple: contains two values (min, max)
        #                               list: contains strings for the enum text,
        #                                     the index is the enum value
        #                               function: returns True, if value is valid
        'window.size':          ((480, 360),lambda v: tuple_validator(2, int, v)),
        'window.divider':       (300,       lambda v: type(v) is int),
        'window.toolbar':       (True,      lambda v: type(v) is bool),
        'window.editbar':       (True,      lambda v: type(v) is bool),
        'window.statusbar':     (True,      lambda v: type(v) is bool),
        'draw.default_rotation':((-30.,39.),lambda v: tuple_validator(2, float, v)),
        'draw.lighting':        (True,     lambda v: type(v) is bool),
        'draw.selection':       (1,         ['quadrant', 'simple']),
        'draw.speed':           (30,        (1, 100)),
        'game.type':            (0,         [m.type for m in model.models]),
        'game.size':            ((3,4,2),   lambda v: tuple_validator(3, int, v, (1, 10))),
        'game.blocks':          ('solved',  lambda v: type(v) is str),
        'game.moves':           ('',        lambda v: type(v) is str),
        'game.position':        (0,         lambda v: type(v) is int and v >= 0),
        'theme.face.0.color':   ('#a81407', lambda v: type(v) is str),
        'theme.face.1.color':   ('#d94b1c', lambda v: type(v) is str),
        'theme.face.2.color':   ('#f0c829', lambda v: type(v) is str),
        'theme.face.3.color':   ('#e3e3e3', lambda v: type(v) is str),
        'theme.face.4.color':   ('#1d6311', lambda v: type(v) is str),
        'theme.face.5.color':   ('#00275e', lambda v: type(v) is str),
        'theme.face.#.image':   ('',        lambda v: type(v) is str),
        'theme.face.#.mode':    (0,         ['tiled', 'mosaic']),
        'theme.bgcolor':        ('#B9a177', lambda v: type(v) is str),
        'draw.accels':          ([('r', 'KP+6'), ('r-', 'Shift+KP+Right'),
                                  ('l', 'KP+4'), ('l-', 'Shift+KP+Left'),
                                  ('u', 'KP+8'), ('u-', 'Shift+KP+Up'),
                                  ('d', 'KP+2'), ('d-', 'Shift+KP+Down'),
                                  ('f', 'KP+5'), ('f-', 'Shift+KP+Clear'),
                                  ('b', 'KP+0'), ('b-', 'Shift+KP+Ins'),
                                  ('R', 'Ctrl+KP+8'), ('L', 'Ctrl+KP+2'),
                                  ('U', 'Ctrl+KP+4'), ('D', 'Ctrl+KP+6'),
                                  ('F', 'Ctrl+KP+5'), ('B', 'Ctrl+KP+0'),
                                 ],         lambda v: type(v) is list),
        'draw.zoom':            (1.0,       (0.1, 100.0)),
        # The following 6 words are for the antialiasing levels: disabled, ugly, low, medium, high, higher
        'draw.samples':         (3,         [N_('disabled'), N_('ugly'), N_('low'),
                                             N_('medium'), N_('high'), N_('higher')]),
        'draw.mirror_faces':    (False,     lambda v: type(v) is bool),
        'draw.mirror_distance': (2.1,       (0.1, 10.0)),
        'action.edit_moves':    ('Ctrl+L',  lambda v: type(v) is str),
        'action.edit_model':    ('',        lambda v: type(v) is str),
        'action.reload_shader': ('',        lambda v: type(v) is str),
        'action.selectmodel':   ('',        lambda v: type(v) is str),
        'action.initial_state': ('',        lambda v: type(v) is str),
        'action.reset_rotation':('',        lambda v: type(v) is str),
        'action.invert_moves':  ('',        lambda v: type(v) is str),
        'action.reload_scripts':('',        lambda v: type(v) is str),
        'action.preferences':   ('',        lambda v: type(v) is str),
        'action.normalize_complete_rotations':('',lambda v: type(v) is str),
       }
       
    changed = Signal(str)
    error = Signal(str)
    
    def __init__(self, filename):
        QObject.__init__(self)
        self.filename = filename
        self.keystore = {}
        self.read_settings()
        self.write_timer = QTimer(self)
        self.write_timer.setSingleShot(True)
        self.write_timer.setInterval(5000)
        self.write_timer.timeout.connect(self.write_settings)
        
    def get_default(self, key):
        return self.schema[key][0]
    def get_range(self, key):
        return self.schema[key][1]
    
    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)
        valuerange = self.get_range(key)
        if not isinstance(valuerange, list):
            raise ValueError('{} is not an enum'.format(key))
        return valuerange[value]
        
    def set_value(self, key, value):
        self.keystore[key] = value
        self.changed.emit(key)
        if not self.write_timer.isActive():
            self.write_timer.start()
        
    def set_nick(self, key, nick):
        valuerange = self.get_range(key)
        if not isinstance(valuerange, list):
            raise ValueError('{} is not an enum'.format(key))
        value = valuerange.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)
        if not self.write_timer.isActive():
            self.write_timer.start()
        
    def read_settings(self):
        if not self.filename:
            return
        keys = list(self.schema.keys())
        dirname = os.path.dirname(self.filename)
        if dirname and not os.path.exists(dirname):
            os.makedirs(dirname)
            
        # Only convert from gconf to standard location
        write_version_file = False
        if self.filename == config.USER_SETTINGS_FILE and not os.path.exists(config.USER_VERSION_FILE):
            write_version_file = True
            try:
                if not os.path.exists(self.filename):
                    from . import migration
                    migration.gconf_0_5_to_settings_1_0(self.filename)
            except Exception:   # pylint: disable=W0703
                # Never fail, but report error
                sys.excepthook(*sys.exc_info())
                
        # read settings
        try:
            with open(self.filename, 'rt', encoding='utf-8') as settings_file:
                lines = settings_file.readlines()
        except IOError as e:
            if e.errno == errno.ENOENT:
                lines = []
            else:
                raise
        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()
            if key not in keys:
                continue
            try:
                value = literal_eval(strvalue)
            except (ValueError, SyntaxError):
                continue
                
            # translate enums and validate values
            valuerange = self.get_range(key)
            if isinstance(valuerange, list):
                try:
                    value = valuerange.index(value)
                except ValueError:
                    continue
            elif isinstance(valuerange, tuple):
                if not valuerange[0] <= value <= valuerange[1]:
                    continue
            elif valuerange is not None:
                if not valuerange(value):
                    continue
                    
            self.keystore[key] = value
            
        if write_version_file:
            with open(config.USER_VERSION_FILE, 'wt', encoding='utf-8') as version_file:
                version_file.write(config.VERSION)
                
    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
            value = self.get_value(key)
            # translate enums
            valuerange = self.get_range(key)
            if isinstance(valuerange, list):
                value = valuerange[value]
            print(key, '=', repr(value), file=file)
        
    def write_settings(self):
        if not self.filename:
            return
        try:
            with open(self.filename, 'wt', encoding='utf-8') as settings_file:
                self.dump(settings_file)
        except EnvironmentError 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 Settings (object):
    keystore = None
    
    def __init__(self, key=''):
        object.__setattr__(self, '_key', key)
        object.__setattr__(self, '_array_len', 0)
        
    def load(self, filename):
        self.__class__.keystore = KeyStore(filename)
        keys = list(self.keystore.schema.keys())
        deferred = []
        
        for key in keys:
            subkeys = key.split('.')
            settings_parent = self
            for subkey in subkeys[:-1]:
                if subkey == '#':
                    deferred.append(key)
                    break
                if subkey.isdigit():
                    subkey = str(int(subkey))
                try:
                    settings_child = getattr(settings_parent, subkey)
                except KeyError:
                    settings_child = Settings(settings_parent._key + subkey + '.')
                    object.__setattr__(settings_parent, subkey, settings_child)
                assert isinstance(settings_child, Settings)
                settings_parent = settings_child
            else:
                object.__setattr__(settings_parent, subkeys[-1]+'_range', self.keystore.get_range(key))
        changed = False
        for key in deferred:
            subkeys = key.split('.')
            settings_parent = self
            for i, subkey in enumerate(subkeys):
                if subkey == '#':
                    for subkey in list(settings_parent.__dict__.keys()):
                        if subkey[0] == '_':
                            continue
                        expanded = '.'.join(subkeys[:i] + [subkey] + subkeys[i+1:])
                        if expanded not in self.keystore.schema:
                            changed = True
                            self.keystore.schema[expanded] = self.keystore.schema[key]
                    break
                settings_parent = getattr(settings_parent, subkey)
        if changed:
            self.load(filename)
            
    def close(self):
        self.keystore.write_timer.stop()
        self.keystore.write_settings()
        
    def __getattr__(self, key):
        if key.endswith('_nick'):
            return self.keystore.get_nick(self._key + key[:-5])
        return self.keystore.get_value(self._key + key)
        
    def __getitem__(self, key):
        return getattr(self, str(key))
        
    def __setattr__(self, key, value):
        if key.endswith('_nick'):
            key = self._key + key[:-5]
            func = self.keystore.set_nick
        else:
            key = self._key + key
            func = self.keystore.set_value
        if key in list(self.keystore.schema.keys()):
            func(key, value)
        else:
            raise AttributeError('use object.__setattr__ to set attributes')
            
    def __delattr__(self, key):
        _key = self._key + key
        if _key in list(self.keystore.schema.keys()):
            self.keystore.del_value(_key)
        else:
            raise AttributeError('use object.__delattr__ to delete attributes')
            

settings = Settings()

