backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, AppService, DialogService, gettextCatalog) {

    var apputils = this;

    this.exampleOptionString = '--dblock-size=100MB';

    function setMomentLocale() {
        try {
            var browser_lang = navigator.languages ?
                navigator.languages[0] :
                (navigator.language || navigator.userLanguage);
            moment.locale($cookies.get('ui-locale') ? $cookies.get('ui-locale') : browser_lang);
        } catch (e) {
        }
    }
    setMomentLocale();
    $rootScope.$on('ui_language_changed', setMomentLocale);

    this.formatSizeString = function(val) {
        var formatSizes = ['TB', 'GB', 'MB', 'KB'];
        val = parseInt(val || 0);
        var max = formatSizes.length;
        for(var i = 0; i < formatSizes.length; i++) {
            var m = Math.pow(1024, max - i);
            if (val > m) {
                return (val / m).toFixed(2) + ' ' + formatSizes[i];
            }
        }

        return val + ' bytes';
    };

    this.watch = function(scope, m) {
        scope.$on('apputillookupschanged', function() {
            if (m) m();

            $timeout(function() {
                scope.$digest();
            });
        });

        if (m) $timeout(m);
    };

    this.getEntryTypeFromIconCls = function(cls)
    {
        // Entry type is used in as the ALT entry,
        // to guide screen reading software for visually
        // impaired users

        var res = gettextCatalog.getString('Folder');

        if (cls == 'x-tree-icon-mydocuments')
            res = gettextCatalog.getString('My Documents');
        else if (cls == 'x-tree-icon-mymusic')
            res = gettextCatalog.getString('My Music');
        else if (cls == 'x-tree-icon-mypictures')
            res = gettextCatalog.getString('My Pictures');
        else if (cls == 'x-tree-icon-desktop')
            res = gettextCatalog.getString('Desktop');
        else if (cls == 'x-tree-icon-home')
            res = gettextCatalog.getString('Home');
        else if (cls == 'x-tree-icon-hypervmachine')
            res = gettextCatalog.getString('Hyper-V Machine');
        else if (cls == 'x-tree-icon-hyperv')
            res = gettextCatalog.getString('Hyper-V Machines');
        else if (cls == 'x-tree-icon-broken')
            res = gettextCatalog.getString('Broken access');
        else if (cls == 'x-tree-icon-locked')
            res = gettextCatalog.getString('Access denied');
        else if (cls == 'x-tree-icon-symlink')
            res = gettextCatalog.getString('Symbolic link');
        else if (cls == 'x-tree-icon-leaf')
            res = gettextCatalog.getString('File');

        return res;
    };

    function reloadTexts() {
        apputils.fileSizeMultipliers = [
            {name: gettextCatalog.getString('byte'), value: 'b'},
            {name: gettextCatalog.getString('KByte'), value: 'KB'},
            {name: gettextCatalog.getString('MByte'), value: 'MB'},
            {name: gettextCatalog.getString('GByte'), value: 'GB'},
            {name: gettextCatalog.getString('TByte'), value: 'TB'}
        ];

        apputils.timerangeMultipliers = [
            {name: gettextCatalog.getString('Minutes'), value: 'm'},
            {name: gettextCatalog.getString('Hours'), value: 'h'},
            {name: gettextCatalog.getString('Days'), value: 'D'},
            {name: gettextCatalog.getString('Weeks'), value: 'W'},
            {name: gettextCatalog.getString('Months'), value: 'M'},
            {name: gettextCatalog.getString('Years'), value: 'Y'}
        ];

        apputils.shorttimerangeMultipliers = [
            {name: gettextCatalog.getString('Seconds'), value: 's'},
            {name: gettextCatalog.getString('Minutes'), value: 'm'},
            {name: gettextCatalog.getString('Hours'), value: 'h'}
        ];

        apputils.daysOfWeek = [
            {name: gettextCatalog.getString('Mon'), value: 'mon'},
            {name: gettextCatalog.getString('Tue'), value: 'tue'},
            {name: gettextCatalog.getString('Wed'), value: 'wed'},
            {name: gettextCatalog.getString('Thu'), value: 'thu'},
            {name: gettextCatalog.getString('Fri'), value: 'fri'},
            {name: gettextCatalog.getString('Sat'), value: 'sat'},
            {name: gettextCatalog.getString('Sun'), value: 'sun'}
        ];

        apputils.speedMultipliers = [
            {name: gettextCatalog.getString('byte/s'), value: 'b'},
            {name: gettextCatalog.getString('KByte/s'), value: 'KB'},
            {name: gettextCatalog.getString('MByte/s'), value: 'MB'},
            {name: gettextCatalog.getString('GByte/s'), value: 'GB'},
            {name: gettextCatalog.getString('TByte/s'), value: 'TB'}
        ];


        apputils.exampleOptionString = gettextCatalog.getString('Enter one option per line in command-line format, eg. {0}');

        apputils.filterClasses = [{
            name: gettextCatalog.getString('Exclude directories whose names contain'),
            key: '-dir*',
            prefix: '-*',
            suffix: '*!',
            rx: '\\-\\*([^\\!]+)\\*\\!'
        }, {
            name: gettextCatalog.getString('Exclude files whose names contain'),
            key: '-file*',
            prefix: '-[.*',
            suffix: '[^\\!]*]',    // Escape dirsep inside regexp. Required on Windows, no effect on other platforms.
            rx: '\\-\\[\\.\\*[\\^\\!\\]\\*\\]'
        }, {
            name: gettextCatalog.getString('Exclude folder'),
            key: '-folder',
            prefix: '-',
            suffix: '!',
            stripSep: true,
            rx: '\\-(.*)\\!'
        }, {
            name: gettextCatalog.getString('Exclude file'),
            key: '-path',
            prefix: '-',
            stripSep: true,
            exclude: ['*', '?', '{'],
            rx: '\\-([^\\[\\{\\*\\?]+)'
        }, {
            name: gettextCatalog.getString('Exclude file extension'),
            key: '-ext',
            rx: '\\-\\*\.(.*)',
            prefix: '-*.'
        }, {
            name: gettextCatalog.getString('Exclude regular expression'),
            key: '-[]',
            prefix: '-[',
            suffix: ']'
        }, {
            name: gettextCatalog.getString('Include regular expression'),
            key: '+[]',
            prefix: '+[',
            suffix: ']'
        }, {
            name: gettextCatalog.getString('Exclude filter group'),
            key: '-{}',
            prefix: '-{',
            suffix: '}'
        }, {
        //// Since all the current filter groups are intended for exclusion, there isn't a reason to show the 'Include group' section in the UI yet.
        //    name: gettextCatalog.getString('Include filter group'),
        //    key: '+{}',
        //    prefix: '+{',
        //    suffix: '}'
        //}, {
            name: gettextCatalog.getString('Include expression'),
            key: '+',
            prefix: '+'
        }, {
            name: gettextCatalog.getString('Exclude expression'),
            key: '-',
            prefix: '-'
        }];

        apputils.filterGroups = [{
            name: gettextCatalog.getString('Default excludes'),
            value: 'DefaultExcludes'
        }, {
        //// As the DefaultIncludes are currently empty, we don't need to include them in the UI yet.
        //    name: gettextCatalog.getString('Default includes'),
        //    value: 'DefaultIncludes'
        //}, {
            name: gettextCatalog.getString('System Files'),
            value: 'SystemFiles'
        }, {
            name: gettextCatalog.getString('Operating System'),
            value: 'OperatingSystem'
        }, {
            name: gettextCatalog.getString('Cache Files'),
            value: 'CacheFiles'
        }, {
            name: gettextCatalog.getString('Temporary Files'),
            value: 'TemporaryFiles'
        }, {
            name: gettextCatalog.getString('Applications'),
            value: 'Applications'
        }];

        apputils.filterTypeMap = {};
        for (var i = apputils.filterClasses.length - 1; i >= 0; i--)
            apputils.filterTypeMap[apputils.filterClasses[i].key] = apputils.filterClasses[i];

        $rootScope.$broadcast('apputillookupschanged');
    };

    apputils.filterGroupMap = null;
    apputils.loadFilterGroups = function (reload) {
        if (reload || apputils.filterGroupMap === null) {
            AppService.get('/systeminfo/filtergroups').then(function (data) {
                apputils.filterGroupMap = angular.copy(data.data.FilterGroups);

                $rootScope.$broadcast('apputillookupschanged');
            }, apputils.connectionError);
        }
    }

    reloadTexts();
    $rootScope.$on('gettextLanguageChanged', reloadTexts);

    this.parseBoolString = function(txt, def) {
        txt = (txt || '').toLowerCase();
        if (txt == '0' || txt == 'false' || txt == 'off' || txt == 'no' || txt == 'f')
            return false;
        else if (txt == '1' || txt == 'true' || txt == 'on' || txt == 'yes' || txt == 't')
            return true;
        else
            return def === undefined ? false : def;
    };


    this.splitSizeString = function (val) {
        var m = (/(\d*)(\w*)/mg).exec(val);
        var mul = null;
        if (!m)
            return [parseInt(val), null];
        else
            return [parseInt(m[1]), m[2]];
    };

    this.parseSizeString = function (val) {
        if (val == null) {
            return null;
        }
        var split = this.splitSizeString(val);
        var formatSizes = ['B', 'KB', 'MB', 'GB', 'TB'];
        var idx = formatSizes.indexOf((split[1]||'').toUpperCase());
        if (idx == -1) {
            idx = 0;
        }
        return split[0] * Math.pow(1024, idx);
    };


    this.toDisplayDateAndTime = function(dt) {
        return moment(dt).format('lll');
    };

    this.parseDate = function(dt) {
        if (typeof(dt) == typeof('')) {
            var msec = Date.parse(dt);
            if (isNaN(msec)) {
                if (dt.length == 16 && dt[8] == 'T' && dt[15] == 'Z') {
                    dt = dt.substr(0, 4) + '-' + dt.substr(4, 2) + '-' + dt.substr(6, 2) + 'T' +
                              dt.substr(9, 2) + ':' + dt.substr(11, 2) + ':' + dt.substr(13, 2) + 'Z';
                }
                return new Date(dt);
            } else {
                return new Date(msec);
            }
        }
        else
            return new Date(dt);
    };

    this.parseOptionStrings = function (val, dict, validateCallback) {
        // Parse options and return a dict with dict['--key']=value options
        // Include and exclude filters are returned as arrays in dict['include'], dict['exclude'] if present
        dict = dict || {};

        var lines = null;

        if (val != null && Array.isArray(val))
            lines = val;
        else
            lines = this.replace_all(val || '', '\r', '\n').split('\n');

        for(var i in lines) {
            var line = lines[i].trim();
            if (line != '' && line[0] != '#') {
                if (line.indexOf('--') == 0) {
                    line = line.substr(2);
                }

                var eqpos = line.indexOf('=');
                var key = line;
                var value = true;
                if (eqpos > 0) {
                    key = line.substr(0, eqpos).trim();
                    value = line.substr(eqpos + 1).trim();
                }

                if (validateCallback)
                    if (!validateCallback(dict, key, value))
                        return null;
                // Special handing for --include and --exclude, which are the only options that can appear multiple times (compare FilterCollector.DoExtractOptions)
                // In all other cases, the last value overrides the earlier values
                var lower = key.toLowerCase();
                if (lower == 'include' || lower == 'exclude') {
                    if (!Array.isArray(dict[lower])) {
                        dict[lower] = [value];
                    } else {
                        dict[lower].push(value);
                    }
                } else {
                    dict['--' + key] = value;
                }
            }
        }

        return dict;
    };

    this.parse_extra_options = function(str, dict) {
        return this.parseOptionStrings(str, dict, function(d, k, v) {
            if (d['--' + k] !== undefined) {
                DialogService.dialog(gettextCatalog.getString('Error'), gettextCatalog.getString('Duplicate option {{opt}}', { opt: k }));
                return false;
            } else if (k == 'include' || k == 'exclude') {
                // Cannot specify filters in extra options
                // Not sure that this is really called.
                DialogService.dialog(gettextCatalog.getString('Error'), gettextCatalog.getString('Cannot specify filter include or excludes in extra options'));
                return false;
            }

            return true;
        }) != null;
    };

    this.serializeAdvancedOptions = function(opts) {
        return this.serializeAdvancedOptionsToArray(opts).join('\n');
    };

    this.serializeAdvancedOptionsToArray = function(opts) {
        var res = [];
        for(var n in opts)
            if (n.indexOf('--') == 0)
                res.push(n + '=' + opts[n]);

        return res;
    };

    this.mergeAdvancedOptions = function(advStr, target, source) {
        var adv = {}
        if (!this.parse_extra_options(advStr, adv))
            return false;

        angular.extend(target, adv);

        // Remove advanced options, no longer in the list
        for(var n in source)
            if (n.indexOf('--') == 0)
                if (target[n] === undefined)
                    target[n] = null;

    };

    this.notifyInputError = function(msg) {
        DialogService.dialog('Error', msg);
        return false;
    };

    this.connectionError = function(txt, msg) {
        if (typeof(txt) == typeof('')) {
            if (msg == null)
                return function (msg) {
                    var msgText = AppService.responseErrorMessage(msg);
                    DialogService.dialog(gettextCatalog.getString('Error'), txt + msgText);
                };
        } else {
            msg = txt;
            txt = '';
        }

        var msgText = AppService.responseErrorMessage(msg);
        DialogService.dialog(gettextCatalog.getString('Error'), txt + msgText);
    };

    this.generatePassphrase = function() {
        var specials = '!@#$%^&*()_+{}:"<>?[];\',./';
        var lowercase = 'abcdefghijklmnopqrstuvwxyz';
        var uppercase = lowercase.toUpperCase();
        var numbers = '0123456789';
        var all = specials + lowercase + uppercase + numbers;

        function choose(str, n) {
            var res = '';
            for (var i = 0; i < n; i++) {
                res += str.charAt(Math.floor(Math.random() * str.length));
            }

            return res;
        };

        var pwd = (
            choose(specials, 2) +
            choose(lowercase, 2) +
            choose(uppercase, 2) +
            choose(numbers, 2) +
            choose(all, (Math.random()*5) + 5)
        ).split('');

        for(var i = 0; i < pwd.length; i++) {
            var pos = parseInt(Math.random() * pwd.length);
            var t = pwd[i]
            pwd[i] = pwd[pos];
            pwd[pos] = t;
        }

        return pwd.join('');
    }

    this.nl2br = function(str, is_xhtml) {
        var breakTag = (is_xhtml || typeof is_xhtml === 'undefined') ? '<br />' : '<br>';
        return (str + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1'+ breakTag +'$2');
    };

    this.preg_quote = function( str ) {
        // http://kevin.vanzonneveld.net
        // +   original by: booeyOH
        // +   improved by: Ates Goral (http://magnetiq.com)
        // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
        // +   bugfixed by: Onno Marsman
        // *     example 1: preg_quote("$40");
        // *     returns 1: '\$40'
        // *     example 2: preg_quote("*RRRING* Hello?");
        // *     returns 2: '\*RRRING\* Hello\?'
        // *     example 3: preg_quote("\\.+*?[^]$(){}=!<>|:");
        // *     returns 3: '\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:'

        return (str+'').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1");
    }

    this.replace_all_insensitive = function(str, pattern, replacement) {
      return str.replace( new RegExp( "(" + this.preg_quote(pattern) + ")" , 'gi' ), replacement );
    };

    this.replace_all = function(str, pattern, replacement) {
      return str.replace( new RegExp( "(" + this.preg_quote(pattern) + ")" , 'g' ), replacement );
    };

    this.globToRegexp = function (str) {
        // Escape special chars, except ? and *
        str = (str + '').replace(/([\\\.\+\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1");
        // Replace ? and * with .? and .*
        return str.replace(/(\?|\*)/g, ".$1");
    };

    this.format = function() {
        if (arguments == null || arguments.length < 1)
            return null;

        if (arguments.length == 1)
            return arguments[0];

        var msg = arguments[0];

        for(var i = 1; i < arguments.length; i++)
            msg = this.replace_all(msg, '{' + (i-1) + '}', arguments[i]);

        return msg;
    };

    this.encodeDictAsUrl = function(obj) {
            if (obj == null)
                return '';

            var str = [];
            for(var p in obj) {
                var x = encodeURIComponent(p);
                if (obj[p] != null)
                    x += "=" + encodeURIComponent(obj[p]);
                str.push(x);
            }

            if (str.length == 0)
                return '';

            return '?' + str.join("&");
    };

    var URL_REGEXP_FIELDS = ['source_uri', 'backend-type', '--auth-username', '--auth-password', 'server-name', 'server-port', 'server-path', 'querystring'];
    var URL_REGEXP = /([^:]+)\:\/\/(?:(?:([^\:]+)(?:\:?:([^@]*))?\@))?(?:([^\/\?\:]*)(?:\:(\d+))?)(?:\/([^\?]*))?(?:\?(.+))?/;
    var QUERY_REGEXP = /(?:^|&)([^&=]*)=?([^&]*)/g;

    this.decode_uri = function(uri, backendlist) {

        var i = URL_REGEXP_FIELDS.length + 1;
        var res = {};

        var m = URL_REGEXP.exec(uri);

        // Invalid URI
        if (!m)
            return res;

        while (i--) {
            res[URL_REGEXP_FIELDS[i]] = m[i] || "";
        }

        res.querystring.replace(QUERY_REGEXP, function(str, key, val) {
            if (key)
                res['--' + key] = decodeURIComponent((val || '').replace(/\+/g, '%20'));
        });

        var backends = {};
        for(var i in backendlist)
            backends[backendlist[i].Key] = true;

        var scheme = res['backend-type'];

        if (scheme && scheme[scheme.length - 1] == 's' && !backends[scheme] && backends[scheme.substr(0, scheme.length-1)]) {
            res['backend-type'] = scheme.substr(0, scheme.length-1);
            res['--use-ssl'] = true;
        }

        return res;
    };

    this.find_scheme = function(uri, backendlist) {
        if (!uri || uri.length == 0)
            return null;

        uri = uri.trim().toLowerCase();
        var ix = uri.indexOf('://');
        if (ix <= 0) {
            for(var i in backendlist)
                if (backendlist[i].Key == 'file')
                    return 'file';

            return backendlist[0];
        }

        return (EDIT_URI.decode_uri(uri)['backend-type'] || '').toLowerCase()
    };

    this.contains_value = function(dict, value) {
        for(var k in dict)
            if (dict[k] == value)
                return true;

        return false;
    };

    this.contains_key = function(dict, key, keyname) {
        for(var i in dict)
            if (keyname == null ? i == key : dict[i][keyname] == key)
                return true;

        return false;
    };


    this.removeEmptyEntries = function(x) {
        for (var i = x.length - 1; i >= 0; i--) {
            if (x[i] == null || x[i] == '')
                x.splice(i, 1);
        };

        return x;
    };

    this.splitFilterIntoTypeAndBody = function(src, dirsep) {
        if (src == null)
            return null;

        if (dirsep == null) {
            throw new Error('No dirsep provided!');
        }

        function matches(txt, n) {
            var pre = apputils.replace_all(n.prefix || '', '!', dirsep);
            var suf = apputils.replace_all(n.suffix || '', '!', dirsep);

            if (txt.indexOf(pre) != 0 || txt.lastIndexOf(suf) != txt.length - suf.length)
                return null;

            var type = n.key;
            var body = txt.substr(pre.length);
            body = body.substr(0, body.length - suf.length);

            if (body.length >= 2 && body[1] == '[') {
                body = body.substr(1, body.length - 2);
            }

            if (n.exclude != null) {
                for (var i = n.exclude.length - 1; i >= 0; i--)
                    if (body.indexOf(apputils.replace_all(n.exclude[i] || '', '!', dirsep)) >= 0)
                        return null;
            }

            return [type, body];
        }

        var m = [];
        for (var i in this.filterClasses) {
            var n = matches(src, this.filterClasses[i]);
            if (n != null)
                m.push(n);
        }
        if (m.length == 0) {
            return null;
        }
        // Select match with shortest body, meaning longest prefix and suffix
        var shortestIdx = 0;
        var shortestLen = src.length;
        for (i = 0; i < m.length; ++i) {
            if (m[i][1].length < shortestLen) {
                shortestIdx = i;
                shortestLen = m[i][1].length;
            }
        }
        return m[shortestIdx];
    };

    this.buildFilter = function(type, body, dirsep) {
        if (type == null || this.filterTypeMap[type] == null)
            return body;

        body = body || '';

        var f = this.filterTypeMap[type];
        var pre = this.replace_all(f.prefix || '', '!', dirsep);
        var suf = this.replace_all(f.suffix || '', '!', dirsep);

        if (pre.length >= 2 && pre[1] == '[') {
            //Regexp encode body ....
        }
        if (f.stripSep == true && body[body.length - 1] == dirsep) {
            // Remove trailing dirsep
            body = body.slice(0, -1);
        }
        return pre + body + suf;
    };

    this.filterToRegexpStr = function (filter) {
        var firstChar = filter.substr(0, 1);
        var lastChar = filter.substr(filter.length - 1, 1);
        var isFilterGroup = firstChar == '{' && lastChar == '}';
        if (isFilterGroup && this.filterGroupMap !== null) {
            // Replace filter groups with filter strings
            var filterGroups = filter.substr(1,filter.length - 2).split(',');
            var filterStrings = [];
            for (var i = 0; i < filterGroups.length; ++i) {
                filterStrings = filterStrings.concat(this.filterGroupMap[filterGroups[i].trim()] || []);
            }

            filter = filterStrings.map(function (s) {
                return '(?:' + apputils.filterToRegexpStr(s) + ')'
            }).join('|');

        } else {
            var rx = firstChar == '[' && lastChar == ']';
            if (rx)
                filter = filter.substr(1, filter.length - 2);
            else {
                filter = this.globToRegexp(filter);
            }
        }

        return filter;
    };

    this.filterListToRegexps = function(filters, caseSensitive) {
        var res = [];

        for(var i = 0; i < filters.length; i++) {
            var f = filters[i];

            if (f == null || f.length == 0)
                continue;

            var flag = f.substr(0, 1);
            var filter = f.substr(1);

            try {
                var regexp = this.filterToRegexpStr(filter);
                res.push([flag == '+', new RegExp(regexp, caseSensitive ? 'g' : 'gi')]);
            } catch (e) {
            }
        }

        return res;
    };

    this.evalFilter = function(path, filters, include) {
        for (var i = 0; i < filters.length; i++) {
            var m = path.match(filters[i][1]);
            // Regex such as '.*' might match empty string at the end which is unwanted
            // Check that the first match covers the full string
            if (m && m.length >= 1 && m[0].length == path.length)
                return filters[i][0];
        }

        return include === undefined ? true : include;
    };

    this.buildOptionList = function(sysinfo, encmodule, compmodule, backmodule) {
        if (sysinfo == null || sysinfo.Options == null)
            return null;

        var items = angular.copy(sysinfo.Options);
        for(var n in items)
            items[n].Category = gettextCatalog.getString('Core options');

        function copyToList(lst, key) {
            if (key != null && typeof(key) != typeof(''))
                key = null;

            for(var n in lst)
            {
                if (key == null || key.toLowerCase() == lst[n].Key.toLowerCase())
                {
                    var m = angular.copy(lst[n].Options);
                    for(var i in m)
                        m[i].Category = lst[n].DisplayName;
                    items.push.apply(items, m);
                }
            }
        }

        copyToList(sysinfo.GenericModules);

        if (encmodule !== false)
            copyToList(sysinfo.EncryptionModules, encmodule);
        if (compmodule !== false)
            copyToList(sysinfo.CompressionModules, compmodule);
        if (backmodule !== false)
            copyToList(sysinfo.BackendModules, backmodule);

        return items;
    };

    this.extractServerModuleOptions = function(advOptionsList, servermodulelist, servermodulesettings, optionlistname) {
        if (optionlistname == null)
            optionlistname = 'SupportedCommands';

        for(var mix in servermodulelist) {
            var mod = servermodulelist[mix];
            for(var oix in mod[optionlistname]) {
                var opt = mod[optionlistname][oix];
                var prefixstr = '--' + opt.Name + '=';
                for (var i = advOptionsList.length - 1; i >= 0; i--) {
                    if (advOptionsList[i].indexOf(prefixstr) == 0) {
                        servermodulesettings[opt.Name] = advOptionsList[i].substr(prefixstr.length);
                        advOptionsList.splice(i, 1);
                    }
                }
            }
        }
    };

    this.formatDuration = function(duration) {
        // duration is a timespan string in format (dd.)hh:mm:ss.sss
        // the part (dd.) is as indicated optional

        if (duration == null) {
            return;
        }

        var timespanArray = duration.split(':');

        if (timespanArray.length < 3) {
            return;
        }

        if (timespanArray[0].indexOf('.') > 0) {
            timespanArray[0] = timespanArray[0].replace('.', " day(s) and ");
        }

        // round second according to ms
        timespanArray[2] = Math.round(parseFloat(timespanArray[2])).toString();
        // zero-padding
        if (timespanArray[2].length == 1) timespanArray[2] = '0' + timespanArray[2];

        return timespanArray.join(':');
    };

});
