import { ADD, CHANGE, CONNECT, DELETE, ERROR, UPDATE, } from './informer.js';
import { ObjectSerializer } from './serializer.js';
export class ListWatch {
    constructor(path, watch, listFn, autoStart = true, labelSelector) {
        this.objects = new Map();
        this.indexCache = {};
        this.callbackCache = {};
        this.stopped = false;
        this.path = path;
        this.watch = watch;
        this.listFn = listFn;
        this.labelSelector = labelSelector;
        this.callbackCache[ADD] = [];
        this.callbackCache[UPDATE] = [];
        this.callbackCache[DELETE] = [];
        this.callbackCache[ERROR] = [];
        this.callbackCache[CONNECT] = [];
        this.resourceVersion = '';
        if (autoStart) {
            this.doneHandler(null);
        }
    }
    async start() {
        this.stopped = false;
        await this.doneHandler(null);
    }
    async stop() {
        this.stopped = true;
        this._stop();
    }
    on(verb, cb) {
        if (verb === CHANGE) {
            this.on('add', cb);
            this.on('update', cb);
            this.on('delete', cb);
            return;
        }
        if (this.callbackCache[verb] === undefined) {
            throw new Error(`Unknown verb: ${verb}`);
        }
        this.callbackCache[verb].push(cb);
    }
    off(verb, cb) {
        if (verb === CHANGE) {
            this.off('add', cb);
            this.off('update', cb);
            this.off('delete', cb);
            return;
        }
        if (this.callbackCache[verb] === undefined) {
            throw new Error(`Unknown verb: ${verb}`);
        }
        const indexToRemove = this.callbackCache[verb].findIndex((cachedCb) => cachedCb === cb);
        if (indexToRemove === -1) {
            return;
        }
        this.callbackCache[verb].splice(indexToRemove, 1);
    }
    get(name, namespace) {
        const nsObjects = this.objects.get(namespace || '');
        if (nsObjects) {
            return nsObjects.get(name);
        }
        return undefined;
    }
    list(namespace) {
        if (!namespace) {
            const allObjects = [];
            for (const nsObjects of this.objects.values()) {
                allObjects.push(...nsObjects.values());
            }
            return allObjects;
        }
        const namespaceObjects = this.objects.get(namespace || '');
        if (!namespaceObjects) {
            return [];
        }
        return Array.from(namespaceObjects.values());
    }
    latestResourceVersion() {
        return this.resourceVersion;
    }
    _stop() {
        if (this.request) {
            this.request.abort();
            this.request = undefined;
        }
    }
    async doneHandler(err) {
        this._stop();
        if (err && err.statusCode === 410) {
            this.resourceVersion = '';
        }
        else if (err) {
            this.callbackCache[ERROR].forEach((elt) => elt(err));
            return;
        }
        if (this.stopped) {
            // do not auto-restart
            return;
        }
        this.callbackCache[CONNECT].forEach((elt) => elt(undefined));
        if (!this.resourceVersion) {
            let list;
            try {
                const promise = this.listFn();
                list = await promise;
            }
            catch (err) {
                this.callbackCache[ERROR].forEach((elt) => elt(err));
                return;
            }
            this.objects = deleteItems(this.objects, list.items, this.callbackCache[DELETE].slice());
            this.addOrUpdateItems(list.items);
            this.resourceVersion = list.metadata.resourceVersion || '';
        }
        const queryParams = {
            resourceVersion: this.resourceVersion,
        };
        if (this.labelSelector !== undefined) {
            queryParams.labelSelector = ObjectSerializer.serialize(this.labelSelector, 'string');
        }
        this.request = await this.watch.watch(this.path, queryParams, this.watchHandler.bind(this), this.doneHandler.bind(this));
    }
    addOrUpdateItems(items) {
        items.forEach((obj) => {
            addOrUpdateObject(this.objects, obj, this.callbackCache[ADD].slice(), this.callbackCache[UPDATE].slice());
        });
    }
    watchHandler(phase, obj, watchObj) {
        switch (phase) {
            case 'ERROR':
                if (obj.code === 410) {
                    this.resourceVersion = '';
                }
                break;
            case 'ADDED':
            case 'MODIFIED':
                addOrUpdateObject(this.objects, obj, this.callbackCache[ADD].slice(), this.callbackCache[UPDATE].slice());
                break;
            case 'DELETED':
                deleteObject(this.objects, obj, this.callbackCache[DELETE].slice());
                break;
            case 'BOOKMARK':
                // nothing to do, here for documentation, mostly.
                break;
        }
        if (watchObj && watchObj.metadata) {
            this.resourceVersion = watchObj.metadata.resourceVersion;
        }
    }
}
// exported for testing
export function cacheMapFromList(newObjects) {
    const objects = new Map();
    // build up the new list
    for (const obj of newObjects) {
        let namespaceObjects = objects.get(obj.metadata.namespace || '');
        if (!namespaceObjects) {
            namespaceObjects = new Map();
            objects.set(obj.metadata.namespace || '', namespaceObjects);
        }
        const name = obj.metadata.name || '';
        namespaceObjects.set(name, obj);
    }
    return objects;
}
// external for testing
export function deleteItems(oldObjects, newObjects, deleteCallback) {
    const newObjectsMap = cacheMapFromList(newObjects);
    for (const [namespace, oldNamespaceObjects] of oldObjects.entries()) {
        const newNamespaceObjects = newObjectsMap.get(namespace);
        if (newNamespaceObjects) {
            for (const [name, oldObj] of oldNamespaceObjects.entries()) {
                if (!newNamespaceObjects.has(name)) {
                    oldNamespaceObjects.delete(name);
                    if (deleteCallback) {
                        deleteCallback.forEach((fn) => fn(oldObj));
                    }
                }
            }
        }
        else {
            oldObjects.delete(namespace);
            oldNamespaceObjects.forEach((obj) => {
                if (deleteCallback) {
                    deleteCallback.forEach((fn) => fn(obj));
                }
            });
        }
    }
    return oldObjects;
}
// Only public for testing.
export function addOrUpdateObject(objects, obj, addCallbacks, updateCallbacks) {
    let namespaceObjects = objects.get(obj.metadata.namespace || '');
    if (!namespaceObjects) {
        namespaceObjects = new Map();
        objects.set(obj.metadata.namespace || '', namespaceObjects);
    }
    const name = obj.metadata.name || '';
    const found = namespaceObjects.get(name);
    if (!found) {
        namespaceObjects.set(name, obj);
        if (addCallbacks) {
            addCallbacks.forEach((elt) => elt(obj));
        }
    }
    else {
        if (!isSameVersion(found, obj)) {
            namespaceObjects.set(name, obj);
            if (updateCallbacks) {
                updateCallbacks.forEach((elt) => elt(obj));
            }
        }
    }
}
function isSameVersion(o1, o2) {
    return (o1.metadata.resourceVersion !== undefined &&
        o1.metadata.resourceVersion !== null &&
        o1.metadata.resourceVersion === o2.metadata.resourceVersion);
}
// Public for testing.
export function deleteObject(objects, obj, deleteCallbacks) {
    const namespace = obj.metadata.namespace || '';
    const name = obj.metadata.name || '';
    const namespaceObjects = objects.get(namespace);
    if (!namespaceObjects) {
        return;
    }
    const deleted = namespaceObjects.delete(name);
    if (deleted) {
        if (deleteCallbacks) {
            deleteCallbacks.forEach((elt) => elt(obj));
        }
        if (namespaceObjects.size === 0) {
            objects.delete(namespace);
        }
    }
}
//# sourceMappingURL=cache.js.map