import { __awaiter } from "tslib";
import { inject, Inject, Injectable, Injector, Optional } from '@angular/core';
import { LOGGER } from '@anglr/common';
import { DynamicItemLoader } from '@anglr/dynamic';
import { isBlank, noop, isStrictEquals } from '@jscrpt/common';
import { Observable, Subscription } from 'rxjs';
import { RelationsComponentManager } from '../relationsComponentManager/relationsComponentManager.service';
import { RelationsManager } from '../relationsManager/relationsManager.service';
import { RELATIONS_COMPONENTS_LOADER, RELATIONS_PROCESSOR_SKIP_INIT } from '../../misc/tokens';
import { RelationsDataTransferInstructionImpl } from './relationsDataTransferInstruction';
import { isAssigned, isSkipInit } from '../../misc/utils';
import { RelationsChangeDetector } from '../relationsChangeDetector/relationsChangeDetector.service';
import { RelationsDebugger } from '../relationsDebugger/relationsDebugger.service';
import * as i0 from "@angular/core";
import * as i1 from "../relationsManager/relationsManager.service";
import * as i2 from "../relationsComponentManager/relationsComponentManager.service";
import * as i3 from "@anglr/dynamic";
/**
 * Processor that applies relations to registered components
 */
export class RelationsProcessor {
    /**
     * Gets or sets relations metadata
     */
    get relations() {
        if (this.parent) {
            return this.parent.relations;
        }
        return this.ɵRelations;
    }
    set relations(value) {
        this.ɵRelations = value;
    }
    /**
     * Gets or sets array of backward relations, relations that are used for obtaining data for inputs
     */
    get backwardRelations() {
        if (this.parent) {
            return this.parent.backwardRelations;
        }
        return this.ɵBackwardRelations;
    }
    set backwardRelations(value) {
        this.ɵBackwardRelations = value;
    }
    //######################### public properties #########################
    /**
     * Gets promise that completes when processor was initialized
     */
    get initialized() {
        return this.ɵInitialized;
    }
    //######################### constructor #########################
    constructor(relationsManager, componentManager, injector, loader, logger, skipInit) {
        this.relationsManager = relationsManager;
        this.componentManager = componentManager;
        this.injector = injector;
        this.loader = loader;
        this.logger = logger;
        //######################### protected fields #########################
        /**
         * Promise used for indication that processor was initialized
         */
        this.ɵInitialized = Promise.resolve();
        /**
         * Relations metadata
         */
        this.ɵRelations = {};
        /**
         * Array of backward relations, relations that are used for obtaining data for inputs
         */
        this.ɵBackwardRelations = {};
        /**
         * Resolves initialized
         */
        this.resolveInitialized = noop;
        /**
         * Subscriptions created during initialization
         */
        this.initSubscriptions = new Subscription();
        /**
         * Instance of parent relations processor
         */
        this.parent = null;
        /**
         * Id of scope
         */
        this.scopeId = null;
        /**
         * Injected relations change detector
         */
        this.relationsChangeDetector = inject(RelationsChangeDetector);
        /**
         * Instance of relations debugger
         */
        this.relationsDebugger = inject(RelationsDebugger, { optional: true });
        if (!skipInit) {
            this.initSubscriptions.add(this.relationsManager.relationsChange.subscribe(() => this.initializeRelations()));
            this.initializeRelations();
        }
    }
    //######################### public methods - implementation of OnDestroy #########################
    /**
     * Called when component is destroyed
     */
    ngOnDestroy() {
        this.initSubscriptions.unsubscribe();
        this.destroyRelations();
    }
    //######################### public methods #########################
    /**
     * Updates relations
     * @param id - Id of component to be registered
     */
    updateRelations(id) {
        var _a, _b, _c, _d, _e, _f;
        (_a = this.logger) === null || _a === void 0 ? void 0 : _a.debug('RelationsProcessor: Updating relations for {{@id}}', { id });
        const relations = this.relations[id];
        const backwardRelations = this.backwardRelations[id];
        let components = this.componentManager.get(id);
        //this component has no relations
        if (!relations || !components) {
            (_b = this.logger) === null || _b === void 0 ? void 0 : _b.warn('RelationsProcessor: No relations for {{@id}}', { id });
            return;
        }
        this.initOptions(components, relations);
        //initialize default value from connection to this
        if (backwardRelations === null || backwardRelations === void 0 ? void 0 : backwardRelations.length) {
            backwardRelations.forEach(inputOutput => {
                this.initBackwardRelation(inputOutput);
            });
        }
        if (relations === null || relations === void 0 ? void 0 : relations.inputOutputs) {
            if (!Array.isArray(components)) {
                components = [components];
            }
            for (const outputComponent of components) {
                (_c = outputComponent.ɵɵRelationsOutputsChangeSubscriptions) === null || _c === void 0 ? void 0 : _c.forEach(subscription => subscription.unsubscribe());
                outputComponent.ɵɵRelationsOutputsChangeSubscriptions = [];
                for (const inputOutput of relations.inputOutputs) {
                    let inputComponents = this.componentManager.get(inputOutput.inputComponentId);
                    if (inputComponents && !Array.isArray(inputComponents)) {
                        inputComponents = [inputComponents];
                    }
                    //TODO revise log output
                    (_d = this.logger) === null || _d === void 0 ? void 0 : _d.verbose('RelationsProcessor: processing input outputs {{@data}} ', { data: { id, inputOutput } });
                    const outputObservable = outputComponent[`${inputOutput.outputName}Change`];
                    //check whether is observable output
                    if (!(outputObservable instanceof Observable)) {
                        (_e = this.logger) === null || _e === void 0 ? void 0 : _e.warn('RelationsProcessor: Output on component {{@data}} is not observable', { data: inputOutput });
                        continue;
                    }
                    //set listening for output changes
                    outputComponent.ɵɵRelationsOutputsChangeSubscriptions.push(outputObservable.subscribe(() => {
                        var _a;
                        let inputs = this.componentManager.get(inputOutput.inputComponentId);
                        if (!inputs) {
                            (_a = this.logger) === null || _a === void 0 ? void 0 : _a.warn('RelationsProcessor: Missing input components {{@data}} on output change', { data: inputOutput });
                            return;
                        }
                        if (!Array.isArray(inputs)) {
                            inputs = [inputs];
                        }
                        for (const input of inputs) {
                            this.transferData(outputComponent, input, inputOutput, false);
                        }
                    }));
                    if (!inputComponents || !Array.isArray(inputComponents)) {
                        (_f = this.logger) === null || _f === void 0 ? void 0 : _f.warn('RelationsProcessor: Missing input components {{@data}}', { data: inputOutput });
                        continue;
                    }
                    for (const inputComponent of inputComponents) {
                        const id = `${inputComponent.ɵɵRelationsComponentId}-${outputComponent.ɵɵRelationsComponentId}`;
                        //init data transfer only if not marked as skip init and was not initialized and was already at least once assigned
                        if (!isSkipInit(outputComponent, inputOutput.outputName) && !inputOutput.initialized[id] && isAssigned(outputComponent, inputOutput.outputName)) {
                            inputOutput.initialized[id] = this.transferData(outputComponent, inputComponent, inputOutput, true);
                        }
                    }
                }
            }
        }
    }
    /**
     * Method used for destroying component
     */
    destroyComponent(id) {
        var _a, _b;
        const metadata = this.relations[id];
        const backwardRelations = this.backwardRelations[id];
        //uninitialize backward relations
        if (backwardRelations) {
            for (const relation of backwardRelations) {
                relation.initialized = {};
            }
        }
        //destroy relations
        if (metadata) {
            let components = this.componentManager.get(id);
            if (components) {
                if (!components) {
                    return;
                }
                if (!Array.isArray(components)) {
                    components = [components];
                }
                for (const cmp of components) {
                    (_a = cmp.ɵɵRelationsOutputsChangeSubscriptions) === null || _a === void 0 ? void 0 : _a.forEach(subscription => subscription.unsubscribe());
                    cmp.ɵɵRelationsOutputsChangeSubscriptions = [];
                    cmp.ɵɵRelationsOptionsInitialized = false;
                }
            }
            if (metadata.inputOutputs && Array.isArray(metadata.inputOutputs)) {
                for (const inputOutput of metadata.inputOutputs) {
                    inputOutput.initialized = {};
                }
            }
            //destroy auto created components and unregister them
            if (metadata.autoCreated) {
                let components = this.componentManager.get(id);
                if (!components) {
                    return;
                }
                if (!Array.isArray(components)) {
                    components = [components];
                }
                this.componentManager.unregisterComponent(id);
                for (const cmp of components) {
                    (_b = cmp.ngOnDestroy) === null || _b === void 0 ? void 0 : _b.call(cmp);
                }
            }
        }
    }
    /**
     * Opens new scope of relations processor
     * @param id - Id of newly created scope
     * @param componentManager - Scoped instance of component manager
     * @param injector - Injector for current scope
     */
    openScope(id, componentManager, injector) {
        const processor = new RelationsProcessor(this.relationsManager, componentManager, injector, this.loader, this.logger, true);
        processor.scopeId = id;
        processor.parent = this;
        for (const componentId in this.relations) {
            const relationsDef = this.relations[componentId];
            //Same scope initialize
            if (relationsDef.scope === id && relationsDef.componentType) {
                const instance = new relationsDef.componentType(injector);
                componentManager.registerComponent(componentId, instance);
                processor.updateRelations(componentId);
            }
        }
        return processor;
    }
    /**
     * Destroyes opened scope itself
     */
    destroyScope() {
        for (const componentId in this.relations) {
            const relationsDef = this.relations[componentId];
            //Same scope initialize
            if (relationsDef.scope === this.scopeId) {
                this.destroyComponent(componentId);
            }
        }
    }
    transferOutputsData(id, delayed = false, forceTransfer = false) {
        var _a, _b, _c, _d;
        var _e;
        const transfers = {};
        const relations = this.relations[id];
        let components = this.componentManager.get(id);
        if (!relations || !components) {
            (_a = this.logger) === null || _a === void 0 ? void 0 : _a.warn('RelationsProcessor: No relations for {{@id}}', { id });
            return delayed ? transfers : null;
        }
        if (relations === null || relations === void 0 ? void 0 : relations.inputOutputs) {
            if (!Array.isArray(components)) {
                components = [components];
            }
            //for each component containing outputs
            for (const outputComponent of components) {
                //for each relation
                for (const inputOutput of relations.inputOutputs) {
                    let inputComponents = this.componentManager.get(inputOutput.inputComponentId);
                    if (inputComponents && !Array.isArray(inputComponents)) {
                        inputComponents = [inputComponents];
                    }
                    if (!inputComponents || !Array.isArray(inputComponents)) {
                        (_b = this.logger) === null || _b === void 0 ? void 0 : _b.warn('RelationsProcessor: Missing input components {{@data}}', { data: inputOutput });
                        continue;
                    }
                    (_c = transfers[_e = inputOutput.inputComponentId]) !== null && _c !== void 0 ? _c : (transfers[_e] = new RelationsDataTransferInstructionImpl(inputComponents));
                    //for each input component
                    for (const inputComponent of inputComponents) {
                        if (!outputComponent || !inputComponent) {
                            continue;
                        }
                        const previousValue = inputComponent[inputOutput.inputName];
                        const currentValue = outputComponent[inputOutput.outputName];
                        //ignore changes when previousValue equals to currentValue.
                        if (!forceTransfer && isStrictEquals(previousValue, currentValue)) {
                            continue;
                        }
                        const change = transfers[inputOutput.inputComponentId].changes[inputOutput.inputName] =
                            {
                                previousValue,
                                currentValue,
                                firstChange: false,
                                isFirstChange: () => false,
                            };
                        if (ngRelationsDebugger) {
                            (_d = this.relationsDebugger) === null || _d === void 0 ? void 0 : _d.transferData(transfers[inputOutput.inputComponentId], {
                                change,
                                inputComponentId: inputOutput.inputComponentId,
                                outputComponentId: inputOutput.outputComponentId,
                                outputName: inputOutput.outputName,
                                inputName: inputOutput.inputName,
                                scope: this.scopeId,
                            });
                        }
                    }
                }
            }
            if (!delayed) {
                //transfers data
                for (const key in transfers) {
                    const transfer = transfers[key];
                    transfer.applyChanges();
                }
            }
        }
        return delayed ? transfers : null;
    }
    /**
     * Transfers data for specified component using its inputs relations, all data are transfered in single change
     * @param id - Id of component whose inputs relations should be applied to transfer data
     * @param delayed - Indication whether delay data transfer and only generate instructions, if true returns transfer instructions
     * @param inputs - Optional array of inputs that should be displayed
     */
    transferInputsData(id, delayed = false, inputs) {
        var _a, _b, _c, _d, _e;
        const backwardRelations = this.backwardRelations[id];
        const inputComponent = this.componentManager.get(id);
        if (!backwardRelations) {
            (_a = this.logger) === null || _a === void 0 ? void 0 : _a.warn('RelationsProcessor: No backward relations for {{@id}}', { id });
            return delayed ? new RelationsDataTransferInstructionImpl([]) : null;
        }
        if (!inputComponent) {
            (_b = this.logger) === null || _b === void 0 ? void 0 : _b.warn('RelationsProcessor: Missing input components for {{@id}}', { id });
            return delayed ? new RelationsDataTransferInstructionImpl([]) : null;
        }
        if (Array.isArray(inputComponent)) {
            (_c = this.logger) === null || _c === void 0 ? void 0 : _c.error('RelationsProcessor: Only one component must be available for id {{@id}}', { id });
            return delayed ? new RelationsDataTransferInstructionImpl([]) : null;
        }
        const transfer = new RelationsDataTransferInstructionImpl([inputComponent]);
        //for each backward relation
        for (const backwardRelation of backwardRelations) {
            //skip if input does not exists and inputs are specified
            if (inputs && inputs.indexOf(backwardRelation.inputName) < 0) {
                continue;
            }
            const outputComponent = this.componentManager.get(backwardRelation.outputComponentId);
            if ((Array.isArray(outputComponent))) {
                (_d = this.logger) === null || _d === void 0 ? void 0 : _d.error('RelationsProcessor: Only one output component must be available for id {{@id}}', { id });
                continue;
            }
            if (!outputComponent || !inputComponent) {
                continue;
            }
            const previousValue = inputComponent[backwardRelation.inputName];
            const currentValue = outputComponent[backwardRelation.outputName];
            //ignore changes when previousValue equals to currentValue.
            if (isStrictEquals(previousValue, currentValue)) {
                continue;
            }
            const change = transfer.changes[backwardRelation.inputName] =
                {
                    previousValue,
                    currentValue,
                    firstChange: false,
                    isFirstChange: () => false,
                };
            if (ngRelationsDebugger) {
                (_e = this.relationsDebugger) === null || _e === void 0 ? void 0 : _e.transferData(transfer, {
                    change,
                    inputComponentId: backwardRelation.inputComponentId,
                    outputComponentId: backwardRelation.outputComponentId,
                    outputName: backwardRelation.outputName,
                    inputName: backwardRelation.inputName,
                    scope: this.scopeId,
                });
            }
        }
        if (!delayed) {
            transfer.applyChanges();
        }
        return delayed ? transfer : null;
    }
    //######################### protected methods #########################
    /**
     * Initialize relations from metadata
     */
    initializeRelations() {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function* () {
            yield this.destroyRelations();
            this.setInitializePromise();
            (_a = this.logger) === null || _a === void 0 ? void 0 : _a.debug('RelationsProcessor: initializing relations');
            //empty relations
            if (!this.relationsManager.relations.length) {
                this.resolveInitialized();
                return;
            }
            for (const meta of this.relationsManager.relations) {
                const outputs = [];
                //no outputs metadata
                if (!meta.outputs) {
                    continue;
                }
                for (const output of meta.outputs) {
                    //no inputs metadata
                    if (!output.inputs) {
                        continue;
                    }
                    for (const input of output.inputs) {
                        const inputOutput = {
                            inputComponentId: input.id,
                            outputComponentId: meta.id,
                            inputName: input.inputName,
                            outputName: output.outputName,
                            initialized: {},
                        };
                        outputs.push(inputOutput);
                        if (!this.backwardRelations[input.id]) {
                            this.backwardRelations[input.id] = [];
                        }
                        this.backwardRelations[input.id].push(inputOutput);
                    }
                }
                //sets options for relations component
                yield this.initComponent(meta, outputs);
            }
            this.relationsChangeDetector.initialize(this.relations);
            if (ngRelationsDebugger) {
                (_b = this.relationsDebugger) === null || _b === void 0 ? void 0 : _b.initialize(this.relations, this.backwardRelations);
            }
            this.resolveInitialized();
        });
    }
    /**
     * Initialize backward relations
     * @param inputOutput - Data for input and output
     */
    initBackwardRelation(inputOutput) {
        var _a;
        let outputComponents = this.componentManager.get(inputOutput.outputComponentId);
        let inputComponents = this.componentManager.get(inputOutput.inputComponentId);
        if ((isBlank(outputComponents) || Array.isArray(outputComponents) && !outputComponents.length) ||
            (isBlank(inputComponents) || Array.isArray(inputComponents) && !inputComponents.length)) {
            (_a = this.logger) === null || _a === void 0 ? void 0 : _a.warn('RelationsProcessor: missing metadata for backward relations {{@data}}', { data: inputOutput });
            return;
        }
        if (!Array.isArray(inputComponents)) {
            inputComponents = [inputComponents];
        }
        if (!Array.isArray(outputComponents)) {
            outputComponents = [outputComponents];
        }
        for (const inputCmp of inputComponents) {
            for (const outputCmp of outputComponents) {
                const id = `${inputCmp.ɵɵRelationsComponentId}-${outputCmp.ɵɵRelationsComponentId}`;
                //init data transfer only if not marked as skip init and was not initialized and was already at least once assigned
                if (!isSkipInit(outputCmp, inputOutput.outputName) && !inputOutput.initialized[id] && isAssigned(outputCmp, inputOutput.outputName)) {
                    inputOutput.initialized[id] = this.transferData(outputCmp, inputCmp, inputOutput, true);
                }
            }
        }
    }
    /**
     * Transfers data from source property to target property
     * @param source - Instance of source object containing source property with data
     * @param target - Instance of target object containing target property for data
     * @param sourceTarget - Definition of source target names and ids
     * @param initial - Indication whether is transfer of data initial, or on event
     */
    transferData(source, target, sourceTarget, initial) {
        var _a;
        if (!source || !target) {
            return false;
        }
        const previousValue = target[sourceTarget.inputName];
        const currentValue = source[sourceTarget.outputName];
        const transfer = new RelationsDataTransferInstructionImpl([target]);
        //ignore changes when previousValue equals to currentValue.
        if (isStrictEquals(previousValue, currentValue)) {
            return false;
        }
        const change = transfer.changes[sourceTarget.inputName] =
            {
                previousValue,
                currentValue,
                firstChange: initial,
                isFirstChange: () => initial
            };
        if (ngRelationsDebugger) {
            (_a = this.relationsDebugger) === null || _a === void 0 ? void 0 : _a.transferData(transfer, {
                change,
                inputComponentId: sourceTarget.inputComponentId,
                inputName: sourceTarget.inputName,
                outputComponentId: sourceTarget.outputComponentId,
                outputName: sourceTarget.outputName,
                scope: this.scopeId,
            });
        }
        transfer.applyChanges();
        return true;
    }
    /**
     * Initialize relation component
     * @param meta - Metadata for relations component
     * @param outputs - Array of outputs data for relations component
     */
    initComponent(meta, outputs) {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function* () {
            const component = this.componentManager.get(meta.id);
            if (component) {
                this.initRelation(false, meta, outputs, null, null);
                this.updateRelations(meta.id);
                return;
            }
            const componentMeta = yield this.loader.loadItem(meta);
            if (!componentMeta) {
                this.initRelation(false, meta, outputs, null, null);
                (_a = this.logger) === null || _a === void 0 ? void 0 : _a.warn('RelationsProcessor: Unable to load relations component! {{@meta}}', { meta: { package: meta.package, name: meta.name } });
                return;
            }
            if (!meta.scope) {
                const instance = new componentMeta.data(this.injector);
                this.componentManager.registerComponent(meta.id, instance);
            }
            this.initRelation(true, meta, outputs, (_b = meta.scope) !== null && _b !== void 0 ? _b : null, meta.scope ? componentMeta.data : null);
            this.updateRelations(meta.id);
        });
    }
    /**
     * Initialize relation for metadata and component
     * @param autoCreated - Indication whether was component auto created or not
     * @param meta - Metadata for relations component
     * @param inputOutputs - Array of outputs data for relations
     * @param scope - Current scope used for this relations component
     * @param componentType - Type used for creation of relations component (only for scoped ones)
     */
    initRelation(autoCreated, meta, inputOutputs, scope, componentType) {
        this.relations[meta.id] =
            {
                autoCreated,
                inputOutputs,
                metadataOptions: meta.relationsOptions,
                componentType,
                scope,
            };
    }
    /**
     * Initialize relations component options
     * @param components - Components which options should be initialized
     * @param meta - Metadata containing options for initialization
     */
    initOptions(components, meta) {
        if (Array.isArray(components)) {
            for (const comp of components) {
                if (!comp.ɵɵRelationsOptionsInitialized) {
                    comp.relationsOptions = meta.metadataOptions;
                    comp.ɵɵRelationsOptionsInitialized = true;
                }
            }
        }
        else {
            if (!components.ɵɵRelationsOptionsInitialized) {
                components.relationsOptions = meta.metadataOptions;
                components.ɵɵRelationsOptionsInitialized = true;
            }
        }
    }
    /**
     * Sets initialized promise
     */
    setInitializePromise() {
        this.ɵInitialized = new Promise(resolve => this.resolveInitialized = resolve);
    }
    /**
     * Destroys initialized relations
     */
    destroyRelations() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.ɵInitialized;
            Object.keys(this.relations).forEach(id => this.destroyComponent(id));
            this.relations = {};
            this.backwardRelations = {};
        });
    }
}
RelationsProcessor.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: RelationsProcessor, deps: [{ token: i1.RelationsManager }, { token: i2.RelationsComponentManager }, { token: i0.Injector }, { token: RELATIONS_COMPONENTS_LOADER }, { token: LOGGER, optional: true }, { token: RELATIONS_PROCESSOR_SKIP_INIT, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
RelationsProcessor.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: RelationsProcessor });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: RelationsProcessor, decorators: [{
            type: Injectable
        }], ctorParameters: () => [{ type: i1.RelationsManager }, { type: i2.RelationsComponentManager }, { type: i0.Injector }, { type: i3.DynamicItemLoader, decorators: [{
                    type: Inject,
                    args: [RELATIONS_COMPONENTS_LOADER]
                }] }, { type: undefined, decorators: [{
                    type: Inject,
                    args: [LOGGER]
                }, {
                    type: Optional
                }] }, { type: undefined, decorators: [{
                    type: Inject,
                    args: [RELATIONS_PROCESSOR_SKIP_INIT]
                }, {
                    type: Optional
                }] }] });
//# sourceMappingURL=relationsProcessor.service.js.map