// key = empty subscription object, value = subscription details object
const allSubscriptions = new Map();

// key = MessageChannel, value = set of subscription details objects for given channel
const subscriptionsByChannel = new Map();

// key = MessageContext, value = set of subscription details objects for given context
const subscriptionsByContext = new Map();

export function subscribe(messageContext, messageChannel, listener) {
    // external facing subscription object
    const subscription = Object.freeze({});

    // internal subscription object w/ additional information
    const _subscription = {
        subscription,
        channel: messageChannel,
        context: messageContext,
        listener,
    };

    // insert into all internal structures
    allSubscriptions.set(subscription, _subscription);

    let subscriptionsForChannel = subscriptionsByChannel.get(messageChannel);
    if (!subscriptionsForChannel) {
        subscriptionsForChannel = new Set();
        subscriptionsByChannel.set(messageChannel, subscriptionsForChannel);
    }
    subscriptionsForChannel.add(_subscription);

    let subscriptionsForContext = subscriptionsByContext.get(messageContext);
    if (!subscriptionsForContext) {
        subscriptionsForContext = new Set();
        subscriptionsByContext.set(messageContext, subscriptionsForContext);
    }
    subscriptionsForContext.add(_subscription);

    return subscription;
}

export function unsubscribe(subscription) {
    const _subscription = allSubscriptions.get(subscription);

    // quietly ignore if unsubscribe called twice for same subscription
    if (!_subscription) {
        return;
    }

    // remove from all internal structures
    const subscriptionsForChannel = subscriptionsByChannel.get(
        _subscription.channel
    );
    subscriptionsForChannel.delete(_subscription);
    if (subscriptionsForChannel.size === 0) {
        subscriptionsByChannel.delete(_subscription.channel);
    }

    const subscriptionsForContext = subscriptionsByContext.get(
        _subscription.context
    );
    subscriptionsForContext.delete(_subscription);
    if (subscriptionsForContext.size === 0) {
        subscriptionsByContext.delete(_subscription.context);
    }

    allSubscriptions.delete(subscription);
}

// messageContext is the context of the component publishing the message
// message is the payload which has already been serialized/deserialized into a new object suitable for LMS
export function publish(
    messageContext,
    messageChannel,
    message,
    publisherOptions
) {
    // deep freeze to prevent subscribers from modifying as each listener is invoked
    if (message) {
        deepFreeze(message);
    }

    // invoke listeners for the provided Message Channel
    const subscriptionsForChannel = subscriptionsByChannel.get(messageChannel);

    if (!subscriptionsForChannel) {
        // no subscribers for the provided channel
        return;
    }

    subscriptionsForChannel.forEach(_subscription => {
        let invokeSubscription = true;

        // option to indicate we shouldn't invoke subscriptions w/ same context as publisher
        if (publisherOptions && publisherOptions.skipSameContext === true) {
            invokeSubscription = messageContext !== _subscription.context;
        }

        if (invokeSubscription) {
            _subscription.listener.apply(undefined, [message]);
        }
    });
}

// clean up all subscriptions associated with the disconnected MessageContext
export function releaseMessageContext(messageContext) {
    const subscriptionsForContext = subscriptionsByContext.get(messageContext);

    if (!subscriptionsForContext) {
        // no subscribers for the provided context
        return;
    }

    subscriptionsForContext.forEach(_subscription => {
        unsubscribe(_subscription.subscription);
    });
}

function deepFreeze(object) {
    Object.keys(object).forEach(key => {
        const value = object[key];
        object[key] =
            value && typeof value === 'object' ? deepFreeze(value) : value;
    });
    return Object.freeze(object);
}
