#!/usr/local/bin/python2.7
# -*- coding: utf-8; indent-tabs-mode: nil -*-

import sys
import re
import os
import inspect
import locale
import subprocess
import getopt
import MySQLdb
import binascii
import warnings

# Some older MySQLdb versions return a list of bytes for VARBINARY fields.
# Convert those to string..
def dbresult2string(dbres):
    if isinstance(dbres, basestring):
        return dbres
    return dbres.tostring()

try:
    from progressbar import ProgressBar, Percentage, Bar, ETA
    def create_pbar(name, total):
        return ProgressBar(widgets=[name, ' ', Bar(left='[',right=']'), ' ', Percentage(), ' ', ETA()], maxval=total)
        
except ImportError:
    class simple_progress:
        def __init__(self, name, total):
            self.name = name
            self.total = total
            self.update(0)
        def update(self, progress):
            self.progress = progress
            self._doPrint()
        def finish(self):
            print ''
        def _doPrint(self):
            sys.stdout.write('\r%s: %u / %u (%d%%)' % (self.name, self.progress, self.total, self.progress * 100 / self.total))
            sys.stdout.flush()
            
    def create_pbar(name, total):
        return simple_progress(name, total)
            

class UpdateException(Exception):
    def __init__(self):
        pass


class update_descriptor:
    def __init__(self, dbrev, desc):
        self.dbrev = dbrev
        self.desc = desc
    def __call__(self, func):
        func.update_db_revision = self.dbrev
        func.update_description = self.desc
        return func


class progress_wrapper:
    def __init__(self, name):
        self.name = name
        self.pbar = None
    def setTotal(self, total):
        self.pbar = create_pbar(self.name, total)
    def setProgress(self, progress):
        self.pbar.update(progress)
    def finish(self):
        self.pbar.finish()


wtf1252_translate_table = u'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\u20ac\x00\u201a\u0192\u201e\u2026\u2020\u2021\u02c6\u2030\u0160\u2039\u0152\x00\u017d\x00\x00\u2018\u2019\u201c\u201d\u2022\u2013\u2014\u02dc\u2122\u0161\u203a\u0153\x00\u017e\u0178\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff'
def decode_wtf1252(s):
    return s.decode('utf-8').translate(wtf1252_translate_table)


@update_descriptor(46, 'Converting database to Unicode')
def UpdateDatabaseConvertToUnicode(db, cursor, progress_callback):
    progress_callback.setTotal(5);

    cursor.execute("UPDATE objectproperty SET value = hex(value) WHERE propname = 'companyid'")
    progress_callback.setProgress(1)
    
    cursor.execute("ALTER TABLE mvproperties MODIFY val_string longtext CHARSET utf8 COLLATE utf8_general_ci")
    progress_callback.setProgress(2)
    
    cursor.execute("ALTER TABLE objectproperty MODIFY propname VARCHAR(255) CHARSET utf8 COLLATE utf8_general_ci, MODIFY value TEXT CHARSET utf8 COLLATE utf8_general_ci")
    progress_callback.setProgress(3)
    
    cursor.execute("ALTER TABLE objectmvproperty MODIFY propname VARCHAR(255) CHARSET utf8 COLLATE utf8_general_ci, MODIFY value TEXT CHARSET utf8 COLLATE utf8_general_ci")
    progress_callback.setProgress(4)

    cursor.execute("UPDATE objectproperty SET value = concat(substr(value,1,instr(value,';')-1),';',hex(substr(value,instr(value,';')+1))) WHERE propname = 'companyadmin'")
    progress_callback.setProgress(5)
    progress_callback.finish()
    

@update_descriptor(47, 'Update stores table usernames')
def UpdateDatabaseConvertStoreUsername(db, cursor, progress_callback):
    progress_callback.setTotal(2);

    cursor.execute("UPDATE stores SET user_name = CAST(CONVERT(user_name USING latin1) AS CHAR(255) CHARACTER SET utf8)")
    progress_callback.setProgress(1)

    cursor.execute("ALTER TABLE stores MODIFY user_name VARCHAR(255) CHARACTER SET utf8 NOT NULL DEFAULT ''")
    progress_callback.setProgress(2)
    progress_callback.finish()


@update_descriptor(48, 'Converting rules to Unicode')
def UpdateDatabaseConvertRules(db, cursor, progress_callback):
    cursor.execute("SELECT p.hierarchyid, p.storeid, p.val_binary FROM properties AS p JOIN receivefolder AS r ON p.hierarchyid=r.objid AND p.storeid=r.storeid JOIN stores AS s ON r.storeid=s.hierarchy_id WHERE p.tag=0x3fe1 AND p.type=0x102 AND r.messageclass='IPM'")
    results = cursor.fetchall()
    if len(results) == 0:
        return
    progress_callback.setTotal(len(results));
    
    processed = 0
    for result in results:
        new_rule_data = decode_wtf1252(dbresult2string(result[2])).encode('utf-8')
        cursor.execute("UPDATE properties SET val_binary=0x%s WHERE hierarchyid=%u AND storeid=%u AND tag=0x3fe1 AND type=0x102" % (binascii.hexlify(new_rule_data), result[0], result[1]))
        processed += 1

        progress_callback.setProgress(processed)

    progress_callback.finish()


@update_descriptor(49, 'Converting search folders to Unicode')
def UpdateDatabaseConvertSearchFolders(db, cursor, progress_callback):
    cursor.execute("SELECT h.id, p.storeid, p.val_string FROM hierarchy AS h JOIN properties AS p ON p.hierarchyid=h.id AND p.tag=0x6706 AND p.type=0x1e WHERE h.type=3 AND h.flags=2")
    results = cursor.fetchall()
    if len(results) == 0:
        return
    progress_callback.setTotal(len(results));
    
    processed = 0    
    for result in results:
        new_rule_data = decode_wtf1252(dbresult2string(result[2])).encode('utf-8')
        cursor.execute("UPDATE properties SET val_string=0x%s WHERE hierarchyid=%u AND storeid=%u AND tag=0x6706 AND type=0x1e" % (binascii.hexlify(new_rule_data), result[0], result[1]))
        processed += 1

        progress_callback.setProgress(processed)

    progress_callback.finish()


@update_descriptor(50, 'Converting properties for IO performance')
def UpdateDatabaseConvertProperties(db, cursor, progress_callback):
    table_def = """CREATE TABLE IF NOT EXISTS `properties_temp` (
                       `hierarchyid` int(11) unsigned NOT NULL default '0',
                       `tag` smallint(6) unsigned NOT NULL default '0',
                       `type` smallint(6) unsigned NOT NULL,
                       `val_ulong` int(11) unsigned default NULL,
                       `val_string` longtext,
                       `val_binary` longblob,
                       `val_double` double default NULL,
                       `val_longint` bigint(20) default NULL,
                       `val_hi` int(11) default NULL,
                       `val_lo` int(11) unsigned default NULL,
                       `comp` bool default false,
                       PRIMARY KEY `ht` (`hierarchyid`,`tag`,`type`)
                   ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci"""

    cursor.execute(table_def)

    # Get the max hierarchyid so we can have a progress estimate
    cursor.execute("SELECT MAX(hierarchyid) FROM properties")
    total = cursor.fetchone()[0]
    if total is None:
        total = 1   # Avoid division by zero in setProgress
    progress_callback.setTotal(total +1)

    while (True):
        # Using the hi key makes it less efficient, but makes it possible to have a meaningful progress
        cursor.execute("INSERT IGNORE INTO properties_temp (hierarchyid,tag,type,val_ulong,val_string,val_binary,val_double,val_longint,val_hi,val_lo) SELECT hierarchyid,tag,type,val_ulong,val_string,val_binary,val_double,val_longint,val_hi,val_lo FROM properties ORDER BY hierarchyid ASC LIMIT 10000")
        cursor.execute("DELETE FROM properties ORDER BY hierarchyid ASC LIMIT 10000")
        cursor.execute("COMMIT")
        cursor.execute("BEGIN")

        cursor.execute("SELECT MIN(hierarchyid) FROM properties")
        progress = cursor.fetchone()[0]
        if progress is None:
            break

        progress_callback.setProgress(progress)

    progress_callback.setProgress(total)

    # we now have a 640 utf8 webaccess-settings string, encoded as latin1 to utf8, called luft8²
    # convert this through binary, so mysql doesn't use iconv, but leaves the converted data as utf8
    cursor.execute("UPDATE properties_temp JOIN hierarchy ON properties_temp.hierarchyid=hierarchy.id AND hierarchy.parent IS NULL SET val_string = CAST(CAST(CONVERT(val_string USING latin1) AS binary) AS CHAR CHARACTER SET utf8) WHERE properties_temp.type=0x1e AND properties_temp.tag=26480")

    progress_callback.setProgress(total+1)

    cursor.execute("RENAME TABLE properties TO properties_old, properties_temp TO properties")
    cursor.execute("DROP TABLE properties_old")
    progress_callback.finish()


@update_descriptor(51, 'Creating counters for IO performance')
def UpdateDatabaseCreateCounters(db, cursor, progress_callback):
    counter_info = (
        ( 0x3602,   0x0003, 5,  0x0040|0x0400,          0,              'COUNT(*)' ),
        ( 0x3603,   0x0003, 5,  0x0040|0x0400|0x0001,   0,              'SUM(IF(flags&1,0,1))' ),
        ( 0x3617,   0x0003, 5,  0x0040|0x0400,          0x0040,         '0' ),
        ( 0x6640,   0x0003, 5,  0x0040|0x0400,          0x0400,         '0' ),
        ( 0x6643,   0x0003, 5,  0x0040|0x0400,          0x0040|0x0400,  '0' ),
        ( 0x360A,   0x000B, 3,  0x0400,                 0,              '0' ),
        ( 0x6638,   0x0003, 3,  0x0400,                 0,              '0' ),
        ( 0x6641,   0x0003, 3,  0x0400,                 0x0400,         '0' )
    )

    properties_query = """REPLACE INTO properties(hierarchyid,tag,type,val_ulong)
                              SELECT parent.id,%s,%s,COUNT(child.id)
                              FROM hierarchy AS parent
                                  LEFT JOIN hierarchy AS child ON parent.id=child.parent AND
                                                              parent.type=3 AND child.type=%s
                                                              AND (child.flags & %s)=%s
                              GROUP BY parent.id"""

    searchfld_query = """REPLACE INTO properties(hierarchyid,tag,type,val_ulong)
                             SELECT folderid,%s,%s,%s FROM searchresults GROUP BY folderid"""

    progress_callback.setTotal(len(counter_info) * 2)

    progress = 0
    for ci in counter_info:
        cursor.execute(properties_query, ci[0:5])
        progress += 1
        progress_callback.setProgress(progress)

        cursor.execute(searchfld_query % (ci[0], ci[1], ci[5]))
        progress += 1
        progress_callback.setProgress(progress)

    progress_callback.finish()


@update_descriptor(52, 'Creating common properties for IO performance')
def UpdateDatabaseCreateCommonProps(db, cursor, progress_callback):
    progress_callback.setTotal(4)

    query = """REPLACE INTO properties(hierarchyid,tag,type,val_hi,val_lo,val_ulong)
                   SELECT h.id,0x3007,0x0040,(UNIX_TIMESTAMP(h.createtime) * 10000000 + 116444736000000000) >> 32,(UNIX_TIMESTAMP(h.createtime) * 10000000 + 116444736000000000) & 0xffffffff, NULL
                   FROM hierarchy AS h
                       WHERE h.type IN (3,5,7)"""
    cursor.execute(query)
    progress_callback.setProgress(1)

    query = """REPLACE INTO properties(hierarchyid,tag,type,val_hi,val_lo,val_ulong)
                   SELECT h.id,0x3008,0x0040,(UNIX_TIMESTAMP(h.modtime) * 10000000 + 116444736000000000) >> 32,(UNIX_TIMESTAMP(h.modtime) * 10000000 + 116444736000000000) & 0xffffffff, NULL
                   FROM hierarchy AS h
                       WHERE h.type IN (3,5,7)"""
    cursor.execute(query)
    progress_callback.setProgress(2)

    query = """REPLACE INTO properties(hierarchyid,tag,type,val_hi,val_lo,val_ulong)
                   SELECT h.id,0x0e07,0x0003,NULL, NULL, h.flags
                   FROM hierarchy AS h
                       WHERE h.type IN (3,5,7)"""
    cursor.execute(query)
    progress_callback.setProgress(3)

    query = """REPLACE INTO properties(hierarchyid,tag,type,val_hi,val_lo,val_ulong)
                   SELECT h.id,0x3601,0x0003,NULL, NULL, h.flags & 0x3
                   FROM hierarchy AS h
                       WHERE h.type=3"""
    cursor.execute(query)
    progress_callback.setProgress(4)
    progress_callback.finish()


@update_descriptor(53, 'Creating message attachment properties for IO performance')
def UpdateDatabaseCheckAttachments(db, cursor, progress_callback):
    progress_callback.setTotal(2)

    query = """REPLACE INTO properties(hierarchyid,tag,type,val_ulong)
                   SELECT h.id,0x0e1b,0x000b,IF(att.id,1,0)
                       FROM hierarchy AS h
                           LEFT JOIN hierarchy AS att ON h.id=att.parent AND att.type=7
                   GROUP BY h.id"""
    cursor.execute(query)
    progress_callback.setProgress(1)

    query = """UPDATE properties AS p
                   JOIN hierarchy AS h ON p.hierarchyid=h.id AND h.type=5
                   LEFT JOIN hierarchy AS c ON c.type=7 AND c.parent=p.hierarchyid
               SET p.val_ulong = IF(c.id,p.val_ulong|0x0400, p.val_ulong & ~0x0400)
               WHERE p.tag=0x0e07 AND p.type=0x0003"""
    cursor.execute(query)
    progress_callback.setProgress(2)
    progress_callback.finish()


@update_descriptor(54, 'Creating tproperties for IO performance')
def UpdateDatabaseCreateTProperties(db, cursor, progress_callback):
    table_def = """CREATE TABLE IF NOT EXISTS `tproperties` (
                       `folderid` int(11) unsigned NOT NULL default '0',
                       `hierarchyid` int(11) unsigned NOT NULL default '0',
                       `tag` smallint(6) unsigned NOT NULL default '0',
                       `type` smallint(6) unsigned NOT NULL,
                       `val_ulong` int(11) unsigned default NULL,
                       `val_string` longtext,
                       `val_binary` longblob,
                       `val_double` double default NULL,
                       `val_longint` bigint(20) default NULL,
                       `val_hi` int(11) default NULL,
                       `val_lo` int(11) unsigned default NULL,
                       PRIMARY KEY `ht` (`folderid`,`tag`,`hierarchyid`,`type`),
                       KEY `hi` (`hierarchyid`)
                   ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci"""

    query = """INSERT IGNORE INTO tproperties (folderid,hierarchyid,tag,type,val_ulong,val_string,val_binary,val_double,val_longint,val_hi,val_lo)
                  SELECT h.id, p.hierarchyid, p.tag, p.type, p.val_ulong, LEFT(p.val_string,255), LEFT(p.val_binary,255), p.val_double, p.val_longint, p.val_hi, p.val_lo
                  FROM properties AS p
                      JOIN hierarchy AS tmp ON p.hierarchyid = tmp.id AND p.tag NOT IN (0x1013,0x1009) AND p.hierarchyid >= %s AND p.hierarchyid < %s
                      JOIN hierarchy AS h ON tmp.parent = h.id AND h.type = 3"""

    cursor.execute(table_def)

    # Get the max hierarchyid so we can have a progress estimate
    cursor.execute("SELECT MAX(hierarchyid) FROM properties")
    total = cursor.fetchone()[0]
    progress_callback.setTotal(total)

    h_id = 0
    while (h_id < total):
        cursor.execute(query, (h_id, h_id + 500))
        progress = h_id + 500
        if progress > total:
            progress = total
        progress_callback.setProgress(progress)
        h_id += 500
    progress_callback.finish()


@update_descriptor(55, 'Converting hierarchy for IO performance')
def UpdateDatabaseConvertHierarchy(db, cursor, progress_callback):
    table_def = """CREATE TABLE IF NOT EXISTS `hierarchy_temp` (
                       `id` int(11) unsigned NOT NULL auto_increment,
                       `parent` int(11) unsigned default '0',
                       `type` tinyint(4) unsigned NOT NULL default '0',
                       `flags` smallint(6) unsigned NOT NULL default '0',
                       `owner` int(11) unsigned NOT NULL default '0',
                       PRIMARY KEY  (`id`),
                       KEY `parenttypeflags` (`parent`, `type`, `flags`)
                   ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci"""

    cursor.execute(table_def)
    
    # Get the max hierarchyid so we can have a progress estimate
    cursor.execute("SELECT MAX(id) FROM hierarchy")
    total = cursor.fetchone()[0]
    if total is None:
        total = 1   # Avoid division by zero in setProgress
    progress_callback.setTotal(total)

    while (True):
        cursor.execute("INSERT INTO hierarchy_temp (id, parent, type, flags, owner) SELECT id, parent, type, CASE type WHEN 3 THEN flags & 0x403 WHEN 5 THEN flags & 0x440 ELSE flags & 0x400 END, owner FROM hierarchy ORDER BY id LIMIT 10000")
        cursor.execute("DELETE FROM hierarchy ORDER BY id LIMIT 10000")
        cursor.execute("COMMIT")
        cursor.execute("BEGIN")

        cursor.execute("SELECT MIN(id) FROM hierarchy")
        progress = cursor.fetchone()[0]
        if progress is None:
            break
            
        progress_callback.setProgress(progress)

    progress_callback.setProgress(total)

    cursor.execute("RENAME TABLE hierarchy TO hierarchy_old, hierarchy_temp TO hierarchy")
    cursor.execute("DROP TABLE hierarchy_old")
    progress_callback.finish()


@update_descriptor(56, 'Creating deferred table for IO performance')
def UpdateDatabaseCreateDeferred(db, cursor, progress_callback):
    progress_callback.setTotal(1)
    table_def = """CREATE TABLE `deferredupdate` (
                       `hierarchyid` int(11) unsigned NOT NULL,
                       `folderid` int(11) unsigned NOT NULL,
                       `srcfolderid` int(11) unsigned,
                       PRIMARY KEY(`hierarchyid`),
                       KEY `folderid` (`folderid`)
                   ) ENGINE=InnoDB"""
    cursor.execute(table_def)
    progress_callback.setProgress(1)
    progress_callback.finish()


@update_descriptor(57, 'Converting changes for IO performance')
def UpdateDatabaseConvertChanges(db, cursor, progress_callback):
    table_def = """CREATE TABLE IF NOT EXISTS `changes_temp` (
                       `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
                       `sourcekey` VARBINARY(64) NOT NULL,
                       `parentsourcekey` VARBINARY(64) NOT NULL,
                       `change_type` INT(11) UNSIGNED NULL,
                       `flags` INT(11) UNSIGNED DEFAULT NULL,
                       `sourcesync` INT(11) UNSIGNED DEFAULT NULL,
                       PRIMARY KEY (`parentsourcekey`,`sourcekey`,`change_type`),
                       UNIQUE KEY `changeid` (`id`),
                       UNIQUE KEY `state` (`parentsourcekey`,`id`)
                   ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci"""

    cursor.execute(table_def)
    
    # Get the max changeid so we can have a progress estimate
    cursor.execute("SELECT MAX(id) FROM changes")
    total = cursor.fetchone()[0]
    if total is None:
        total = 1   # Avoid division by zero in setProgress
    progress_callback.setTotal(total)

    while (True):
        # Using the changeid key makes it less efficient, but makes it possible to have a meaningful progress
        cursor.execute("INSERT INTO changes_temp (id, sourcekey, parentsourcekey, change_type, flags, sourcesync) SELECT id, sourcekey, parentsourcekey, change_type, flags, sourcesync FROM changes ORDER BY id ASC LIMIT 10000")
        cursor.execute("DELETE FROM changes ORDER BY id ASC LIMIT 10000")
        cursor.execute("COMMIT")
        cursor.execute("BEGIN")

        cursor.execute("SELECT MIN(id) FROM changes")
        progress = cursor.fetchone()[0]
        if progress is None:
            break
            
        progress_callback.setProgress(progress)

    progress_callback.setProgress(total)

    cursor.execute("RENAME TABLE changes TO changes_old, changes_temp TO changes")
    cursor.execute("DROP TABLE changes_old")
    progress_callback.finish()

    
@update_descriptor(58, 'Converting names table to Unicode')
def UpdateDatabaseConvertNames(db, cursor, progress_callback):
    table_def = """CREATE TABLE IF NOT EXISTS `names_temp` (
                       `id` int(11) NOT NULL auto_increment,
                       `nameid` int(11) default NULL,
                       `namestring` varchar(255) binary default NULL,
                       `guid` blob NOT NULL,
                       PRIMARY KEY  (`id`),
                       KEY `nameid` (`nameid`),
                       KEY `namestring` (`namestring`),
                       KEY `guidnameid` (`guid`(16),`nameid`),
                       KEY `guidnamestring` (`guid`(16),`namestring`)
                   ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci"""

    cursor.execute(table_def)

    cursor.execute("SELECT max(id) FROM names")
    results = cursor.fetchall()
    if len(results) == 0:
        return
    maxid = results[0][0]
    if maxid == 0:
        return

    progress_callback.setTotal(maxid)
    warnings.simplefilter("error", Warning)

    for i in xrange(1,maxid+1):
        try:
            cursor.execute("INSERT INTO names_temp (id,nameid,namestring,guid) SELECT id,nameid,CAST(CAST(CONVERT(namestring USING latin1) AS binary) AS CHAR CHARACTER SET utf8),guid FROM names WHERE id = %d" % i)
        except:
            # retry as windows-1252
            cursor.execute("REPLACE INTO names_temp (id,nameid,namestring,guid) SELECT id,nameid,CAST(namestring AS CHAR CHARACTER SET utf8),guid FROM names WHERE id = %d" % i)
        progress_callback.setProgress(i)

    warnings.resetwarnings()
    
    cursor.execute("RENAME TABLE names TO names_old, names_temp TO names")
    cursor.execute("DROP TABLE names_old")
    progress_callback.finish()


def ReadConfig(filename):
    r = re.compile(r'^\s*(\S+)\s*=\s*([^\n\r]*?)[\s\n\r]?$')
    l = []
    for line in open(filename, 'r'):
        if line.startswith('#') or line.startswith('!'):    # comment or special directive
            continue
        m = r.match(line)
        if m:
            l.append(m.groups())
    return dict(l)
    

def ConnectDB(config):
    host = 'localhost'
    port = 3306
    user = 'root'
    password = ''
    database = 'zarafa'

    if 'mysql_host' in config: host = config['mysql_host']
    if 'mysql_port' in config: port = int(config['mysql_port'])
    if 'mysql_user' in config: user = config['mysql_user']
    if 'mysql_password' in config: password = config['mysql_password']
    if 'mysql_database' in config: database = config['mysql_database']

    return MySQLdb.connect(host=host, port=port, user=user, passwd=password, db=database)


def ExpandUpdates():
    updates = []
    for item in inspect.getmembers(sys.modules[__name__], inspect.isfunction):
        func = item[1]
        if hasattr(func, 'update_db_revision'):
            updates.append(func)
    return sorted(updates, key = lambda func: func.update_db_revision)


def GetZarafaVersion():
    r = re.compile(r'^Product version:\s*(\d+),(\d+),\d+,(\d+)\s*$')
    result = None
    try:
        p = subprocess.Popen(['zarafa-server', '-V'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        for line in p.stdout:
            m = r.match(line)
            if not m is None:
                result = m.groups()
        p.wait()
    except: pass
    return result

def GetTempSize():
    r = re.compile(r'^/\S+\s+\d+\s+\d+\s+(\d+)')
    try:
        p = subprocess.Popen(['df', '-P','-B1',os.getenv('TEMP','/tmp')], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        for line in p.stdout:
            m = r.match(line)
            if not m is None:
                return int(m.groups()[0])
        p.wait()
    except: pass
    return 0

def StorageSize(size):
    KiB = 1000
    MiB = 1000 * KiB
    GiB = 1000 * MiB
    if (size > GiB):
        return str(size / MiB) + " MB"
    if (size > MiB):
        return str(size / KiB) + " KB"
    return str(size) + " B"

def UpdateVersion(cursor, zarafa_version, databaserevision):
    cursor.execute("INSERT INTO versions (major, minor, revision, databaserevision, updatetime) VALUES (%s, %s, %s, %u,NOW())" % (zarafa_version[0], zarafa_version[1], zarafa_version[2], databaserevision))


def ValidateDBVersions(db):
    cursor = db.cursor()
    cursor.execute('SELECT DISTINCT databaserevision FROM versions ORDER BY databaserevision ASC')
    result = cursor.fetchone()
    if result is None:
        print "No entries in the versions table. This indicates a fresh install. No need"
        print "to run this tool."
        sys.exit(1)
    rev = result[0]
    results = cursor.fetchall()
    for result in results:
        rev += 1
        if result[0] != rev:
            if rev == 4 or rev == 23:
                # skip non-mandatory revisions
                rev += 1
            else:
                print "Your database is not fully up to date. Please start your server and let it"
                print "upgrade as far as possible. It will stop and inform you once it can"
                print "be upgraded with this tool."
                sys.exit(1)
    if rev < 45:
        print "Your database is not up to date. Please start your server and let it"
        print "upgrade as far as possible. It will stop and inform you once it can"
        print "be upgraded with this tool"
        sys.exit(1)
    return rev

def GetPropertiesTableSize(db):
    cursor = db.cursor()
    cursor.execute('SHOW TABLE STATUS WHERE name = \'properties\'')
    result = cursor.fetchone()
    if result is None:
        print "No properties table size entry in the database. This indicates a fresh install."
        print "No need to run this tool."
        sys.exit(1)
    return result[6]

def UpdateDB(db):
    # get database size, compare to disksize
    psize = GetPropertiesTableSize(db)
    tsize = GetTempSize()
    if (tsize < psize * 2):
        print "You may not have enough free diskspace on " + os.getenv('TEMP','/tmp')
        print "Properties table size: " + StorageSize(psize)
        print "Diskfree size: " + StorageSize(tsize)
        print "Press <ENTER> to continue anyway, or CTRL-C to stop"
        try:
            raw_input()
        except:
            sys.exit(1)

    # First check the version, should be at least 45
    dbrevision = ValidateDBVersions(db)

    zarafa_version = GetZarafaVersion()
    if zarafa_version is None:
        print "Unable to determine Zarafa version, make sure Zarafa is installed"
        print "on this system."
        sys.exit(1)

    updates = ExpandUpdates()
    for update in updates:
        if update.update_db_revision <= dbrevision:
            print "Skipping update: %s" % update.update_description
            continue
        #print "Starting update: %s" % update.update_description
        p = progress_wrapper(update.update_description)
        try:
            cursor = db.cursor()
            cursor.execute("BEGIN")
            update(db, cursor, p)
            UpdateVersion(cursor, zarafa_version, update.update_db_revision)
            cursor.execute("COMMIT")
            #print "Update completed: %s" % update.update_description
        except UpdateException, ue:
            cursor.execute("ROLLBACK")
            print "Update failed: %s" % update.update_description
            print "  Reason: %s" % ue.message
            sys.exit(1)


def print_help():
    print ''
    print 'Usage: zarafa7-upgrade [options] [server-config]'
    print 'Options:'
    print '  -h | --help    Print this help message and exit.'
    print ''
    print 'If no server configuration is provided /etc/zarafa/server.cfg'
    print 'will be used.'
    print ''

def main(argv = None):
    if argv is None:
        argv = sys.argv

    try:
        opts, args = getopt.gnu_getopt(argv[1:], "h", ["help"])
    except getopt.GetoptError, err:
        # print help information and exit:
        print str(err)
        return 1

    for o, a in opts:
        if o in ('-h', '--help'):
            print_help()
            sys.exit(0)
        else:
            assert False, "unhandled option"

    if len(args) > 1:
        print >> sys.stderr, 'At most one configuration file can be provided.'
        return 1


    configfile = '/etc/zarafa/server.cfg'
    if len(args) >= 1:
        configfile = args[0]

    config = ReadConfig(configfile)
    db = ConnectDB(config)
    UpdateDB(db)
    return 0


if __name__ == '__main__':
    locale.setlocale(locale.LC_ALL, '')
    sys.exit(main())
