// EnumMeta.js -- helper module that maps related enum values to printable names and ids

/* eslint-env commonjs */
/* global DriveKeys, console */

var VERSION = '0.0.1';
var WANT_DEBUG = false;

function _debugPrint() {
    // eslint-disable-next-line no-console
    (typeof Script === 'object' ? print : console.log)('EnumMeta | ' + [].slice.call(arguments).join(' '));
}

var debugPrint = WANT_DEBUG ? _debugPrint : function(){};

try {
    /* global process */
    if (process.title === 'node') {
        _defineNodeJSMocks();
    }
} catch (e) { /* eslint-disable-line empty-block */ }

_debugPrint(VERSION);

// FIXME: C++ emits this action event, but doesn't expose it yet to scripting
//   (ie: as Actions.ZOOM or Actions.TranslateCameraZ)
var ACTION_TRANSLATE_CAMERA_Z = {
    actionName: 'TranslateCameraZ',
    actionID: 12,
    driveKeyName: 'ZOOM'
};

module.exports = {
    version: VERSION,
    DriveKeyNames: invertKeys(DriveKeys),
    Controller: {
        Hardware: Object.keys(Controller.Hardware).reduce(function(names, prop) {
            names[prop+'Names'] = invertKeys(Controller.Hardware[prop]);
            return names;
        }, {}),
        ActionNames: _getActionNames(),
        StandardNames: invertKeys(Controller.Standard)
    },
    getDriveKeyNameFromActionName: getDriveKeyNameFromActionName,
    getActionNameFromDriveKeyName: getActionNameFromDriveKeyName,
    eventKeyText2KeyboardName: eventKeyText2KeyboardName,
    keyboardName2eventKeyText: keyboardName2eventKeyText,
    ACTION_TRANSLATE_CAMERA_Z: ACTION_TRANSLATE_CAMERA_Z,
    INVALID_ACTION_ID: Controller.findAction('INVALID_ACTION_ID_FOO')
};

_debugPrint('///'+VERSION, Object.keys(module.exports));

var actionsMapping = {}, driveKeyMapping = {};

initializeMappings(actionsMapping, driveKeyMapping);

function invertKeys(object) {
    if (!object) {
        return object;
    }
    return Object.keys(object).reduce(function(out, key) {
        out[object[key]] = key;
        return out;
    }, {});
}

function _getActionNames() {
    var ActionNames = invertKeys(Controller.Hardware.Actions);
    ActionNames[ACTION_TRANSLATE_CAMERA_Z.actionID] = ACTION_TRANSLATE_CAMERA_Z.actionName;
    function mapActionName(actionName) {
        var actionKey = Controller.Hardware.Actions[actionName],
            actionID = Controller.findAction(actionName),
            similarName = eventKeyText2KeyboardName(actionName),
            existingName = ActionNames[actionID];

        var keyName = actionName;

        if (actionID === module.exports.INVALID_ACTION_ID) {
            _debugPrint('actionID === INVALID_ACTION_ID', actionName);
        }
        switch (actionName) {
            case 'StepTranslateX': actionName = 'StepTranslate'; break;
            case 'StepTranslateY': actionName = 'StepTranslate'; break;
            case 'StepTranslateZ': actionName = 'StepTranslate'; break;
            case 'ACTION1': actionName = 'PrimaryAction'; break;
            case 'ACTION2': actionName = 'SecondaryAction'; break;
        }
        debugPrint(keyName, actionName, actionKey, actionID);

        similarName = similarName.replace('Lateral','Strafe').replace(/^(?:Longitudinal|Vertical)/, '');
        if (actionID in ActionNames) {
            // check if overlap is just BoomIn <=> BOOM_IN
            if (similarName !== existingName && actionName !== existingName) {
                throw new Error('assumption failed: overlapping actionID:'+JSON.stringify({
                    actionID: actionID,
                    actionKey: actionKey,
                    actionName: actionName,
                    similarName: similarName,
                    keyName: keyName,
                    existingName: existingName
                },0,2));
            }
        } else {
            ActionNames[actionID] = actionName;
            ActionNames[actionKey] = keyName;
        }
    }
    // first map non-legacy (eg: Up and not VERTICAL_UP) actions
    Object.keys(Controller.Hardware.Actions).filter(function(name) {
        return /[a-z]/.test(name);
    }).sort().reverse().forEach(mapActionName);
    // now legacy actions
    Object.keys(Controller.Hardware.Actions).filter(function(name) {
        return !/[a-z]/.test(name);
    }).sort().reverse().forEach(mapActionName);

    return ActionNames;
}

// attempts to brute-force translate an Action name into a DriveKey name
//   eg: _translateActionName('TranslateX') === 'TRANSLATE_X'
//   eg: _translateActionName('Yaw') === 'YAW'
function _translateActionName(name, _index) {
    name = name || '';
    var key = name;
    var re = new RegExp('[A-Z][a-z0-9]+', 'g');
    key = key.replace(re, function(Word) {
        return Word.toUpperCase()+'_';
    })
        .replace(/_$/, '');

    if (key in DriveKeys) {
        debugPrint('getDriveKeyFromEventName', _index, name, key, DriveKeys[key]);
        return key;
    }
}

function getActionNameFromDriveKeyName(driveKeyName) {
    return driveKeyMapping[driveKeyName];
}
// maps an action lookup value to a DriveKey name
//  eg: actionName: 'Yaw' === 'YAW'
//      actionKey:  Controller.Actions.Yaw => 'YAW'
//      actionID:   Controller.findAction('Yaw') => 'YAW'
function getDriveKeyNameFromActionName(lookupValue) {
    if (lookupValue === ACTION_TRANSLATE_CAMERA_Z.actionName ||
        lookupValue === ACTION_TRANSLATE_CAMERA_Z.actionID) {
        return ACTION_TRANSLATE_CAMERA_Z.driveKeyName;
    }
    if (typeof lookupValue === 'string') {
        lookupValue = Controller.findAction(lookupValue);
    }
    return actionsMapping[lookupValue];
}

// maps a Controller.key*Event event.text -> Controller.Hardware.Keyboard[name]
//   eg: ('Page Up') === 'PgUp'
//   eg: ('LEFT') === 'Left'
function eventKeyText2KeyboardName(text) {
    if (eventKeyText2KeyboardName[text]) {
        // use memoized value
        return eventKeyText2KeyboardName[text];
    }
    var keyboardName = (text||'').toUpperCase().split(/[ _]/).map(function(WORD) {
        return WORD.replace(/([A-Z])(\w*)/g, function(_, A, b) {
            return (A.toUpperCase() + b.toLowerCase());
        });
    }).join('').replace('Page','Pg');
    return eventKeyText2KeyboardName[text] = eventKeyText2KeyboardName[keyboardName] = keyboardName;
}

// maps a Controller.Hardware.Keyboard[name] -> Controller.key*Event event.text
//   eg: ('PgUp') === 'PAGE UP'
//   eg: ('Shift') === 'SHIFT'
function keyboardName2eventKeyText(keyName) {
    if (keyboardName2eventKeyText[keyName]) {
        // use memoized value
        return keyboardName2eventKeyText[keyName];
    }
    var text = keyName.replace('Pg', 'Page');
    var caseWords = text.match(/[A-Z][a-z0-9]+/g) || [ text ];
    var eventText = caseWords.map(function(str) {
        return str.toUpperCase();
    }).join('_');
    return keyboardName2eventKeyText[keyName] = eventText;
}

function initializeMappings(actionMap, driveKeyMap) {
    _debugPrint('creating mapping');
    var ref = ACTION_TRANSLATE_CAMERA_Z;
    actionMap[ref.actionName] = actionMap[ref.actionID] = ref.driveKeyName;
    actionMap.BoomIn = 'ZOOM';
    actionMap.BoomOut = 'ZOOM';

    Controller.getActionNames().sort().reduce(
        function(out, name, index, arr) {
            var actionKey = arr[index];
            var actionID = Controller.findAction(name);
            var value = actionID in out ? out[actionID] : _translateActionName(name, index);
            if (value !== undefined) {
                var prefix = (actionID in out ? '+++' : '---');
                debugPrint(prefix + ' Action2DriveKeyName['+name+'('+actionID+')] = ' + value);
                driveKeyMap[value] = driveKeyMap[value] || name;
            }
            out[name] = out[actionID] = out[actionKey] = value;
            return out;
        }, actionMap);
}

// ----------------------------------------------------------------------------
// mocks for litmus testing using Node.js command line tools
function _defineNodeJSMocks() {
    /* eslint-disable no-global-assign */
    DriveKeys = {
        TRANSLATE_X: 12345
    };
    Controller = {
        getActionNames: function() {
            return Object.keys(this.Hardware.Actions);
        },
        findAction: function(name) {
            return this.Hardware.Actions[name] || 4095;
        },
        Hardware: {
            Actions: {
                TranslateX: 54321
            },
            Application: {
                Grounded: 1
            },
            Keyboard: {
                A: 65
            }
        }
    };
    /* eslint-enable no-global-assign */
}