/********************************************************************************
 * Copyright (c) 2023-2026 EclipseSource and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the Eclipse
 * Public License v. 2.0 are satisfied: GNU General Public License, version 2
 * with the GNU Classpath Exception which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 ********************************************************************************/

import { ContainerModule, interfaces } from 'inversify';
import { MaybeArray, asArray } from '../utils/array-util';
import { BindingContext } from './inversify-util';

/**
 * Optional constructor options for {@link FeatureModule}s.
 */
export interface FeatureModuleOptions {
    /**
     * The set of feature modules that is required in order for this module to load.
     */
    requires?: MaybeArray<FeatureModule>;
    /**
     * Optional `featureId` that should be used. If omitted an id will be autogenerated
     */
    featureId?: symbol;
}

/**
 * A `FeatureModule` is a specialized {@link ContainerModule} that can declare dependencies to other {@link FeatureModule}.
 * A feature module will only be loaded into a container if all of its required modules haven been loaded before. T
 * Each feature module binds its `featureId` be default. This enables querying of existing container to check wether a
 * feature module has been loaded into this container.
 */

export class FeatureModule extends ContainerModule {
    /**
     * Global flag to enable/disable additional debug log output when loading feature modules
     * Default is `false`.
     */
    public static DEBUG_LOG_ENABLED = false;
    readonly featureId: symbol;

    readonly requires?: MaybeArray<FeatureModule>;

    constructor(registry: interfaces.ContainerModuleCallBack, options: FeatureModuleOptions = {}) {
        super((bind, unbind, isBound, ...rest) => {
            if (this.configure(bind, isBound)) {
                registry(bind, unbind, isBound, ...rest);
                this.debugLog(`Loading of feature module with id '${this.featureId.toString()}' completed`);
            }
        });
        this.featureId = options.featureId ?? this.createFeatureId();
        this.requires = options.requires;
    }

    protected createFeatureId(): symbol {
        return Symbol(this.id);
    }

    /**
     * Configures the feature module i.e. checks if the requirements are met.
     * If this is the case the {@link FeatureModule.featureId} will be bound and the module will be loaded
     * @param bind container bind function
     * @param isBound container isBound function
     * @returns `true` if all requirements are met and the module is loaded. `false` otherwise
     */
    configure(bind: interfaces.Bind, isBound: interfaces.IsBound): boolean {
        this.debugLog(`Trying to load feature module with id '${this.featureId.toString()}'`);
        if (this.isLoaded({ isBound })) {
            const message = `Could not load feature module. Another module with id '${this.featureId.toString()}' is already loaded`;
            this.debugLog(message);
            throw new Error(message);
        }
        if (this.checkRequirements(isBound)) {
            this.debugLog(`Requirements are met, continue loading of feature module with id '${this.featureId.toString()}'`);
            bind(this.featureId).toConstantValue(this.featureId);
            return true;
        }
        return false;
    }

    protected debugLog(message?: any, ...optionalParams: any[]): void {
        if (FeatureModule.DEBUG_LOG_ENABLED) {
            console.log(message, ...optionalParams);
        }
    }

    /**
     * Checks if all required {@link FeatureModule}s are already loaded/bound in the container.
     * @param isBound The `isBound` property of the module callback. Used to check the required modules.
     * @returns `true` if all requirements are met, `false` otherwise
     */
    protected checkRequirements(isBound: interfaces.IsBound): boolean {
        const requires = asArray(this.requires ?? []);
        if (requires.length === 0) {
            return true;
        }
        const missing = requires.filter(module => !module.isLoaded({ isBound }));
        if (missing.length > 0) {
            this.debugLog(
                `Could not load feature module. Required modules are not loaded. Feature ids: ${missing.map(m => m.featureId.toString()).join(', ')}`
            );
            return false;
        }
        return true;
    }

    isLoaded(context: Pick<BindingContext, 'isBound'>): boolean {
        return context.isBound(this.featureId);
    }
}
