'use strict';
// let context = {
//     adapter,
//     mods,
//     errorLogFunction,
//     subscriptions,
//     subscribedPatterns,
//     states,
//     adapterSubs,
//     objects,
//     cacheObjectEnums,
//     stateIds,
//     logWithLineInfo,
//     timers,
//     enums,
//     channels,
//     devices,
//     isEnums
// };

/** 
 * @typedef {Object} SandboxContext
 * @property {Record<string, string[]>} channels
 * @property {Record<string, string[]>} devices
 * @property {string[]} stateIds
 */

/**
 * @param {{[prop: string]: any} & SandboxContext} context
 */
function sandBox(script, name, verbose, debug, context) {
    const consts   = require('./consts');
    const words    = require('./words');
    const eventObj = require('./eventObj');
    const patternCompareFunctions = require('./patternCompareFunctions');
    const nodeSchedule = require('node-schedule');

    const adapter  = context.adapter;
    const mods     = context.mods;
    const states   = context.states;
    const objects  = context.objects;
    const timers   = context.timers;
    const enums    = context.enums;

    function errorInCallback(e) {
        context.logError('Error in callback', e);
    }

    function unsubscribePattern(script, pattern) {
        if (adapter.config.subscribe) {
            if (script.subscribes[pattern]) {
                script.subscribes[pattern]--;
                if (!script.subscribes[pattern]) delete script.subscribes[pattern];
            }

            if (context.subscribedPatterns[pattern]) {
                context.subscribedPatterns[pattern]--;
                if (!context.subscribedPatterns[pattern]) {
                    adapter.unsubscribeForeignStates(pattern);
                    delete context.subscribedPatterns[pattern];

                    // if pattern was regex or with * some states will stay in RAM, but it is OK.
                    if (states[pattern]) delete states[pattern];
                }
            }
        }
    }

    function subscribePattern(script, pattern) {
        if (adapter.config.subscribe) {
            if (!script.subscribes[pattern]) {
                script.subscribes[pattern] = 1;
            } else {
                script.subscribes[pattern]++;
            }

            if (!context.subscribedPatterns[pattern]) {
                context.subscribedPatterns[pattern] = 1;
                adapter.subscribeForeignStates(pattern);

                // request current value to deliver old value on change.
                if (typeof pattern === 'string' && pattern.indexOf('*') === -1) {
                    adapter.getForeignState(pattern, function (err, state) {
                        if (state) states[pattern] = state;
                    });
                } else {
                    adapter.getForeignStates(pattern, function (err, _states) {
                        if (_states) {
                            for (const id in _states) {
                                if (!_states.hasOwnProperty(id)) continue;
                                states[id] = _states[id];
                            }
                        }
                    });
                }
            } else {
                context.subscribedPatterns[pattern]++;
            }
        }
    }

    /**
     * @typedef PatternCompareFunctionArray
     * @type {Array<any> & {logic?: string}}
     */

    function getPatternCompareFunctions(pattern) {
        let func;
        /** @type {PatternCompareFunctionArray} */
        const functions = [];
        functions.logic = pattern.logic || 'and';
        //adapter.log.info('## '+JSON.stringify(pattern));
        for (const key in pattern) {
            if (!pattern.hasOwnProperty(key)) continue;
            if (key === 'logic') continue;
            if (key === 'change' && pattern.change === 'any') continue;
            if (!(func = patternCompareFunctions[key])) continue;
            if (typeof (func = func(pattern)) !== 'function') continue;
            functions.push(func);
        }
        return functions;
    }

    /** @typedef {{attr: string, value: string, idRegExp?: RegExp}} Selector */
    /**
     * Splits a selector string into attribute and value
     * @param {string} selector The selector string to split
     * @returns {Selector}
     */
    function splitSelectorString(selector) {
        const parts = selector.split('=', 2);
        if (parts[1] && parts[1][0] === '"') {
            parts[1] = parts[1].substring(1);
            const len = parts[1].length;
            if (parts[1] && parts[1][len - 1] === '"') parts[1] = parts[1].substring(0, len - 1);
        }
        if (parts[1] && parts[1][0] === "'") {
            parts[1] = parts[1].substring(1);
            const len = parts[1].length;
            if (parts[1] && parts[1][len - 1] === "'") parts[1] = parts[1].substring(0, len - 1);
        }

        if (parts[1]) parts[1] = parts[1].trim();
        parts[0] = parts[0].trim();

        return {attr: parts[0], value: parts[1]};
    }

    /**
     * Transforms a selector string with wildcards into a regular expression
     * @param {string} str The selector string to transform into a regular expression
     */
    function selectorStringToRegExp(str) {
        if (str[0] === '*') {
            // wildcard at the start, match the end of the string
            return new RegExp(str.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
        } else if (str[str.length - 1] === '*') {
            // wildcard at the end, match the start of the string
            return new RegExp('^' + str.replace(/\./g, '\\.').replace(/\*/g, '.*'));
        } else {
            // wildcard potentially in the middle, match whatever
            return new RegExp(str.replace(/\./g, '\\.').replace(/\*/g, '.*'));
        }
    }

    /**
     * Adds a regular expression for selectors targeting the state ID
     * @param {Selector} selector The selector to apply the transform to
     * @returns {Selector}
     */
    function addRegExpToIdAttrSelectors(selector) {
        if (
            (selector.attr === 'id' || selector.attr === 'state.id')
            && (!selector.idRegExp && selector.value)
        ) {
            return {
                attr: selector.attr,
                value: selector.value,
                idRegExp: selectorStringToRegExp(selector.value)
            };
        } else {
            return selector;
        }
    }

    /**
     * Tests if a value loosely equals (==) the reference string. 
     * In contrast to the equality operator, this treats true == "true" aswell
     * so we can test common and native attributes for boolean values
     * @param {boolean | string | number | undefined} value The value to compare with the reference
     * @param {string} reference The reference to compare the value to
     */
    function looselyEqualsString(value, reference) {
        // For booleans, compare the string representation
        // For other types do a loose comparison
        return (typeof value === 'boolean') 
            ? (value && reference === 'true') || (!value && reference === 'false')
            : value == reference
        ;
    }

    const sandbox = {
        mods:      mods,
        _id:       script._id,
        name:      name, // deprecated
        scriptName: name,
        instance:  adapter.instance,
        verbose:   verbose,
        request:   mods.request,
        exports:   {}, // Polyfill for the exports object in TypeScript modules
        require:   function (md) {
            console.log('REQUIRE: ' + md);
            if (mods[md]) {
                return mods[md];
            } else {
                try {
                    mods[md] = require(__dirname + '/../node_modules/' + md);
                    return mods[md];
                } catch (e) {
                    try {
                        mods[md] = require(__dirname + '/../../' + md);
                        return mods[md];
                    } catch (e) {
                        context.logError(name, e, 6);
                    }
                }
            }
        },
        Buffer:    Buffer,
        __engine: {
            __subscriptions: 0,
            __schedules: 0
        },
        /**
         * @param {string} selector
         * @returns {iobJS.QueryResult}
         */
        $:         function (selector) {
            // following is supported
            // 'type[commonAttr=something]', 'id[commonAttr=something]', id(enumName="something")', id{nativeName="something"}
            // Type can be state, channel or device
            // Attr can be any of the common attributes and can have wildcards *
            // E.g. "state[id='hm-rpc.0.*]" or "hm-rpc.0.*" returns all states of adapter instance hm-rpc.0
            // channel(room="Living room") => all states in room "Living room"
            // channel{TYPE=BLIND}[state.id=*.LEVEL]
            // Switch all states with .STATE of channels with role "switch" in "Wohnzimmer" to false
            // $('channel[role=switch][state.id=*.STATE](rooms=Wohnzimmer)').setState(false);
            //
            // Following functions are possible, setValue, getValue (only from first), on, each

            // Todo CACHE!!!

            const result  = {};

            let name      = '';
            /** @type {string[]} */
            const commonStrings = [];
            /** @type {string[]} */
            const enumStrings  = [];
            /** @type {string[]} */
            const nativeStrings = [];
            let isInsideName    = true;
            let isInsideCommonString = false;
            let isInsideEnumString   = false;
            let isInsideNativeString = false;
            let currentCommonString    = '';
            let currentNativeString    = '';
            let currentEnumString     = '';

            // parse string
            let selectorHasInvalidType = false;
            if (typeof selector === 'string') {
                for (let i = 0; i < selector.length; i++) {
                    if (selector[i] === '{') {
                        isInsideName = false;
                        if (isInsideCommonString || isInsideEnumString || isInsideNativeString) {
                            // Error
                            break;
                        }
                        isInsideNativeString = true;
                    } else if (selector[i] === '}') {
                        isInsideNativeString = false;
                        nativeStrings.push(currentNativeString);
                        currentNativeString = '';
                    } else if (selector[i] === '[') {
                        isInsideName = false;
                        if (isInsideCommonString || isInsideEnumString || isInsideNativeString) {
                            // Error
                            break;
                        }
                        isInsideCommonString = true;
                    } else if (selector[i] === ']') {
                        isInsideCommonString = false;
                        commonStrings.push(currentCommonString);
                        currentCommonString = '';
                    } else if (selector[i] === '(') {
                        isInsideName = false;
                        if (isInsideCommonString || isInsideEnumString || isInsideNativeString) {
                            // Error
                            break;
                        }
                        isInsideEnumString = true;
                    } else if (selector[i] === ')') {
                        isInsideEnumString = false;
                        enumStrings.push(currentEnumString);
                        currentEnumString = '';
                    } else if (isInsideName) {
                        name += selector[i];
                    } else if (isInsideCommonString) {
                        currentCommonString += selector[i];
                    } else if (isInsideEnumString) {
                        currentEnumString += selector[i];
                    } else if (isInsideNativeString) {
                        currentNativeString += selector[i];
                    } //else {
                    // some error
                    //}
                }
            } else {
                selectorHasInvalidType = true;
            }

            // If some error in the selector
            if (selectorHasInvalidType || isInsideEnumString || isInsideCommonString || isInsideNativeString) {
                result.length = 0;
                result.each = function () {
                    return this;
                };
                result.getState = function () {
                    return null;
                };
                result.setState = function () {
                    return this;
                };
                result.on = function () {
                    return this;
                };
            }

            if (isInsideEnumString) {
                adapter.log.warn('Invalid selector: enum close bracket ")" cannot be found in "' + selector + '"');
                result.error = 'Invalid selector: enum close bracket ")" cannot be found';
                return result;
            } else if (isInsideCommonString) {
                adapter.log.warn('Invalid selector: common close bracket "]" cannot be found in "' + selector + '"');
                result.error = 'Invalid selector: common close bracket "]" cannot be found';
                return result;
            } else if (isInsideNativeString) {
                adapter.log.warn('Invalid selector: native close bracket "}" cannot be found in "' + selector + '"');
                result.error = 'Invalid selector: native close bracket "}" cannot be found';
                return result;
            } else if (selectorHasInvalidType) {
                const message = `Invalid selector: selector must be a string but is of type ${typeof selector}`;
                adapter.log.warn(message);
                result.error = message;
                return result;
            }

            /** @type {Selector[]} */
            let commonSelectors = commonStrings.map(selector => splitSelectorString(selector));
            let nativeSelectors = nativeStrings.map(selector => splitSelectorString(selector));
            const enumSelectorObjects = enumStrings.map(_enum => splitSelectorString(_enum));
            const allSelectors = commonSelectors.concat(nativeSelectors, enumSelectorObjects);

            // These selectors match the state or object ID and don't belong in the common/native selectors
            // Also use RegExp for the ID matching
            const stateIdSelectors = allSelectors
                .filter(selector => selector.attr === 'state.id')
                .map(selector => addRegExpToIdAttrSelectors(selector))
                ;
            const objectIdSelectors = allSelectors
                .filter(selector => selector.attr === 'id')
                .map(selector => addRegExpToIdAttrSelectors(selector))
                ;

            commonSelectors = commonSelectors.filter(selector => selector.attr !== 'state.id' && selector.attr !== 'id');
            nativeSelectors = nativeSelectors.filter(selector => selector.attr !== 'state.id' && selector.attr !== 'id');
            const enumSelectors = enumSelectorObjects
                .filter(selector => selector.attr !== 'state.id' && selector.attr !== 'id')
                // enums are filtered by their enum id, so transform the selector into that
                .map(selector => `enum.${selector.attr}.${selector.value}`)
            ;

            name = name.trim();
            if (name === 'channel' || name === 'device') {
                // Fill the channels and devices objects with the IDs of all their states
                // so we can loop over them afterwards
                if (!context.channels || !context.devices) {
                    context.channels = {};
                    context.devices  = {};
                    for (const _id in objects) {
                        if (objects.hasOwnProperty(_id) && objects[_id].type === 'state') {
                            const parts = _id.split('.');
                            parts.pop();
                            const chn = parts.join('.');

                            parts.pop();
                            const dev = parts.join('.');

                            context.devices[dev] = context.devices[dev] || [];
                            context.devices[dev].push(_id);

                            context.channels[chn] = context.channels[chn] || [];
                            context.channels[chn].push(_id);
                        }
                    }
                }
            }

            /** 
             * applies all selectors targeting an object or state ID
             * @param {string} objId
             * @param {Selector[]} selectors
             */
            function applyIDSelectors(objId, selectors) {
                // Only keep the ID if it matches every ID selector
                return selectors.every(selector => {
                    return selector.idRegExp == null || selector.idRegExp.test(objId);
                });
            }

            /** 
             * Applies all selectors targeting the Object common properties
             * @param {string} objId - The ID of the object in question
             */
            function applyCommonSelectors(objId) {
                const obj = objects[objId];
                if (obj == null || obj.common == null) return false;
                const objCommon = obj.common;
                // make sure this object satisfies all selectors
                return commonSelectors.every(selector => {
                    return (
                        // ensure a property exists
                        (selector.value === undefined && objCommon[selector.attr] !== undefined)
                        // or match exact values
                        || looselyEqualsString(objCommon[selector.attr], selector.value)
                    );
                });
            }

            /** 
             * Applies all selectors targeting the Object native properties
             * @param {string} objId - The ID of the object in question
             */
            function applyNativeSelectors(objId) {
                const obj = objects[objId];
                if (obj == null || obj.native == null) return false;
                const objNative = obj.native;
                // make sure this object satisfies all selectors
                return nativeSelectors.every(selector => {
                    return (
                        // ensure a property exists
                        (selector.value === undefined && objNative[selector.attr] !== undefined)
                        // or match exact values
                        || looselyEqualsString(objNative[selector.attr], selector.value)
                    );
                });
            }

            /** 
             * Applies all selectors targeting the Objects enums
             * @param {string} objId - The ID of the object in question
             */
            function applyEnumSelectors(objId) {
                const enumIds = [];
                eventObj.getObjectEnumsSync(context, objId, enumIds);
                // make sure this object satisfies all selectors
                return enumSelectors.every(_enum => enumIds.indexOf(_enum) > -1);
            }

            /** @type {string[]} */
            let res = [];

            if (name === 'channel') {
                // go through all channels
                res = Object.keys(context.channels)
                    // filter out those that don't match every ID selector for the channel ID
                    .filter(channelId => applyIDSelectors(channelId, objectIdSelectors))
                    // filter out those that don't match every common selector
                    .filter(channelId => applyCommonSelectors(channelId))
                    // filter out those that don't match every native selector
                    .filter(channelId => applyNativeSelectors(channelId))
                    // filter out those that don't match every enum selector
                    .filter(channelId => applyEnumSelectors(channelId))
                    // retrieve the state ID collection for all remaining channels
                    .map(id => context.channels[id])
                    // and flatten the array to get only the state IDs
                    .reduce((acc, next) => acc.concat(next), [])
                    // now filter out those that don't match every ID selector for the state ID
                    .filter(stateId => applyIDSelectors(stateId, stateIdSelectors))
                ;

            } else if (name === 'device') {
                // go through all devices
                res = Object.keys(context.devices)
                    // filter out those that don't match every ID selector for the channel ID
                    .filter(deviceId => applyIDSelectors(deviceId, objectIdSelectors))
                    // filter out those that don't match every common selector
                    .filter(deviceId => applyCommonSelectors(deviceId))
                    // filter out those that don't match every native selector
                    .filter(deviceId => applyNativeSelectors(deviceId))
                    // filter out those that don't match every enum selector
                    .filter(deviceId => applyEnumSelectors(deviceId))
                    // retrieve the state ID collection for all remaining devices
                    .map(id => context.devices[id])
                    // and flatten the array to get only the state IDs
                    .reduce((acc, next) => acc.concat(next), [])
                    // now filter out those that don't match every ID selector for the state ID
                    .filter(stateId => applyIDSelectors(stateId, stateIdSelectors))
                ;

            } else {
                // go through all states
                res = context.stateIds;
                // if the "name" is not state then we filter for the ID aswell
                if (name && name !== 'state') {
                    const r = new RegExp('^' + name.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
                    res = res.filter(id => r.test(id));
                }

                res = res
                    // filter out those that don't match every ID selector for the object ID or the state ID
                    .filter(id => applyIDSelectors(id, objectIdSelectors))
                    .filter(id => applyIDSelectors(id, stateIdSelectors))
                    // filter out those that don't match every common selector
                    .filter(id => applyCommonSelectors(id))
                    // filter out those that don't match every native selector
                    .filter(id => applyNativeSelectors(id))
                    // filter out those that don't match every enum selector
                    .filter(id => applyEnumSelectors(id))
                ;
            }

            for (let i = 0; i < res.length; i++) {
                result[i] = res[i];
            }
            result.length = res.length;
            result.each = function (callback) {
                if (typeof callback === 'function') {
                    let r;
                    for (let i = 0; i < this.length; i++) {
                        r = callback(result[i], i);
                        if (r === false) break;
                    }
                }
                return this;
            };
            result.getState = function (callback) {
                if (adapter.config.subscribe) {
                    if (typeof callback !== 'function') {
                        sandbox.log('You cannot use this function synchronous', 'error');
                    } else {
                        adapter.getForeignState(this[0], callback);
                    }
                } else {
                    if (this[0]) return states[this[0]];
                    return null;
                }
            };
            result.setState = function (state, isAck, callback) {
                if (typeof isAck === 'function') {
                    callback = isAck;
                    isAck = undefined;
                }

                if (isAck === true || isAck === false || isAck === 'true' || isAck === 'false') {
                    if (typeof state === 'object') {
                        state.ack = isAck;
                    } else {
                        state = {val: state, ack: isAck};
                    }
                }
                let cnt = 0;
                for (let i = 0; i < this.length; i++) {
                    cnt++;
                    adapter.setForeignState(this[i], state, function () {
                        if (!--cnt && typeof callback === 'function') callback();
                    });
                }
                return this;
            };
            result.on = function (callbackOrId, value) {
                for (let i = 0; i < this.length; i++) {
                    sandbox.subscribe(this[i], callbackOrId, value);
                }
                return this;
            };
            return result;
        },
        log:       function (msg, sev) {
            if (!sev) sev = 'info';
            if (!adapter.log[sev]) {
                msg = 'Unknown severity level "' + sev + '" by log of [' + msg + ']';
                sev = 'warn';
            }
            adapter.log[sev](name + ': ' + msg);
        },
        exec:      function (cmd, callback) {
            if (!adapter.config.enableExec) {
                const error = 'exec is not available. Please enable "Enable Exec" option in instance settings';
                adapter.log.error(error);
                sandbox.log(error);
                if (typeof callback === 'function') {
                    setImmediate(callback, error);
                }
            } else {
                if (sandbox.verbose) {
                    sandbox.log('exec: ' + cmd, 'info');
                }
                if (debug) {
                    sandbox.log(words._('Command %s was not executed, while debug mode is active', cmd), 'warn');
                    if (typeof callback === 'function') {
                        setImmediate(function () {
                            callback();
                        });
                    }
                } else {
                    return mods.child_process.exec(cmd, callback);
                }
            }
        },
        email:     function (msg) {
            if (sandbox.verbose) sandbox.log('email(msg=' + JSON.stringify(msg) + ')', 'info');
            adapter.sendTo('email', msg);
        },
        pushover:  function (msg) {
            if (sandbox.verbose) sandbox.log('pushover(msg=' + JSON.stringify(msg) + ')', 'info');
            adapter.sendTo('pushover', msg);
        },
        subscribe: function (pattern, callbackOrId, value) {
            if (pattern && Array.isArray(pattern)) {
                const result = [];
                for (let t = 0; t < pattern.length; t++) {
                    result.push(sandbox.subscribe(pattern[t], callbackOrId, value));
                }
                return result;
            }
            if (pattern && pattern.id && Array.isArray(pattern.id)) {
                const result_ = [];
                for (let tt = 0; tt < pattern.id.length; tt++) {
                    const pa = JSON.parse(JSON.stringify(pattern));
                    pa.id = pattern.id[tt];
                    result_.push(sandbox.subscribe(pa, callbackOrId, value));
                }
                return result_;
            }

            // try to detect astro or cron (by spaces)
            if (typeof pattern === 'object' || (typeof pattern === 'string' && pattern.match(/[,/\d*]+\s[,/\d*]+\s[,/\d*]+/))) {
                if (pattern.astro) {
                    return sandbox.schedule(pattern, callbackOrId);
                } else if (pattern.time) {
                    return sandbox.schedule(pattern.time, callbackOrId);
                }
            }

            let callback;

            sandbox.__engine.__subscriptions += 1;

            // source is set by regexp if defined as /regexp/
            if (typeof pattern !== 'object' || pattern instanceof RegExp || pattern.source) {
                pattern = {id: pattern, change: 'ne'};
            }

            if (pattern.id !== undefined && !pattern.id) {
                adapter.log.error('Error by subscription: empty ID defined. All states matched.');
                return;
            }

            // add adapter namespace if nothing given
            if (pattern.id && typeof pattern.id === 'string' && pattern.id.indexOf('.') === -1) {
                pattern.id = adapter.namespace + '.' + pattern.id;
            }

            if (typeof callbackOrId === 'function') {
                callback = callbackOrId;
            } else {
                const that = this;
                if (typeof value === 'undefined') {
                    callback = function (obj) {
                        that.setState(callbackOrId, obj.newState.val);
                    };
                } else {
                    callback = function (/* obj */) {
                        that.setState(callbackOrId, value);
                    };
                }
            }

            const subs = {
                pattern:  pattern,
                callback: function (obj) {
                    if (typeof callback === 'function') {
                        try {
                            callback.call(sandbox, obj);
                        } catch (e) {
                            errorInCallback(e); // adapter.log.error('Error in callback: ' + e);
                        }
                    }
                },
                name:     name
            };

            // try to extract adapter
            if (pattern.id && typeof pattern.id === 'string') {
                const parts = pattern.id.split('.');
                const a = parts[0] + '.' + parts[1];
                const _adapter = 'system.adapter.' + a;

                if (objects[_adapter] && objects[_adapter].common && objects[_adapter].common.subscribable) {
                    const alive = 'system.adapter.' + a + '.alive';
                    context.adapterSubs[alive] = context.adapterSubs[alive] || [];

                    const subExists = context.adapterSubs[alive].filter(function (sub) {
                        return sub === pattern.id;
                    }).length > 0;

                    if (!subExists) {
                        context.adapterSubs[alive].push(pattern.id);
                        adapter.sendTo(a, 'subscribe', pattern.id);
                    }
                }
            }
            if (sandbox.verbose) sandbox.log('subscribe: ' + JSON.stringify(subs), 'info');

            subscribePattern(script, pattern.id);

            subs.patternCompareFunctions = getPatternCompareFunctions(pattern);
            context.subscriptions.push(subs);

            if (pattern.enumName || pattern.enumId) context.isEnums = true;
            return subs;
        },
        getSubscriptions: function () {
            const result = {};
            for (let s = 0; s < context.subscriptions.length; s++) {
                result[context.subscriptions[s].pattern.id] = result[context.subscriptions[s].pattern.id] || [];
                result[context.subscriptions[s].pattern.id].push({ name: context.subscriptions[s].name, pattern: context.subscriptions[s].pattern });
            }
            if (sandbox.verbose) sandbox.log('getSubscriptions() => ' + JSON.stringify(result), 'info');
            return result;
        },
        adapterSubscribe: function (id) {
            if (typeof id !== 'string') {
                adapter.log.error('adapterSubscribe: invalid type of id' + typeof id);
                return;
            }
            const parts = id.split('.');
            const _adapter = 'system.adapter.' + parts[0] + '.' + parts[1];
            if (objects[_adapter] && objects[_adapter].common && objects[_adapter].common.subscribable) {
                const a = parts[0] + '.' + parts[1];
                const alive = 'system.adapter.' + a + '.alive';
                context.adapterSubs[alive] = context.adapterSubs[alive] || [];
                context.adapterSubs[alive].push(id);
                if (sandbox.verbose) sandbox.log('adapterSubscribe: ' + a + ' - ' + id, 'info');
                adapter.sendTo(a, 'subscribe', id);
            }
        },
        adapterUnsubscribe: function (id) {
            return sandbox.unsubscribe(id);
        },
        unsubscribe:    function (idOrObject) {
            if (idOrObject && Array.isArray(idOrObject)) {
                const result = [];
                for (let t = 0; t < idOrObject.length; t++) {
                    result.push(sandbox.unsubscribe(idOrObject[t]));
                }
                return result;
            }
            if (sandbox.verbose) sandbox.log('adapterUnsubscribe(id=' + idOrObject + ')', 'info');
            if (typeof idOrObject === 'object') {
                for (let i = context.subscriptions.length - 1; i >= 0; i--) {
                    if (context.subscriptions[i] === idOrObject) {
                        unsubscribePattern(context.subscriptions[i].pattern.id);
                        context.subscriptions.splice(i, 1);
                        sandbox.__engine.__subscriptions--;
                        return true;
                    }
                }
            } else {
                let deleted = 0;
                for (let i = context.subscriptions.length - 1; i >= 0; i--) {
                    if (context.subscriptions[i].name === name && context.subscriptions[i].pattern.id === idOrObject) {
                        deleted++;
                        unsubscribePattern(context.subscriptions[i].pattern.id);
                        context.subscriptions.splice(i, 1);
                        sandbox.__engine.__subscriptions--;
                    }
                }
                return !!deleted;
            }
        },
        on:             function (pattern, callbackOrId, value) {
            return sandbox.subscribe(pattern, callbackOrId, value);
        },
        schedule:       function (pattern, callback) {
            if (typeof callback !== 'function') {
                adapter.log.error(name + ': schedule callback missing');
                return;
            }

            sandbox.__engine.__schedules += 1;

            if (pattern.astro) {

                const nowdate = new Date();

                if (adapter.config.latitude === undefined || adapter.config.longitude === undefined ||
                    adapter.config.latitude === ''        || adapter.config.longitude === '' ||
                    adapter.config.latitude === null      || adapter.config.longitude === null) {
                    adapter.log.error('Longitude or latitude does not set. Cannot use astro.');
                    return;
                }

                let ts = mods.suncalc.getTimes(nowdate, adapter.config.latitude, adapter.config.longitude)[pattern.astro];

                if (ts.getTime().toString() === 'NaN') {
                    adapter.log.warn('Cannot calculate "' + pattern.astro + '" for ' + adapter.config.latitude + ', ' + adapter.config.longitude);
                    ts = new Date(nowdate.getTime());

                    if (pattern.astro === 'sunriseEnd'       ||
                        pattern.astro === 'goldenHourEnd'    ||
                        pattern.astro === 'sunset'           ||
                        pattern.astro === 'nightEnd'         ||
                        pattern.astro === 'nauticalDusk') {
                        ts.setMinutes(59);
                        ts.setHours(23);
                        ts.setSeconds(59);
                    } else {
                        ts.setMinutes(59);
                        ts.setHours(23);
                        ts.setSeconds(58);
                    }
                }

                if (ts && pattern.shift) {
                    ts = new Date(ts.getTime() + (pattern.shift * 60000));
                }

                if (!ts || ts < nowdate) {
                    const date = new Date(nowdate);
                    // Event doesn't occur today - try again tomorrow
                    // Calculate time till 24:00 and set timeout
                    date.setDate(date.getDate() + 1);
                    date.setMinutes(1); // Somtimes timer fires at 23:59:59
                    date.setHours(0);
                    date.setSeconds(0);
                    date.setMilliseconds(0);
                    date.setMinutes(-date.getTimezoneOffset());


                    // Calculate new schedule in the next day
                    sandbox.setTimeout(function () {
                        if (sandbox.__engine.__schedules > 0) sandbox.__engine.__schedules--;

                        sandbox.schedule(pattern, callback);
                    }, date.getTime() - nowdate.getTime());

                    return;
                }

                sandbox.setTimeout(function () {
                    try {
                        callback.call(sandbox);
                    } catch (e) {
                        errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                    }
                    // Reschedule in 2 seconds
                    sandbox.setTimeout(function () {
                        if (sandbox.__engine.__schedules > 0) sandbox.__engine.__schedules--;
                        sandbox.schedule(pattern, callback);
                    }, 2000);

                }, ts.getTime() - nowdate.getTime());

                if (sandbox.verbose) sandbox.log('schedule(astro=' + pattern.astro + ', offset=' + pattern.shift + ')', 'info');

            } else {
                // fix problem with sunday and 7
                if (typeof pattern === 'string') {
                    const parts = pattern.replace(/\s+/g, ' ').split(' ');
                    if (parts.length >= 5 && parts[5] >= 7) parts[5] = 0;
                    pattern = parts.join(' ');
                }
                const schedule = nodeSchedule.scheduleJob(pattern, function () {
                    try {
                        callback.call(sandbox);
                    } catch (e) {
                        errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                    }
                });

                script.schedules.push(schedule);

                if (sandbox.verbose) sandbox.log('schedule(cron=' + pattern + ')', 'info');

                return schedule;
            }
        },
        getAstroDate:   function (pattern, date, offsetMinutes) {
            if (date === undefined) date = new Date();

            if (consts.astroList.indexOf(pattern) === -1) {
                const pos = consts.astroListLow.indexOf(pattern.toLowerCase());
                if (pos !== -1) pattern = consts.astroList[pos];
            }

            if ((!adapter.config.latitude  && adapter.config.latitude  !== 0) ||
                (!adapter.config.longitude && adapter.config.longitude !== 0)) {
                adapter.log.error('Longitude or latitude does not set. Cannot use astro.');
                return;
            }

            let ts = mods.suncalc.getTimes(date, adapter.config.latitude, adapter.config.longitude)[pattern];

            if (ts === undefined || ts.getTime().toString() === 'NaN') {
                adapter.log.error('Cannot get astro date for "' + pattern + '"');
            }

            if (sandbox.verbose) sandbox.log('getAstroDate(pattern=' + pattern + ', date=' + date + ') => ' + ts, 'info');

            if (offsetMinutes !== undefined) {
                ts = new Date(ts.getTime() + (offsetMinutes * 60000));
            }
            return ts;
        },
        isAstroDay:     function () {
            const nowDate  = new Date();
            const dayBegin = sandbox.getAstroDate('sunrise');
            const dayEnd   = sandbox.getAstroDate('sunset');

            if (dayBegin === undefined || dayEnd === undefined) return;

            if (sandbox.verbose) sandbox.log('isAstroDay() => ' + (nowDate >= dayBegin && nowDate <= dayEnd), 'info');

            return (nowDate >= dayBegin && nowDate <= dayEnd);
        },
        clearSchedule:  function (schedule) {
            for (let i = 0; i < script.schedules.length; i++) {
                if (script.schedules[i] === schedule) {
                    if (!nodeSchedule.cancelJob(script.schedules[i])) {
                        adapter.log.error('Error by canceling scheduled job');
                    }
                    delete script.schedules[i];
                    script.schedules.splice(i, 1);
                    if (sandbox.verbose) sandbox.log('clearSchedule() => cleared', 'info');
                    return true;
                }
            }
            if (sandbox.verbose) sandbox.log('clearSchedule() => invalid handler', 'warn');
            return false;
        },
        setState:       function (id, state, isAck, callback) {
            if (typeof isAck === 'function') {
                callback = isAck;
                isAck = undefined;
            }

            if (state === null) {
                state = {val: null};
            }

            if (isAck === true || isAck === false || isAck === 'true' || isAck === 'false') {
                if (typeof state === 'object') {
                    state.ack = isAck;
                } else {
                    state = {val: state, ack: isAck};
                }
            }

            // Check type of state
            if (!objects[id] && objects[adapter.namespace + '.' + id]) {
                id = adapter.namespace + '.' + id;
            }

            const common = objects[id] ? objects[id].common : null;
            if (common &&
                common.type &&
                common.type !== 'mixed' &&
                common.type !== 'file'  &&
                common.type !== 'json') {
                if (state && typeof state === 'object' && state.val !== undefined) {
                    if (common.type !== typeof state.val) {
                        context.logWithLineInfo.warn('Wrong type of ' + id + ': "' + typeof state.val + '". Please fix, while deprecated and will not work in next versions.');
                        //return;
                    }
                } else {
                    if (common.type !== typeof state) {
                        context.logWithLineInfo.warn('Wrong type of ' + id + ': "' + typeof state + '". Please fix, while deprecated and will not work in next versions.');
                        //return;
                    }
                }
            }
            // Check min and max of value
            if (typeof state === 'object' && state) {
                if (common && typeof state.val === 'number') {
                    if (common.min !== undefined && state.val < common.min) state.val = common.min;
                    if (common.max !== undefined && state.val > common.max) state.val = common.max;
                }
            } else if (common && typeof state === 'number') {
                if (common.min !== undefined && state < common.min) state = common.min;
                if (common.max !== undefined && state > common.max) state = common.max;
            }

            if (objects[id]) {
                if (sandbox.verbose) sandbox.log('setForeignState(id=' + id + ', state=' + JSON.stringify(state) + ')', 'info');

                if (debug) {
                    sandbox.log('setForeignState(id=' + id + ', state=' + JSON.stringify(state) + ') - ' + words._('was not executed, while debug mode is active'), 'warn');

                    if (typeof callback === 'function') {
                        setTimeout(function () {
                            try {
                                callback.call(sandbox);
                            } catch (e) {
                                errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                            }
                        }, 0);
                    }
                } else {
                    adapter.setForeignState(id, state, function (err) {
                        if (err) sandbox.log('setForeignState: ' + err, 'error');

                        if (typeof callback === 'function') {
                            try {
                                callback.call(sandbox);
                            } catch (e) {
                                errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                            }
                        }
                    });
                }
            } else if (objects[adapter.namespace + '.' + id]) {
                if (sandbox.verbose) sandbox.log('setState(id=' + id + ', state=' + JSON.stringify(state) + ')', 'info');

                if (debug) {
                    sandbox.log('setState(' + id + ', ' + JSON.stringify(state) + ') - ' + words._('was not executed, while debug mode is active'), 'warn');
                    if (typeof callback === 'function') {
                        setTimeout(function () {
                            try {
                                callback.call(sandbox);
                            } catch (e) {
                                errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                            }
                        }, 0);
                    }
                } else {
                    adapter.setState(id, state, function (err) {
                        if (err) sandbox.log('setState: ' + err, 'error');

                        if (typeof callback === 'function') {
                            try {
                                callback.call(sandbox);
                            } catch (e) {
                                errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                            }
                        }
                    });
                }
            } else {
                if (objects[id]) {
                    if (objects[id].type === 'state') {
                        if (sandbox.verbose) sandbox.log('setForeignState(id=' + id + ', state=' + JSON.stringify(state) + ')', 'info');

                        if (debug) {
                            sandbox.log('setForeignState(id=' + id + ', state=' + JSON.stringify(state) + ') - ' + words._('was not executed, while debug mode is active'), 'warn');
                            if (typeof callback === 'function') {
                                setTimeout(function () {
                                    try {
                                        callback.call(sandbox);
                                    } catch (e) {
                                        errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                                    }
                                }, 0);
                            }
                        } else {
                            adapter.setForeignState(id, state, function (err) {
                                if (err) sandbox.log('setForeignState: ' + err, 'error');

                                if (typeof callback === 'function') {
                                    try {
                                        callback.call(sandbox);
                                    } catch (e) {
                                        errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                                    }
                                }
                            });
                        }
                    } else {
                        adapter.log.warn('Cannot set value of non-state object "' + id + '"');
                        if (typeof callback === 'function') {
                            try {
                                callback.call(sandbox, 'Cannot set value of non-state object "' + id + '"');
                            } catch (e) {
                                errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                            }
                        }
                    }
                } else if (objects[adapter.namespace + '.' + id]) {
                    if (objects[adapter.namespace + '.' + id].type === 'state') {
                        if (sandbox.verbose) sandbox.log('setState(id=' + id + ', state=' + JSON.stringify(state) + ')', 'info');

                        if (debug) {
                            sandbox.log('setState(id=' + id + ', state=' + JSON.stringify(state) + ') - ' + words._('was not executed, while debug mode is active'), 'warn');
                            if (typeof callback === 'function') {
                                setTimeout(function () {
                                    try {
                                        callback.call(sandbox);
                                    } catch (e) {
                                        errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                                    }
                                }, 0);
                            }
                        } else {
                            adapter.setState(id, state, function (err) {
                                if (err) sandbox.log('setState: ' + err, 'error');

                                if (typeof callback === 'function') {
                                    try {
                                        callback.call(sandbox);
                                    } catch (e) {
                                        errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                                    }
                                }
                            });
                        }
                    } else {
                        adapter.log.warn('Cannot set value of non-state object "' + adapter.namespace + '.' + id + '"');
                        if (typeof callback === 'function') {
                            try {
                                callback.call(sandbox, 'Cannot set value of non-state object "' + adapter.namespace + '.' + id + '"');
                            } catch (e) {
                                errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                            }
                        }
                    }
                } else {
                    context.logWithLineInfo.warn('State "' + id + '" not found');
                    if (typeof callback === 'function') {
                        try {
                            callback.call(sandbox, 'State "' + id + '" not found');
                        } catch (e) {
                            errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                        }
                    }
                }
            }
        },
        setStateDelayed: function (id, state, isAck, delay, clearRunning, callback) {
            // find arguments
            if (typeof isAck !== 'boolean') {
                callback        = clearRunning;
                clearRunning    = delay;
                delay           = isAck;
                isAck           = false;
            }
            if (typeof delay !== 'number') {
                callback        = clearRunning;
                clearRunning    = delay;
                delay           = 0;
            }
            if (typeof clearRunning !== 'boolean') {
                callback        = clearRunning;
                clearRunning    = true;
            }

            // Check type of state
            if (!objects[id] && objects[adapter.namespace + '.' + id]) {
                id = adapter.namespace + '.' + id;
            }

            if (clearRunning === undefined) clearRunning = true;

            if (sandbox.verbose) sandbox.log('setStateDelayed(id=' + id + ', state=' + state + ', isAck=' + isAck + ', delay=' + delay + ', clearRunning=' + clearRunning + ')', 'info');

            if (clearRunning) {
                if (timers[id]) {
                    if (sandbox.verbose) sandbox.log('setStateDelayed: clear ' + timers[id].length + ' running timers', 'info');

                    for (let i = 0; i < timers[id].length; i++) {
                        clearTimeout(timers[id][i].t);
                    }
                    delete timers[id];
                } else {
                    if (sandbox.verbose) sandbox.log('setStateDelayed: no running timers', 'info');
                }
            }
            // If no delay => start immediately
            if (!delay) {
                sandbox.setState(id, state, isAck, callback);
                return null;
            } else {
                // If delay
                timers[id] = timers[id] || [];

                // calculate timerId
                context.timerId++;
                if (context.timerId > 0xFFFFFFFE) context.timerId = 0;

                // Start timeout
                const timer = setTimeout(function (_timerId, _id, _state, _isAck) {
                    sandbox.setState(_id, _state, _isAck, callback);
                    // delete timer handler
                    if (timers[_id]) {
                        // optimisation
                        if (timers[_id].length === 1) {
                            delete timers[_id];
                        } else {
                            for (let t = 0; t < timers[_id].length; t++) {
                                if (timers[_id][t].id === _timerId) {
                                    timers[_id].splice(t, 1);
                                    break;
                                }
                            }
                            if (!timers[_id].length) delete timers[_id];
                        }

                    }
                }, delay, context.timerId, id, state, isAck);

                // add timer handler
                timers[id].push({
                    t:      timer,
                    id:     context.timerId,
                    ts:     Date.now(),
                    delay:  delay,
                    val:    typeof state === 'object' && state.val !== undefined ? state.val : state,
                    ack:    typeof state === 'object' && state.val !== undefined && state.ack !== undefined ? state.ack : isAck
                });
                return context.timerId;
            }
        },
        clearStateDelayed: function (id, timerId) {
            // Check type of state
            if (!objects[id] && objects[adapter.namespace + '.' + id]) {
                id = adapter.namespace + '.' + id;
            }

            if (sandbox.verbose) sandbox.log('clearStateDelayed(id=' + id + ', timerId=' + timerId + ')', 'info');

            if (timers[id]) {

                for (let i = timers[id].length - 1; i >= 0; i--) {
                    if (timerId === undefined || timers[id][i].id === timerId) {
                        clearTimeout(timers[id][i].t);
                        if (timerId !== undefined) timers[id].splice(i, 1);
                        if (sandbox.verbose) sandbox.log('clearStateDelayed: clear timer ' + timers[id][i].id, 'info');
                    }
                }
                if (timerId === undefined) {
                    delete timers[id];
                } else {
                    if (!timers[id].length) delete timers[id];
                }
                return true;
            }
            return false;
        },
        getStateDelayed: function (id) {
            let result;
            const now = Date.now();
            if (id) {
                // Check type of state
                if (!objects[id] && objects[adapter.namespace + '.' + id]) {
                    id = adapter.namespace + '.' + id;
                }
                // If timerId given
                if (typeof id === 'number') {
                    for (const _id_ in timers) {
                        if (timers.hasOwnProperty(_id_)) {
                            for (let ttt = 0; ttt < timers[_id_].length; ttt++) {
                                if (timers[_id_][ttt].id === id) {
                                    return {
                                        id:         _id_,
                                        left:       timers[_id_][ttt].delay - (now - timers[id][ttt].ts),
                                        delay:      timers[_id_][ttt].delay,
                                        val:        timers[_id_][ttt].val,
                                        ack:        timers[_id_][ttt].ack
                                    };
                                }
                            }
                        }
                    }
                    return null;
                }

                result = [];
                if (timers.hasOwnProperty(id) && timers[id] && timers[id].length) {
                    for (let tt = 0; tt < timers[id].length; tt++) {
                        result.push({
                            timerId:    timers[id][tt].id,
                            left:       timers[id][tt].delay - (now - timers[id][tt].ts),
                            delay:      timers[id][tt].delay,
                            val:        timers[id][tt].val,
                            ack:        timers[id][tt].ack
                        });
                    }
                }
                return result;
            } else {
                result = {};
                for (const _id in timers) {
                    if (timers.hasOwnProperty(_id) && timers[_id] && timers[_id].length) {
                        result[_id] = [];
                        for (let t = 0; t < timers[_id].length; t++) {
                            result[_id].push({
                                timerId:    timers[_id][t].id,
                                left:       timers[_id][t].delay - (now - timers[_id][t].ts),
                                delay:      timers[_id][t].delay,
                                val:        timers[_id][t].val,
                                ack:        timers[_id][t].ack
                            });
                        }
                    }
                }
            }
            return result;
        },
        getState:       function (id, callback) {
            if (typeof callback === 'function') {
                if (id.indexOf('.') === -1) {
                    adapter.getState(id, callback);
                } else {
                    adapter.getForeignState(id, callback);
                }
            } else {
                if (adapter.config.subscribe) {
                    sandbox.log('Cannot use sync getState, use callback instead getState("' + id + '", function (err, state){}); or disable the "Do not subscribe all states on start" option in instance configuration.', 'error');
                } else {
                    if (states[id]) {
                        if (sandbox.verbose) sandbox.log('getState(id=' + id + ', timerId=' + timers[id] + ') => ' + JSON.stringify(states[id]), 'info');
                        return states[id];
                    }
                    if (states[adapter.namespace + '.' + id]) {
                        if (sandbox.verbose) sandbox.log('getState(id=' + id + ', timerId=' + timers[id] + ') => ' + states[adapter.namespace + '.' + id], 'info');
                        return states[adapter.namespace + '.' + id];
                    }

                    if (sandbox.verbose) sandbox.log('getState(id=' + id + ', timerId=' + timers[id] + ') => not found', 'info');

                    context.logWithLineInfo.warn('getState "' + id + '" not found (3)' + (states[id] !== undefined ? ' states[id]=' + states[id] : ''));     ///xxx
                    return {val: null, notExist: true};
                }
            }
        },
        existsState:    function (id) {
            return states.get(id) !== undefined;
        },
        existsObject:   function (id) {
            return objects.get(id) !== undefined;
        },
        getIdByName:    function (name, alwaysArray) {
            if (sandbox.verbose) sandbox.log('getIdByName(name=' + name + ', alwaysArray=' + alwaysArray + ') => ' + context.names[name], 'info');
            if (alwaysArray) {
                if (typeof context.names[name] === 'string') {
                    return [context.names[name]];
                }
                return context.names[name];
            } else {
                return context.names[name];
            }
        },
        getObject:      function (id, enumName, cb) {
            if (typeof enumName === 'function') {
                cb = enumName;
                enumName = null;
            }
            if (typeof cb === 'function') {
                adapter.getForeignObject(id, (err, obj) => {
                    if (obj) {
                        objects[id] = obj;
                    } else if (objects[id]) {
                        delete objects[id];
                    }
                    let result;
                    try {
                        result = JSON.parse(JSON.stringify(objects[id]));
                    } catch (err) {
                        adapter.log.error('Object "' + id + '" can\'t be copied');
                        return null;
                    }
                    if (sandbox.verbose) sandbox.log('getObject(id=' + id + ', enumName=' + enumName + ') => ' + JSON.stringify(result), 'info');
                    cb(err, result);
                });
            } else {
                if (!objects[id]) {
                    if (sandbox.verbose) sandbox.log('getObject(id=' + id + ', enumName=' + enumName + ') => does not exist', 'info');
                    adapter.log.warn('Object "' + id + '" does not exist');
                    return null;
                } else if (enumName) {
                    const e = eventObj.getObjectEnumsSync(context, id);
                    const obj = JSON.parse(JSON.stringify(objects[id]));
                    obj.enumIds   = JSON.parse(JSON.stringify(e.enumIds));
                    obj.enumNames = JSON.parse(JSON.stringify(e.enumNames));
                    if (typeof enumName === 'string') {
                        const r = new RegExp('^enum\\.' + enumName + '\\.');
                        for (let i = obj.enumIds.length - 1; i >= 0; i--) {
                            if (!r.test(obj.enumIds[i])) {
                                obj.enumIds.splice(i, 1);
                                obj.enumNames.splice(i, 1);
                            }
                        }
                    }
                    if (sandbox.verbose) sandbox.log('getObject(id=' + id + ', enumName=' + enumName + ') => ' + JSON.stringify(obj), 'info');

                    return obj;
                } else {
                    let result;
                    try {
                        result = JSON.parse(JSON.stringify(objects[id]));
                    } catch (err) {
                        adapter.log.error('Object "' + id + '" can\'t be copied');
                        return null;
                    }
                    if (sandbox.verbose) sandbox.log('getObject(id=' + id + ', enumName=' + enumName + ') => ' + JSON.stringify(result), 'info');
                    return result;
                }
            }
        },
        setObject:      function (id, obj, callback) {
            adapter.log.error('Function "setObject" is not allowed. Use adapter settings to allow it.');
            if (typeof callback === 'function') {
                try {
                    callback.call(sandbox, 'Function "setObject" is not allowed. Use adapter settings to allow it.');
                } catch (e) {
                    errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                }
            }
        },
        extendObject:   function (id, obj, callback) {
            adapter.log.error('Function "extendObject" is not allowed. Use adapter settings to allow it.');
            if (typeof callback === 'function') {
                try {
                    callback.call(sandbox, 'Function "extendObject" is not allowed. Use adapter settings to allow it.');
                } catch (e) {
                    errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                }
            }
        },
        getEnums:       function (enumName) {
            const result = [];
            const r = enumName ? new RegExp('^enum\\.' + enumName + '\\.') : false;
            for (let i = 0; i < enums.length; i++) {
                if (!r || r.test(enums[i])) {
                    result.push({
                        id:      enums[i],
                        members: (objects[enums[i]].common) ? objects[enums[i]].common.members : [],
                        name:    objects[enums[i]].common.name
                    });
                }
            }
            if (sandbox.verbose) sandbox.log('getEnums(enumName=' + enumName + ') => ' + JSON.stringify(result), 'info');
            return JSON.parse(JSON.stringify(result));
        },
        createState:    function (name, initValue, forceCreation, common, native, callback) {
            if (typeof native === 'function') {
                callback = native;
                native   = {};
            }
            if (typeof common === 'function') {
                callback      = common;
                common        = undefined;
            }
            if (typeof initValue === 'function') {
                callback      = initValue;
                initValue     = undefined;
            }
            if (typeof forceCreation === 'function') {
                callback      = forceCreation;
                forceCreation = undefined;
            }
            if (typeof initValue === 'object') {
                common        = initValue;
                native        = forceCreation;
                forceCreation = undefined;
                initValue     = undefined;
            }
            if (typeof forceCreation === 'object') {
                common        = forceCreation;
                native        = common;
                forceCreation = undefined;
            }
            common = common || {};
            common.name = common.name || name;
            common.role = common.role || 'javascript';
            common.type = common.type || 'mixed';
            if (initValue === undefined) initValue = common.def;

            native = native || {};

            // Check min, max and def values for number
            if (common.type !== undefined && common.type === 'number') {
                let min = 0;
                let max = 0;
                let def = 0;
                let err;
                if (common.min !== undefined) {
                    min = common.min;
                    if (typeof min !== 'number') {
                        min = parseFloat(min);
                        if (isNaN(min)) {
                            err = 'Wrong type of ' + name + '.common.min';
                            sandbox.log(err, 'error');
                            if (typeof callback === 'function') {
                                try {
                                    callback.call(sandbox, err);
                                } catch (e) {
                                    errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                                }
                            }
                            return;
                        } else {
                            common.min = min;
                        }
                    }
                }
                if (common.max !== undefined) {
                    max = common.max;
                    if (typeof max !== 'number') {
                        max = parseFloat(max);
                        if (isNaN(max)) {
                            err = 'Wrong type of ' + name + '.common.max';
                            sandbox.log(err, 'error');
                            if (typeof callback === 'function') {
                                try {
                                    callback.call(sandbox, err);
                                } catch (e) {
                                    errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                                }
                            }
                            return;
                        } else {
                            common.max = max;
                        }
                    }
                }
                if (common.def !== undefined) {
                    def = common.def;
                    if (typeof def !== 'number') {
                        def = parseFloat(def);
                        if (isNaN(def)) {
                            err = 'Wrong type of ' + name + '.common.def';
                            sandbox.log(err, 'error');
                            if (typeof callback === 'function') {
                                try {
                                    callback.call(sandbox, err);
                                } catch (e) {
                                    errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                                }
                            }
                            return;
                        } else {
                            common.def = def;
                        }
                    }
                }
                if (common.min !== undefined && common.max !== undefined && min > max) {
                    common.max = min;
                    common.min = max;
                }
                if (common.def !== undefined && common.min !== undefined && def < min) common.def = min;
                if (common.def !== undefined && common.max !== undefined && def > max) common.def = max;
            }

            if (sandbox.verbose) sandbox.log('createState(name=' + name + ', initValue=' + initValue + ', forceCreation=' + forceCreation + ', common=' + JSON.stringify(common) + ', native=' + JSON.stringify(native) + ')', 'debug');

            if (forceCreation) {
                // todo: store object in objects to have this object directly after callback
                adapter.setObject(name, {
                    common: common,
                    native: native,
                    type:   'state'
                }, function (err) {
                    if (err) adapter.log.warn('Cannot set object "' + name + '": ' + err);

                    if (initValue !== undefined) {
                        if (typeof initValue === 'object' && initValue.ack !== undefined) {
                            adapter.setState(name, initValue, callback);
                        } else {
                            adapter.setState(name, initValue, true, callback);
                        }
                    } else {
                        if (typeof callback === 'function') {
                            try {
                                callback.call(sandbox, name);
                            } catch (e) {
                                errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                            }
                        }
                    }
                });
            } else {
                adapter.getObject(name, function (err, obj) {
                    if (err || !obj) {
                        // todo: store object in objects to have this object directly after callback
                        // create new one
                        if (name.match(/^javascript\.\d+\./)) {
                            adapter.setForeignObject(name, {
                                common: common,
                                native: native,
                                type:   'state'
                            }, function (err) {
                                if (err) adapter.log.warn('Cannot set object "' + name + '": ' + err);

                                if (initValue !== undefined) {
                                    adapter.setForeignState(name, initValue, callback);
                                    if (typeof initValue === 'object' && initValue.ack !== undefined) {
                                        adapter.setForeignState(name, initValue, callback);
                                    } else {
                                        adapter.setForeignState(name, initValue, true, callback);
                                    }
                                } else {
                                    adapter.setForeignState(name, null, true, callback);
                                }
                            });
                        } else {
                            adapter.setObject(name, {
                                common: common,
                                native: native,
                                type:   'state'
                            }, function (err) {
                                if (err) adapter.log.warn('Cannot set object "' + name + '": ' + err);

                                if (initValue !== undefined) {
                                    if (typeof initValue === 'object' && initValue.ack !== undefined) {
                                        adapter.setState(name, initValue, callback);
                                    } else {
                                        adapter.setState(name, initValue, true, callback);
                                    }
                                } else {
                                    adapter.setState(name, null, true, callback);
                                }
                            });

                        }
                    } else {
                        if (!adapter.config.subscribe && !states[name] && !states[adapter.namespace + '.' + name]) {
                            if (name.substring(0, adapter.namespace.length) !== adapter.namespace) {
                                states[adapter.namespace + '.' + name] = {val: null, ack: true};
                            } else {
                                states[name] = {val: null, ack: true};
                            }
                        }
                        // state yet exists
                        if (typeof callback === 'function') {
                            try {
                                callback.call(sandbox, name);
                            } catch (e) {
                                errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                            }
                        }
                    }
                });
            }
        },
        deleteState:    function (id, callback) {
            // todo: check rights
            let found = false;
            if (objects[id]) {
                found = true;
                delete objects[id];
            }
            if (states[id]) delete states[id];
            if (objects[adapter.namespace + '.' + id]) {
                delete objects[adapter.namespace + '.' + id];
                found = true;
            }
            if (states[adapter.namespace + '.' + id]) delete states[adapter.namespace + '.' + id];

            if (sandbox.verbose) sandbox.log('deleteState(id=' + id + ')', 'debug');
            adapter.delObject(id, function (err) {
                if (err) adapter.log.warn('Object for state "' + id + '" does not exist: ' + err);

                adapter.delState(id, function (err) {
                    if (err) adapter.log.error('Cannot delete state "' + id + '": ' + err);
                    if (typeof callback === 'function') {
                        if (typeof callback === 'function') {
                            try {
                                callback.call(sandbox, err, found);
                            } catch (e) {
                                errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                            }
                        }
                    }
                });

            });
        },
        sendTo:         function (_adapter, cmd, msg, callback) {
            if (sandbox.verbose) sandbox.log('sendTo(adapter=' + _adapter + ', cmd=' + cmd + ', msg=' + JSON.stringify(msg) + ')', 'info');
            adapter.sendTo(_adapter, cmd, msg, callback);
        },
        sendto:         function (_adapter, cmd, msg, callback) {
            return sandbox.sendTo(_adapter, cmd, msg, callback);
        },
        sendToHost:     function (host, cmd, msg, callback) {
            if (!adapter.config.enableSendToHost) {
                const error = 'sendToHost is not available. Please enable "Enable SendToHost" option in instance settings';
                adapter.log.error(error);
                sandbox.log(error);
                if (typeof callback === 'function') {
                    setImmediate(function () {
                        callback(error);
                    });
                }
            } else {
                if (sandbox.verbose) sandbox.log('sendToHost(adapter=' + host + ', cmd=' + cmd + ', msg=' + JSON.stringify(msg) + ')', 'info');
                adapter.sendToHost(host, cmd, msg, callback);
            }
        },
        setInterval:    function (callback, ms, arg1, arg2, arg3, arg4) {
            if (typeof callback === 'function') {
                const int = setInterval(function (_arg1, _arg2, _arg3, _arg4) {
                    try {
                        callback.call(sandbox, _arg1, _arg2, _arg3, _arg4);
                    } catch (e) {
                        errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                    }
                }, ms, arg1, arg2, arg3, arg4);
                script.intervals.push(int);

                if (sandbox.verbose) sandbox.log('setInterval(ms=' + ms + ')', 'info');
                return int;
            } else {
                sandbox.log('Invalid callback for setInterval! - ' + typeof callback, 'error');
                return null;
            }
        },
        clearInterval:  function (id) {
            const pos = script.intervals.indexOf(id);
            if (pos !== -1) {
                if (sandbox.verbose) sandbox.log('clearInterval() => cleared', 'info');
                clearInterval(id);
                script.intervals.splice(pos, 1);
            } else {
                if (sandbox.verbose) sandbox.log('clearInterval() => not found', 'warn');
            }
        },
        setTimeout:     function (callback, ms, arg1, arg2, arg3, arg4) {
            if (typeof callback === 'function') {
                const to = setTimeout(function (_arg1, _arg2, _arg3, _arg4) {
                    // Remove timeout from the list
                    const pos = script.timeouts.indexOf(to);
                    if (pos !== -1) script.timeouts.splice(pos, 1);

                    try {
                        callback.call(sandbox, _arg1, _arg2, _arg3, _arg4);
                    } catch (e) {
                        errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                    }
                }, ms, arg1, arg2, arg3, arg4);
                if (sandbox.verbose) sandbox.log('setTimeout(ms=' + ms + ')', 'info');

                script.timeouts.push(to);
                return to;
            } else {
                sandbox.log('Invalid callback for setTimeout! - ' + typeof callback, 'error');
                return null;
            }
        },
        clearTimeout:   function (id) {
            const pos = script.timeouts.indexOf(id);
            if (pos !== -1) {
                if (sandbox.verbose) sandbox.log('clearTimeout() => cleared', 'info');
                clearTimeout(id);
                script.timeouts.splice(pos, 1);
            } else {
                if (sandbox.verbose) sandbox.log('clearTimeout() => not found', 'warn');
            }
        },
        setImmediate:   function (callback, arg1, arg2, arg3, arg4, arg5) {
            if (typeof callback === 'function') {
                setImmediate(function (_arg1, _arg2, _arg3, _arg4, _arg5) {
                    try {
                        callback.call(sandbox, _arg1, _arg2, _arg3, _arg4, _arg5);
                    } catch (e) {
                        errorInCallback(e);
                    }
                }, arg1, arg2, arg3, arg4, arg5);
                if (sandbox.verbose) sandbox.log('setImmediate()', 'info');
            } else {
                sandbox.log('Invalid callback for setImmediate! - ' + typeof callback, 'error');
            }
        },
        cb:             function (callback) {
            return function () {
                if (context.scripts[name] && context.scripts[name]._id === sandbox._id) {
                    if (typeof callback === 'function') {
                        try {
                            callback.apply(this, arguments);
                        } catch (e) {
                            errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                        }
                    }
                } else {
                    adapter.log.warn('Callback for old version of script: ' + name);
                }
            };
        },
        compareTime:    function (startTime, endTime, operation, time) {
            let pos;
            if (startTime && typeof startTime === 'string') {
                if ((pos = consts.astroListLow.indexOf(startTime.toLowerCase())) !== -1) {
                    startTime = sandbox.getAstroDate(consts.astroList[pos]);
                    startTime = startTime.toLocaleTimeString([], {
                        hour:   '2-digit',
                        minute: '2-digit',
                        hour12: false
                    });
                }
            } else if (startTime && typeof startTime === 'object' && startTime.astro) {
                startTime = sandbox.getAstroDate(startTime.astro, startTime.date || new Date(), startTime.offset || 0);
                startTime = startTime.toLocaleTimeString([], {
                    hour:   '2-digit',
                    minute: '2-digit',
                    hour12: false
                });
            }
            if (endTime && typeof endTime === 'string') {
                if ((pos = consts.astroListLow.indexOf(endTime.toLowerCase())) !== -1) {
                    endTime = sandbox.getAstroDate(consts.astroList[pos]);
                    endTime = endTime.toLocaleTimeString([], {
                        hour:   '2-digit',
                        minute: '2-digit',
                        hour12: false
                    });
                }
            } else if (endTime && typeof endTime === 'object' && endTime.astro) {
                endTime = sandbox.getAstroDate(endTime.astro, endTime.date || new Date(), endTime.offset || 0);
                endTime = endTime.toLocaleTimeString([], {
                    hour:   '2-digit',
                    minute: '2-digit',
                    hour12: false
                });
            }
            if (time && typeof time === 'string') {
                if ((pos = consts.astroListLow.indexOf(time.toLowerCase())) !== -1) {
                    time = sandbox.getAstroDate(consts.astroList[pos]);
                }
            } else if (time && typeof time === 'object' && time.astro) {
                time = sandbox.getAstroDate(time.astro, time.date || new Date(), time.offset || 0);
            }

            let daily = true;
            if (time) {
                daily = false;
            }
            if (time && typeof time !== 'object') {
                if (typeof time === 'string' && time.indexOf(' ') === -1 && time.indexOf('T') === -1) {
                    const parts = time.split(':');
                    time = new Date();
                    time.setHours(parseInt(parts[0], 10));
                    time.setMinutes(parseInt(parts[1], 10));
                    time.setMilliseconds(0);

                    if (parts.length === 3) {
                        time.setSeconds(parseInt(parts[2], 10));
                    } else {
                        time.setSeconds(0);
                    }
                } else {
                    time = new Date(time);
                }
            } else if (!time) {
                time = new Date();
                time.setMilliseconds(0);
            }

            if (typeof startTime === 'string') {
                if (startTime.indexOf(' ') === -1 && startTime.indexOf('T') === -1) {
                    const parts = startTime.split(':');
                    startTime = new Date();
                    startTime.setHours(parseInt(parts[0], 10));
                    startTime.setMinutes(parseInt(parts[1], 10));
                    startTime.setMilliseconds(0);

                    if (parts.length === 3) {
                        startTime.setSeconds(parseInt(parts[2], 10));
                    } else {
                        startTime.setSeconds(0);
                    }
                } else {
                    daily = false;
                    startTime = new Date(startTime);
                }
            } else {
                daily = false;
                startTime = new Date(startTime);
            }
            startTime = startTime.getTime();

            if (endTime && typeof endTime === 'string') {
                if (endTime.indexOf(' ') === -1 && endTime.indexOf('T') === -1) {
                    const parts = endTime.split(':');
                    endTime = new Date();
                    endTime.setHours(parseInt(parts[0], 10));
                    endTime.setMinutes(parseInt(parts[1], 10));
                    endTime.setMilliseconds(0);

                    if (parts.length === 3) {
                        endTime.setSeconds(parseInt(parts[2], 10));
                    } else {
                        endTime.setSeconds(0);
                    }
                } else {
                    daily = false;
                    endTime = new Date(endTime);
                }
            } else if (endTime) {
                daily = false;
                endTime = new Date(endTime);
            } else {
                endTime = null;
            }

            if (endTime) endTime = endTime.getTime();

            if (operation === 'between') {
                if (endTime) {
                    if (startTime > endTime && daily) return !(time >= endTime && time < startTime);
                    else return time >= startTime && time < endTime;
                } else {
                    adapter.log.warn('missing or unrecognized endTime expression: ' + endTime);
                    return false;
                }
            } else if (operation === 'not between') {
                if (endTime) {
                    if (startTime > endTime && daily) return time >= endTime && time < startTime;
                    else return !(time >= startTime && time < endTime);
                } else {
                    adapter.log.warn('missing or unrecognized endTime expression: ' + endTime);
                    return false;
                }
            } else if (operation === '>') {
                return time > startTime;
            } else if (operation === '>=') {
                return time >= startTime;
            } else if (operation === '<') {
                return time < startTime;
            } else if (operation === '<=') {
                return time <= startTime;
            } else if (operation === '==') {
                return time === startTime;
            } else if (operation === '<>') {
                return time !== startTime;
            } else {
                adapter.log.warn('Invalid operator: ' + operation);
                return false;
            }
        },
        onStop:         function (cb, timeout) {
            if (sandbox.verbose) sandbox.log('onStop(timeout=' + timeout + ')', 'info');

            script.onStopCb = cb;
            script.onStopTimeout = timeout || 1000;
        },
        formatValue:    function (value, decimals, format) {
            if (!format && objects['system.config']) {
                format = objects['system.config'].common.isFloatComma ? '.,' : ',.';
            }
            return adapter.formatValue(value, decimals, format);
        },

        formatDate:     function (date, format, language) {
            if (!format) {
                format = objects['system.config'] ? (objects['system.config'].common.dateFormat || 'DD.MM.YYYY') : 'DD.MM.YYYY';
            }
            if (format.match(/[WНOО]+/)) {
                let text = adapter.formatDate(date, format);
                if (!language || !consts.dayOfWeeksFull[language]) language = objects['system.config'].common.language;
                const d = date.getDay();
                text = text.replace('WW',  consts.dayOfWeeksFull[language][d]);
                text = text.replace('НН',  consts.dayOfWeeksFull[language][d]);
                text = text.replace('W',   consts.dayOfWeeksShort[language][d]);
                text = text.replace('Н',   consts.dayOfWeeksShort[language][d]);
                text = text.replace('W',   consts.dayOfWeeksShort[language][d]);
                text = text.replace('Н',   consts.dayOfWeeksShort[language][d]);
                const m = date.getMonth();
                text = text.replace('OOO', consts.monthFullGen[language][m]);
                text = text.replace('ООО', consts.monthFullGen[language][m]);
                text = text.replace('OO',  consts.monthFull[language][m]);
                text = text.replace('ОО',  consts.monthFull[language][m]);
                text = text.replace('O',   consts.monthShort[language][m]);
                text = text.replace('О',   consts.monthShort[language][m]);
                text = text.replace('O',   consts.monthShort[language][m]);
                text = text.replace('О',   consts.monthShort[language][m]);
                return text;
            } else {
                return adapter.formatDate(date, format);
            }
        },

        getDateObject:  function (date) {
            if (typeof date === 'object') return date;
            if (typeof date !== 'string') return new Date(date);
            if (date.match(/^\d?\d$/)) {
                const _now = new Date();
                date = _now.getFullYear() + '-' + (_now.getMonth() + 1) + '-' + _now.getDate() + ' ' + date + ':00';
            } else {
                // 20:00, 2:00, 20:00:00, 2:00:00
                if (date.match(/^\d?\d:\d\d(:\d\d)?$/)) {
                    const now = new Date();
                    date = now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate() + ' ' + date;
                }
            }
            return new Date(date);
        },

        writeFile:      function (_adapter, fileName, data, callback) {
            if (typeof data === 'function' || !data) {
                callback = data;
                data     = fileName;
                fileName = _adapter;
                _adapter = null;
            }

            if (debug) {
                sandbox.log('readFile(adapter=' + _adapter + ', fileName=' + fileName + ') - ' + words._('was not executed, while debug mode is active'), 'warn');
                if (typeof callback === 'function') {
                    setTimeout(function () {
                        try {
                            callback.call(sandbox);
                        } catch (e) {
                            errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                        }
                    }, 0);
                }
            } else {
                if (sandbox.verbose) sandbox.log('readFile(adapter=' + _adapter + ', fileName=' + fileName + ')', 'info');
                adapter.writeFile(_adapter, fileName, data, callback);
            }
        },
        readFile:       function (_adapter, fileName, callback) {
            if (typeof fileName === 'function') {
                callback = fileName;
                fileName = _adapter;
                _adapter = null;
            }
            if (sandbox.verbose) sandbox.log('readFile(adapter=' + _adapter + ', fileName=' + fileName + ')', 'info');

            adapter.readFile(_adapter, fileName, callback);
        },
        unlink:         function (_adapter, fileName, callback) {
            if (typeof fileName === 'function') {
                callback = fileName;
                fileName = _adapter;
                _adapter = null;
            }
            if (sandbox.verbose) sandbox.log('unlink(adapter=' + _adapter + ', fileName=' + fileName + ')', 'info');

            if (debug) {
                sandbox.log('unlink(adapter=' + _adapter + ', fileName=' + fileName + ') - ' + words._('was not executed, while debug mode is active'), 'warn');
                if (typeof callback === 'function') {
                    setTimeout(function () {
                        try {
                            callback.call(sandbox);
                        } catch (e) {
                            errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                        }
                    }, 0);
                }
            } else {
                if (sandbox.verbose) sandbox.log('unlink(adapter=' + _adapter + ', fileName=' + fileName + ')', 'info');
                adapter.unlink(_adapter, fileName, callback);
            }
        },
        delFile:        function (_adapter, fileName, callback) {
            return sandbox.unlink(_adapter, fileName, callback);
        },
        getHistory:     function (instance, options, callback) {
            if (typeof instance === 'object') {
                callback = options;
                options  = instance;
                instance = null;
            }

            if (typeof callback !== 'function') {
                adapter.log.error('No callback found!');
                return;
            }
            if (typeof options !== 'object') {
                adapter.log.error('No options found!');
                return;
            }
            if (!options.id) {
                adapter.log.error('No ID found!');
                return;
            }
            const timeoutMs = parseInt(options.timeout, 10) || 20000;

            if (!instance) {
                instance = objects['system.config'] ? objects['system.config'].common.defaultHistory : null;
            }

            if (sandbox.verbose) sandbox.log('getHistory(instance=' + instance + ', options=' + JSON.stringify(options) + ')', 'debug');

            if (!instance) {
                adapter.log.error('No default history instance found!');
                try {
                    callback.call(sandbox, 'No default history instance found!');
                } catch (e) {
                    errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                }
                return;
            }
            if (instance.match(/^system\.adapter\./)) instance = instance.substring('system.adapter.'.length);

            if (!objects['system.adapter.' + instance]) {
                adapter.log.error('Instance "' + instance + '" not found!');
                try {
                    callback.call(sandbox, 'Instance "' + instance + '" not found!');
                } catch (e) {
                    errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                }
                return;
            }
            let timeout = setTimeout(function () {
                timeout = null;

                if (sandbox.verbose) sandbox.log('getHistory => timeout', 'debug');

                if (typeof callback === 'function') {
                    try {
                        callback.call(sandbox, 'Timeout', null, options, instance);
                    } catch (e) {
                        errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                    }
                    callback = null;
                }
            }, timeoutMs);

            adapter.sendTo(instance, 'getHistory', {id: options.id, options: options}, function (result) {
                if (timeout) clearTimeout(timeout);

                if (sandbox.verbose && result.error)  sandbox.log('getHistory => ' + result.error, 'error');
                if (sandbox.verbose && result.result) sandbox.log('getHistory => ' + result.result.length + ' items', 'debug');

                if (typeof callback === 'function') {
                    try {
                        callback.call(sandbox, result.error, result.result, options, instance);
                    } catch (e) {
                        errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                    }
                    callback = null;
                }

            });
        },
        runScript:      function (scriptName, callback) {
            scriptName = scriptName || name;
            if (!scriptName.match(/^script\.js\./)) scriptName = 'script.js.' + scriptName;
            // start other script
            if (!objects[scriptName] || !objects[scriptName].common) {
                sandbox.log('Cannot start "' + scriptName + '", because not found', 'error');
                return false;
            } else {
                if (debug) {
                    sandbox.log('runScript(scriptName=' + scriptName + ') - ' + words._('was not executed, while debug mode is active'), 'warn');
                } else {
                    if (objects[scriptName].common.enabled) {
                        objects[scriptName].common.enabled = false;
                        adapter.extendForeignObject(scriptName, {common: {enabled: false}}, function (/* err, obj */) {
                            adapter.extendForeignObject(scriptName, {common: {enabled: true}}, function (err) {
                                if (callback === 'function') callback(err);
                            });
                            scriptName = null;
                        });
                    } else {
                        adapter.extendForeignObject(scriptName, {common: {enabled: true}}, function (err) {
                            if (callback === 'function') callback(err);
                        });
                    }
                }
                return true;
            }
        },
        startScript:    function (scriptName, ignoreIfStarted, callback) {
            if (typeof ignoreIfStarted === 'function') {
                callback = ignoreIfStarted;
                ignoreIfStarted = false;
            }
            scriptName = scriptName || name;
            if (!scriptName.match(/^script\.js\./)) scriptName = 'script.js.' + scriptName;
            // start other script
            if (!objects[scriptName] || !objects[scriptName].common) {
                sandbox.log('Cannot start "' + scriptName + '", because not found', 'error');
                return false;
            } else {
                console.log('STARTING!');
                if (debug) {
                    sandbox.log('startScript(scriptName=' + scriptName + ') - ' + words._('was not executed, while debug mode is active'), 'warn');
                } else {
                    if (objects[scriptName].common.enabled) {
                        if (!ignoreIfStarted) {
                            objects[scriptName].common.enabled = false;
                            adapter.extendForeignObject(scriptName, {common: {enabled: false}}, function (err) {
                                adapter.extendForeignObject(scriptName, {common: {enabled: true}}, function (err) {
                                    if (callback === 'function') callback(err, true);
                                });
                                scriptName = null;
                            });
                        } else if (callback === 'function') {
                            callback(null, false);
                        }
                    } else {
                        adapter.extendForeignObject(scriptName, {common: {enabled: true}}, function (err) {
                            if (callback === 'function') callback(err, true);
                        });
                    }
                }
                return true;
            }
        },
        stopScript:     function (scriptName, callback) {
            scriptName = scriptName || name;

            if (!scriptName.match(/^script\.js\./)) scriptName = 'script.js.' + scriptName;

            // stop other script
            if (!objects[scriptName] || !objects[scriptName].common) {
                sandbox.log('Cannot stop "' + scriptName + '", because not found', 'error');
                return false;
            } else {
                if (debug) {
                    sandbox.log('stopScript(scriptName=' + scriptName + ') - ' + words._('was not executed, while debug mode is active'), 'warn');
                } else {
                    if (objects[scriptName].common.enabled) {
                        objects[scriptName].common.enabled = false;
                        adapter.extendForeignObject(scriptName, {common: {enabled: false}}, function (err) {
                            if (callback === 'function') callback(err, true);
                            scriptName = null;
                        });
                    } else if (callback === 'function') {
                        callback(null, false);
                    }
                }
                return true;
            }
        },
        isScriptActive: function (scriptName) {
            if (!scriptName.match(/^script\.js\./)) scriptName = 'script.js.' + scriptName;
            if (!objects[scriptName] || !objects[scriptName].common) {
                sandbox.log('Script does not exist', 'error');
                return false;
            } else {
                return objects[scriptName].common.enabled;
            }
        },
        toInt:          function (val) {
            if (val === true || val === 'true') val = 1;
            if (val === false || val === 'false') val = 0;
            val = parseInt(val) || 0;
            return val;
        },
        toFloat:        function (val) {
            if (val === true || val === 'true') val = 1;
            if (val === false || val === 'false') val = 0;
            val = parseFloat(val) || 0;
            return val;
        },
        toBoolean:      function (val) {
            if (val === '1' || val === 'true') val = true;
            if (val === '0' || val === 'false') val = false;
            return !!val;
        },
        getAttr:        function (obj, path) {
            if (typeof path === 'string') {
                path = path.split('.');
            }
            if (typeof obj === 'string') {
                try {
                    obj = JSON.parse(obj);
                } catch (e) {
                    sandbox.log('Cannot parse "' + obj.substring(0, 30) + '"' + e, 'error');
                    return null;
                }
            }

            const attr = path.shift();
            try {
                obj = obj[attr];
            } catch (e) {
                sandbox.log(`Cannot get ${attr} of ${JSON.stringify(obj)}`, 'error');
                return null;
            }

            if (!path.length) {
                return obj;
            } else {
                const type = typeof obj;
                if (obj === null || obj === undefined || type === 'boolean' || type === 'number') {
                    return null;
                } else {
                    return sandbox.getAttr(obj, path);
                }
            }
        },
        console: {
            log:    function (msg) {
                sandbox.log(msg, 'info');
            },
            error:  function (msg) {
                sandbox.log(msg, 'error');
            },
            warn:   function (msg) {
                sandbox.log(msg, 'warn');
            },
            debug:  function (msg) {
                sandbox.log(msg, 'debug');
            }
        }
    };

    if (adapter.config.enableSetObject) {
        sandbox.setObject = function (id, obj, callback) {
            if (debug) {
                sandbox.log('setObject(id=' + id + ', obj=' + JSON.stringify(obj) + ') - ' + words._('was not executed, while debug mode is active'), 'warn');
                if (typeof callback === 'function') {
                    setTimeout(function () {
                        try {
                            callback.call(sandbox);
                        } catch (e) {
                            errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                        }
                    }, 0);
                }
            } else {
                if (sandbox.verbose) sandbox.log('setObject(id=' + id + ', obj=' + JSON.stringify(obj) + ')', 'info');
                adapter.setForeignObject(id, obj, callback);
            }
        };
        sandbox.extendObject = function (id, obj, callback) {
            if (debug) {
                sandbox.log('extendObject(id=' + id + ', obj=' + JSON.stringify(obj) + ') - ' + words._('was not executed, while debug mode is active'), 'warn');
                if (typeof callback === 'function') {
                    setTimeout(function () {
                        try {
                            callback.call(sandbox);
                        } catch (e) {
                            errorInCallback(e); //adapter.log.error('Error in callback: ' + e)
                        }
                    }, 0);
                }
            } else {
                if (sandbox.verbose) sandbox.log('extendObject(id=' + id + ', obj=' + JSON.stringify(obj) + ')', 'info');
                adapter.extendForeignObject(id, obj, callback);
            }
        };
    }

    // Make all predefined properties and methods readonly so scripts cannot overwrite them
    for (const prop in sandbox) {
        if (sandbox.hasOwnProperty(prop)) {
            Object.defineProperty(sandbox, prop, {
                configurable: false,
                writable: false
            });
        }
    }

    return sandbox;
}

module.exports = sandBox;