/** @internal */
namespace ts {
    export function createGetSymbolWalker(
        getRestTypeOfSignature: (sig: Signature) => Type,
        getTypePredicateOfSignature: (sig: Signature) => TypePredicate | undefined,
        getReturnTypeOfSignature: (sig: Signature) => Type,
        getBaseTypes: (type: Type) => Type[],
        resolveStructuredTypeMembers: (type: ObjectType) => ResolvedType,
        getTypeOfSymbol: (sym: Symbol) => Type,
        getResolvedSymbol: (node: Node) => Symbol,
        getIndexTypeOfStructuredType: (type: Type, kind: IndexKind) => Type | undefined,
        getConstraintOfTypeParameter: (typeParameter: TypeParameter) => Type | undefined,
        getFirstIdentifier: (node: EntityNameOrEntityNameExpression) => Identifier,
        getTypeArguments: (type: TypeReference) => readonly Type[]) {

        return getSymbolWalker;

        function getSymbolWalker(accept: (symbol: Symbol) => boolean = () => true): SymbolWalker {
            const visitedTypes: Type[] = []; // Sparse array from id to type
            const visitedSymbols: Symbol[] = []; // Sparse array from id to symbol

            return {
                walkType: type => {
                    try {
                        visitType(type);
                        return { visitedTypes: getOwnValues(visitedTypes), visitedSymbols: getOwnValues(visitedSymbols) };
                    }
                    finally {
                        clear(visitedTypes);
                        clear(visitedSymbols);
                    }
                },
                walkSymbol: symbol => {
                    try {
                        visitSymbol(symbol);
                        return { visitedTypes: getOwnValues(visitedTypes), visitedSymbols: getOwnValues(visitedSymbols) };
                    }
                    finally {
                        clear(visitedTypes);
                        clear(visitedSymbols);
                    }
                },
            };

            function visitType(type: Type | undefined): void {
                if (!type) {
                    return;
                }

                if (visitedTypes[type.id]) {
                    return;
                }
                visitedTypes[type.id] = type;

                // Reuse visitSymbol to visit the type's symbol,
                //  but be sure to bail on recuring into the type if accept declines the symbol.
                const shouldBail = visitSymbol(type.symbol);
                if (shouldBail) return;

                // Visit the type's related types, if any
                if (type.flags & TypeFlags.Object) {
                    const objectType = type as ObjectType;
                    const objectFlags = objectType.objectFlags;
                    if (objectFlags & ObjectFlags.Reference) {
                        visitTypeReference(type as TypeReference);
                    }
                    if (objectFlags & ObjectFlags.Mapped) {
                        visitMappedType(type as MappedType);
                    }
                    if (objectFlags & (ObjectFlags.Class | ObjectFlags.Interface)) {
                        visitInterfaceType(type as InterfaceType);
                    }
                    if (objectFlags & (ObjectFlags.Tuple | ObjectFlags.Anonymous)) {
                        visitObjectType(objectType);
                    }
                }
                if (type.flags & TypeFlags.TypeParameter) {
                    visitTypeParameter(type as TypeParameter);
                }
                if (type.flags & TypeFlags.UnionOrIntersection) {
                    visitUnionOrIntersectionType(type as UnionOrIntersectionType);
                }
                if (type.flags & TypeFlags.Index) {
                    visitIndexType(type as IndexType);
                }
                if (type.flags & TypeFlags.IndexedAccess) {
                    visitIndexedAccessType(type as IndexedAccessType);
                }
            }

            function visitTypeReference(type: TypeReference): void {
                visitType(type.target);
                forEach(getTypeArguments(type), visitType);
            }

            function visitTypeParameter(type: TypeParameter): void {
                visitType(getConstraintOfTypeParameter(type));
            }

            function visitUnionOrIntersectionType(type: UnionOrIntersectionType): void {
                forEach(type.types, visitType);
            }

            function visitIndexType(type: IndexType): void {
                visitType(type.type);
            }

            function visitIndexedAccessType(type: IndexedAccessType): void {
                visitType(type.objectType);
                visitType(type.indexType);
                visitType(type.constraint);
            }

            function visitMappedType(type: MappedType): void {
                visitType(type.typeParameter);
                visitType(type.constraintType);
                visitType(type.templateType);
                visitType(type.modifiersType);
            }

            function visitSignature(signature: Signature): void {
                const typePredicate = getTypePredicateOfSignature(signature);
                if (typePredicate) {
                    visitType(typePredicate.type);
                }
                forEach(signature.typeParameters, visitType);

                for (const parameter of signature.parameters) {
                    visitSymbol(parameter);
                }
                visitType(getRestTypeOfSignature(signature));
                visitType(getReturnTypeOfSignature(signature));
            }

            function visitInterfaceType(interfaceT: InterfaceType): void {
                visitObjectType(interfaceT);
                forEach(interfaceT.typeParameters, visitType);
                forEach(getBaseTypes(interfaceT), visitType);
                visitType(interfaceT.thisType);
            }

            function visitObjectType(type: ObjectType): void {
                const stringIndexType = getIndexTypeOfStructuredType(type, IndexKind.String);
                visitType(stringIndexType);
                const numberIndexType = getIndexTypeOfStructuredType(type, IndexKind.Number);
                visitType(numberIndexType);

                // The two checks above *should* have already resolved the type (if needed), so this should be cached
                const resolved = resolveStructuredTypeMembers(type);
                for (const signature of resolved.callSignatures) {
                    visitSignature(signature);
                }
                for (const signature of resolved.constructSignatures) {
                    visitSignature(signature);
                }
                for (const p of resolved.properties) {
                    visitSymbol(p);
                }
            }

            function visitSymbol(symbol: Symbol | undefined): boolean {
                if (!symbol) {
                    return false;
                }
                const symbolId = getSymbolId(symbol);
                if (visitedSymbols[symbolId]) {
                    return false;
                }
                visitedSymbols[symbolId] = symbol;
                if (!accept(symbol)) {
                    return true;
                }
                const t = getTypeOfSymbol(symbol);
                visitType(t); // Should handle members on classes and such
                if (symbol.exports) {
                    symbol.exports.forEach(visitSymbol);
                }
                forEach(symbol.declarations, d => {
                    // Type queries are too far resolved when we just visit the symbol's type
                    //  (their type resolved directly to the member deeply referenced)
                    // So to get the intervening symbols, we need to check if there's a type
                    // query node on any of the symbol's declarations and get symbols there
                    if ((d as any).type && (d as any).type.kind === SyntaxKind.TypeQuery) {
                        const query = (d as any).type as TypeQueryNode;
                        const entity = getResolvedSymbol(getFirstIdentifier(query.exprName));
                        visitSymbol(entity);
                    }
                });
                return false;
            }
        }
    }
}