NETDATA.unitsConversion = {
    keys: {},       // keys for data-common-units
    latest: {},     // latest selected units for data-common-units

    globalReset: function () {
        this.keys = {};
        this.latest = {};
    },

    scalableUnits: {
        'packets/s': {
            'pps': 1,
            'Kpps': 1000,
            'Mpps': 1000000
        },
        'pps': {
            'pps': 1,
            'Kpps': 1000,
            'Mpps': 1000000
        },
        'kilobits/s': {
            'bits/s': 1 / 1000,
            'kilobits/s': 1,
            'megabits/s': 1000,
            'gigabits/s': 1000000,
            'terabits/s': 1000000000
        },
        'bytes/s': {
            'bytes/s': 1,
            'kilobytes/s': 1024,
            'megabytes/s': 1024 * 1024,
            'gigabytes/s': 1024 * 1024 * 1024,
            'terabytes/s': 1024 * 1024 * 1024 * 1024
        },
        'kilobytes/s': {
            'bytes/s': 1 / 1024,
            'kilobytes/s': 1,
            'megabytes/s': 1024,
            'gigabytes/s': 1024 * 1024,
            'terabytes/s': 1024 * 1024 * 1024
        },
        'B/s': {
            'B/s': 1,
            'KiB/s': 1024,
            'MiB/s': 1024 * 1024,
            'GiB/s': 1024 * 1024 * 1024,
            'TiB/s': 1024 * 1024 * 1024 * 1024
        },
        'KB/s': {
            'B/s': 1 / 1024,
            'KB/s': 1,
            'MB/s': 1024,
            'GB/s': 1024 * 1024,
            'TB/s': 1024 * 1024 * 1024
        },
        'KiB/s': {
            'B/s': 1 / 1024,
            'KiB/s': 1,
            'MiB/s': 1024,
            'GiB/s': 1024 * 1024,
            'TiB/s': 1024 * 1024 * 1024
        },
        'B': {
            'B': 1,
            'KiB': 1024,
            'MiB': 1024 * 1024,
            'GiB': 1024 * 1024 * 1024,
            'TiB': 1024 * 1024 * 1024 * 1024,
            'PiB': 1024 * 1024 * 1024 * 1024 * 1024
        },
        'KB': {
            'B': 1 / 1024,
            'KB': 1,
            'MB': 1024,
            'GB': 1024 * 1024,
            'TB': 1024 * 1024 * 1024
        },
        'KiB': {
            'B': 1 / 1024,
            'KiB': 1,
            'MiB': 1024,
            'GiB': 1024 * 1024,
            'TiB': 1024 * 1024 * 1024
        },
        'MB': {
            'B': 1 / (1024 * 1024),
            'KB': 1 / 1024,
            'MB': 1,
            'GB': 1024,
            'TB': 1024 * 1024,
            'PB': 1024 * 1024 * 1024
        },
        'MiB': {
            'B': 1 / (1024 * 1024),
            'KiB': 1 / 1024,
            'MiB': 1,
            'GiB': 1024,
            'TiB': 1024 * 1024,
            'PiB': 1024 * 1024 * 1024
        },
        'GB': {
            'B': 1 / (1024 * 1024 * 1024),
            'KB': 1 / (1024 * 1024),
            'MB': 1 / 1024,
            'GB': 1,
            'TB': 1024,
            'PB': 1024 * 1024,
            'EB': 1024 * 1024 * 1024
        },
        'GiB': {
            'B': 1 / (1024 * 1024 * 1024),
            'KiB': 1 / (1024 * 1024),
            'MiB': 1 / 1024,
            'GiB': 1,
            'TiB': 1024,
            'PiB': 1024 * 1024,
            'EiB': 1024 * 1024 * 1024
        },
        'num': {
            'num': 1,
            'num (K)': 1000,
            'num (M)': 1000000,
            'num (G)': 1000000000,
            'num (T)': 1000000000000
        }
        /*
        'milliseconds': {
            'seconds': 1000
        },
        'seconds': {
            'milliseconds': 0.001,
            'seconds': 1,
            'minutes': 60,
            'hours': 3600,
            'days': 86400
        }
        */
    },

    convertibleUnits: {
        'Celsius': {
            'Fahrenheit': {
                check: function (max) {
                    void(max);
                    return NETDATA.options.current.temperature === 'fahrenheit';
                },
                convert: function (value) {
                    return value * 9 / 5 + 32;
                }
            }
        },
        'celsius': {
            'fahrenheit': {
                check: function (max) {
                    void(max);
                    return NETDATA.options.current.temperature === 'fahrenheit';
                },
                convert: function (value) {
                    return value * 9 / 5 + 32;
                }
            }
        },
        'seconds': {
            'time': {
                check: function (max) {
                    void(max);
                    return NETDATA.options.current.seconds_as_time;
                },
                convert: function (seconds) {
                    return NETDATA.unitsConversion.seconds2time(seconds);
                }
            }
        },
        'milliseconds': {
            'milliseconds': {
                check: function (max) {
                    return NETDATA.options.current.seconds_as_time && max < 1000;
                },
                convert: function (milliseconds) {
                    let tms = Math.round(milliseconds * 10);
                    milliseconds = Math.floor(tms / 10);

                    tms -= milliseconds * 10;

                    return (milliseconds).toString() + '.' + tms.toString();
                }
            },
            'seconds': {
                check: function (max) {
                    return NETDATA.options.current.seconds_as_time && max >= 1000 && max < 60000;
                },
                convert: function (milliseconds) {
                    milliseconds = Math.round(milliseconds);

                    let seconds = Math.floor(milliseconds / 1000);
                    milliseconds -= seconds * 1000;

                    milliseconds = Math.round(milliseconds / 10);

                    return seconds.toString() + '.'
                        + NETDATA.zeropad(milliseconds);
                }
            },
            'M:SS.ms': {
                check: function (max) {
                    return NETDATA.options.current.seconds_as_time && max >= 60000;
                },
                convert: function (milliseconds) {
                    milliseconds = Math.round(milliseconds);

                    let minutes = Math.floor(milliseconds / 60000);
                    milliseconds -= minutes * 60000;

                    let seconds = Math.floor(milliseconds / 1000);
                    milliseconds -= seconds * 1000;

                    milliseconds = Math.round(milliseconds / 10);

                    return minutes.toString() + ':'
                        + NETDATA.zeropad(seconds) + '.'
                        + NETDATA.zeropad(milliseconds);
                }
            }
        },
        'nanoseconds': {
            'nanoseconds': {
                check: function (max) {
                    return NETDATA.options.current.seconds_as_time && max < 1000;
                },
                convert: function (nanoseconds) {
                    let tms = Math.round(nanoseconds * 10);
                    nanoseconds = Math.floor(tms / 10);

                    tms -= nanoseconds * 10;

                    return (nanoseconds).toString() + '.' + tms.toString();
                }
            },
            'microseconds': {
                check: function (max) {
                    return NETDATA.options.current.seconds_as_time
                           && max >= 1000 && max < 1000 * 1000;
                },
                convert: function (nanoseconds) {
                    nanoseconds = Math.round(nanoseconds);

                    let microseconds = Math.floor(nanoseconds / 1000);
                    nanoseconds -= microseconds * 1000;

                    nanoseconds = Math.round(nanoseconds / 10 );

                    return microseconds.toString() + '.'
                        + NETDATA.zeropad(nanoseconds);
                }
            },
            'milliseconds': {
                check: function (max) {
                    return NETDATA.options.current.seconds_as_time
                           && max >= 1000 * 1000 && max < 1000 * 1000 * 1000;
                },
                convert: function (nanoseconds) {
                    nanoseconds = Math.round(nanoseconds);

                    let milliseconds = Math.floor(nanoseconds / 1000 / 1000);
                    nanoseconds -= milliseconds * 1000 * 1000;

                    nanoseconds = Math.round(nanoseconds / 1000 / 10);

                    return milliseconds.toString() + '.'
                        + NETDATA.zeropad(nanoseconds);
                }
            },
            'seconds': {
                check: function (max) {
                    return NETDATA.options.current.seconds_as_time
                           && max >= 1000 * 1000 * 1000;
                },
                convert: function (nanoseconds) {
                    nanoseconds = Math.round(nanoseconds);

                    let seconds = Math.floor(nanoseconds / 1000 / 1000 / 1000);
                    nanoseconds -= seconds * 1000 * 1000 * 1000;

                    nanoseconds = Math.round(nanoseconds / 1000 / 1000 / 10);

                    return seconds.toString() + '.'
                        + NETDATA.zeropad(nanoseconds);
                }
            },
        }
    },

    seconds2time: function (seconds) {
        seconds = Math.abs(seconds);

        let days = Math.floor(seconds / 86400);
        seconds -= days * 86400;

        let hours = Math.floor(seconds / 3600);
        seconds -= hours * 3600;

        let minutes = Math.floor(seconds / 60);
        seconds -= minutes * 60;

        seconds = Math.round(seconds);

        let ms_txt = '';
        /*
        let ms = seconds - Math.floor(seconds);
        seconds -= ms;
        ms = Math.round(ms * 1000);

        if (ms > 1) {
            if (ms < 10)
                ms_txt = '.00' + ms.toString();
            else if (ms < 100)
                ms_txt = '.0' + ms.toString();
            else
                ms_txt = '.' + ms.toString();
        }
        */

        return ((days > 0) ? days.toString() + 'd:' : '').toString()
            + NETDATA.zeropad(hours) + ':'
            + NETDATA.zeropad(minutes) + ':'
            + NETDATA.zeropad(seconds)
            + ms_txt;
    },

    // get a function that converts the units
    // + every time units are switched call the callback
    get: function (uuid, min, max, units, desired_units, common_units_name, switch_units_callback) {
        // validate the parameters
        if (typeof units === 'undefined') {
            units = 'undefined';
        }

        // check if we support units conversion
        if (typeof this.scalableUnits[units] === 'undefined' && typeof this.convertibleUnits[units] === 'undefined') {
            // we can't convert these units
            //console.log('DEBUG: ' + uuid.toString() + ' can\'t convert units: ' + units.toString());
            return function (value) {
                return value;
            };
        }

        // check if the caller wants the original units
        if (typeof desired_units === 'undefined' || desired_units === null || desired_units === 'original' || desired_units === units) {
            //console.log('DEBUG: ' + uuid.toString() + ' original units wanted');
            switch_units_callback(units);
            return function (value) {
                return value;
            };
        }

        // now we know we can convert the units
        // and the caller wants some kind of conversion

        let tunits = null;
        let tdivider = 0;

        if (typeof this.scalableUnits[units] !== 'undefined') {
            // units that can be scaled
            // we decide a divider

            // console.log('NETDATA.unitsConversion.get(' + units.toString() + ', ' + desired_units.toString() + ', function()) decide divider with min = ' + min.toString() + ', max = ' + max.toString());

            if (desired_units === 'auto') {
                // the caller wants to auto-scale the units

                // find the absolute maximum value that is rendered on the chart
                // based on this we decide the scale
                min = Math.abs(min);
                max = Math.abs(max);
                if (min > max) {
                    max = min;
                }

                // find the smallest scale that provides integers
                // for (x in this.scalableUnits[units]) {
                //     if (this.scalableUnits[units].hasOwnProperty(x)) {
                //         let m = this.scalableUnits[units][x];
                //         if (m <= max && m > tdivider) {
                //             tunits = x;
                //             tdivider = m;
                //         }
                //     }
                // }
                const sunit = this.scalableUnits[units];
                for (var x of Object.keys(sunit)) {
                    let m = sunit[x];
                    if (m <= max && m > tdivider) {
                        tunits = x;
                        tdivider = m;
                    }
                }

                if (tunits === null || tdivider <= 0) {
                    // we couldn't find one
                    //console.log('DEBUG: ' + uuid.toString() + ' cannot find an auto-scaling candidate for units: ' + units.toString() + ' (max: ' + max.toString() + ')');
                    switch_units_callback(units);
                    return function (value) {
                        return value;
                    };
                }

                if (typeof common_units_name === 'string' && typeof uuid === 'string') {
                    // the caller wants several charts to have the same units
                    // data-common-units

                    let common_units_key = common_units_name + '-' + units;

                    // add our divider into the list of keys
                    let t = this.keys[common_units_key];
                    if (typeof t === 'undefined') {
                        this.keys[common_units_key] = {};
                        t = this.keys[common_units_key];
                    }
                    t[uuid] = {
                        units: tunits,
                        divider: tdivider
                    };

                    // find the max divider of all charts
                    let common_units = t[uuid];
                    for (var x in t) {
                        if (t.hasOwnProperty(x) && t[x].divider > common_units.divider) {
                            common_units = t[x];
                        }
                    }

                    // save our common_max to the latest keys
                    let latest = this.latest[common_units_key];
                    if (typeof latest === 'undefined') {
                        this.latest[common_units_key] = {};
                        latest = this.latest[common_units_key];
                    }
                    latest.units = common_units.units;
                    latest.divider = common_units.divider;

                    tunits = latest.units;
                    tdivider = latest.divider;

                    //console.log('DEBUG: ' + uuid.toString() + ' converted units: ' + units.toString() + ' to units: ' + tunits.toString() + ' with divider ' + tdivider.toString() + ', common-units=' + common_units_name.toString() + ((t[uuid].divider !== tdivider)?' USED COMMON, mine was ' + t[uuid].units:' set common').toString());

                    // apply it to this chart
                    switch_units_callback(tunits);
                    return function (value) {
                        if (tdivider !== latest.divider) {
                            // another chart switched our common units
                            // we should switch them too
                            //console.log('DEBUG: ' + uuid + ' switching units due to a common-units change, from ' + tunits.toString() + ' to ' + latest.units.toString());
                            tunits = latest.units;
                            tdivider = latest.divider;
                            switch_units_callback(tunits);
                        }

                        return value / tdivider;
                    };
                } else {
                    // the caller did not give data-common-units
                    // this chart auto-scales independently of all others
                    //console.log('DEBUG: ' + uuid.toString() + ' converted units: ' + units.toString() + ' to units: ' + tunits.toString() + ' with divider ' + tdivider.toString() + ', autonomously');

                    switch_units_callback(tunits);
                    return function (value) {
                        return value / tdivider;
                    };
                }
            } else {
                // the caller wants specific units

                if (typeof this.scalableUnits[units][desired_units] !== 'undefined') {
                    // all good, set the new units
                    tdivider = this.scalableUnits[units][desired_units];
                    // console.log('DEBUG: ' + uuid.toString() + ' converted units: ' + units.toString() + ' to units: ' + desired_units.toString() + ' with divider ' + tdivider.toString() + ', by reference');
                    switch_units_callback(desired_units);
                    return function (value) {
                        return value / tdivider;
                    };
                } else {
                    // oops! switch back to original units
                    console.log('Units conversion from ' + units.toString() + ' to ' + desired_units.toString() + ' is not supported.');
                    switch_units_callback(units);
                    return function (value) {
                        return value;
                    };
                }
            }
        } else if (typeof this.convertibleUnits[units] !== 'undefined') {
            // units that can be converted
            if (desired_units === 'auto') {
                for (var x in this.convertibleUnits[units]) {
                    if (this.convertibleUnits[units].hasOwnProperty(x)) {
                        if (this.convertibleUnits[units][x].check(max)) {
                            //console.log('DEBUG: ' + uuid.toString() + ' converting ' + units.toString() + ' to: ' + x.toString());
                            switch_units_callback(x);
                            return this.convertibleUnits[units][x].convert;
                        }
                    }
                }

                // none checked ok
                //console.log('DEBUG: ' + uuid.toString() + ' no conversion available for ' + units.toString() + ' to: ' + desired_units.toString());
                switch_units_callback(units);
                return function (value) {
                    return value;
                };
            } else if (typeof this.convertibleUnits[units][desired_units] !== 'undefined') {
                switch_units_callback(desired_units);
                return this.convertibleUnits[units][desired_units].convert;
            } else {
                console.log('Units conversion from ' + units.toString() + ' to ' + desired_units.toString() + ' is not supported.');
                switch_units_callback(units);
                return function (value) {
                    return value;
                };
            }
        } else {
            // hm... did we forget to implement the new type?
            console.log(`Unmatched unit conversion method for units ${units.toString()}`);
            switch_units_callback(units);
            return function (value) {
                return value;
            };
        }
    }
};
