/*
 * Copyright 2018-2022 VMware, Inc. All rights reserved. VMware Confidential
 */

import { filter } from "rxjs/operators";
import {
    ComponentRef,
    NgModuleFactory,
    NgModuleRef,
    Provider,
    ReflectiveInjector,
    Type,
    ViewContainerRef,
} from "@angular/core";
import { TranslationSet } from "@vcd/i18n";
import { Observable, Subject } from "rxjs";
import { AngularAppBundle, ExtensionPointManifest } from "./extensibility.model";

/**
 * A registration for an extension-provided component.
 */
export class ExtensionComponentRegistration {
    /**
     * Counter value used for the ID of new registrations.
     */
    private static maxId = 0;

    /**
     * This unique ID is used for breaking ties when sorting registrations by ordering.
     */
    readonly id: number;

    /**
     * An arbitrary URN keying for splitting registrations into different types.
     * This is intended to represent an 'area' of the UI where components may be injected,
     * for example the dashboard or VM details.
     */
    readonly scope: string;

    /**
     * The module containing the component we wish to create.
     */
    readonly module: NgModuleRef<any>;

    /**
     * The factory of the extension point module.
     */
    readonly moduleFactory: NgModuleFactory<any>;

    /**
     * The component class itself.
     */
    readonly component: Type<any>;

    /**
     * Any extra providers to create the component with.
     */
    readonly providers: Provider[];

    /**
     * An explicit ordering.  Registrations with the same scope are ordered by this field value, with lower
     * numbers being first.
     */
    readonly ordering: number;

    /**
     * The manifest associated with this registration.
     */
    readonly manifest: ExtensionPointManifest;

    /**
     * Translations
     */
    readonly translations: TranslationSet;

    /**
     * Extension Point Route
     */
    readonly vcdRoute: string;

    /**
     * The hashed version of the vendor used when constructing routes.
     */
    readonly hashedVendor: string;

    /**
     * Details how to setup a separate Angular Application next to vCD Platform.
     *
     * angularAppBundle - Contains lifecycle hooks, such as: bootstrap, activate and de-activate.
     * those hooks are called by the platfrom at a give point of time depending on the conditions.
     * The implementation and the whole complexity is outside the platform.
     *
     * bootstrapped and activated - flags to determine when to de-activate certain Angular Application.
     */
    readonly angularAppDetails: {
        angularAppBundle?: AngularAppBundle<any>;
        bootstrapped?: boolean;
        activated?: boolean;
    };

    readonly resourcePath: string;

    /**
     * Create a registration.  The maxId counter is used to set the id field, then increased.
     */
    constructor(
        scope: string,
        module: NgModuleRef<any>,
        moduleFactory: NgModuleFactory<any>,
        component: Type<any>,
        providers: Provider[],
        ordering: number,
        manifest: ExtensionPointManifest,
        translations: TranslationSet,
        vcdRoute: string,
        hashedVendor: string,
        angularAppBundle?: AngularAppBundle<any>,
        resourcePath?: string
    ) {
        this.id = ExtensionComponentRegistration.maxId++;
        this.scope = scope;
        this.module = module;
        this.moduleFactory = moduleFactory;
        this.component = component;
        this.providers = providers;
        this.ordering = ordering;
        this.manifest = manifest;
        this.translations = translations;
        this.vcdRoute = vcdRoute;
        this.hashedVendor = hashedVendor;
        this.angularAppDetails = {
            angularAppBundle: angularAppBundle,
            bootstrapped: false,
            activated: false,
        };
        this.resourcePath = resourcePath;
    }

    /**
     * Helper to instantiate the component.
     * @param viewContainerRef Where to place the created component.
     * @param index Optional position to place the component at in the view.
     */
    createComponent(viewContainerRef: ViewContainerRef, index?: number): ComponentRef<any> {
        const componentFactory = this.module.componentFactoryResolver.resolveComponentFactory(this.component);
        const injector = ReflectiveInjector.resolveAndCreate(this.providers, this.module.injector);
        return viewContainerRef.createComponent(componentFactory, index, injector);
    }
}

/**
 * Comparator function that provides stable sort for registrations based on their ordering field (lower value
 * comes first), followed by their ID field in the case of a tie.
 */
function registrationOrderingComparator(a: ExtensionComponentRegistration, b: ExtensionComponentRegistration): number {
    const comparison = a.ordering - b.ordering;
    if (comparison != 0) {
        return comparison;
    }
    return a.id - b.id;
}

/**
 * Implement a registry for (Angular) components provided by extensions.  Allows registering arbitrary components
 * under arbitrary "scopes."
 *
 * A scope is string keying for splitting registrations into different 'areas' of the UI where components may
 * be injected, for example the dashboard or VM details.  Scopes are intended to be URNs.
 *
 * Parts of the UI that wish to render these components can obtain registered components - likely by scope -
 * containing all necessary references to add the component to an (Angular) view.
 */
export class ExtensionComponentRegistry {
    /**
     * Registrations by scope.  Registrations are stored in a sorted order to avoid constant need for sorting.
     */
    private registrations: Map<string, ExtensionComponentRegistration[]> = new Map<
        string,
        ExtensionComponentRegistration[]
    >();

    /**
     * Subject dispatched to whenever a component is registered.
     */
    private componentRegisteredSubject = new Subject<ExtensionComponentRegistration>();

    /**
     * This observable emits whenever a component is registered.
     */
    readonly allRegistrationsObservable = this.componentRegisteredSubject.asObservable();

    /**
     * This observable whenever a component is registered under the given scope.
     * @param scope given scope
     */
    public scopedRegistrationsObservable(scope: string): Observable<ExtensionComponentRegistration> {
        return this.allRegistrationsObservable.pipe(filter((reg) => reg.scope == scope));
    }

    /**
     * This returns whether a given scope has any components registered or not.
     * @param scope given scope
     * @return if there are any components registered for the scope.
     */
    public hasRegisteredComponents(scope: string): boolean {
        return this.registrations.has(scope);
    }

    /**
     * Obtain all component registrations under a given scope.
     * @param scope given scope
     * @return copy of all registrations, ordered
     */
    public getRegisteredComponents(scope: string): ExtensionComponentRegistration[] {
        if (!this.registrations.has(scope)) {
            return [];
        }
        return [...this.registrations.get(scope)];
    }

    /**
     * Register an extension component.
     * @param registration new component registration.
     */
    public registerExtensionComponent(registration: ExtensionComponentRegistration): void {
        if (!this.registrations.has(registration.scope)) {
            const registrations = [];
            this.registrations.set(registration.scope, registrations);
        }
        const registrations = this.registrations.get(registration.scope);
        registrations.push(registration);
        registrations.sort(registrationOrderingComparator);
        this.componentRegisteredSubject.next(registration);
    }
}
