// config-utils.js -- helpers for coordinating Application runtime vs. Settings configuration
//
//   * ApplicationConfig -- Menu items and API values.
//   * SettingsConfig -- scoped Settings values.

"use strict";
/* eslint-env commonjs */
/* global log */
module.exports = {
    version: '0.0.1a',
    ApplicationConfig: ApplicationConfig,
    SettingsConfig: SettingsConfig
};

var _utils = require('./_utils.js'),
    assert = _utils.assert;
Object.assign = Object.assign || _utils.assign;

function _debugPrint() {
    print('config-utils | ' + [].slice.call(arguments).join(' '));
}

var debugPrint = function() {};

// ----------------------------------------------------------------------------
// Application-specific configuration values using runtime state / API props
//
// options.config[] supports the following item formats:
//   'settingsName': { menu: 'Menu > MenuItem'}, // assumes MenuItem is a checkbox / checkable value
//   'settingsName': { object: [ MyAvatar, 'property' ] },
//   'settingsName': { object: [ MyAvatar, 'getterMethod', 'setterMethod' ] },
//   'settingsName': { menu: 'Menu > MenuItem', object: [ MyAvatar, 'property' ] },
//   'settingsName': { get: function getter() { ...}, set: function(nv) { ... } },

function ApplicationConfig(options) {
    options = options || {};
    assert('namespace' in options && 'config' in options);
    if (options.debug) {
        debugPrint = _debugPrint;
        debugPrint('debugPrinting enabled');
    }
    this.namespace = options.namespace;
    this.valueUpdated = _utils.signal(function valueUpdated(key, newValue, oldValue, origin){});

    this.config = {};
    this.register(options.config);
}
ApplicationConfig.prototype = {
    resolve: function resolve(key) {
        assert(typeof key === 'string', 'ApplicationConfig.resolve error: key is not a string: ' + key);
        if (0 !== key.indexOf('.') && !~key.indexOf('/')) {
            key = [ this.namespace, key ].join('/');
        }
        return (key in this.config) ? key : (debugPrint('ApplicationConfig -- could not resolve key: ' + key),undefined);
    },
    registerItem: function(settingName, item) {
        item._settingName = settingName;
        item.settingName = ~settingName.indexOf('/') ? settingName : [ this.namespace, settingName ].join('/');
        return this.config[item.settingName] = this.config[settingName] = new ApplicationConfigItem(item);
    },
    // process items into fully-qualfied ApplicationConfigItem instances
    register: function(items) {
        for (var p in items) {
            var item = items[p];
            item && this.registerItem(p, item);
        }
    },
    _getItem: function(key) {
        return this.config[this.resolve(key)];
    },
    getValue: function(key, defaultValue) {
        var item = this._getItem(key);
        if (!item) {
            return defaultValue;
        }
        return item.get();
    },
    setValue: function setValue(key, value) {
        key = this.resolve(key);
        var lastValue = this.getValue(key, value);
        var ret = this._getItem(key).set(value);
        if (lastValue !== value) {
            this.valueUpdated(key, value, lastValue, 'ApplicationConfig.setValue');
        }
        return ret;
    },
    // sync dual-source (ie: Menu + API) items
    resyncValue: function(key) {
        var item = this._getItem(key);
        return item && item.resync();
    },
    // sync Settings values -> Application state
    applyValue: function applyValue(key, value, origin) {
        if (this.resolve(key)) {
            var appValue = this.getValue(key, value);
            debugPrint('applyValue', key, value, origin ? '['+origin+']' : '', appValue);
            if (appValue !== value) {
                this.setValue(key, value);
                debugPrint('applied new setting', key, value, '(was:'+appValue+')');
                return true;
            }
        }
    }
};

// ApplicationConfigItem represents a single API/Menu item accessor
function ApplicationConfigItem(item) {
    Object.assign(this, item);
    Object.assign(this, {
        _item: item.get && item,
        _object: this._parseObjectConfig(this.object),
        _menu: this._parseMenuConfig(this.menu)
    });
    this.authority = this._item ? 'item' : this._object ? 'object' : this._menu ? 'menu' : null;
    this._authority = this['_'+this.authority];
    debugPrint('_authority', this.authority, this._authority, Object.keys(this._authority));
    assert(this._authority, 'expected item.get, .object or .menu definition; ' + this.settingName);
}
ApplicationConfigItem.prototype = {
    resync: function resync() {
        var authoritativeValue = this._authority.get();
        if (this._menu && this._menu.get() !== authoritativeValue) {
            _debugPrint(this.settingName, this._menu.menuItem,
                        '... menu value ('+this._menu.get()+') out of sync;',
                        'setting to authoritativeValue ('+authoritativeValue+')');
            this._menu.set(authoritativeValue);
        }
        if (this._object && this._object.get() !== authoritativeValue) {
            _debugPrint(this.settingName, this._object.getter || this._object.property,
                        '... object value ('+this._object.get()+') out of sync;',
                        'setting to authoritativeValue ('+authoritativeValue+')');
            this._object.set(authoritativeValue);
        }
    },
    toString: function() {
        return '[ApplicationConfigItem ' + [
            'setting:' + JSON.stringify(this.settingName),
            'authority:' + JSON.stringify(this.authority),
            this._object && 'object:' + JSON.stringify(this._object.property || this._object.getter),
            this._menu && 'menu:' + JSON.stringify(this._menu.menu)
        ].filter(Boolean).join(' ') + ']';
    },
    get: function get() {
        return this._authority.get();
    },
    set: function set(nv) {
        this._object && this._object.set(nv);
        this._menu && this._menu.set(nv);
        return nv;
    },
    _raiseError: function(errorMessage) {
        if (this.debug) {
            throw new Error(errorMessage);
        } else {
            _debugPrint('ERROR: ' + errorMessage);
        }
    },
    _parseObjectConfig: function(parts) {
        if (!Array.isArray(parts) || parts.length < 2) {
            return null;
        }
        var object = parts[0], getter = parts[1], setter = parts[2];
        if (typeof object[getter] === 'function' && typeof object[setter] === 'function') {
            // [ API, 'getter', 'setter' ]
            return {
                object: object, getter: getter, setter: setter,
                get: function getObjectValue() {
                    return this.object[this.getter]();
                },
                set: function setObjectValue(nv) {
                    return this.object[this.setter](nv), nv;
                }
            };
        } else if (getter in object) {
            // [ API, 'property' ]
            return {
                object: object, property: getter,
                get: function() {
                    return this.object[this.property];
                },
                set: function(nv) {
                    return this.object[this.property] = nv;
                }
            };
        }
        this._raiseError('{ object: [ Object, getterOrPropertyName, setterName ] } -- invalid params or does not exist: ' +
                         [ this.settingName, this.object, getter, setter ].join(' | '));
    },
    _parseMenuConfig: function(menu) {
        if (!menu || typeof menu !== 'string') {
            return null;
        }
        var parts = menu.split(/\s*>\s*/), menuItemName = parts.pop(), menuName = parts.join(' > ');
        if (menuItemName && Menu.menuItemExists(menuName, menuItemName)) {
            return {
                menu: menu, menuName: menuName, menuItemName: menuItemName,
                get: function() {
                    return Menu.isOptionChecked(this.menuItemName);
                },
                set: function(nv) {
                    return Menu.setIsOptionChecked(this.menuItemName, nv), nv;
                }
            };
        }
        this._raiseError('{ menu: "Menu > Item" } structure -- invalid params or does not exist: ' +
                         [ this.settingName, this.menu, menuName, menuItemName ].join(' | '));
    }
}; // ApplicationConfigItem.prototype

// ----------------------------------------------------------------------------
// grouped configuration using the Settings.* API
function SettingsConfig(options) {
    options = options || {};
    assert('namespace' in options);
    this.namespace = options.namespace;
    this.defaultValues = {};
    this.valueUpdated = _utils.signal(function valueUpdated(key, newValue, oldValue, origin){});
    if (options.defaultValues) {
        Object.keys(options.defaultValues)
            .forEach(_utils.bind(this, function(key) {
                var fullSettingsKey = this.resolve(key);
                this.defaultValues[fullSettingsKey] = options.defaultValues[key];
            }));
    }
}
SettingsConfig.prototype = {
    resolve: function(key) {
        assert(typeof key === 'string', 'SettingsConfig.resolve error: key is not a string: ' + key);
        return (0 !== key.indexOf('.') && !~key.indexOf('/')) ?
            [ this.namespace, key ].join('/') : key;
    },
    getValue: function(key, defaultValue) {
        key = this.resolve(key);
        defaultValue = defaultValue === undefined ? this.defaultValues[key] : defaultValue;
        return Settings.getValue(key, defaultValue);
    },
    setValue: function setValue(key, value) {
        key = this.resolve(key);
        var lastValue = this.getValue(key);
        var ret = Settings.setValue(key, value);
        if (lastValue !== value) {
            this.valueUpdated(key, value, lastValue, 'SettingsConfig.setValue');
        }
        return ret;
    },
    getFloat: function getFloat(key, defaultValue) {
        key = this.resolve(key);
        defaultValue = defaultValue === undefined ? this.defaultValues[key] : defaultValue;
        var value = parseFloat(this.getValue(key, defaultValue));
        return isFinite(value) ? value : isFinite(defaultValue) ? defaultValue : 0.0;
    }
};