/*
 * ******************************************************************
 * Copyright (c) 2024 Broadcom. All Rights Reserved.
 * Broadcom Confidential. The term "Broadcom" refers to Broadcom Inc.
 * and/or its subsidiaries.
 * ******************************************************************
 */

import { AfterViewInit, Component, ElementRef, EventEmitter, Inject, Input, OnDestroy, Optional, Output, Self, ViewEncapsulation } from "@angular/core";
import { ActivatedRoute, ActivatedRouteSnapshot, Router, RouterStateSnapshot } from "@angular/router";
import { INTERENAL_EXTENSION_POINT_TYPE } from "../extension-point-orchestrator/extension-point-events";
import { ANGULAR_APP_PRIMARY_NAVIGATION } from "../angular-apps/angular-apps.extension-point";
import { EntityActionComponentContainer } from "../extension-components.directive";
import { ExtensionComponentRegistration, ExtensionComponentRegistry } from "../extension-component.registry";
import { ExtensionContainerService } from "./extension-container.service";
import {
    ComponentCreatedEventDetails,
    CORE_UI_CUSTOM_EVENTS,
    InjectPluginEventDetails,
    PluginCanDeactivateEventDetails,
    PluginDestroyedEventDetails,
    PluginInjectEventDetails,
    ThemeChange,
    VMvAppActionComponentChangeEventDetails,
    WizardModalValidityEventDetails,
} from "./core-ui-events";
import { SubscriptionTracker } from "../../../../subscription-tracker.service";
import { Observable, filter, interval, map, mergeMap, take } from "rxjs";
import { GeneralExtensibilityService } from "../../../../consumer/general-extensibility.service";
import { HYPER_PLUGIN_THEME, HYPER_PLUGIN_THEME_TYPE, THEME_MODE } from "../../../../lib-interfaces";

class IdGenerator {
    private prefix: string;
    private counter: number;

    constructor(prefix: string) {
        this.prefix = prefix;
        this.counter = 0;
    }

    public generate() {
        this.counter += 1;
        return `${this.prefix}-${this.counter}`;
    }
}

const idGenerator = new IdGenerator("vcd-extension-container");

@Component({
    templateUrl: "./extension-container.component.html",
    selector: "vcd-extension-container",
    styleUrls: ["./extension-container.component.scss"],
    // Sets full height styles in case fullHeight is true
    host: {
        "[class.extension-host]": "fullHeight",
    },
    providers: [SubscriptionTracker],
    encapsulation: ViewEncapsulation.None,
})
export class ExtensionContainerComponent implements AfterViewInit, OnDestroy {
    /**
     * Plugin route is explicitly specified when we want to show up a Plugin component
     * in specific core ui route.
     *
     * Keep in mind showing Plugin component doesn't invove Plugin routes, it's purely
     * compoent thing.
     */
    @Input()
    set pluginRoute(value: string) {
        this._pluginRoute = value;

        if (!this.emitInjectPluginEventManually) {
            if (this._pluginRoute) {
                this.alreadyInjected = false;
                this.emitInjectPlugin();
                return;
            }
        }
    }

    /**
     * List of Modal/Wizard Plugin components to be renderd on the page
     */
    @Input()
    set extensions(value: ExtensionComponentRegistration[]) {
        this._extensions = value;
        this.fullHeight = false;
    }

    /**
     * If true the plugin injection won't happen automatically and you
     * will have to call emitInjectPlugin manually.
     */
    @Input()
    emitInjectPluginEventManually: boolean = false;

    @Input()
    internalExtensionPointType: INTERENAL_EXTENSION_POINT_TYPE;

    /**
     * Sets the scope of the components to be displayed.
     * Resets the directive and listens for registrations on the new scope, triggering an update if one comes in.
     * Immediately triggers an update.
     * @param scope the scope of the components to be displayed.
     */
    @Input()
    set extensionComponents(extensionComponents: string) {
        this.scope = extensionComponents;
        this.fullHeight = false;
    }

    /**
     * Expose getter for this.scope, in case somebody references it by extensionComponents.
     */
    get extensionComponents(): string {
        return this.scope;
    }

    /**
     * Based on that input the compoent will be full height or not.
     */
    @Input()
    fullHeight: boolean = true;

    /**
     * Event emitter that can be listened for changes to the components.
     */
    @Output()
    readonly onComponentsChange = new EventEmitter<EntityActionComponentContainer[]>();

    /**
     * When a Plugin compoent is being created event is emitted to the Core UI later handled
     * appropriately.
     */
    @Output()
    componentCreated = new EventEmitter();

    /**
     * In case of Modal/Wizard Extensibility and especially Create Org
     * a Plugin is able to dictate that if the Core UI form is valid or not.
     *
     * This Output is used by the Core UI to manage the form validity.
     */
    @Output()
    allExtensionsAreValid = new EventEmitter<boolean>();

    public loading: boolean = true;
    public scope: string;

    hyperPluginRegistration: ExtensionComponentRegistration;
    hostingSelector: string;
    _pluginRoute: string;
    _extensions: ExtensionComponentRegistration[];

    /**
     * Unique identifier of the current extension container instace.
     * Allows to render subset of extension points in given instance of
     * the extension container. Extension Points rendered can be from different
     * or the same Extension Point type.
     *
     * Example:
     * Loads in One Place ExtPoint1/2
     * <vcd-extension-container [extensions]=[extPoint1, extPoint2]>
     *
     * Loads in Another Place ExtPointA/B
     * <vcd-extension-container [extensions]=[extPointA, extPointB]>
     */
    private id: string;
    private alreadyInjected: boolean = false;

    /**
     * Holds loaded CSS files for given theme for each hyper plugin.
     * The links in that list must be removed from the DOM on each
     * `theme` change.
     */
    private linkElementsPerHyperPlugin: {
        [hyperPluginUrn: string]: HTMLLinkElement[]
    } = {};

    constructor(
        private router: Router,
        private route: ActivatedRoute,
        private elRef: ElementRef,
        @Self() private subscriptionTracker: SubscriptionTracker,
        private extensionContainerService: ExtensionContainerService,
        private registry: ExtensionComponentRegistry,
        private generalExtensibilityService: GeneralExtensibilityService,
        @Optional() @Inject(HYPER_PLUGIN_THEME) public HYPER_PLUGIN_THEME: HYPER_PLUGIN_THEME_TYPE,
    ) {
        this.extensionContainerService.extensionContainerComponent = this;
        this.id = idGenerator.generate();

        if (!this.emitInjectPluginEventManually) {
            if (this._pluginRoute) {
                this.emitInjectPlugin();
                return;
            }

            if (this.route.data) {
                this.subscriptionTracker.subscribe(this.route.data, (data: { vcdRoute: string; fullHeight: boolean }) => {
                    this._pluginRoute = data.vcdRoute;

                    if (data.fullHeight !== undefined || data.fullHeight !== null) {
                        this.fullHeight = data.fullHeight;
                    }

                    this.emitInjectPlugin();
                });
            }
        }

        window.addEventListener(CORE_UI_CUSTOM_EVENTS.PLUGIN_INJECTED, this.onPluginInjectedListener.bind(this));
    }

    ngAfterViewInit(): void {
        if (!this.emitInjectPluginEventManually) {
            const timeout = setTimeout(() => {
                this.emitInjectPlugin();
                clearTimeout(timeout);
            });
        }
    }

    ngOnDestroy(): void {
        window.dispatchEvent(
            new CustomEvent<PluginDestroyedEventDetails>(CORE_UI_CUSTOM_EVENTS.ON_PLUGIN_DESTROYED, {
                detail: {
                    pluginRoute: this._pluginRoute,
                    hostingSelector: this.hostingSelector,
                    scope: this.scope,
                    navigateAwayTo: null,
                },
            })
        );
    }

    canDeactivate(
        component: ExtensionContainerComponent,
        currentRoute: ActivatedRouteSnapshot,
        currentState: RouterStateSnapshot,
        nextState: RouterStateSnapshot
    ): Promise<boolean> {
        window.dispatchEvent(
            new CustomEvent<PluginDestroyedEventDetails>(CORE_UI_CUSTOM_EVENTS.ON_PLUGIN_DESTROYED, {
                detail: {
                    pluginRoute: this._pluginRoute,
                    hostingSelector: this.hostingSelector,
                    scope: this.scope,
                    navigateAwayTo: nextState.url,
                },
            })
        );

        return new Promise<boolean>((resolve, reject) => {
            const listener = (event: CustomEvent<PluginCanDeactivateEventDetails>): void => {
                if (event.detail.canDeactivate) {
                    resolve(true);
                    window.removeEventListener(CORE_UI_CUSTOM_EVENTS.ON_PLUGIN_CAN_DEACTIVATE, listener);
                    return;
                }

                resolve(false);
                window.removeEventListener(CORE_UI_CUSTOM_EVENTS.ON_PLUGIN_CAN_DEACTIVATE, listener);
            };

            window.addEventListener(CORE_UI_CUSTOM_EVENTS.ON_PLUGIN_CAN_DEACTIVATE, listener);

            // If Hyper Plugin is missing allow navigation away from the plugin route
            if (!this.hyperPluginRegistration) {
                resolve(true);
                return;
            }
        });
    }

    onPluginInjectedListener(event: CustomEvent<PluginInjectEventDetails>): void {
        const hyperPluginHtmlEl: HTMLElement = this.elRef.nativeElement.querySelector(event.detail.componentSelector);

        if (!hyperPluginHtmlEl) {
            return;
        }

        const hyperPluginRegistration = this.getHyperPluginRegistration(event.detail.hyperPluginUrn);
        this.hyperPluginRegistration = hyperPluginRegistration;

        const angularAppShadowRoot = hyperPluginHtmlEl.shadowRoot;
        // Wait for styles to load and show the plugin
        if (angularAppShadowRoot && hyperPluginRegistration.manifest.styles) {
            const darkThemeStyles = hyperPluginRegistration.manifest.styles.filter((stylesFile) => {
                if (!stylesFile.includes("shadowdom")) {
                    return false;
                }

                return stylesFile.startsWith("dark");
            });

            const lightThemeStyles = hyperPluginRegistration.manifest.styles.filter((stylesFile) => {
                if (!stylesFile.includes("shadowdom")) {
                    return false;
                }

                return !stylesFile.startsWith("dark");
            });

            this.linkElementsPerHyperPlugin[hyperPluginRegistration.manifest.urn] = [];
            if (this.HYPER_PLUGIN_THEME) {
                this.subscriptionTracker.subscribe(
                    this.HYPER_PLUGIN_THEME,
                    (theme) => {
                        this.loadShadowDomStyles(theme, darkThemeStyles, lightThemeStyles, hyperPluginRegistration, angularAppShadowRoot);
                    }  
                );
            } else {
                this.loadShadowDomStyles("light", darkThemeStyles, lightThemeStyles, hyperPluginRegistration, angularAppShadowRoot);
            }
        } else {
            // No styles provided show the plugin
            this.loading = false;
        }

        hyperPluginHtmlEl.addEventListener(
            CORE_UI_CUSTOM_EVENTS.COMPONENT_CREATED,
            (event: CustomEvent<ComponentCreatedEventDetails>) => {
                this.componentCreated.emit(event.detail.data);
            }
        );

        hyperPluginHtmlEl.addEventListener(
            CORE_UI_CUSTOM_EVENTS.WIZARD_MODAL_FORM_VALIDITY,
            (event: CustomEvent<WizardModalValidityEventDetails>) => {
                this.allExtensionsAreValid.emit(event.detail.allExtensionsAreValid);
            }
        );

        hyperPluginHtmlEl.addEventListener(
            CORE_UI_CUSTOM_EVENTS.VM_VAPP_ACTION_COMPONENT_CHANGE,
            (event: CustomEvent<VMvAppActionComponentChangeEventDetails>) => {
                this.onComponentsChange.emit(event.detail.components);
            }
        );

        window.removeEventListener(CORE_UI_CUSTOM_EVENTS.PLUGIN_INJECTED, this.onPluginInjectedListener.bind(this));
    }

    emitInjectPlugin(): void {
        if (this.alreadyInjected) {
            return;
        }
        this.alreadyInjected = true;

        this.allHyperPluginsAreActivated().subscribe(() => {
            if (!this._pluginRoute && !this._extensions?.length && !this.scope) {
                return;
            }
    
            this.loading = true;
    
            let hyperPluginHostingSelector;
            if (this._extensions?.length > 0) {
                hyperPluginHostingSelector = this._extensions[0].scope;
                this.hostingSelector = `${hyperPluginHostingSelector}-${this.id}`;
            }
            const event = new CustomEvent<InjectPluginEventDetails>(CORE_UI_CUSTOM_EVENTS.INJECT_PLUGIN, {
                detail: {
                    hostingSelector: hyperPluginHostingSelector,
                    route: this._pluginRoute,
                    extensions: this._extensions,
                    id: this.id,
                    internalExtensionPointType: this.internalExtensionPointType,
                    scope: this.scope,
                    theme: "light",
                    fullHeight: this.fullHeight,
                },
            });
            window.dispatchEvent(event);

            if (this.HYPER_PLUGIN_THEME) {
                this.subscriptionTracker.subscribe(
                    this.HYPER_PLUGIN_THEME,
                    (theme) => {
                        const event = new CustomEvent<ThemeChange>(CORE_UI_CUSTOM_EVENTS.ON_HYPER_PLUGIN_THEME_CHANGE, {
                            detail: {
                                theme: theme,
                            }
                        });
                        window.dispatchEvent(event);
                    }
                );
            }
        });
    }

    private getHyperPluginRegistration(hyperPluginUrn: string): ExtensionComponentRegistration {
        return this.registry.getRegisteredComponents(ANGULAR_APP_PRIMARY_NAVIGATION).find((registration) => {
            return registration.manifest.urn === hyperPluginUrn;
        });
    }

    private allHyperPluginsAreActivated(): Observable<void> {
        const waitUntilAllHyperPluginsAreActive = interval(1000).pipe(
            filter(() => {
                const registrations = this.registry.getRegisteredComponents(ANGULAR_APP_PRIMARY_NAVIGATION);
                if (!registrations.length) {
                    return false;
                }

                const someAreNotActiveYet = registrations.some((registration) => {
                    return !registration.angularAppDetails.activated;
                });

                if (someAreNotActiveYet) {
                    return false;
                }

                return true;
            }),
            take(1),
            map(() => {
                return;
            })
        );

        return this.generalExtensibilityService.allExtensionsLoaded.pipe(
            filter(Boolean),
            take(1),
            mergeMap(() => {
                return waitUntilAllHyperPluginsAreActive;
            }),
        );
    }

    private loadShadowDomStyles(theme: THEME_MODE, darkThemeStyles: string[],
        lightThemeStyles: string[], hyperPluginRegistration: ExtensionComponentRegistration,
        angularAppShadowRoot: ShadowRoot) {
        this.linkElementsPerHyperPlugin[hyperPluginRegistration.manifest.urn].forEach((link) => {
            link.remove();
        });
        this.linkElementsPerHyperPlugin[hyperPluginRegistration.manifest.urn] = [];

        let stylesToLoad: string[];
        if (theme === "dark" && darkThemeStyles.length > 0) {
            stylesToLoad = darkThemeStyles;
        } else {
            stylesToLoad = lightThemeStyles;
        }

        stylesToLoad.forEach((stylesFile) => {
            if (!stylesFile.includes("shadowdom")) {
                return;
            }

            const linkElem = document.createElement("link");
            linkElem.setAttribute("rel", "stylesheet");
            linkElem.setAttribute("href", `${hyperPluginRegistration.resourcePath}/${stylesFile}`);
            angularAppShadowRoot.insertBefore(linkElem, angularAppShadowRoot.firstChild);
            this.linkElementsPerHyperPlugin[hyperPluginRegistration.manifest.urn].push(linkElem);

            linkElem.onload = linkElem.onerror = () => {
                this.loading = false;
            };
        });
    }
}
