#coding=utf-8
# dump python dict to ini format file
# Author: limodou (limodou@gmail.com)
# Copyleft BSD
# you can see http://wiki.woodpecker.org.cn/moin/Dict4Ini for more details
# and the new source project is in http://code.google.com/p/dict4ini/
#
# manatlan patch (10/05/08):
#   dont save things which should not be saved !
#
# Updates:
# 0.9.5-----------------------
#   2008/03/14
#     Fix __init__.py bug, and you can import dict4ini from dict4ini package
# 0.9.4-----------------------
#   2008/02/16
#     Fix comment process, if you comment with node._comment, then if the comment
#     does not begin with '#'(commentdelimeter), it'll automatically add it.
# 0.9.3-----------------------
#   2007/09/22
#     Improve the comment process
#     Improve empty section process
#     Add normal format support, so each value should be treated string type.
# 0.9.2.5-----------------------
#   2007/09/19
#     Save boolean value to 0 or 1
#     Add iterator support, and you can use for i in ini to iterator (key,value) now
#     Add 'in' test support, so you can test if a key is in a ini via key in ini,
#         it's the same with ini.has_key(key)
# 0.9.2.4-----------------------
#   2007/08/23
#     Fix the end string is \" bug
# 0.9.2.3-----------------------
#   2007/08/08
#     Fix sub section bug, for secretSection argument, dict4ini will encrypt all
#     subsections of a secretSection, for example, secretSection=['a', 'c'], then
#     all subsections of 'a' and 'c' will be encrypted.
# 0.9.2.2-----------------------
#   2007/07/08
#     Add missing __init__.py file.
# 0.9.2.1-----------------------
#   2007/07/09 thanks to Mayowa Akinyemi
#     Add the ability to protect specific sections.
#     When use with the secretKey or hideData only section names listed in the
#     secretSections parameter will be encrypted
# 0.9.2-----------------------
#   2007/07/03
#     Add clear method
#     Added encryption code thanks to Mayowa Akinyemi
#     using the secretKey parameter will encrypt the values using
#     Paul Rubin's p3.py encryption module
#     using the hideData parameter will perform base64 enc/dec of the values
# 0.9.1-----------------------
#   2007/06/26
#     Fix float convert bug
# 0.9-------------------------
#   2007/06/13
#     Thanks for Victor Stinner giving a output format patch, so you can define your own
#     output format "%s = %s" to anything you want, for example "%s=%s" or "%s:%s". And 
#     Dict4Ini will auto remove '%s' from the fromat string, and the strip() the rest of 
#     the string, then use it to split the key and value in Ini file. For example, if you 
#     using "%s:%s", Dict4Ini will get "%s:%s".replace('%s', '').strip(), then use ':' to 
#     split the line.
# 0.8-------------------------
#   2007/04/20
#     Add exception process when parsing file
#     Add BOM detect
# 0.7-------------------------
#   2007/04/19
#     Fix '\' escape
# 0.6-------------------------
#   2006/01/18
#     Fix ordereditems bug.
# 0.5-------------------------
#   2006/01/04
#     Add ordereditems() method, so you can get the items according the ini file definition
#   2005/12/30
#     Support onelevel parameter, you can set it True, than only one level can be used, so that
#        the key can include section delimeter char in it.
#     Support sectiondelimeter parmeter, you can set the section delimeter to which you want
# 0.4-------------------------
#   2005/12/12
#     Fixed "\" bug in option's value
#   2005/12/09
#     Adding dict() method, then you can change the DictIni object to dict type
#   2005/10/16
#     Saving the order of the items
#     Adding float format
#

__version__ = '0.9.5'

import sys
import locale
import os.path

section_delimeter = '/'

import base64
try:
    import p3 as crypt
except:
    crypt = None
    
class DictNode(object):
    def __init__(self, values, encoding=None, root=None, section=[], orders=[], 
            sectiondelimeter=section_delimeter, onelevel=False, format="%s = %s", 
            normal=False, commentdelimeter='#'):
        self._items = values
        self._orders = orders
        self._encoding = encoding
        self._root = root
        self._section = section
        self._section_delimeter = sectiondelimeter
        self._onelevel = onelevel
        self._format = format
        self._normal = normal
        self._commentdelimeter = commentdelimeter

    def __getitem__(self, name):
        if self._items.has_key(name):
            value = self._items[name]
            if isinstance(value, dict):
                return DictNode(value, self._encoding, self._root, self._section + [name], 
                    sectiondelimeter=self._section_delimeter, onelevel=self._onelevel, 
                    format=self._format, normal=self._normal, commentdelimeter=self._commentdelimeter)
            else:
                return value
        else:
            self._items[name] = {}
            self._root.setorder(self.get_full_keyname(name))
            return DictNode(self._items[name], self._encoding, self._root, self._section + [name], 
                sectiondelimeter=self._section_delimeter, onelevel=self._onelevel, 
                format=self._format, normal=self._normal, commentdelimeter=self._commentdelimeter)

    def __setitem__(self, name, value):
        if not self._normal and self._section_delimeter and self._section_delimeter in name:
            if self._onelevel:
                sec = name.split(self._section_delimeter, 1)
            else:
                sec = name.split(self._section_delimeter)
            obj = self._items

            _s = self._section[:]
            for i in sec[:-1]:
                _s.append(i)
                if obj.has_key(i):
                    if isinstance(obj[i], dict):
                        obj = obj[i]
                    else:
                        obj[i] = {} #may lost some data
                        obj = obj[i]
                else:
                    obj[i] = {}
                    self._root.setorder(self._section_delimeter.join(_s))
                    obj = obj[i]
            obj[sec[-1]] = value
            self._root.setorder(self._section_delimeter.join(_s + [sec[-1]]))
        else:
            self._items[name] = value
            self._root.setorder(self.get_full_keyname(name))

    def __delitem__(self, name):
        if self._items.has_key(name):
            del self._items[name]

    def __repr__(self):
        return repr(self._items)

    def __getattr__(self, name):
        return self.__getitem__(name)

    def __setattr__(self, name, value):
        if name.startswith('_'):
            if name == '_comment':
                self._root._comments[self._section_delimeter.join(self._section)] = self._get_comment_value(value)
            else:
                self.__dict__[name] = value
        else:
            self.__setitem__(name, value)

    def _get_comment_value(self, value):
        lines = value.splitlines()
        s = []
        for x in lines:
            if not x.startswith(self._commentdelimeter):
                s.append(self._commentdelimeter + x)
            else:
                s.append(x)
        return '\n'.join(s)
    
    def comment(self, name, comment):
        comment = self._get_comment_value(comment)
        if name:
            self._root._comments[self._section_delimeter.join(self._section + [name])] = comment
        else:
            self._root._comments[self._section_delimeter.join(self._section)] = comment

    def __delattr__(self, name):
        if self._items.has_key(name):
            del self._items[name]

    def __str__(self):
        return repr(self._items)

    def __len__(self):
        return len(self._items)
    
    #add iterator support
    def __iter__(self):
        return self._items.iteritems()

    def has_key(self, name):
        return self._items.has_key(name)
    
    #add in test support
    def __contains__(self, name):
        return self.has_key(name)

    def items(self):
        return self._items.items()

    def setdefault(self, name, value):
        return self._items.setdefault(name, value)

    def get(self, name, default=None):
        return self._items.get(name, default)

    def keys(self):
        return self._items.keys()

    def values(self):
        return self._items.values()

    def get_full_keyname(self, key):
        return self._section_delimeter.join(self._section + [key])

    def ordereditems(self, values, sec=[]):
        s = []
        for key, value in values.items():
            s.append((self._root._orders.get(self._section_delimeter.join(sec + [key]), 99999), key, value))
        s.sort()
        return [(x, y) for z, x, y in s]
    
    def clear(self):
        self._items = {}
        self._orders = []
        self._section = []

class DictIni(DictNode):
    def __init__(self, inifile=None, values=None, encoding=None,
                    commentdelimeter='#', sectiondelimeter=section_delimeter,
                    onelevel=False, format="%s = %s", normal=False,
                    hideData=False, secretKey=None, secretSections=None ):
                        
        self._items = {}
        self._inifile = inifile
        self._root = self
        self._section = []
        self._commentdelimeter = commentdelimeter
        self._comments = {}
        self._orders = {}
        self._ID = 1
        self._encoding = getdefaultencoding(encoding)
        self._section_delimeter = sectiondelimeter
        self._format = format
        self._secretKey = secretKey
        self._hideData = hideData
        self._secretSections = secretSections
        self._onelevel = onelevel
        self._normal = normal
        if type(secretSections) is str:
            self._secretSections = secretSections.split(',')
        assert not self
        if not self._section_delimeter:
            raise Exception, "section_delimeter cann't be empty!"
        if values is not None:
            self._items = values

        if self._inifile and os.path.exists(self._inifile):
            self.read(self._inifile, self._encoding)

    def setfilename(self, filename):
        self._inifile = filename

    def getfilename(self):
        return self._inifile

    def save(self, inifile=None, encoding=None):
        if inifile is None:
            inifile = self._inifile

        if isinstance(inifile, (str, unicode)):
            f = file(inifile, 'w')
        elif isinstance(inifile, file):
            f = inifile
        else:
            f = inifile

        if not f:
            f = sys.stdout

        if encoding is None:
            encoding = self._encoding

        f.write(self._savedict([], self._items, encoding))
        if isinstance(inifile, (str, unicode)):
            f.close()

    def _savedict(self, section, values, encoding):
        if values:
            buf = []
            default = []
            for key, value in self.ordereditems(values, sec=section):
                if isinstance(value, dict):
                    sec = section[:]
                    sec.append(key)
                    buf.append(self._savedict(sec, value, encoding))
                else:
                    c = self._comments.get(self._section_delimeter.join(section + [key]), '')
                    if c:
                        lines = c.splitlines()
                        default.append('\n'.join(lines))

                    default.append(self._format % (key, self.uni_str(value, encoding, section)))
            if default:
                buf.insert(0, '\n'.join(default))
                if section:
                    buf.insert(0, '[%s]' % self._section_delimeter.join(section))
                c = self._comments.get(self._section_delimeter.join(section), '')
                if c:
                    lines = c.splitlines()
                    buf.insert(0, '\n'.join(lines))
            return '\n'.join(buf + [''])
        else:
            #===================================================================
            # manatlan's patch (commented)
            #===================================================================
            #buf = []
            #if section:
            #    buf.insert(0, '[%s=]' % self._section_delimeter.join(section))
            #    c = self._comments.get(self._section_delimeter.join(section), '')
            #    if c:
            #        lines = c.splitlines()
            #        buf.insert(0, '\n'.join(['%s' % x for x in lines]))
            #    return '\n'.join(buf)
            #===================================================================
            return ''

    def dict(self):
        return self._dict(self)

    def _dict(self, v):
        if isinstance(v, tuple):
            return tuple([self._dict(x) for x in v])
        elif isinstance(v, list):
            return [self._dict(x) for x in v]
        elif isinstance(v, (dict, DictNode)):
            d = {}
            for key, value in v.items():
                d[key] = self._dict(value)
            return d
        else:
            return v

    def read(self, inifile=None, encoding=None):
        if inifile is None:
            inifile = self._inifile

        if isinstance(inifile, (str, unicode)):
            try:
                f = file(inifile, 'r')
            except:
                return  #may raise Exception is better
        elif isinstance(inifile, file):
            f = inifile
        else:
            f = inifile

        if not f:
            f = sys.stdin

        if encoding is None:
            encoding = self._encoding

        comments = []
        section = ''
        for lineno, line in enumerate(f.readlines()):
            try:
                if lineno == 0:
                    if line.startswith('\xEF\xBB\xBF'):
                        line = line[3:]
                        encoding = 'utf-8'
                line =  line.strip()
                if not line:
                    if comments:
                        comments.append('')
                        continue
                    else:
                        continue
                if line.startswith(self._commentdelimeter):
                    comments.append(line.rstrip())
                    continue
                if line.startswith('['):    #section
                    section = line[1:-1]
                    #if comment then set it
                    if comments:
                        self.comment(section, '\n'.join(comments))
                        comments = []
                    self.__setitem__(section, {})
                    continue
                key, value = line.split(self._format.replace('%s', '').strip(), 1)
                key = key.strip()
                value = self.process_value(value.strip(), encoding, section)
                if section:
                    if self._normal:
                        self[section][key] = value
                    else:
                        self.__setitem__(section + self._section_delimeter + key, value)
                    #if comment then set it
                    if comments:
                        self[section].comment(key, '\n'.join(comments))
#                        self.__getitem__(section).comment(key, '\n'.join(comments))
                        comments = []
                else:
                    self[key] = value
#                    self.__setitem__(key, value)
                    #if comment then set it
                    if comments:
                        self.comment(key, '\n'.join(comments))
                        comments = []
            except Exception, err:
                import traceback
                traceback.print_exc()
                print 'Error in [line %d]: %s' % (lineno, line)
                print line
        if isinstance(inifile, (str, unicode)):
            f.close()

    def setorder(self, key):
        if not self._orders.has_key(key):
            self._orders[key] = self._ID
            self._ID += 1
            
    def clear(self):
        self._items = {}
        self._section = []
        self._comments = {}
        self._orders = {}
        
    def process_value(self, value, encoding=None, section=None):
        value = self.protect_value(value, 1, section)
        
        if self._normal:
            return value
        
        length = len(value)
        t = value
        i = 0
        r = []
        buf = []
        listflag = False
        while i < length:
            if t[i] == '"': #string quote
                buf.append(t[i])
                i += 1
                while t[i] != '"':
                    if t[i] == '\\':
                        buf.append(t[i])
                        buf.append(t[i+1])
                        i += 2
                    else:
                        buf.append(t[i])
                        i += 1
                buf.append(t[i])
                i += 1
            elif t[i] == ',':
                r.append(''.join(buf).strip())
                buf = []
                i += 1
                listflag = True
            elif t[i] == 'u':
                buf.append(t[i])
                i += 1
            else:
                buf.append(t[i])
                i += 1
                while i < length and t[i] != ',':
                    buf.append(t[i])
                    i += 1
        if buf:
            r.append(''.join(buf).strip())
        result = []
        for i in r:
            if i.isdigit():
                result.append(int(i))
            elif i and i.startswith('u"'):
                result.append(unicode(unescstr(i[1:]), encoding))
            else:
                try:
                    b = float(i)
                    result.append(b)
                except:
                    result.append(unescstr(i))
    
        if listflag:
            return result
        elif result:
            return result[0]
        else:
            return ''
    
    def protect_value(self, value, mode=0, section=None):
        if self._secretSections is not None and section is not None:
            for s in self._secretSections:
                if section.startswith(s):
                    break
            else:
                return value
            
        if mode == 0:
            # encrypt
            if crypt != None and self._secretKey != None :
                cipher = crypt.p3_encrypt(value, self._secretKey)
                return base64.b64encode(cipher)
            elif self._hideData is True:
                return base64.b64encode(value)
        else:
            # decrypt
            if crypt != None and self._secretKey != None :
                cipher = base64.b64decode(value)
                return crypt.p3_decrypt(cipher, self._secretKey)
            elif self._hideData is True:
                return base64.b64decode(value)

        return value
    
    def uni_str(self, a, encoding=None, section=None):
        if section and isinstance(section, list):
            current_section = section[0]
        else:
            current_section = section
        
        if self._normal:
            if isinstance(a, (int, float, str)):
                v = str(a)
            elif isinstance(a, unicode):
                v = a.encode(encoding)
            else:
                print 'Error to save: does not support this type %r' % type(a)
                return
        else:
            v = uni_prt(a, encoding)
        return self.protect_value(v, section=self._section_delimeter.join(section))
    
    
unescapechars = {'"':'"', 't':'\t', 'r':'\r', 'n':'\n', '\\':'\\', 'a':'\a', 'f':'\f', 
    "'":"'", 'b':'\b', 'v':'\v'}
def unescstr(value):
    if value.startswith('"') and value.endswith('"') or value.startswith("'") and value.endswith("'"):
        s = []
        i = 1
        end = len(value) - 1
        while i < end:
            if value[i] == '\\' and unescapechars.has_key(value[i+1]):
                s.append(unescapechars[value[i+1]])
                i += 2
            else:
                s.append(value[i])
                i += 1
        value = ''.join(s)
    return value

def escstr(value):
    s = []
    for c in value:
        if c == "'":
            s.append(c)
            continue
        v = repr(c).replace('"', r'\"')
        if '\\' in v and ord(c)<128:
            if isinstance(c, str):
                s.append(v[1:-1])
            else:
                s.append(v[2:-1])
        else:
            s.append(c)
    return ''.join(s)

def uni_prt(a, encoding=None):
    s = []
    if isinstance(a, (list, tuple)):
        for i, k in enumerate(a):
            s.append(uni_prt(k, encoding))
            s.append(',')
    elif isinstance(a, str):
        t = escstr(a)
        if (' ' in t) or (',' in t) or ('"' in t) or t.isdigit() or ('\\' in t):
            s.append('"%s"' % t)
        else:
            s.append("%s" % t)
    elif isinstance(a, unicode):
        t = escstr(a)
        s.append('u"%s"' % t.encode(encoding))
    elif isinstance(a, bool):
        s.append(str(int(a)))
    else:
        s.append(str(a))

    return ''.join(s)

def getdefaultencoding(encoding):
    import codecs

    if not encoding:
        encoding = locale.getdefaultlocale()[1]
    try:
        codecs.lookup(encoding)
    except:
        encoding = 'utf-8'
    return encoding



if __name__ == '__main__':
    #d = DictIni('t.ini', format="%s:%s", secretKey='mayowa')
    #print d
    #d._comment = 'Test\nTest2'
    #d.a = 'b'
    #d.b = 1
    #d['b'] = 3
    #d.c.d = (1,2,'b asf "aaa')
    #d['s']['t'] = u'中国'
    #d['s'].a = 1
    #d['m/m'] = 'testing'
    #d['p'] = r'\?'
    #d.t.m.p = 'd:\\'
    #print d
    #d.save()
    #
    #d = DictIni('t.ini', format="%s:%s", secretKey='mayowa')
    #print d.p, d.t.m.p
    #
    #d = DictIni('t2.ini', format="%s:%s", hideData=True)
    #d.a.a = 'mama'
    #d.a.b = 'lubs me!'
    #
    #d.b.a = 'i'
    #d.b.b = 'lub bosunmi!'
    #
    #d.c.a = 'dada'
    #d.c.b = 'lubs me too!'
    #d.save()
    #
    #d = DictIni('t2.ini', format="%s:%s", hideData=True)
    #print d.a.a, d.a.b
    #print d.b.a, d.b.b
    #print d.c.a, d.c.b
    #
    ## secret sections test
    #d = DictIni('t3.ini', format="%s:%s", hideData=True, secretSections=['a', 'c/c'])
    #d.a.a = 'mama'
    #d.a.b = 'lubs me!'
    #
    #d.b.a = 'i'
    #d.b.b = 'lub bosunmi!'
    #
    #d.c.a = 'dada'
    #d.c.b = 'lubs me too!'
    #d.c.c.a = 'far out!'
    #d.c.c.b.a = 'ppppp'
    #
    #d.save()
    #
    #d = DictIni('t3.ini', format="%s:%s", hideData=True, secretSections=['a', 'c/c'])
    #print d.a.a, d.a.b
    #print d.b.a, d.b.b
    #print d.c.a, d.c.b
    #print d.c.c.b.a


    d = DictIni('jojo.ini')
    d.kij.jim=12
    print d.kij.jo  # should'nt appears in ini file !!! (manatlan's patch)
    d.save()
    
