import * as i0 from '@angular/core';
import { EventEmitter, isDevMode, TemplateRef, ViewContainerRef, INJECTOR, IterableDiffers, ChangeDetectorRef, NgZone, Directive, Inject, Input, Output } from '@angular/core';
import * as i1 from '@rxap/data-source';
import { BaseDataSource, DataSourceLoader } from '@rxap/data-source';
import { Subscription } from 'rxjs';
import { take, tap, filter, distinctUntilChanged } from 'rxjs/operators';
import { noop } from '@rxap/utilities';

class DataSourceCollectionTemplateContext {
    constructor($implicit, connection$, index, count) {
        this.$implicit = $implicit;
        this.connection$ = connection$;
        this.index = index;
        this.count = count;
    }
    get first() {
        return this.index === 0;
    }
    get last() {
        return this.index === this.count - 1;
    }
    get even() {
        return this.index % 2 === 0;
    }
    get odd() {
        return !this.even;
    }
}
function getTypeName(type) {
    return type['name'] || typeof type;
}
class RecordViewTuple {
    constructor(record, view) {
        this.record = record;
        this.view = view;
    }
}
class DataSourceCollectionDirective {
    constructor(dataSourceLoader, template, viewContainerRef, injector, differs, cdr, zone) {
        this.dataSourceLoader = dataSourceLoader;
        this.template = template;
        this.viewContainerRef = viewContainerRef;
        this.injector = injector;
        this.differs = differs;
        this.cdr = cdr;
        this.zone = zone;
        this.viewer = { id: '[rxapDataSourceCollection]' };
        this.dataSource = null;
        this.loaded = new EventEmitter();
        this.error = new EventEmitter();
        this.connection$ = null;
        this.subscription = new Subscription();
        this._differ = null;
        this._dirty = true;
        /**
         * Idecates that the data source returned a empty collection
         *
         * true - is empty
         * false - is NOT empty
         * null - unknown
         *
         * @private
         */
        this._empty = null;
        /**
         * Holds the empty template view ref.
         *
         * Is used to determine if a empty template is added to the
         * view. And used to destruct the empty template if the data source
         * changes from empty to not empty.
         *
         * @private
         */
        this._emptyTemplateViewRef = null;
        this._dataSourceLoadingSubscription = null;
        /**
         * Holds the data that should be displayed
         * @private
         */
        this._data = null;
    }
    /**
     * A function that defines how to track changes for items in the iterable.
     *
     * When items are added, moved, or removed in the iterable,
     * the directive must re-render the appropriate DOM nodes.
     * To minimize churn in the DOM, only nodes that have changed
     * are re-rendered.
     *
     * By default, the change detector assumes that
     * the object instance identifies the node in the iterable.
     * When this function is supplied, the directive uses
     * the result of calling this function to identify the item node,
     * rather than the identity of the object itself.
     *
     * The function receives two inputs,
     * the iteration index and the node object ID.
     */
    set rxapDataSourceCollectionTrackBy(fn) {
        if (isDevMode() && fn != null && typeof fn !== 'function') {
            // TODO(vicb): use a log service once there is a public one available
            if (console && console.warn) {
                console.warn(`trackBy must be a function, but received ${JSON.stringify(fn)}. ` +
                    `See https://angular.io/api/common/NgForOf#change-propagation for more information.`);
            }
        }
        this._trackByFn = fn;
    }
    get ngForTrackBy() {
        return this._trackByFn;
    }
    set data(data) {
        this._data = data;
        this._dirty = true;
    }
    /**
     * Asserts the correct type of the context for the template that `NgForOf` will render.
     *
     * The presence of this method is a signal to the Ivy template type-check compiler that the
     * `NgForOf` structural directive renders its template with a specific context type.
     */
    static ngTemplateContextGuard(dir, ctx) {
        return true;
    }
    // TODO : handel that case: a new data source instance is provided and an open connection to the old data source exists.
    ngOnChanges(changes) {
        const dataSourceOrIdOrTokenChange = changes['dataSourceOrIdOrToken'];
        if (dataSourceOrIdOrTokenChange) {
            this.dataSource = this.loadDataSource();
            // Dont connect to the data source on the first change.
            // Else the parent/sibling components are not initialized
            // and the parameters from the parent/sibling components can not
            // be used in the connect logic.
            // Example: The PaginationDataSource required the pageSize, but the pageSize
            // in the MatPaginator will be set after the ngOnChanges logic is called.
            // So the initial pageSize is undefined.
            if (!dataSourceOrIdOrTokenChange.firstChange) {
                this.connect();
            }
        }
    }
    ngAfterViewInit() {
        this.connect();
    }
    ngOnDestroy() {
        this._differ = null;
        this._empty = null;
        this.viewContainerRef.clear();
        this.dataSource?.disconnect(this.viewer);
        this.subscription.unsubscribe();
        this._dataSourceLoadingSubscription?.unsubscribe();
    }
    /**
     * Applies the changes when needed.
     */
    ngDoCheck() {
        if (this._empty === true) {
            this.viewContainerRef.clear();
            this._differ = null;
            // attaches the empty template if the data source is empty
            if (this.emptyTemplate) {
                this._emptyTemplateViewRef = this.viewContainerRef.createEmbeddedView(this.emptyTemplate);
            }
        }
        else if (this._empty === false) {
            // detach and destroy the empty template if the data source is not
            // empty any more
            if (this._emptyTemplateViewRef) {
                this._emptyTemplateViewRef.detach();
                this._emptyTemplateViewRef.destroy();
                this._emptyTemplateViewRef = null;
            }
            if (this._data) {
                this._dirty = false;
                // React on ngForOf changes only once all inputs have been initialized
                const value = this._data;
                if (!this._differ && value) {
                    try {
                        this._differ = this.differs.find(value).create(this.ngForTrackBy);
                    }
                    catch {
                        throw new Error(`Cannot find a differ supporting object '${value}' of type '${getTypeName(value)}'. NgFor only supports binding to Iterables such as Arrays.`);
                    }
                }
            }
            if (this._differ) {
                const changes = this._differ.diff(this._data);
                if (changes) {
                    this.applyChanges(changes);
                }
            }
        }
    }
    embedErrorTemplate(error) {
        if (!this.errorTemplate) {
            if (isDevMode()) {
                console.log('Skip error template embedding. ErrorTemplate is not defined');
            }
            return;
        }
        const context = {
            $implicit: error,
            // eslint-disable-next-line @typescript-eslint/no-empty-function
            refresh: this.dataSource?.refresh.bind(this.dataSource) ?? (() => {
            }),
        };
        this.embeddedErrorViewRef?.destroy();
        this.viewContainerRef.clear();
        this.embeddedErrorViewRef = this.viewContainerRef.createEmbeddedView(this.errorTemplate, context);
        this.cdr.detectChanges();
    }
    connect() {
        if (this.dataSource) {
            this.connection$ = this.dataSource.connect(this.viewer);
            this.zone.onStable
                .pipe(take(1), tap(() => {
                this.zone.run(() => {
                    this.subscription.add(this.connection$.pipe(tap({
                        next: (response) => {
                            this._empty = response.length === 0;
                            this._data = response;
                            this.cdr.detectChanges();
                        },
                        error: (error) => {
                            this.error.emit(error);
                            console.error(`Connection failure in ${this.dataSource.constructor.name}: ${error.message}`, error);
                            this.embedErrorTemplate(error);
                        },
                    })).subscribe());
                });
            }))
                .subscribe();
        }
        else {
            throw new Error('Can not connect to the data source. The data source is not loaded!');
        }
    }
    applyChanges(changes) {
        const insertTuples = [];
        changes.forEachOperation((item, adjustedPreviousIndex, currentIndex) => {
            if (item.previousIndex == null) {
                // NgForOf is never "null" or "undefined" here because the differ detected
                // that a new item needs to be inserted from the iterable. This implies that
                // there is an iterable value for "_ngForOf".
                const view = this.viewContainerRef.createEmbeddedView(this.template, new DataSourceCollectionTemplateContext(null, this.connection$, -1, -1), currentIndex === null ? undefined : currentIndex);
                const tuple = new RecordViewTuple(item, view);
                insertTuples.push(tuple);
            }
            else if (currentIndex == null) {
                this.viewContainerRef.remove(adjustedPreviousIndex === null ? undefined : adjustedPreviousIndex);
            }
            else if (adjustedPreviousIndex !== null) {
                const view = this.viewContainerRef.get(adjustedPreviousIndex);
                this.viewContainerRef.move(view, currentIndex);
                const tuple = new RecordViewTuple(item, view);
                insertTuples.push(tuple);
            }
        });
        for (let i = 0; i < insertTuples.length; i++) {
            this.perViewChange(insertTuples[i].view, insertTuples[i].record);
        }
        for (let i = 0, ilen = this.viewContainerRef.length; i < ilen; i++) {
            const viewRef = this.viewContainerRef.get(i);
            viewRef.context.index = i;
            viewRef.context.count = ilen;
            viewRef.context.connection$ = this.connection$;
        }
        changes.forEachIdentityChange((record) => {
            const viewRef = this.viewContainerRef.get(record.currentIndex);
            viewRef.context.$implicit = record.item;
        });
    }
    loadDataSource() {
        let dataSource = null;
        if (typeof this.dataSourceOrIdOrToken === 'string') {
            dataSource = this.dataSourceLoader.load(this.dataSourceOrIdOrToken, undefined, this.injector);
        }
        else if (this.dataSourceOrIdOrToken instanceof BaseDataSource) {
            dataSource = this.dataSourceOrIdOrToken;
        }
        else if (this.dataSourceOrIdOrToken !== null) {
            dataSource = this.injector.get(this.dataSourceOrIdOrToken);
        }
        this._dataSourceLoadingSubscription?.unsubscribe();
        this._dataSourceLoadingSubscription =
            dataSource?.loading$.pipe(filter(Boolean)).subscribe(this.loaded) ?? null;
        return dataSource;
    }
    perViewChange(view, record) {
        view.context.$implicit = record.item;
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.3", ngImport: i0, type: DataSourceCollectionDirective, deps: [{ token: DataSourceLoader }, { token: TemplateRef }, { token: ViewContainerRef }, { token: INJECTOR }, { token: IterableDiffers }, { token: ChangeDetectorRef }, { token: NgZone }], target: i0.ɵɵFactoryTarget.Directive }); }
    static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.1.3", type: DataSourceCollectionDirective, isStandalone: true, selector: "[rxapDataSourceCollection]", inputs: { dataSourceOrIdOrToken: ["rxapDataSourceCollectionFrom", "dataSourceOrIdOrToken"], viewer: ["rxapDataSourceCollectionViewer", "viewer"], emptyTemplate: ["rxapDataSourceCollectionEmpty", "emptyTemplate"], errorTemplate: ["rxapDataSourceCollectionErrorTemplate", "errorTemplate"], rxapDataSourceCollectionTrackBy: "rxapDataSourceCollectionTrackBy" }, outputs: { loaded: "loaded", error: "error" }, usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.3", ngImport: i0, type: DataSourceCollectionDirective, decorators: [{
            type: Directive,
            args: [{
                    selector: '[rxapDataSourceCollection]',
                    standalone: true,
                }]
        }], ctorParameters: () => [{ type: i1.DataSourceLoader, decorators: [{
                    type: Inject,
                    args: [DataSourceLoader]
                }] }, { type: i0.TemplateRef, decorators: [{
                    type: Inject,
                    args: [TemplateRef]
                }] }, { type: i0.ViewContainerRef, decorators: [{
                    type: Inject,
                    args: [ViewContainerRef]
                }] }, { type: undefined, decorators: [{
                    type: Inject,
                    args: [INJECTOR]
                }] }, { type: i0.IterableDiffers, decorators: [{
                    type: Inject,
                    args: [IterableDiffers]
                }] }, { type: i0.ChangeDetectorRef, decorators: [{
                    type: Inject,
                    args: [ChangeDetectorRef]
                }] }, { type: i0.NgZone, decorators: [{
                    type: Inject,
                    args: [NgZone]
                }] }], propDecorators: { dataSourceOrIdOrToken: [{
                type: Input,
                args: [{
                        required: true,
                        alias: 'rxapDataSourceCollectionFrom',
                    }]
            }], viewer: [{
                type: Input,
                args: ['rxapDataSourceCollectionViewer']
            }], emptyTemplate: [{
                type: Input,
                args: ['rxapDataSourceCollectionEmpty']
            }], errorTemplate: [{
                type: Input,
                args: ['rxapDataSourceCollectionErrorTemplate']
            }], loaded: [{
                type: Output
            }], error: [{
                type: Output
            }], rxapDataSourceCollectionTrackBy: [{
                type: Input
            }] } });

class DataSourceDirective {
    /**
     * Asserts the correct type of the context for the template that `NgForOf` will render.
     *
     * The presence of this method is a signal to the Ivy template type-check compiler that the
     * `NgForOf` structural directive renders its template with a specific context type.
     */
    static ngTemplateContextGuard(dir, ctx) {
        return true;
    }
    constructor(dataSourceLoader, template, viewContainerRef, injector, cdr, zone) {
        this.dataSourceLoader = dataSourceLoader;
        this.template = template;
        this.viewContainerRef = viewContainerRef;
        this.injector = injector;
        this.cdr = cdr;
        this.zone = zone;
        this.embedded = new EventEmitter();
        this.loaded = new EventEmitter();
        this.error = new EventEmitter();
        this.dataSource = null;
        /**
         * @deprecated removed
         * @protected
         */
        this.subscription = new Subscription();
        this.embeddedViewRef = null;
        this.embeddedErrorViewRef = null;
        this._dataSourceLoadingSubscription = null;
        this._dataSourceConnectionSubscription = null;
        this.viewer = this;
    }
    ngOnChanges(changes) {
        const dataSourceOrIdOrTokenChange = changes['dataSourceOrIdOrToken'];
        if (dataSourceOrIdOrTokenChange) {
            this.dataSource = this.loadDataSource();
            // Dont connect to the data source on the first change.
            // Else the parent/sibling components are not initialized
            // and the parameters from the parent/sibling components can not
            // be used in the connect logic.
            // Example: The PaginationDataSource required the pageSize, but the pageSize
            // in the MatPaginator will be set after the ngOnChanges logic is called.
            // So the initial pageSize is undefined.
            if (!dataSourceOrIdOrTokenChange.firstChange) {
                this.connect();
            }
        }
    }
    ngAfterViewInit() {
        this.connect();
    }
    embedErrorTemplate(error) {
        if (!this.errorTemplate) {
            if (isDevMode()) {
                console.log('Skip error template embedding. ErrorTemplate is not defined');
            }
            return;
        }
        const context = {
            $implicit: error,
            // eslint-disable-next-line @typescript-eslint/no-empty-function
            refresh: this.dataSource?.refresh.bind(this.dataSource) ?? noop,
        };
        this.embeddedViewRef?.destroy();
        this.embeddedViewRef = null;
        this.embeddedErrorViewRef?.destroy();
        this.embeddedErrorViewRef = this.viewContainerRef.createEmbeddedView(this.errorTemplate, context);
        this.cdr.detectChanges();
    }
    embedTemplate(response) {
        this.embeddedErrorViewRef?.destroy();
        this.embeddedErrorViewRef = null;
        const context = {
            $implicit: response,
            connection$: this.connection$,
        };
        if (this.embeddedViewRef && !this.hasChanged(this.embeddedViewRef.context.$implicit, response)) {
            this.embeddedViewRef.context = context;
        }
        else {
            this.embeddedViewRef?.destroy();
            this.embeddedViewRef = this.viewContainerRef.createEmbeddedView(this.template, context);
        }
        this.embedded.emit(response);
        this.cdr.detectChanges();
    }
    ngOnDestroy() {
        this.dataSource?.disconnect(this.viewer);
        this.subscription.unsubscribe();
        this._dataSourceConnectionSubscription?.unsubscribe();
        this._dataSourceLoadingSubscription?.unsubscribe();
    }
    loadDataSource() {
        let dataSource = null;
        if (typeof this.dataSourceOrIdOrToken === 'string') {
            dataSource = this.dataSourceLoader.load(this.dataSourceOrIdOrToken, undefined, this.injector);
        }
        else if (this.dataSourceOrIdOrToken instanceof BaseDataSource) {
            dataSource = this.dataSourceOrIdOrToken;
        }
        else if (this.dataSourceOrIdOrToken !== null) {
            dataSource = this.injector.get(this.dataSourceOrIdOrToken);
        }
        this._dataSourceLoadingSubscription?.unsubscribe();
        this._dataSourceLoadingSubscription =
            dataSource?.loading$.pipe(filter(Boolean)).subscribe(this.loaded) ?? null;
        return dataSource;
    }
    connect() {
        if (this.dataSource) {
            this.dataSource.hasError$.pipe(distinctUntilChanged(), tap(hasError => {
                if (hasError) {
                    this.embedErrorTemplate(null);
                }
                else {
                    this.embeddedErrorViewRef?.destroy();
                    this.embeddedErrorViewRef = null;
                }
            })).subscribe();
            this.connection$ = this.dataSource.connect(this.viewer);
            this.zone.onStable
                .pipe(take(1), tap(() => {
                this.zone.run(() => {
                    this._dataSourceConnectionSubscription?.unsubscribe();
                    this._dataSourceConnectionSubscription = this.connection$
                        .pipe(tap({
                        next: (response) => {
                            this.embedTemplate(response);
                        },
                        error: (error) => {
                            this.error.emit(error);
                            console.error(`Connection failure in ${this.dataSource.constructor.name}: ${error.message}`, error);
                            this.embedErrorTemplate(error);
                        },
                    }))
                        .subscribe();
                });
            }))
                .subscribe();
        }
    }
    hasChanged(last, current) {
        const lastKey = this.trackBy ? this.trackBy(last) : last;
        const currentKey = this.trackBy ? this.trackBy(current) : current;
        return lastKey !== currentKey;
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.3", ngImport: i0, type: DataSourceDirective, deps: [{ token: i1.DataSourceLoader }, { token: i0.TemplateRef }, { token: i0.ViewContainerRef }, { token: INJECTOR }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Directive }); }
    static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.1.3", type: DataSourceDirective, isStandalone: true, selector: "[rxapDataSource]", inputs: { dataSourceOrIdOrToken: ["rxapDataSourceFrom", "dataSourceOrIdOrToken"], viewer: ["rxapDataSourceViewer", "viewer"], errorTemplate: ["rxapDataSourceErrorTemplate", "errorTemplate"], trackBy: ["rxapDataSourceTrackBy", "trackBy"] }, outputs: { embedded: "embedded", loaded: "loaded", error: "error" }, exportAs: ["rxapDataSource"], usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.3", ngImport: i0, type: DataSourceDirective, decorators: [{
            type: Directive,
            args: [{
                    selector: '[rxapDataSource]',
                    exportAs: 'rxapDataSource',
                    standalone: true,
                }]
        }], ctorParameters: () => [{ type: i1.DataSourceLoader }, { type: i0.TemplateRef }, { type: i0.ViewContainerRef }, { type: undefined, decorators: [{
                    type: Inject,
                    args: [INJECTOR]
                }] }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }], propDecorators: { dataSourceOrIdOrToken: [{
                type: Input,
                args: [{
                        required: true,
                        alias: 'rxapDataSourceFrom',
                    }]
            }], viewer: [{
                type: Input,
                args: ['rxapDataSourceViewer']
            }], errorTemplate: [{
                type: Input,
                args: ['rxapDataSourceErrorTemplate']
            }], embedded: [{
                type: Output
            }], loaded: [{
                type: Output
            }], error: [{
                type: Output
            }], trackBy: [{
                type: Input,
                args: ['rxapDataSourceTrackBy']
            }] } });

// region 
// endregion

/**
 * Generated bundle index. Do not edit.
 */

export { DataSourceCollectionDirective, DataSourceCollectionTemplateContext, DataSourceDirective };
//# sourceMappingURL=rxap-data-source-directive.mjs.map
