odoo.define('point_of_sale.ClassRegistry', function (require) {
    'use strict';

    /**
     * **Usage:**
     * ```
     * const Registry = new ClassRegistry();
     *
     * class A {}
     * Registry.add(A);
     *
     * const AExt1 = A => class extends A {}
     * Registry.extend(A, AExt1)
     *
     * const B = A => class extends A {}
     * Registry.addByExtending(B, A)
     *
     * const AExt2 = A => class extends A {}
     * Registry.extend(A, AExt2)
     *
     * Registry.get(A)
     * // above returns: AExt2 -> AExt1 -> A
     * // Basically, 'A' in the registry points to
     * // the inheritance chain above.
     *
     * Registry.get(B)
     * // above returns: B -> AExt2 -> AExt1 -> A
     * // Even though B extends A before applying all
     * // the extensions of A, when getting it from the
     * // registry, the return points to a class with
     * // inheritance chain that includes all the extensions
     * // of 'A'.
     *
     * Registry.freeze()
     * // Example 'B' above is lazy. Basically, it is only
     * // computed when we call `get` from the registry.
     * // If we know that no more dynamic inheritances will happen,
     * // we can freeze the registry and cache the final form
     * // of each class in the registry.
     * ```
     *
     * IMPROVEMENT:
     * * So far, mixin can be accomplished by creating a method
     *  the takes a class and returns a class expression. This is then
     *  used before the extends keyword like so:
     *
     *  ```js
     *  class A {}
     *  Registry.add(A)
     *  const Mixin = x => class extends x {}
     *  //                          apply mixin
     *  //                              |
     *  //                              v
     *  const B = x => class extends Mixin(x) {}
     *  Registry.addByExtending(B, A)
     *  ```
     *
     *  In the example, `|B| => B -> Mixin -> A`, and this is pretty convenient
     *  already. However, this can still be improved since classes are only
     *  compiled after `Registry.freeze()`. Perhaps, we can make the
     *  Mixins extensible as well, such as so:
     *
     *  ```
     *  class A {}
     *  Registry.add(A)
     *  const Mixin = x => class extends x {}
     *  Registry.add(Mixin)
     *  const OtherMixin = x => class extends x {}
     *  Registry.add(OtherMixin)
     *  const B = x => class extends x {}
     *  Registry.addByExtending(B, A, [Mixin, OtherMixin])
     *  const ExtendMixin = x => class extends x {}
     *  Registry.extend(Mixin, ExtendMixin)
     *  ```
     *
     *  In the above, after `Registry.freeze()`,
     *  `|B| => B -> OtherMixin -> ExtendMixin -> Mixin -> A`
     */
    class ClassRegistry {
        constructor() {
            // base name map
            this.baseNameMap = {};
            // Object that maps `baseClass` to the class implementation extended in-place.
            this.includedMap = new Map();
            // Object that maps `baseClassCB` to the array of callbacks to generate the extended class.
            this.extendedCBMap = new Map();
            // Object that maps `baseClassCB` extended class to the `baseClass` of its super in the includedMap.
            this.extendedSuperMap = new Map();
            // For faster access, we can `freeze` the registry so that instead of dynamically generating
            // the extended classes, it is taken from the cache instead.
            this.cache = new Map();
        }
        /**
         * Add a new class in the Registry.
         * @param {Function} baseClass `class`
         */
        add(baseClass) {
            this.includedMap.set(baseClass, []);
            this.baseNameMap[baseClass.name] = baseClass;
        }
        /**
         * Add a new class in the Registry based on other class
         * in the registry.
         * @param {Function} baseClassCB `class -> class`
         * @param {Function} base `class | class -> class`
         */
        addByExtending(baseClassCB, base) {
            this.extendedCBMap.set(baseClassCB, [baseClassCB]);
            this.extendedSuperMap.set(baseClassCB, base);
            this.baseNameMap[baseClassCB.name] = baseClassCB;
        }
        /**
         * Extend in-place a class in the registry. E.g.
         * ```
         * // Using the following notation:
         * //  * |A| -  compiled class in the registry
         * //  *  A  -  class or an extension callback
         * //  * |A| => A2 -> A1 -> A
         * //       -  the above means, compiled class A
         * //          points to the class inheritance derived from
         * //          A2(A1(A))
         *
         * class A {};
         * Registry.add(A);
         * // |A| => A
         *
         * let A1 = x => class extends x {};
         * Registry.extend(A, A1);
         * // |A| => A1 -> A
         *
         * let B = x => class extends x {};
         * Registry.addByExtending(B, A);
         * // |B| => B -> |A|
         * // |B| => B -> A1 -> A
         *
         * let B1 = x => class extends x {};
         * Registry.extend(B, B1);
         * // |B| => B1 -> B -> |A|
         *
         * let C = x => class extends x {};
         * Registry.addByExtending(C, B);
         * // |C| => C -> |B|
         *
         * let B2 = x => class extends x {};
         * Registry.extend(B, B2);
         * // |B| => B2 -> B1 -> B -> |A|
         *
         * // Overall:
         * // |A| => A1 -> A
         * // |B| => B2 -> B1 -> B -> A1 -> A
         * // |C| => C -> B2 -> B1 -> B -> A1 -> A
         * ```
         * @param {Function} base `class | class -> class`
         * @param {Function} extensionCB `class -> class`
         */
        extend(base, extensionCB) {
            if (typeof base === 'string') {
                base = this.baseNameMap[base];
            }
            let extensionArray;
            if (this.includedMap.get(base)) {
                extensionArray = this.includedMap.get(base);
            } else if (this.extendedCBMap.get(base)) {
                extensionArray = this.extendedCBMap.get(base);
            } else {
                throw new Error(
                    `'${base.name}' is not in the Registry. Add it to Registry before extending.`
                );
            }
            extensionArray.push(extensionCB);
            const locOfNewExtension = extensionArray.length - 1;
            const self = this;
            const oldCompiled = this.isFrozen ? this.cache.get(base) : null;
            return {
                remove() {
                    extensionArray.splice(locOfNewExtension, 1);
                    self._recompute(base, oldCompiled);
                },
                compile() {
                    self._recompute(base);
                }
            };
        }
        _compile(base) {
            let res;
            if (this.includedMap.has(base)) {
                res = this.includedMap.get(base).reduce((acc, ext) => ext(acc), base);
            } else {
                const superClass = this.extendedSuperMap.get(base);
                const extensionCBs = this.extendedCBMap.get(base);
                res = extensionCBs.reduce((acc, ext) => ext(acc), this._compile(superClass));
            }
            Object.defineProperty(res, 'name', { value: base.name });
            return res;
        }
        /**
         * Return the compiled class (containing all the extensions) of the base class.
         * @param {Function} base `class | class -> class` function used in adding the class
         */
        get(base) {
            if (typeof base === 'string') {
                base = this.baseNameMap[base];
            }
            if (this.isFrozen) {
                return this.cache.get(base);
            }
            return this._compile(base);
        }
        /**
         * Uses the callbacks registered in the registry to compile the classes.
         */
        freeze() {
            // Step: Compile the `included classes`.
            for (let [baseClass, extensionCBs] of this.includedMap.entries()) {
                const compiled = extensionCBs.reduce((acc, ext) => ext(acc), baseClass);
                this.cache.set(baseClass, compiled);
            }

            // Step: Compile the `extended classes` based on `included classes`.
            // Also gather those the are based on `extended classes`.
            const remaining = [];
            for (let [baseClassCB, extensionCBArray] of this.extendedCBMap.entries()) {
                const compiled = this.cache.get(this.extendedSuperMap.get(baseClassCB));
                if (!compiled) {
                    remaining.push([baseClassCB, extensionCBArray]);
                    continue;
                }
                const extendedClass = extensionCBArray.reduce(
                    (acc, extensionCB) => extensionCB(acc),
                    compiled
                );
                this.cache.set(baseClassCB, extendedClass);
            }

            // Step: Compile the `extended classes` based on `extended classes`.
            for (let [baseClassCB, extensionCBArray] of remaining) {
                const compiled = this.cache.get(this.extendedSuperMap.get(baseClassCB));
                const extendedClass = extensionCBArray.reduce(
                    (acc, extensionCB) => extensionCB(acc),
                    compiled
                );
                this.cache.set(baseClassCB, extendedClass);
            }

            // Step: Set the name of the compiled classess
            for (let [base, compiledClass] of this.cache.entries()) {
                Object.defineProperty(compiledClass, 'name', { value: base.name });
            }

            // Step: Set the flag to true;
            this.isFrozen = true;
        }
        _recompute(base, old) {
            if (typeof base === 'string') {
                base = this.baseNameMap[base];
            }
            return old ? old : this._compile(base);
        }
    }

    return ClassRegistry;
});
