"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const analysis_1 = require("./analysis");
const immutable_1 = require("./immutable");
const resolvable_1 = require("./resolvable");
/**
 * The metadata for all features and elements defined in one document
 */
class ScannedDocument {
    constructor(document, features, warnings = []) {
        this.isInline = false;
        this.document = document;
        this.features = features;
        this.warnings = warnings;
        this.isInline = document.isInline;
    }
    get sourceRange() {
        return this.document.sourceRange;
    }
    get astNode() {
        return this.document.astNode;
    }
    get url() {
        return this.document.url;
    }
    /**
     * Gets all features in this scanned document and all inline documents it
     * contains.
     */
    getNestedFeatures() {
        const result = [];
        this._getNestedFeatures(result);
        return result;
    }
    _getNestedFeatures(features) {
        for (const feature of this.features) {
            // Ad hoc test needed here to avoid a problematic import loop.
            const maybeInlineDoc = feature;
            if (maybeInlineDoc.constructor.name === 'ScannedInlineDocument' &&
                maybeInlineDoc.scannedDocument) {
                maybeInlineDoc.scannedDocument._getNestedFeatures(features);
            }
            else {
                features.push(feature);
            }
        }
    }
}
exports.ScannedDocument = ScannedDocument;
class Document {
    constructor(base, analyzer, languageAnalysis) {
        this.kinds = new Set(['document']);
        this.identifiers = new Set();
        this._localFeatures = new Set();
        /**
         * To handle recursive dependency graphs we must track whether we've started
         * resolving this Document so that we can reliably early exit even if one
         * of our dependencies tries to resolve this document.
         */
        this._begunResolving = false;
        /**
         * True after this document and all of its children are finished resolving.
         */
        this._doneResolving = false;
        this._featuresByKind = null;
        this._featuresByKindAndId = null;
        if (base == null) {
            throw new Error('base is null');
        }
        if (analyzer == null) {
            throw new Error('analyzer is null');
        }
        this._scannedDocument = base;
        this._analysisContext = analyzer;
        this.languageAnalysis = languageAnalysis;
        if (!base.isInline) {
            immutable_1.unsafeAsMutable(this.identifiers).add(this.url);
        }
        immutable_1.unsafeAsMutable(this.kinds).add(`${this.parsedDocument.type}-document`);
        this.warnings = Array.from(base.warnings);
    }
    get url() {
        return this._scannedDocument.url;
    }
    get isInline() {
        return this._scannedDocument.isInline;
    }
    get sourceRange() {
        return this._scannedDocument.sourceRange;
    }
    get astNode() {
        return this._scannedDocument.astNode;
    }
    get parsedDocument() {
        return this._scannedDocument.document;
    }
    get resolved() {
        return this._doneResolving;
    }
    get type() {
        return this.parsedDocument.type;
    }
    /**
     * Resolves all features of this document, so that they have references to all
     * their dependencies.
     *
     * This method can only be called once
     */
    // TODO(justinfagnani): move to ScannedDocument
    resolve() {
        if (this._doneResolving) {
            throw new Error('resolve can only be called once');
        }
        if (this._begunResolving) {
            return;
        }
        this._begunResolving = true;
        this._addFeature(this);
        for (const scannedFeature of this._scannedDocument.features) {
            if (resolvable_1.isResolvable(scannedFeature)) {
                const feature = scannedFeature.resolve(this);
                if (feature) {
                    this._addFeature(feature);
                }
            }
        }
        this._doneResolving = true;
    }
    /**
     * Adds and indexes a feature to this documentled before resolve().
     */
    _addFeature(feature) {
        if (this._doneResolving) {
            throw new Error('_addFeature can not be called after _resolve()');
        }
        this._indexFeature(feature);
        this._localFeatures.add(feature);
    }
    getFeatures(query = {}) {
        if (query.id && query.kind) {
            return this._getById(query.kind, query.id, query);
        }
        else if (query.kind) {
            return this._getByKind(query.kind, query);
        }
        const features = new Set();
        this._listFeatures(features, new Set(), query);
        const queryId = query.id;
        if (queryId) {
            const filteredFeatures = Array.from(features).filter((f) => f.identifiers.has(queryId));
            return new Set(filteredFeatures);
        }
        return features;
    }
    _getByKind(kind, query = {}) {
        if (this._featuresByKind && this._isCachable(query)) {
            // We have a fast index! Use that.
            const features = this._featuresByKind.get(kind) || new Set();
            if (!query.externalPackages) {
                return this._filterOutExternal(features);
            }
            return features;
        }
        else if (this._doneResolving && this._isCachable(query)) {
            // We're done discovering features in this document and its children so
            // we can safely build up the indexes.
            this._buildIndexes();
            return this._getByKind(kind, query);
        }
        return this._getSlowlyByKind(kind, query);
    }
    _getById(kind, identifier, query = {}) {
        if (this._featuresByKindAndId && this._isCachable(query)) {
            // We have a fast index! Use that.
            const idMap = this._featuresByKindAndId.get(kind);
            const features = (idMap && idMap.get(identifier)) || new Set();
            if (!query.externalPackages) {
                return this._filterOutExternal(features);
            }
            return features;
        }
        else if (this._doneResolving && this._isCachable(query)) {
            // We're done discovering features in this document and its children so
            // we can safely build up the indexes.
            this._buildIndexes();
            return this._getById(kind, identifier, query);
        }
        const result = new Set();
        for (const featureOfKind of this._getByKind(kind, query)) {
            if (featureOfKind.identifiers.has(identifier)) {
                result.add(featureOfKind);
            }
        }
        return result;
    }
    _isCachable(query = {}) {
        return !!query.imported && !query.noLazyImports;
    }
    _getSlowlyByKind(kind, query) {
        const allFeatures = new Set();
        this._listFeatures(allFeatures, new Set(), query);
        const result = new Set();
        for (const feature of allFeatures) {
            if (feature.kinds.has(kind)) {
                result.add(feature);
            }
        }
        return result;
    }
    /**
     * Walks the graph of documents, starting from `this`, finding features which
     * match the given query and adding them to the `result` set. Uses `visited`
     * to deal with cycles.
     *
     * This method is O(numFeatures), though it does not walk documents that are
     * not relevant to the query (e.g. based on whether the query follows imports,
     * or excludes lazy imports)
     */
    _listFeatures(result, visited, query) {
        if (visited.has(this)) {
            return;
        }
        visited.add(this);
        for (const feature of this._localFeatures) {
            result.add(feature);
            if (feature.kinds.has('document')) {
                feature._listFeatures(result, visited, query);
            }
            if (feature.kinds.has('import') && query.imported) {
                const imprt = feature;
                const isPackageInternal = imprt.document && !analysis_1.Analysis.isExternal(imprt.document.url);
                const externalityOk = query.externalPackages || isPackageInternal;
                const lazinessOk = !query.noLazyImports || !imprt.lazy;
                if (externalityOk && lazinessOk) {
                    imprt.document._listFeatures(result, visited, query);
                }
            }
        }
    }
    _filterOutExternal(features) {
        const result = new Set();
        for (const feature of features) {
            if (feature.sourceRange &&
                analysis_1.Analysis.isExternal(feature.sourceRange.file)) {
                continue;
            }
            result.add(feature);
        }
        return result;
    }
    /**
     * Get warnings for the document and all matched features.
     */
    getWarnings(query = {}) {
        const warnings = new Set(this.warnings);
        for (const feature of this.getFeatures(query)) {
            for (const warning of feature.warnings) {
                warnings.add(warning);
            }
        }
        return Array.from(warnings);
    }
    toString() {
        return this._toString(new Set()).join('\n');
    }
    _toString(documentsWalked) {
        let result = [`<Document type=${this.parsedDocument.type} url=${this.url}>\n`];
        if (documentsWalked.has(this)) {
            return result;
        }
        documentsWalked.add(this);
        for (const localFeature of this._localFeatures) {
            if (localFeature instanceof Document) {
                result = result.concat(localFeature._toString(documentsWalked).map((line) => `  ${line}`));
            }
            else {
                let subResult = localFeature.toString();
                if (subResult === '[object Object]') {
                    subResult = `<${localFeature.constructor.name} kinds="${Array.from(localFeature.kinds).join(', ')}" ids="${Array.from(localFeature.identifiers).join(',')}">}`;
                }
                result.push(`  ${subResult}`);
            }
        }
        return result;
    }
    stringify() {
        const inlineDocuments = Array.from(this._localFeatures)
            .filter((f) => f instanceof Document && f.isInline)
            .map((d) => d.parsedDocument);
        return this.parsedDocument.stringify({ inlineDocuments: inlineDocuments });
    }
    _indexFeature(feature) {
        if (!this._featuresByKind || !this._featuresByKindAndId) {
            return;
        }
        for (const kind of feature.kinds) {
            const kindSet = this._featuresByKind.get(kind) || new Set();
            kindSet.add(feature);
            this._featuresByKind.set(kind, kindSet);
            for (const id of feature.identifiers) {
                const identifiersMap = this._featuresByKindAndId.get(kind) ||
                    new Map();
                this._featuresByKindAndId.set(kind, identifiersMap);
                const idSet = identifiersMap.get(id) || new Set();
                identifiersMap.set(id, idSet);
                idSet.add(feature);
            }
        }
    }
    _buildIndexes() {
        if (this._featuresByKind) {
            throw new Error('Tried to build indexes multiple times. This should never happen.');
        }
        if (!this._doneResolving) {
            throw new Error(`Tried to build indexes before finished resolving. ` +
                `Need to wait until afterwards or the indexes would be incomplete.`);
        }
        this._featuresByKind = new Map();
        this._featuresByKindAndId = new Map();
        for (const feature of this.getFeatures({ imported: true, externalPackages: true })) {
            this._indexFeature(feature);
        }
    }
}
exports.Document = Document;

//# sourceMappingURL=document.js.map
