# GNU Solfege - ear training for GNOME
# Copyright (C) 2000, 2001, 2002, 2003, 2004  Tom Cato Amundsen
#
# 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 2 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, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
"""
TODO:
    - cfg.get_list
    - cfg.set_list
    - watches outside ConfigUtils
>>> import cfg
>>> cfg.initialise(None, "default.config", "~/.cfg-test")
>>> cfg.set_string("section/stringvar", "value")
>>> cfg.get_string("section/stringvar")
'value'
>>> cfg.set_int("section/intvar", 14)
>>> cfg.get_int("section/intvar")
14
>>> cfg.set_float("section/floatvar", 3.141592)
>>> cfg.get_float("section/floatvar") == 3.141592
1
>>> cfg.set_bool("section/boolvar", 4)
>>> cfg.get_bool("section/boolvar")
1
>>> cfg.set_bool("section/boolvar", 0)
>>> cfg.get_bool("section/boolvar")
0
>>> cfg.set_list("section/listv", [1, 2, 3])
>>> cfg.get_list("section/listv")
[1, 2, 3]
>>> c = ConfigUtils("section")
>>> c.get_string("section/stringvar")
'value'
>>> c.get_int("section/intvar")
14
>>> c.get_bool("section/boolvar")
0
>>> c.get_float("section/floatvar") == 3.141592
1
>>> cfg.initialise(None, None, "~/.solfege-cfg-testing")
>>> c.get_string("ss/a")
''
>>> c.get_string("ss/a=hei")
'hei'
>>> c.get_int("ss/a=3")
3
>>> c.get_list("ss/a")
[]
>>> c.get_list("ss/a=[1, 2, 3]")
[1, 2, 3]
>>> comment_re.match("# basd") != None
True
>>> section_re.match("#[section]") == None
True
>>> section_re.match(" [section]") == None
True
>>> section_re.match("[section]") != None
True
>>> value_re.match("key=value") != None
True
>>> value_re.match("#key=value") == None
True
>>> value_re.match(" key=value") == None
True
"""

import re
import os, os.path, sys

section_re = re.compile("^\[([\w-]*?)\]")
value_re = re.compile(  "^([\w-]*?)=(.*)")
comment_re = re.compile("#.*")

def split(key):
    return key.split("/")

def parse_file_into_dict(dictionary, filename):
    f = file(filename, "r")
    line = f.readline()
    section = None
    while line:
        section_m = section_re.match(line)
        value_m = value_re.match(line)
        comment_m = comment_re.match(line)
        if comment_m:
            pass
        elif section_m:
            section = section_m.groups()[0]
            if section not in dictionary:
                dictionary[section] = {}
        elif value_m:
            assert section
            dictionary[section][value_m.groups()[0]] = value_m.groups()[1]
        elif line == "\n":
            pass
        else:
            print filename
            raise Exception("Shit happended line %s" % line)
        line = f.readline()
    f.close()
    return dictionary


def sync():
    dump(data, _user_filename)

def _maybe_create_key(key):
    global data
    section, k = split(key)
    if section not in data:
        data[section] = {}

########################
## set_XXXX functions ##
########################

def set_string(key, val):
    global data, _watches, _blocked_watches
    _maybe_create_key(key)
    section, k = split(key)
    oldval = get_string(key)
    data[section][k] = val
    if key in _watches and (oldval != get_string(key)):
        if key in _blocked_watches and _blocked_watches[key] > 0:
            return
        for cb in _watches[key].values():
            cb(key)

def set_int(key, val):
    global data, _watches, _blocked_watches
    assert isinstance(val, int)
    _maybe_create_key(key)
    section, k = split(key)
    oldval = get_string(key)
    data[section][k] = str(val)
    if key in _watches and (oldval != get_string(key)):
        if key in _blocked_watches and _blocked_watches[key] > 0:
            return
        for cb in _watches[key].values():
            cb(key)

def set_float(key, val):
    global data, _watches, _blocked_watches
    assert isinstance(val, float)
    _maybe_create_key(key)
    section, k = split(key)
    oldval = get_string(key)
    data[section][k] = str(val)
    if key in _watches and (oldval != get_string(key)):
        if key in  _blocked_watches and _blocked_watches[key] > 0:
            return
        for cb in _watches[key].values():
            cb(key)

def set_bool(key, val):
    _maybe_create_key(key)
    if val:
        set_string(key, "true")
    else:
        set_string(key, "false")

def set_list(key, val):
    set_string(key, str(val))

########################
## get_XXX functions ##
########################

def get_string(key):
    if len(key.split("=")) == 2:
        key, default = key.split("=")
    else:
        default = ""
    section, k = split(key)
    try:
        return data[section][k]
    except KeyError:
        return default

def get_int(key):
    if len(key.split("=")) == 2:
        key, default = key.split("=")
    else:
        default = 0
    section, k = split(key)
    try:
        # UGH win32 fix
        if not data[section][k]:
            return 0
        return int(float(data[section][k]))
    except KeyError:
        return int(default)

def get_float(key):
    if len(key.split("=")) == 2:
        key, default = key.split("=")
    else:
        default = 0
    section, k = split(key)
    try:
        return float(data[section][k])
    except KeyError:
        return float(default)

def get_list(key):
    if len(key.split("=")) == 2:
        key, default = key.split("=")
    else:
        default = []
    section, k = split(key)
    try:
        return eval(data[section][k])
    except KeyError:
        return default

def get_bool(key):
    if get_string(key) == 'true':
        return 1
    elif get_string(key) == 'false':
        return 0
    else:
        print "warning: cfg.get_bool: value of '%s' is not true or false" % key
        return 0

#####################################
def del_key(key):
    global data, _watches, _blocked_watches
    section, k = split(key)
    #FIXME watches
    if section not in data:
        print >> sys.stderr, "cfg.del_key: section does not exist"
        return
    if k not in data[section]:
        print >> sys.stderr, "cfg.del_key: section '%s' does not have key '%s'" % (section, k)
        return
    del data[section][k]
    if not data[section]:
        del data[section]

######################################
## reading and writing the database ##
######################################

def dump(dict, fn):
    f = open(fn, 'w')
    for section in dict:
        f.write("[%s]\n" % section)
        for name in dict[section]:
            f.write("%s=%s\n" % (name, dict[section][name]))
        f.write("\n")
    f.close()

def drop_user_config():
    """
    Reread the config data, but only read the systems defaults from
    /usr/share/solfege/default.config and /etc/solfege, not
    $HOME/.solfegerc
    """
    global data, _watches, _watch_counter, _blocked_watches
    data = {}
    _watches = {}
    _watch_counter = 0
    _blocked_watches = {}
    data = parse_file_info_dict(data, _app_defaults_filename)
    data = parse_file_into_dict(data, _system_filename)

def reread_data():
    global data, _watches, _watch_counter, _blocked_watches
    _watches = {}
    _watch_counter = 0
    _blocked_watches = {}
    if _app_defaults_filename:
        data = parse_file_into_dict({}, _app_defaults_filename)
    if _system_filename:
        data = parse_file_into_dict(data, _system_filename)
    if _user_filename and os.path.isfile(_user_filename):
        data = parse_file_into_dict(data, _user_filename)

def initialise(app_defaults_filename, system_filename, user_filename):
    """
    app_defaults_filename: file must exist!
    system_filename: sys admin can override app_defaults_filename
    user_filename: the state of the app is stored here, also user config.
    """
    global data, _watches, _watch_counter, _blocked_watches
    global _app_defaults_filename, _system_filename, _user_filename
    _watches = {}
    _watch_counter = 0
    _blocked_watches = {}
    _app_defaults_filename = app_defaults_filename
    _system_filename = system_filename
    _user_filename = user_filename = os.path.expanduser(user_filename)
    if app_defaults_filename:
        try:
            data = parse_file_into_dict({}, app_defaults_filename)
        except:
            raise Exception("Solfege install is completely broken.\nCannot find required file %s" % app_defaults_filename)
    else:
        data = {}
    if system_filename and os.path.isfile(system_filename):
        data = parse_file_into_dict(data, system_filename)
    if os.path.isfile(user_filename):
        data = parse_file_into_dict(data, user_filename)


class ConfigUtils(object):
    def __init__(self, exname):
        self.m_exname = exname
    def block_watch(self, name):
        """
        Stop functions watching a name being called. You have to call the
        same number of unblock_watch calls as block_calls to remove a blocking.
        """
        global _blocked_watches
        name = self._expand_name(name)
        if name not in _blocked_watches:
            _blocked_watches[name] = 0
        _blocked_watches[name] += 1
    def unblock_watch(self, name):
        global _blocked_watches
        name = self._expand_name(name)
        assert name in _blocked_watches
        assert _blocked_watches[name] > 0
        _blocked_watches[name] -= 1
    def add_watch(self, name, callback):
        global _watches, _watch_counter
        name = self._expand_name(name)
        if name not in _watches:
            _watches[name] = {}
        _watches[name][_watch_counter] = callback
        _watch_counter += 1
        return _watch_counter - 1
    def remove_watch(self, name, id):
        global _watches, _watch_counter
        name = self._expand_name(name)
        if name in _watches:
            if id in _watches[name]:
                del _watches[name][id]
                if _watches[name] == {}:
                    del _watches[name]
            else:
                print "warning: remove_watch: id don't exist:", id
        else:
            print "warning: remove_watch: name is not watched:", name
    def _expand_name(self, name):
        if name.count("/") == 0:
            return "%s/%s" % (self.m_exname, name)
        return name
    ######
    # set
    ######
    def set_string(self, name, val):
        set_string(self._expand_name(name), val)
    def set_int(self, name, val):
        set_int(self._expand_name(name), val)
    def set_float(self, name, val):
        set_float(self._expand_name(name), val)
    def set_bool(self, name, val):
        set_bool(self._expand_name(name), val)
    def set_list(self, name, val):
        set_list(self._expand_name(name), val)
    ######
    # get
    ######
    def _get(self, func, name, default=""):
        return func(self._expand_name(name)+default)
    def get_string(self, name):
        r = self._get(get_string, name)
        if r is None:
            return ""
        else:
            return r
    def get_int(self, name):
        return self._get(get_int, name)
    def get_int_with_default(self, name, default):
        assert type(default) is type(0)
        return self.get_int(name+"=%i" % default)
    def get_float(self, name):
        return self._get(get_float, name)
    def get_bool(self, name):
        return self._get(get_bool, name)
    def get_list(self, name):
        if '=' in name:
            return eval(self._get(get_string, name))
        return eval(self._get(get_string, name, "=[]"))


if __name__ == '__main__':
    import doctest, cfg
    print doctest.testmod(cfg)
