"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ImplementsPlugin = void 0;
const ts = require("typescript");
const index_1 = require("../../models/reflections/index");
const types_1 = require("../../models/types");
const array_1 = require("../../utils/array");
const components_1 = require("../components");
const converter_1 = require("../converter");
const reflections_1 = require("../utils/reflections");
/**
 * A plugin that detects interface implementations of functions and
 * properties on classes and links them.
 */
let ImplementsPlugin = class ImplementsPlugin extends components_1.ConverterComponent {
    constructor() {
        super(...arguments);
        this.resolved = new WeakSet();
        this.postponed = new WeakMap();
    }
    /**
     * Create a new ImplementsPlugin instance.
     */
    initialize() {
        this.listenTo(this.owner, converter_1.Converter.EVENT_RESOLVE, this.onResolve, -10);
        this.listenTo(this.owner, converter_1.Converter.EVENT_CREATE_DECLARATION, this.onDeclaration, -1000);
        this.listenTo(this.owner, converter_1.Converter.EVENT_CREATE_SIGNATURE, this.onSignature, 1000);
    }
    /**
     * Mark all members of the given class to be the implementation of the matching interface member.
     *
     * @param context  The context object describing the current state the converter is in.
     * @param classReflection  The reflection of the classReflection class.
     * @param interfaceReflection  The reflection of the interfaceReflection interface.
     */
    analyzeClass(context, classReflection, interfaceReflection) {
        if (!interfaceReflection.children) {
            return;
        }
        interfaceReflection.children.forEach((interfaceMember) => {
            let classMember;
            if (!classReflection.children) {
                return;
            }
            for (let index = 0, count = classReflection.children.length; index < count; index++) {
                const child = classReflection.children[index];
                if (child.name !== interfaceMember.name) {
                    continue;
                }
                if (child.flags.isStatic !== interfaceMember.flags.isStatic) {
                    continue;
                }
                classMember = child;
                break;
            }
            if (!classMember) {
                return;
            }
            const interfaceMemberName = interfaceReflection.name + "." + interfaceMember.name;
            classMember.implementationOf =
                types_1.ReferenceType.createResolvedReference(interfaceMemberName, interfaceMember, context.project);
            (0, reflections_1.copyComment)(classMember, interfaceMember);
            if (interfaceMember.kindOf(index_1.ReflectionKind.Property) &&
                classMember.kindOf(index_1.ReflectionKind.Accessor)) {
                if (classMember.getSignature) {
                    (0, reflections_1.copyComment)(classMember.getSignature, interfaceMember);
                    classMember.getSignature.implementationOf =
                        classMember.implementationOf;
                }
                if (classMember.setSignature) {
                    (0, reflections_1.copyComment)(classMember.setSignature, interfaceMember);
                    classMember.setSignature.implementationOf =
                        classMember.implementationOf;
                }
            }
            if (interfaceMember.kindOf(index_1.ReflectionKind.FunctionOrMethod) &&
                interfaceMember.signatures &&
                classMember.signatures) {
                for (const [clsSig, intSig] of (0, array_1.zip)(classMember.signatures, interfaceMember.signatures)) {
                    if (clsSig.implementationOf) {
                        clsSig.implementationOf =
                            types_1.ReferenceType.createResolvedReference(clsSig.implementationOf.name, intSig, context.project);
                    }
                    (0, reflections_1.copyComment)(clsSig, intSig);
                }
            }
        });
    }
    analyzeInheritance(context, reflection) {
        var _a, _b, _c, _d, _e;
        const extendedTypes = (0, array_1.filterMap)((_a = reflection.extendedTypes) !== null && _a !== void 0 ? _a : [], (type) => {
            return type instanceof types_1.ReferenceType &&
                type.reflection instanceof index_1.DeclarationReflection
                ? type
                : void 0;
        });
        for (const parent of extendedTypes) {
            for (const parentMember of (_b = parent.reflection.children) !== null && _b !== void 0 ? _b : []) {
                const child = (_c = reflection.children) === null || _c === void 0 ? void 0 : _c.find((child) => child.name == parentMember.name &&
                    child.flags.isStatic === parentMember.flags.isStatic);
                if (child) {
                    const key = child.overwrites
                        ? "overwrites"
                        : "inheritedFrom";
                    for (const [childSig, parentSig] of (0, array_1.zip)((_d = child.signatures) !== null && _d !== void 0 ? _d : [], (_e = parentMember.signatures) !== null && _e !== void 0 ? _e : [])) {
                        childSig[key] = types_1.ReferenceType.createResolvedReference(`${parent.name}.${parentMember.name}`, parentSig, context.project);
                        (0, reflections_1.copyComment)(childSig, parentSig);
                    }
                    child[key] = types_1.ReferenceType.createResolvedReference(`${parent.name}.${parentMember.name}`, parentMember, context.project);
                    (0, reflections_1.copyComment)(child, parentMember);
                }
            }
        }
    }
    /**
     * Triggered when the converter resolves a reflection.
     *
     * @param context  The context object describing the current state the converter is in.
     * @param reflection  The reflection that is currently resolved.
     */
    onResolve(context, reflection) {
        this.tryResolve(context, reflection);
    }
    tryResolve(context, reflection) {
        var _a, _b, _c, _d;
        const requirements = (0, array_1.filterMap)([
            ...((_a = reflection.implementedTypes) !== null && _a !== void 0 ? _a : []),
            ...((_b = reflection.extendedTypes) !== null && _b !== void 0 ? _b : []),
        ], (type) => {
            return type instanceof types_1.ReferenceType ? type.reflection : void 0;
        });
        if (requirements.every((req) => this.resolved.has(req))) {
            this.doResolve(context, reflection);
            this.resolved.add(reflection);
            for (const refl of (_c = this.postponed.get(reflection)) !== null && _c !== void 0 ? _c : []) {
                this.tryResolve(context, refl);
            }
            this.postponed.delete(reflection);
        }
        else {
            for (const req of requirements) {
                const future = (_d = this.postponed.get(req)) !== null && _d !== void 0 ? _d : new Set();
                future.add(reflection);
                this.postponed.set(req, future);
            }
        }
    }
    doResolve(context, reflection) {
        if (reflection.kindOf(index_1.ReflectionKind.Class) &&
            reflection.implementedTypes) {
            reflection.implementedTypes.forEach((type) => {
                if (!(type instanceof types_1.ReferenceType)) {
                    return;
                }
                if (type.reflection &&
                    type.reflection.kindOf(index_1.ReflectionKind.Interface)) {
                    this.analyzeClass(context, reflection, type.reflection);
                }
            });
        }
        if (reflection.kindOf([
            index_1.ReflectionKind.Class,
            index_1.ReflectionKind.Interface,
        ]) &&
            reflection.extendedTypes) {
            this.analyzeInheritance(context, reflection);
        }
    }
    getExtensionInfo(context, reflection) {
        var _a;
        if (!reflection || !reflection.kindOf(index_1.ReflectionKind.Inheritable)) {
            return;
        }
        // Need this because we re-use reflections for type literals.
        if (!reflection.parent ||
            !reflection.parent.kindOf(index_1.ReflectionKind.ClassOrInterface)) {
            return;
        }
        const symbol = context.project.getSymbolFromReflection(reflection.parent);
        if (!symbol) {
            return;
        }
        const declaration = (_a = symbol
            .getDeclarations()) === null || _a === void 0 ? void 0 : _a.find((n) => ts.isClassDeclaration(n) || ts.isInterfaceDeclaration(n));
        if (!declaration) {
            return;
        }
        return { symbol, declaration };
    }
    onSignature(context, reflection) {
        this.onDeclaration(context, reflection.parent);
    }
    /**
     * Responsible for setting the {@link DeclarationReflection.inheritedFrom},
     * {@link DeclarationReflection.overwrites}, and {@link DeclarationReflection.implementationOf}
     * properties on the provided reflection temporarily, these links will be replaced
     * during the resolve step with links which actually point to the right place.
     */
    onDeclaration(context, reflection) {
        var _a, _b, _c, _d, _e;
        const info = this.getExtensionInfo(context, reflection);
        if (!info) {
            return;
        }
        if (reflection.kind === index_1.ReflectionKind.Constructor) {
            const ctor = info.declaration.members.find(ts.isConstructorDeclaration);
            constructorInheritance(context, reflection, info.declaration, ctor);
            return;
        }
        const childType = reflection.flags.isStatic
            ? context.checker.getTypeOfSymbolAtLocation(info.symbol, info.declaration)
            : context.checker.getDeclaredTypeOfSymbol(info.symbol);
        const property = findProperty(reflection, childType);
        if (!property) {
            // We're probably broken... but I don't think this should be fatal.
            context.logger.warn(`Failed to retrieve${reflection.flags.isStatic ? " static" : ""} member "${(_a = reflection.escapedName) !== null && _a !== void 0 ? _a : reflection.name}" of "${(_b = reflection.parent) === null || _b === void 0 ? void 0 : _b.name}" for inheritance analysis. Please report a bug.`);
            return;
        }
        // Need to check both extends and implements clauses.
        out: for (const clause of (_c = info.declaration.heritageClauses) !== null && _c !== void 0 ? _c : []) {
            // No point checking implemented types for static members, they won't exist.
            if (reflection.flags.isStatic &&
                clause.token === ts.SyntaxKind.ImplementsKeyword) {
                continue;
            }
            for (const expr of clause.types) {
                const parentType = context.checker.getTypeAtLocation(reflection.flags.isStatic ? expr.expression : expr);
                const parentProperty = findProperty(reflection, parentType);
                if (parentProperty) {
                    const isInherit = (_e = (_d = property
                        .getDeclarations()) === null || _d === void 0 ? void 0 : _d.some((d) => d.parent !== info.declaration)) !== null && _e !== void 0 ? _e : true;
                    createLink(context, reflection, clause, expr, parentProperty, isInherit);
                    // Can't always break because we need to also set `implementationOf` if we
                    // inherit from a base class and also implement an interface.
                    if (clause.token === ts.SyntaxKind.ImplementsKeyword) {
                        break out;
                    }
                }
            }
        }
    }
};
ImplementsPlugin = __decorate([
    (0, components_1.Component)({ name: "implements" })
], ImplementsPlugin);
exports.ImplementsPlugin = ImplementsPlugin;
function constructorInheritance(context, reflection, childDecl, constructorDecl) {
    var _a, _b, _c, _d;
    const extendsClause = (_a = childDecl.heritageClauses) === null || _a === void 0 ? void 0 : _a.find((cl) => cl.token === ts.SyntaxKind.ExtendsKeyword);
    if (!extendsClause)
        return;
    const name = `${extendsClause.types[0].getText()}.constructor`;
    const key = constructorDecl ? "overwrites" : "inheritedFrom";
    (_b = reflection[key]) !== null && _b !== void 0 ? _b : (reflection[key] = types_1.ReferenceType.createBrokenReference(name, context.project));
    for (const sig of (_c = reflection.signatures) !== null && _c !== void 0 ? _c : []) {
        (_d = sig[key]) !== null && _d !== void 0 ? _d : (sig[key] = types_1.ReferenceType.createBrokenReference(name, context.project));
    }
}
function findProperty(reflection, parent) {
    return parent.getProperties().find((prop) => {
        return reflection.escapedName
            ? prop.escapedName === reflection.escapedName
            : prop.name === reflection.escapedName;
    });
}
function createLink(context, reflection, clause, expr, symbol, isOverwrite) {
    var _a;
    const project = context.project;
    const name = `${expr.expression.getText()}.${symbol.name}`;
    link(reflection);
    link(reflection.getSignature);
    link(reflection.setSignature);
    link(reflection.indexSignature);
    for (const sig of (_a = reflection.signatures) !== null && _a !== void 0 ? _a : []) {
        link(sig);
    }
    // Intentionally create broken links here. These will be replaced with real links during
    // resolution if we can do so.
    function link(target) {
        var _a, _b, _c;
        if (!target)
            return;
        if (clause.token === ts.SyntaxKind.ImplementsKeyword) {
            (_a = target.implementationOf) !== null && _a !== void 0 ? _a : (target.implementationOf = types_1.ReferenceType.createBrokenReference(name, project));
            return;
        }
        if (isOverwrite) {
            (_b = target.inheritedFrom) !== null && _b !== void 0 ? _b : (target.inheritedFrom = types_1.ReferenceType.createBrokenReference(name, project));
        }
        else {
            (_c = target.overwrites) !== null && _c !== void 0 ? _c : (target.overwrites = types_1.ReferenceType.createBrokenReference(name, project));
        }
    }
}
