/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
import { Emitter } from '../../../base/common/event.js';
import { doHash } from '../../../base/common/hash.js';
import { toDisposable } from '../../../base/common/lifecycle.js';
import { LRUCache } from '../../../base/common/map.js';
import { MovingAverage } from '../../../base/common/numbers.js';
import { score } from './languageSelector.js';
import { shouldSynchronizeModel } from '../services/model.js';
function isExclusive(selector) {
    if (typeof selector === 'string') {
        return false;
    }
    else if (Array.isArray(selector)) {
        return selector.every(isExclusive);
    }
    else {
        return !!selector.exclusive; // TODO: microsoft/TypeScript#42768
    }
}
export class LanguageFeatureRegistry {
    constructor() {
        this._clock = 0;
        this._entries = [];
        this._onDidChange = new Emitter();
    }
    get onDidChange() {
        return this._onDidChange.event;
    }
    register(selector, provider) {
        let entry = {
            selector,
            provider,
            _score: -1,
            _time: this._clock++
        };
        this._entries.push(entry);
        this._lastCandidate = undefined;
        this._onDidChange.fire(this._entries.length);
        return toDisposable(() => {
            if (entry) {
                const idx = this._entries.indexOf(entry);
                if (idx >= 0) {
                    this._entries.splice(idx, 1);
                    this._lastCandidate = undefined;
                    this._onDidChange.fire(this._entries.length);
                    entry = undefined;
                }
            }
        });
    }
    has(model) {
        return this.all(model).length > 0;
    }
    all(model) {
        if (!model) {
            return [];
        }
        this._updateScores(model);
        const result = [];
        // from registry
        for (let entry of this._entries) {
            if (entry._score > 0) {
                result.push(entry.provider);
            }
        }
        return result;
    }
    ordered(model) {
        const result = [];
        this._orderedForEach(model, entry => result.push(entry.provider));
        return result;
    }
    orderedGroups(model) {
        const result = [];
        let lastBucket;
        let lastBucketScore;
        this._orderedForEach(model, entry => {
            if (lastBucket && lastBucketScore === entry._score) {
                lastBucket.push(entry.provider);
            }
            else {
                lastBucketScore = entry._score;
                lastBucket = [entry.provider];
                result.push(lastBucket);
            }
        });
        return result;
    }
    _orderedForEach(model, callback) {
        if (!model) {
            return;
        }
        this._updateScores(model);
        for (const entry of this._entries) {
            if (entry._score > 0) {
                callback(entry);
            }
        }
    }
    _updateScores(model) {
        const candidate = {
            uri: model.uri.toString(),
            language: model.getLanguageId()
        };
        if (this._lastCandidate
            && this._lastCandidate.language === candidate.language
            && this._lastCandidate.uri === candidate.uri) {
            // nothing has changed
            return;
        }
        this._lastCandidate = candidate;
        for (let entry of this._entries) {
            entry._score = score(entry.selector, model.uri, model.getLanguageId(), shouldSynchronizeModel(model));
            if (isExclusive(entry.selector) && entry._score > 0) {
                // support for one exclusive selector that overwrites
                // any other selector
                for (let entry of this._entries) {
                    entry._score = 0;
                }
                entry._score = 1000;
                break;
            }
        }
        // needs sorting
        this._entries.sort(LanguageFeatureRegistry._compareByScoreAndTime);
    }
    static _compareByScoreAndTime(a, b) {
        if (a._score < b._score) {
            return 1;
        }
        else if (a._score > b._score) {
            return -1;
        }
        else if (a._time < b._time) {
            return 1;
        }
        else if (a._time > b._time) {
            return -1;
        }
        else {
            return 0;
        }
    }
}
const _hashes = new WeakMap();
let pool = 0;
function weakHash(obj) {
    let value = _hashes.get(obj);
    if (value === undefined) {
        value = ++pool;
        _hashes.set(obj, value);
    }
    return value;
}
/**
 * Keeps moving average per model and set of providers so that requests
 * can be debounce according to the provider performance
 */
export class LanguageFeatureRequestDelays {
    constructor(_registry, min, max = Number.MAX_SAFE_INTEGER) {
        this._registry = _registry;
        this.min = min;
        this.max = max;
        this._cache = new LRUCache(50, 0.7);
    }
    _key(model) {
        return model.id + this._registry.all(model).reduce((hashVal, obj) => doHash(weakHash(obj), hashVal), 0);
    }
    _clamp(value) {
        if (value === undefined) {
            return this.min;
        }
        else {
            return Math.min(this.max, Math.max(this.min, Math.floor(value * 1.3)));
        }
    }
    get(model) {
        const key = this._key(model);
        const avg = this._cache.get(key);
        return this._clamp(avg === null || avg === void 0 ? void 0 : avg.value);
    }
    update(model, value) {
        const key = this._key(model);
        let avg = this._cache.get(key);
        if (!avg) {
            avg = new MovingAverage();
            this._cache.set(key, avg);
        }
        avg.update(value);
        return this.get(model);
    }
}
