import { loadModule } from '../load-module.js';
/**
 * Get TypeScript tools
 */
export function getTypeScriptTools(context) {
    const ts = getTypeScript(context);
    if (!ts) {
        return null;
    }
    const sourceCode = context.sourceCode;
    const { program, esTreeNodeToTSNodeMap, tsNodeToESTreeNodeMap } = sourceCode.parserServices;
    if (!program || !esTreeNodeToTSNodeMap || !tsNodeToESTreeNodeMap) {
        return null;
    }
    const hasFullTypeInformation = sourceCode.parserServices.hasFullTypeInformation ?? true;
    if (!hasFullTypeInformation) {
        // Full type information is required. User must specify parserOptions.project.
        return null;
    }
    return {
        service: {
            esTreeNodeToTSNodeMap,
            tsNodeToESTreeNodeMap,
            hasFullTypeInformation,
            program
        },
        ts
    };
}
let cacheTypeScript = null;
/**
 * Get TypeScript tools
 */
export function getTypeScript(context) {
    if (cacheTypeScript) {
        return cacheTypeScript;
    }
    cacheTypeScript = loadModule(context, 'typescript');
    if (cacheTypeScript) {
        return cacheTypeScript;
    }
    try {
        // eslint-disable-next-line @typescript-eslint/no-require-imports -- ignore
        cacheTypeScript ?? (cacheTypeScript = require('typescript'));
    }
    catch {
        // ignore
    }
    return cacheTypeScript;
}
/**
 * Check whether the given type is a truthy literal type or not.
 */
export function isTruthyLiteral(type, tsTools) {
    if (type.isUnion()) {
        return type.types.every((t) => isTruthyLiteral(t, tsTools));
    }
    return ((isBooleanLiteralType(type, tsTools.ts) &&
        tsTools.service.program.getTypeChecker().typeToString(type) === 'true') ||
        (type.isLiteral() && Boolean(type.value)));
}
/**
 * Check whether the given type is a falsy type or not.
 */
export function isFalsyType(type, tsTools) {
    if (type.isUnion()) {
        return type.types.every((t) => isFalsyType(t, tsTools));
    }
    if (isUndefinedType(type, tsTools.ts) ||
        isNullType(type, tsTools.ts) ||
        isVoidType(type, tsTools.ts))
        return true;
    if (type.isLiteral())
        return !type.value;
    return (isBooleanLiteralType(type, tsTools.ts) &&
        tsTools.service.program.getTypeChecker().typeToString(type) === 'false');
}
/**
 * Check whether the given type is a nullish type or not.
 */
export function isNullishType(type, ts) {
    if (type.isUnion()) {
        return type.types.every((t) => isNullishType(t, ts));
    }
    return isNullType(type, ts) || isUndefinedType(type, ts);
}
/**
 * Checks whether the given type is nullable or not.
 */
export function isNullableType(type, ts) {
    if (type.isUnion()) {
        return type.types.some((t) => isNullableType(t, ts));
    }
    return isNullType(type, ts) || isUndefinedType(type, ts);
}
/**
 * Check whether the given type is a boolean literal type or not.
 */
export function isBooleanLiteralType(type, ts) {
    return (type.flags & ts.TypeFlags.BooleanLiteral) !== 0;
}
/**
 * Check whether the given type is an object type or not.
 */
export function isObjectType(type, ts) {
    return (type.flags & ts.TypeFlags.Object) !== 0;
}
/**
 * Check whether the given type is a reference type or not.
 */
export function isReferenceObjectType(type, ts) {
    return isObjectType(type, ts) && (type.objectFlags & ts.ObjectFlags.Reference) !== 0;
}
/**
 * Check whether the given type is a tuple type or not.
 */
export function isTupleObjectType(type, ts) {
    return isObjectType(type, ts) && (type.objectFlags & ts.ObjectFlags.Tuple) !== 0;
}
/**
 * Check whether the given type is a tuple type or not.
 * Unlike isTupleObjectType, it also refers to reference types.
 */
export function isTupleType(type, ts) {
    return (isTupleObjectType(type, ts) ||
        (isReferenceObjectType(type, ts) && isTupleObjectType(type.target, ts)));
}
/**
 * Check whether the given type is an any type or not.
 */
export function isAnyType(type, ts) {
    return (type.flags & ts.TypeFlags.Any) !== 0;
}
/**
 * Check whether the given type is an unknown type or not.
 */
export function isUnknownType(type, ts) {
    return (type.flags & ts.TypeFlags.Unknown) !== 0;
}
/**
 * Check whether the given type is a never type or not.
 */
export function isNeverType(type, ts) {
    return (type.flags & ts.TypeFlags.Never) !== 0;
}
/**
 * Check whether the given type is an undefined type or not.
 */
export function isUndefinedType(type, ts) {
    return (type.flags & ts.TypeFlags.Undefined) !== 0;
}
/**
 * Check whether the given type is a void type or not.
 */
export function isVoidType(type, ts) {
    return (type.flags & ts.TypeFlags.Void) !== 0;
}
/**
 * Check whether the given type is a null type or not.
 */
export function isNullType(type, ts) {
    return (type.flags & ts.TypeFlags.Null) !== 0;
}
/**
 * Check whether the given type is a possibly falsy type or not.
 */
export function isPossiblyFalsyType(type, ts) {
    if (type.isUnion()) {
        return type.types.some((t) => isPossiblyFalsyType(t, ts));
    }
    return (type.flags & ts.TypeFlags.PossiblyFalsy) !== 0;
}
/**
 * Get the call signatures from the given type.
 * This method is heavily inspired by tsutils. https://github.com/ajafff/tsutils
 * The MIT License (MIT) Copyright (c) 2017 Klaus Meinhardt
 * https://github.com/ajafff/tsutils/blob/master/LICENSE
 */
export function getCallSignaturesOfType(type) {
    if (type.isUnion()) {
        return type.types.flatMap((t) => getCallSignaturesOfType(t));
    }
    if (type.isIntersection()) {
        let signatures = [];
        for (const t of type.types) {
            const sig = getCallSignaturesOfType(t);
            if (sig.length !== 0) {
                if (signatures.length) {
                    // if more than one type of the intersection has call signatures, none of them is useful for inference
                    return [];
                }
                signatures = sig;
            }
        }
        return signatures;
    }
    return type.getCallSignatures();
}
/**
 * Resolves the given node's type. Will resolve to the type's generic constraint, if it has one.
 * Copied this method from @typescript-eslint/type-utils. https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/type-utils
 * The MIT License (MIT) Copyright (c) 2021 TypeScript ESLint and other contributors
 * https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/type-utils/LICENSE
 */
export function getConstrainedTypeAtLocation(checker, node) {
    const nodeType = checker.getTypeAtLocation(node);
    const constrained = checker.getBaseConstraintOfType(nodeType);
    return constrained ?? nodeType;
}
/**
 * Get the type name of a given type.
 *
 * Copied this method from @typescript-eslint/type-utils. https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/type-utils
 * The MIT License (MIT) Copyright (c) 2021 TypeScript ESLint and other contributors
 * https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/type-utils/LICENSE
 *
 * @param type The type to get the name of.
 */
export function getTypeName(type, tsTools) {
    const { ts } = tsTools;
    // It handles `string` and string literal types as string.
    if ((type.flags & ts.TypeFlags.StringLike) !== 0) {
        return 'string';
    }
    const typeChecker = tsTools.service.program.getTypeChecker();
    // If the type is a type parameter which extends primitive string types,
    // but it was not recognized as a string like. So check the constraint
    // type of the type parameter.
    if ((type.flags & ts.TypeFlags.TypeParameter) !== 0) {
        // `type.getConstraint()` method doesn't return the constraint type of
        // the type parameter for some reason. So this gets the constraint type
        // via AST.
        const symbol = type.getSymbol();
        const decls = symbol?.getDeclarations();
        const typeParamDecl = decls?.[0];
        if (ts.isTypeParameterDeclaration(typeParamDecl) && typeParamDecl.constraint != null) {
            return getTypeName(typeChecker.getTypeFromTypeNode(typeParamDecl.constraint), tsTools);
        }
    }
    // If the type is a union and all types in the union are string like,
    // return `string`. For example:
    // - `"a" | "b"` is string.
    // - `string | string[]` is not string.
    if (type.isUnion() &&
        type.types.map((value) => getTypeName(value, tsTools)).every((t) => t === 'string')) {
        return 'string';
    }
    // If the type is an intersection and a type in the intersection is string
    // like, return `string`. For example: `string & {__htmlEscaped: void}`
    if (type.isIntersection() &&
        type.types.map((value) => getTypeName(value, tsTools)).some((t) => t === 'string')) {
        return 'string';
    }
    return typeChecker.typeToString(type);
}
/**
 * Return the type of the given property in the given type, or undefined if no such property exists
 */
export function getTypeOfPropertyOfType(type, name, checker) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- getTypeOfPropertyOfType is an internal API of TS.
    return checker.getTypeOfPropertyOfType(type, name);
}
/**
 * Check whether the given symbol is a method type or not.
 */
export function isMethodSymbol(type, ts) {
    return (type.getFlags() & ts.SymbolFlags.Method) !== 0;
}
/**
 * Check whether the given node is a property signature kind or not.
 */
export function isPropertySignatureKind(node, ts) {
    return node.kind === ts.SyntaxKind.PropertySignature;
}
/**
 * Check whether the given node is a function type kind or not.
 */
export function isFunctionTypeKind(node, ts) {
    return node.kind === ts.SyntaxKind.FunctionType;
}
/**
 * Check whether the given node is a method signature kind or not.
 */
export function isMethodSignatureKind(node, ts) {
    return node.kind === ts.SyntaxKind.MethodSignature;
}
/**
 * Check whether the given node is a type reference kind or not.
 */
export function isTypeReferenceKind(node, ts) {
    return node.kind === ts.SyntaxKind.TypeReference;
}
/**
 * Check whether the given node is an identifier kind or not.
 */
export function isIdentifierKind(node, ts) {
    return node.kind === ts.SyntaxKind.Identifier;
}
