/**
 * Project:  Open Vehicle Monitor System
 * Date:     14th Jan 2019
 * Based on: https://github.com/mroderick/PubSubJS
 *  +merged: https://github.com/mroderick/PubSubJS/issues/115
 * Copyright (c) 2010,2011,2012,2013,2014 Morgan Roderick http://roderick.dk
 * License: MIT - http://mrgnrdrck.mit-license.org
 */

'use strict';

var messages = {},
    lastUid = -1;

function hasKeys(obj)
  {
  var key;
  for (key in obj)
    {
    if ( Object.prototype.hasOwnProperty.call(obj, key) )
      {
      return true;
      }
    }
  return false;
  }

function callSubscriberWithImmediateExceptions( subscriber, message, data )
  {
  subscriber( message, data );
  }

function deliverMessage( originalMessage, matchedMessage, data )
  {
  var subscribers = messages[matchedMessage],
      s;

  if ( !Object.prototype.hasOwnProperty.call(messages, matchedMessage ) )
    {
    return;
    }

  for (s in subscribers)
    {
    if ( Object.prototype.hasOwnProperty.call(subscribers, s))
      {
      callSubscriberWithImmediateExceptions( subscribers[s], originalMessage, data );
      }
    }
  }

function createDeliveryFunction( message, data )
  {
  return function deliverNamespaced()
    {
    var topic = String( message ),
        position = topic.lastIndexOf( '.' );
    // deliver the message as it is now
    deliverMessage(message, message, data);
      // trim the hierarchy and deliver message to each level
    while( position !== -1 )
      {
      topic = topic.substr( 0, position );
      position = topic.lastIndexOf('.');
      deliverMessage( message, topic, data );
      }
    };
  }

function messageHasSubscribers( message )
  {
  var topic = String( message ),
      found = Boolean(Object.prototype.hasOwnProperty.call(messages, topic) && hasKeys(messages[topic])),
      position = topic.lastIndexOf( '.' );

  while ( !found && position !== -1 )
    {
    topic = topic.substr( 0, position );
    position = topic.lastIndexOf( '.' );
    found = Boolean(Object.prototype.hasOwnProperty.call(messages, topic) && hasKeys(messages[topic]));
    }

  return found;
  }

function publish( message, data )
  {
  message = (typeof message === 'symbol') ? message.toString() : message;

  var deliver = createDeliveryFunction( message, data ),
      hasSubscribers = messageHasSubscribers( message );

  if ( !hasSubscribers )
    {
    return false;
    }

  deliver();
  return true;
  }

/**
 * Publishes the message, passing the data to it's subscribers
 * @function
 * @alias publish
 * @param { String } message The message to publish
 * @param {} data The data to pass to subscribers
 * @return { Boolean }
 */
exports.publish = function( message, data )
  {
  return publish( message, data );
  };

/**
 * Subscribes the passed function to the passed message. Every returned token is unique and should be stored if you need to unsubscribe
 * @function
 * @alias subscribe
 * @param { String } message The message to subscribe to
 * @param { Function } func The function to call when a new message is published
 * @return { String }
 */
exports.subscribe = function( message, func )
  {
  if ( typeof func !== 'function')
    {
    return false;
    }

  message = (typeof message === 'symbol') ? message.toString() : message;

  // message is not registered yet
  if ( !Object.prototype.hasOwnProperty.call(messages, message) )
    {
    messages[message] = {};
    }

  // forcing token as String, to allow for future expansions without breaking usage
  // and allow for easy use as key names for the 'messages' object
  var token = 'uid_' + String(++lastUid);
  messages[message][token] = func;

  // return token for unsubscribing
  return token;
  };

/**
 * Clears all subscriptions
 * @function
 * @public
 * @alias clearAllSubscriptions
 */
exports.clearAllSubscriptions = function clearAllSubscriptions()
  {
  messages = {};
  };

/**
 * Clear subscriptions by the topic
 * @function
 * @public
 * @alias clearAllSubscriptions
 */
exports.clearSubscriptions = function clearSubscriptions(topic)
  {
  var m;
  for (m in messages)
    {
    if (Object.prototype.hasOwnProperty.call(messages, m) && m.indexOf(topic) === 0)
      {
      delete messages[m];
      }
    }
  };

/**
 * Removes subscriptions
 *
 * - When passed a token, removes a specific subscription.
 *
 * - When passed a function, removes all subscriptions for that function
 *
 * - When passed a topic, removes all subscriptions for that topic (hierarchy)
 * @function
 * @public
 * @alias subscribeOnce
 * @param { String | Function } value A token, function or topic to unsubscribe from
 * @example // Unsubscribing with a token
 * var token = PubSub.subscribe('mytopic', myFunc);
 * PubSub.unsubscribe(token);
 * @example // Unsubscribing with a function
 * PubSub.unsubscribe(myFunc);
 * @example // Unsubscribing from a topic
 * PubSub.unsubscribe('mytopic');
 */
exports.unsubscribe = function(value)
  {
  var descendantTopicExists = function(topic)
    {
    var m;
    for ( m in messages )
      {
      if ( Object.prototype.hasOwnProperty.call(messages, m) && m.indexOf(topic) === 0 )
        {
        // a descendant of the topic exists:
        return true;
        }
      }

    return false;
    },
    isTopic    = typeof value === 'string' && ( Object.prototype.hasOwnProperty.call(messages, value) || descendantTopicExists(value) ),
    isToken    = !isTopic && typeof value === 'string',
    isFunction = typeof value === 'function',
    result = false,
    m, message, t;

  if (isTopic)
    {
    exports.clearSubscriptions(value);
    return;
    }

  for ( m in messages )
    {
    if ( Object.prototype.hasOwnProperty.call(messages, m) )
      {
      message = messages[m];

      if ( isToken && message[value] )
        {
        delete message[value];
        result = value;
        // tokens are unique, so we can just stop here
        break;
        }

      if (isFunction)
        {
        for ( t in message )
          {
          if (Object.prototype.hasOwnProperty.call(message, t) && message[t] === value)
            {
            delete message[t];
            result = true;
            }
          }
        }
      }
    }

  return result;
  };

/**
 * Debugging: output / return subscriptions
 */
exports.dump = function ()
  {
  JSON.print(messages);
  };
exports.data = function ()
  {
  return messages;
  };
