"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PlanResultCacheService = void 0;
const BindingType_1 = require("../../binding/models/BindingType");
const WeakList_1 = require("../../common/models/WeakList");
const addRootServiceNodeBindingIfContextFree_1 = require("../actions/addRootServiceNodeBindingIfContextFree");
const addServiceNodeBindingIfContextFree_1 = require("../actions/addServiceNodeBindingIfContextFree");
const removeRootServiceNodeBindingIfContextFree_1 = require("../actions/removeRootServiceNodeBindingIfContextFree");
const removeServiceNodeBindingIfContextFree_1 = require("../actions/removeServiceNodeBindingIfContextFree");
const CacheBindingInvalidationKind_1 = require("../models/CacheBindingInvalidationKind");
const LazyPlanServiceNode_1 = require("../models/LazyPlanServiceNode");
const CHAINED_MASK = 0x4;
const IS_MULTIPLE_MASK = 0x2;
const OPTIONAL_MASK = 0x1;
const MAP_ARRAY_LENGTH = 0x8;
/**
 * Service to cache plans.
 *
 * This class is used to cache plans and to notify PlanService subscribers when the cache is cleared.
 * The cache should be cleared when a new binding is registered or when a binding is unregistered.
 *
 * Subscribers are supposed to be plan services from child containers.
 *
 * Ancestor binding constraints are the reason to avoid reusing plans from plan children nodes.
 */
class PlanResultCacheService {
    #serviceIdToNonCachedServiceNodeMapMap;
    #serviceIdToValuePlanMap;
    #namedServiceIdToValuePlanMap;
    #namedTaggedServiceIdToValuePlanMap;
    #taggedServiceIdToValuePlanMap;
    #subscribers;
    constructor() {
        this.#serviceIdToNonCachedServiceNodeMapMap = new Map();
        this.#serviceIdToValuePlanMap = this.#buildInitializedMapArray();
        this.#namedServiceIdToValuePlanMap = this.#buildInitializedMapArray();
        this.#namedTaggedServiceIdToValuePlanMap = this.#buildInitializedMapArray();
        this.#taggedServiceIdToValuePlanMap = this.#buildInitializedMapArray();
        this.#subscribers = new WeakList_1.WeakList();
    }
    clearCache() {
        for (const map of this.#getMaps()) {
            map.clear();
        }
        for (const subscriber of this.#subscribers) {
            subscriber.clearCache();
        }
    }
    get(options) {
        if (options.name === undefined) {
            if (options.tag === undefined) {
                return this.#getMapFromMapArray(this.#serviceIdToValuePlanMap, options).get(options.serviceIdentifier);
            }
            else {
                return this.#getMapFromMapArray(this.#taggedServiceIdToValuePlanMap, options)
                    .get(options.serviceIdentifier)
                    ?.get(options.tag.key)
                    ?.get(options.tag.value);
            }
        }
        else {
            if (options.tag === undefined) {
                return this.#getMapFromMapArray(this.#namedServiceIdToValuePlanMap, options)
                    .get(options.serviceIdentifier)
                    ?.get(options.name);
            }
            else {
                return this.#getMapFromMapArray(this.#namedTaggedServiceIdToValuePlanMap, options)
                    .get(options.serviceIdentifier)
                    ?.get(options.name)
                    ?.get(options.tag.key)
                    ?.get(options.tag.value);
            }
        }
    }
    invalidateServiceBinding(invalidation) {
        this.#invalidateServiceMap(invalidation);
        this.#invalidateNamedServiceMap(invalidation);
        this.#invalidateNamedTaggedServiceMap(invalidation);
        this.#invalidateTaggedServiceMap(invalidation);
        this.#invalidateNonCachedServiceNodeSetMap(invalidation);
        for (const subscriber of this.#subscribers) {
            subscriber.invalidateServiceBinding(invalidation);
        }
    }
    set(options, planResult) {
        if (options.name === undefined) {
            if (options.tag === undefined) {
                this.#getMapFromMapArray(this.#serviceIdToValuePlanMap, options).set(options.serviceIdentifier, planResult);
            }
            else {
                this.#getOrBuildMapValueFromMapMap(this.#getOrBuildMapValueFromMapMap(this.#getMapFromMapArray(this.#taggedServiceIdToValuePlanMap, options), options.serviceIdentifier), options.tag.key).set(options.tag.value, planResult);
            }
        }
        else {
            if (options.tag === undefined) {
                this.#getOrBuildMapValueFromMapMap(this.#getMapFromMapArray(this.#namedServiceIdToValuePlanMap, options), options.serviceIdentifier).set(options.name, planResult);
            }
            else {
                this.#getOrBuildMapValueFromMapMap(this.#getOrBuildMapValueFromMapMap(this.#getOrBuildMapValueFromMapMap(this.#getMapFromMapArray(this.#namedTaggedServiceIdToValuePlanMap, options), options.serviceIdentifier), options.name), options.tag.key).set(options.tag.value, planResult);
            }
        }
    }
    setNonCachedServiceNode(node, context) {
        let nonCachedMap = this.#serviceIdToNonCachedServiceNodeMapMap.get(node.serviceIdentifier);
        if (nonCachedMap === undefined) {
            nonCachedMap = new Map();
            this.#serviceIdToNonCachedServiceNodeMapMap.set(node.serviceIdentifier, nonCachedMap);
        }
        nonCachedMap.set(node, context);
    }
    subscribe(subscriber) {
        this.#subscribers.push(subscriber);
    }
    #buildInitializedMapArray() {
        const mapArray = new Array(MAP_ARRAY_LENGTH);
        for (let i = 0; i < mapArray.length; ++i) {
            mapArray[i] = new Map();
        }
        return mapArray;
    }
    #buildUpdatePlanParams(invalidation, index, name, tag) {
        const isMultiple = (index & IS_MULTIPLE_MASK) !== 0;
        let planParamsConstraint;
        if (isMultiple) {
            const isChained = (index & IS_MULTIPLE_MASK & CHAINED_MASK) !== 0;
            planParamsConstraint = {
                chained: isChained,
                isMultiple,
                serviceIdentifier: invalidation.binding.serviceIdentifier,
            };
        }
        else {
            planParamsConstraint = {
                isMultiple,
                serviceIdentifier: invalidation.binding.serviceIdentifier,
            };
        }
        const isOptional = (index & OPTIONAL_MASK) !== 0;
        if (isOptional) {
            planParamsConstraint.isOptional = true;
        }
        if (name !== undefined) {
            planParamsConstraint.name = name;
        }
        if (tag !== undefined) {
            planParamsConstraint.tag = tag;
        }
        return {
            autobindOptions: undefined,
            operations: invalidation.operations,
            rootConstraints: planParamsConstraint,
            servicesBranch: [],
        };
    }
    #getOrBuildMapValueFromMapMap(map, key) {
        let valueMap = map.get(key);
        if (valueMap === undefined) {
            valueMap = new Map();
            map.set(key, valueMap);
        }
        return valueMap;
    }
    #getMapFromMapArray(mapArray, options) {
        return mapArray[this.#getMapArrayIndex(options)];
    }
    #getMaps() {
        return [
            this.#serviceIdToNonCachedServiceNodeMapMap,
            ...this.#serviceIdToValuePlanMap,
            ...this.#namedServiceIdToValuePlanMap,
            ...this.#namedTaggedServiceIdToValuePlanMap,
            ...this.#taggedServiceIdToValuePlanMap,
        ];
    }
    #getMapArrayIndex(options) {
        if (options.isMultiple) {
            return ((options.chained ? CHAINED_MASK : 0) |
                (options.optional ? OPTIONAL_MASK : 0) |
                IS_MULTIPLE_MASK);
        }
        else {
            return options.optional ? OPTIONAL_MASK : 0;
        }
    }
    #invalidateNamedServiceMap(invalidation) {
        for (const [index, map] of this.#namedServiceIdToValuePlanMap.entries()) {
            const servicePlans = map.get(invalidation.binding.serviceIdentifier);
            if (servicePlans !== undefined) {
                for (const [name, servicePlan] of servicePlans.entries()) {
                    this.#updatePlan(invalidation, servicePlan, index, name, undefined);
                }
            }
        }
    }
    #invalidateNamedTaggedServiceMap(invalidation) {
        for (const [index, map,] of this.#namedTaggedServiceIdToValuePlanMap.entries()) {
            const servicePlanMapMapMap = map.get(invalidation.binding.serviceIdentifier);
            if (servicePlanMapMapMap !== undefined) {
                for (const [name, servicePlanMapMap,] of servicePlanMapMapMap.entries()) {
                    for (const [tag, servicePlanMap] of servicePlanMapMap.entries()) {
                        for (const [tagValue, servicePlan] of servicePlanMap.entries()) {
                            this.#updatePlan(invalidation, servicePlan, index, name, {
                                key: tag,
                                value: tagValue,
                            });
                        }
                    }
                }
            }
        }
    }
    #invalidateNonCachePlanBindingNodeDescendents(planBindingNode) {
        switch (planBindingNode.binding.type) {
            case BindingType_1.bindingTypeValues.ServiceRedirection:
                for (const redirection of planBindingNode.redirections) {
                    this.#invalidateNonCachePlanBindingNodeDescendents(redirection);
                }
                break;
            case BindingType_1.bindingTypeValues.Instance:
                for (const constructorParam of planBindingNode
                    .constructorParams) {
                    if (constructorParam !== undefined) {
                        this.#invalidateNonCachePlanServiceNode(constructorParam);
                    }
                }
                for (const propertyParam of planBindingNode.propertyParams.values()) {
                    this.#invalidateNonCachePlanServiceNode(propertyParam);
                }
                break;
            case BindingType_1.bindingTypeValues.ResolvedValue:
                for (const resolvedValue of planBindingNode.params) {
                    this.#invalidateNonCachePlanServiceNode(resolvedValue);
                }
                break;
            default:
        }
    }
    #invalidateNonCachePlanServiceNode(planServiceNode) {
        const serviceNonCachedMap = this.#serviceIdToNonCachedServiceNodeMapMap.get(planServiceNode.serviceIdentifier);
        if (serviceNonCachedMap === undefined ||
            !serviceNonCachedMap.has(planServiceNode)) {
            return;
        }
        serviceNonCachedMap.delete(planServiceNode);
        this.#invalidateNonCachePlanServiceNodeDescendents(planServiceNode);
    }
    #invalidateNonCachePlanServiceNodeDescendents(planServiceNode) {
        if (LazyPlanServiceNode_1.LazyPlanServiceNode.is(planServiceNode) &&
            !planServiceNode.isExpanded()) {
            return;
        }
        if (planServiceNode.bindings === undefined) {
            return;
        }
        if (Array.isArray(planServiceNode.bindings)) {
            for (const binding of planServiceNode.bindings) {
                this.#invalidateNonCachePlanBindingNodeDescendents(binding);
            }
        }
        else {
            this.#invalidateNonCachePlanBindingNodeDescendents(planServiceNode.bindings);
        }
    }
    #invalidateNonCachedServiceNodeSetMap(invalidation) {
        const serviceNonCachedServiceNodeMap = this.#serviceIdToNonCachedServiceNodeMapMap.get(invalidation.binding.serviceIdentifier);
        if (serviceNonCachedServiceNodeMap !== undefined) {
            switch (invalidation.kind) {
                case CacheBindingInvalidationKind_1.CacheBindingInvalidationKind.bindingAdded:
                    for (const [serviceNode, context] of serviceNonCachedServiceNodeMap) {
                        const result = (0, addServiceNodeBindingIfContextFree_1.addServiceNodeBindingIfContextFree)({
                            autobindOptions: undefined,
                            operations: invalidation.operations,
                            servicesBranch: [],
                        }, serviceNode, invalidation.binding, context.bindingConstraintsList, context.chainedBindings);
                        if (result.isContextFreeBinding) {
                            if (result.shouldInvalidateServiceNode &&
                                LazyPlanServiceNode_1.LazyPlanServiceNode.is(serviceNode)) {
                                this.#invalidateNonCachePlanServiceNodeDescendents(serviceNode);
                                serviceNode.invalidate();
                            }
                        }
                        else {
                            this.clearCache();
                        }
                    }
                    break;
                case CacheBindingInvalidationKind_1.CacheBindingInvalidationKind.bindingRemoved:
                    for (const [serviceNode, context] of serviceNonCachedServiceNodeMap) {
                        const result = (0, removeServiceNodeBindingIfContextFree_1.removeServiceNodeBindingIfContextFree)(serviceNode, invalidation.binding, context.bindingConstraintsList, context.optionalBindings);
                        if (result.isContextFreeBinding) {
                            if (result.bindingNodeRemoved !== undefined) {
                                this.#invalidateNonCachePlanBindingNodeDescendents(result.bindingNodeRemoved);
                            }
                        }
                        else {
                            this.clearCache();
                        }
                    }
                    break;
            }
        }
    }
    #invalidateServiceMap(invalidation) {
        for (const [index, map] of this.#serviceIdToValuePlanMap.entries()) {
            const servicePlan = map.get(invalidation.binding.serviceIdentifier);
            this.#updatePlan(invalidation, servicePlan, index, undefined, undefined);
        }
    }
    #invalidateTaggedServiceMap(invalidation) {
        for (const [index, map] of this.#taggedServiceIdToValuePlanMap.entries()) {
            const servicePlanMapMap = map.get(invalidation.binding.serviceIdentifier);
            if (servicePlanMapMap !== undefined) {
                for (const [tag, servicePlanMap] of servicePlanMapMap.entries()) {
                    for (const [tagValue, servicePlan] of servicePlanMap.entries()) {
                        this.#updatePlan(invalidation, servicePlan, index, undefined, {
                            key: tag,
                            value: tagValue,
                        });
                    }
                }
            }
        }
    }
    #updatePlan(invalidation, servicePlan, index, name, tag) {
        if (servicePlan !== undefined &&
            LazyPlanServiceNode_1.LazyPlanServiceNode.is(servicePlan.tree.root)) {
            const planParams = this.#buildUpdatePlanParams(invalidation, index, name, tag);
            switch (invalidation.kind) {
                case CacheBindingInvalidationKind_1.CacheBindingInvalidationKind.bindingAdded:
                    {
                        const result = (0, addRootServiceNodeBindingIfContextFree_1.addRootServiceNodeBindingIfContextFree)(planParams, servicePlan.tree.root, invalidation.binding);
                        if (result.isContextFreeBinding) {
                            if (result.shouldInvalidateServiceNode) {
                                this.#invalidateNonCachePlanServiceNodeDescendents(servicePlan.tree.root);
                                servicePlan.tree.root.invalidate();
                            }
                        }
                        else {
                            this.clearCache();
                        }
                    }
                    break;
                case CacheBindingInvalidationKind_1.CacheBindingInvalidationKind.bindingRemoved:
                    {
                        const result = (0, removeRootServiceNodeBindingIfContextFree_1.removeRootServiceNodeBindingIfContextFree)(planParams, servicePlan.tree.root, invalidation.binding);
                        if (result.isContextFreeBinding) {
                            if (result.bindingNodeRemoved !== undefined) {
                                this.#invalidateNonCachePlanBindingNodeDescendents(result.bindingNodeRemoved);
                            }
                        }
                        else {
                            this.clearCache();
                        }
                    }
                    break;
            }
        }
    }
}
exports.PlanResultCacheService = PlanResultCacheService;
//# sourceMappingURL=PlanResultCacheService.js.map