/*******************************************************************************

    µMatrix - a Chromium browser extension to black/white list requests.
    Copyright (C) 2014 Raymond Hill

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see {http://www.gnu.org/licenses/}.

    Home: https://github.com/gorhill/uMatrix
*/

// So there might be memory leaks related to the direct use of sendMessage(),
// as per https://code.google.com/p/chromium/issues/detail?id=320723. The issue
// is not marked as resolved, and the last message from chromium dev is:
//
// "You can construct Port objects (runtime.connect) and emulate sendMessage
// "behaviour. The bug is that sendMessage doesn't clean up its Ports."
//
// So the point here is to have an infrastructure which allows relying more on
// direct use of Port objects rather than going through sendMessage().

/******************************************************************************/
/*******************************************************************************

// Here this is the "server"-side implementation.
//
// Reference client-side implementation is found in:
//
//    messaging-client.js
//
// For instance, it needs to be cut & pasted for content scripts since
// I can not include in a simple way js file content from another js file.

*******************************************************************************/
/******************************************************************************/

µMatrix.messaging = (function() {

/******************************************************************************/

var µm = µMatrix;
var runtimeIdGenerator = 1;
var nameToPortMap = {};
var nameToListenerMap = {};
var nullFunc = function(){};

/******************************************************************************/

var listenerNameFromPortName = function(portName) {
    var pos = portName.indexOf('/');
    if ( pos === -1 ) {
        return '';
    }
    return portName.slice(0, pos);
};

var listenerFromPortName = function(portName) {
    return nameToListenerMap[listenerNameFromPortName(portName)];
};

/******************************************************************************/

var listen = function(portName, callback) {
    var listener = nameToListenerMap[portName];
    if ( listener && listener !== callback ) {
        throw 'Only one listener allowed';
    }
    nameToListenerMap[portName] = callback;
};

/******************************************************************************/

var tell = function(target, msg) {
    target += '/';
    for ( var portName in nameToPortMap ) {
        if ( nameToPortMap.hasOwnProperty(portName) === false ) {
            continue;
        }
        if ( portName.indexOf(target) === 0 ) {
            nameToPortMap[portName].postMessage({ id: -1, msg: msg });
        }
    }
};

/******************************************************************************/

var announce = function(msg) {
    // Background page handler
    defaultHandler(msg, null, nullFunc);

    // Extension pages & content scripts handlers
    for ( var portName in nameToPortMap ) {
        if ( nameToPortMap.hasOwnProperty(portName) === false ) {
            continue;
        }
        nameToPortMap[portName].postMessage({ id: -1, msg: msg });
    }
};

/******************************************************************************/

var onMessage = function(request, port) {
    var reqId = request.id;
    // Annoucement: dispatch everywhere.
    if ( reqId < 0 ) {
        announce(request.msg);
        return;
    }
    var listener = listenerFromPortName(port.name) || defaultHandler;
    // Being told
    if ( reqId === 0 ) {
        listener(request.msg, port.sender, nullFunc);
        return;
    }
    // Being asked
    listener(request.msg, port.sender, function(response) {
        port.postMessage({
            id: reqId,
            msg: response !== undefined ? response : null
        });
    });
};

/******************************************************************************/

// Default is for commonly used message.

function defaultHandler(request, sender, callback) {
    // Async
    switch ( request.what ) {
        case 'getAssetContent':
            return µm.assets.getLocal(request.url, callback);

        default:
            break;
    }

    // Sync
    var response;

    switch ( request.what ) {
        case 'forceReloadTab':
            µm.forceReload(request.pageURL);
            break;

        case 'getUserSettings':
            response = µm.userSettings;
            break;

        case 'gotoExtensionURL':
            µm.utils.gotoExtensionURL(request.url);
            break;

        case 'gotoURL':
            µm.utils.gotoURL(request);
            break;

        case 'reloadHostsFiles':
            µm.reloadHostsFiles(request.switches, request.update);
            break;

        case 'userSettings':
            response = µm.changeUserSettings(request.name, request.value);
            break;

        default:
            // console.error('messaging.js / defaultHandler > unknown request: %o', request);
            break;
    }

    callback(response);
}

/******************************************************************************/

// Port disconnected, relay this information to apropriate listener.

var onDisconnect = function(port) {
    // Notify listener of the disconnection -- using a reserved message id.
    var listener = listenerFromPortName(port.name) || defaultHandler;
    var msg = {
        'what': 'disconnected',
        'which': listenerNameFromPortName(port.name)
    };
    listener(msg, port.sender, nullFunc);

    // Cleanup port if no longer in use.
    if ( nameToPortMap.hasOwnProperty(port.name) ) {
        delete nameToPortMap[port.name];
        port.onMessage.removeListener(onMessage);
        port.onDisconnect.removeListener(onDisconnect);
    }
};

/******************************************************************************/

var onConnect = function(port) {
    // We must have a port name.
    if ( typeof port.name !== 'string' || port.name === '' ) {
        console.error('messaging.js / onConnectHandler(): no port name!');
        return;
    }

    // Ensure port name is unique
    port.name += '/' + runtimeIdGenerator++;

    nameToPortMap[port.name] = port;
    port.onMessage.addListener(onMessage);
    port.onDisconnect.addListener(onDisconnect);
};

/******************************************************************************/

chrome.runtime.onConnect.addListener(onConnect);

/******************************************************************************/

return {
    listen: listen,
    tell: tell,
    announce: announce,
    defaultHandler: defaultHandler
};

/******************************************************************************/

})();

/******************************************************************************/
