/**
 * This is a referral implementation for a custom context provider
 * that can be installed on any element in the dom, and can provide
 * value to any LWC component using wire.
 *
 * This provide is sharing the same value with every child, and the
 * identity of the consumer is not tracked.
 *
 * Per Context Component Instance, track the current context data
 *
 * All mentions of the "reference implementation" refer to
 * https://github.com/salesforce/lwc/pull/1408/files
 */
import { register, ValueChangedEvent } from 'wire-service';
import {
    releaseMessageContext,
    PUBLIC_MODULE_NAME,
} from './messageServiceInternal';

const ContextValueMap = new WeakMap();
const RegisteredContexts = new Set();
const MessageContext = Symbol('MessageContext');

/**
 * This will get called in the `register` callback when no `MessageContext` providers are found,
 * which is the exclusive case in Beta. No product requirements in Beta necessitate creating
 * `MessageContext` providers. To keep the Beta-level codebase focused, will add provisioning
 * functionality in future releases.
 *
 * This function registers the context after creation.
 */
export function getDefaultContext() {
    // The important thing is that this context is unique per consumer.
    // An object — Object.freeze({}) — gets turned into a `Proxy`, losing identity.
    // A function — () => {} — gets turned into a `SecureFunction` due to Locker, losing identity.
    // A symbol fulfills our requirements.
    const context = Symbol();

    // Register context
    RegisteredContexts.add(context);

    return context;
}

/**
 * Validates that the context has been supplied by this module.
 *
 * @param {*} context - the context to validate.
 */
export function validateContext(context) {
    if (!isRegisteredContext(context)) {
        throw new Error(`${PUBLIC_MODULE_NAME}: invalid message context`);
    }
}
/**
 * Ask if this context has been registered.
 *
 * @param {*} context - the context to check if registered.
 * @returns {Boolean} - whether this context has been registered
 */
export function isRegisteredContext(context) {
    return RegisteredContexts.has(context);
}

/**
 * This removes the context from the collection of registered contexts.
 *
 * @param {*} context - the context to unregister
 */
export function unregisterContext(context) {
    RegisteredContexts.delete(context);
}

/**
 * Registers the `MessageContext` `@wire` adapter with the `@wire` service.
 */
register(MessageContext, eventTarget => {
    eventTarget.addEventListener('connect', () => {
        // No provider was found, in which case the default context should be set.
        const defaultContext = getDefaultContext();

        // The reference implementation does not initialize the `ContextValueMap` in this
        // default case, but we want to because we want to be able to read the context
        // value on `disconnectedCallback`, regardless of if there are no providers.
        initializeContextData(eventTarget, defaultContext);

        eventTarget.dispatchEvent(new ValueChangedEvent(defaultContext));
    });

    eventTarget.addEventListener('disconnect', () => {
        // Get the context value and unregister all listeners.
        const contextData = getContextData(eventTarget);
        releaseMessageContext(contextData);
    });
});

/**
 * Puts the context value in the `ContextValueMap`, indexed by the event target.
 * @param {*} eventTarget - the event target
 * @param {*} initialValue - the initial value
 */
function initializeContextData(eventTarget, initialValue) {
    const contextData = initialValue;
    ContextValueMap.set(eventTarget, contextData);

    return contextData;
}

/**
 * Gets the context data from the `ContextValueMap` based on the `eventTaget`
 * @param {*} eventTarget - the event target
 */
function getContextData(eventTarget) {
    let contextData = ContextValueMap.get(eventTarget);

    // `contextData` is guaranteed to exist here since we run `initializeContextData()`
    // 100% of the time on `connectedCallback()` in Beta. The following conditional is an artifact
    // of what we _would_ do using the established pattern from the reference implementation where
    // 1) we have context providers and 2) we add our context data to the `ContextValueMap` lazily,
    // i.e. the first time we call `getContextData()`.
    // This is still a useful check to ensure this function always returns a value.
    if (contextData === undefined) {
        contextData = initializeContextData(eventTarget);
    }
    return contextData;
}

export { MessageContext };
