import * as t from "@babel/types";
export import Node = t.Node;
export import RemovePropertiesOptions = t.RemovePropertiesOptions;

declare const traverse: {
    <S>(parent: Node, opts: TraverseOptions<S>, scope: Scope | undefined, state: S, parentPath?: NodePath): void;
    (parent: Node, opts?: TraverseOptions, scope?: Scope, state?: any, parentPath?: NodePath): void;

    visitors: typeof visitors;
    verify: typeof visitors.verify;
    explode: typeof visitors.explode;

    cheap: (node: Node, enter: (node: Node) => void) => void;
    node: (
        node: Node,
        opts: TraverseOptions,
        scope?: Scope,
        state?: any,
        path?: NodePath,
        skipKeys?: Record<string, boolean>,
    ) => void;
    clearNode: (node: Node, opts?: RemovePropertiesOptions) => void;
    removeProperties: (tree: Node, opts?: RemovePropertiesOptions) => Node;
    hasType: (tree: Node, type: Node["type"], denylistTypes?: string[]) => boolean;

    cache: typeof cache;
};

export namespace visitors {
    /**
     * `explode()` will take a `Visitor` object with all of the various shorthands
     * that we support, and validates & normalizes it into a common format, ready
     * to be used in traversal.
     *
     * The various shorthands are:
     * - `Identifier() { ... }` -> `Identifier: { enter() { ... } }`
     * - `"Identifier|NumericLiteral": { ... }` -> `Identifier: { ... }, NumericLiteral: { ... }`
     * - Aliases in `@babel/types`: e.g. `Property: { ... }` -> `ObjectProperty: { ... }, ClassProperty: { ... }`
     *
     * Other normalizations are:
     * - Visitors of virtual types are wrapped, so that they are only visited when their dynamic check passes
     * - `enter` and `exit` functions are wrapped in arrays, to ease merging of visitors
     */
    function explode<S = unknown>(
        visitor: Visitor<S>,
    ): {
        [Type in Exclude<Node, t.DeprecatedAliases>["type"]]?: VisitNodeObject<S, Extract<Node, { type: Type }>>;
    };
    function verify(visitor: Visitor): void;
    function merge<State>(visitors: Array<Visitor<State>>): Visitor<State>;
    function merge(
        visitors: Visitor[],
        states?: any[],
        wrapper?: (
            stateKey: any,
            visitorKey: keyof Visitor,
            func: VisitNodeFunction<unknown, Node>,
        ) => VisitNodeFunction<unknown, Node> | null,
    ): Visitor;
}

export namespace cache {
    let path: WeakMap<t.Node, Map<t.Node, NodePath>>;
    let scope: WeakMap<t.Node, Scope>;
    function clear(): void;
    function clearPath(): void;
    function clearScope(): void;
}

export default traverse;

export type TraverseOptions<S = Node> = {
    scope?: Scope;
    noScope?: boolean;
    denylist?: NodeType[];
    /** @deprecated will be removed in Babel 8 */
    blacklist?: NodeType[];
    shouldSkip?: (node: NodePath) => boolean;
} & Visitor<S>;

export class Scope {
    /**
     * This searches the current "scope" and collects all references/bindings
     * within.
     */
    constructor(path: NodePath, parentScope?: Scope);
    uid: number;
    path: NodePath;
    block: Node;
    labels: Map<string, NodePath<t.LabeledStatement>>;
    parentBlock: Node;
    parent: Scope;
    hub: HubInterface;
    bindings: { [name: string]: Binding };
    references: { [name: string]: true };
    globals: { [name: string]: t.Identifier | t.JSXIdentifier };
    uids: { [name: string]: boolean };
    data: Record<string | symbol, unknown>;
    crawling: boolean;

    static globals: string[];
    /** Variables available in current context. */
    static contextVariables: string[];

    /** Traverse node with current scope and path. */
    traverse<S>(node: Node | Node[], opts: TraverseOptions<S>, state: S): void;
    traverse(node: Node | Node[], opts?: TraverseOptions, state?: any): void;

    /** Generate a unique identifier and add it to the current scope. */
    generateDeclaredUidIdentifier(name?: string): t.Identifier;

    /** Generate a unique identifier. */
    generateUidIdentifier(name?: string): t.Identifier;

    /** Generate a unique `_id1` binding. */
    generateUid(name?: string): string;

    /** Generate a unique identifier based on a node. */
    generateUidIdentifierBasedOnNode(parent: Node, defaultName?: string): t.Identifier;

    /**
     * Determine whether evaluating the specific input `node` is a consequenceless reference. ie.
     * evaluating it wont result in potentially arbitrary code from being ran. The following are
     * whitelisted and determined not to cause side effects:
     *
     *  - `this` expressions
     *  - `super` expressions
     *  - Bound identifiers
     */
    isStatic(node: Node): boolean;

    /** Possibly generate a memoised identifier if it is not static and has consequences. */
    maybeGenerateMemoised(node: Node, dontPush?: boolean): t.Identifier;

    checkBlockScopedCollisions(local: Binding, kind: BindingKind, name: string, id: object): void;

    rename(oldName: string, newName?: string, block?: Node): void;

    dump(): void;

    toArray(
        node: t.Node,
        i?: number | boolean,
        arrayLikeIsIterable?: boolean,
    ): t.ArrayExpression | t.CallExpression | t.Identifier;

    hasLabel(name: string): boolean;

    getLabel(name: string): NodePath<t.LabeledStatement> | undefined;

    registerLabel(path: NodePath<t.LabeledStatement>): void;

    registerDeclaration(path: NodePath): void;

    buildUndefinedNode(): t.UnaryExpression;

    registerConstantViolation(path: NodePath): void;

    registerBinding(kind: BindingKind, path: NodePath, bindingPath?: NodePath): void;

    addGlobal(node: t.Identifier | t.JSXIdentifier): void;

    hasUid(name: string): boolean;

    hasGlobal(name: string): boolean;

    hasReference(name: string): boolean;

    isPure(node: Node, constantsOnly?: boolean): boolean;

    /**
     * Set some arbitrary data on the current scope.
     */
    setData(key: string, val: any): any;

    /**
     * Recursively walk up scope tree looking for the data `key`.
     */
    getData(key: string): any;

    /**
     * Recursively walk up scope tree looking for the data `key` and if it exists,
     * remove it.
     */
    removeData(key: string): void;

    crawl(): void;

    push(opts: {
        id: t.LVal;
        init?: t.Expression;
        unique?: boolean;
        _blockHoist?: number | undefined;
        kind?: "var" | "let" | "const";
    }): void;

    /** Walk up to the top of the scope tree and get the `Program`. */
    getProgramParent(): Scope;

    /** Walk up the scope tree until we hit either a Function or return null. */
    getFunctionParent(): Scope | null;

    /**
     * Walk up the scope tree until we hit either a BlockStatement/Loop/Program/Function/Switch or reach the
     * very top and hit Program.
     */
    getBlockParent(): Scope;

    /**
     * Walk up from a pattern scope (function param initializer) until we hit a non-pattern scope,
     * then returns its block parent
     * @returns An ancestry scope whose path is a block parent
     */
    getPatternParent(): Scope;

    /** Walks the scope tree and gathers **all** bindings. */
    getAllBindings(): Record<string, Binding>;

    /** Walks the scope tree and gathers all declarations of `kind`. */
    getAllBindingsOfKind(...kinds: string[]): Record<string, Binding>;

    bindingIdentifierEquals(name: string, node: Node): boolean;

    getBinding(name: string): Binding | undefined;

    getOwnBinding(name: string): Binding | undefined;

    getBindingIdentifier(name: string): t.Identifier;

    getOwnBindingIdentifier(name: string): t.Identifier;

    hasOwnBinding(name: string): boolean;

    hasBinding(
        name: string,
        optsOrNoGlobals?:
            | boolean
            | {
                noGlobals?: boolean;
                noUids?: boolean;
            },
    ): boolean;

    parentHasBinding(
        name: string,
        opts?: {
            noGlobals?: boolean;
            noUids?: boolean;
        },
    ): boolean;

    /** Move a binding of `name` to another `scope`. */
    moveBindingTo(name: string, scope: Scope): void;

    removeOwnBinding(name: string): void;

    removeBinding(name: string): void;
}

export type BindingKind = "var" | "let" | "const" | "module" | "hoisted" | "param" | "local" | "unknown";

/**
 * This class is responsible for a binding inside of a scope.
 *
 * It tracks the following:
 *
 *  * Node path.
 *  * Amount of times referenced by other nodes.
 *  * Paths to nodes that reassign or modify this binding.
 *  * The kind of binding. (Is it a parameter, declaration etc)
 */
export class Binding {
    constructor(opts: { identifier: t.Identifier; scope: Scope; path: NodePath; kind: BindingKind });
    identifier: t.Identifier;
    scope: Scope;
    path: NodePath;
    kind: BindingKind;
    referenced: boolean;
    references: number;
    referencePaths: NodePath[];
    constant: boolean;
    constantViolations: NodePath[];
    hasDeoptedValue: boolean;
    hasValue: boolean;
    value: any;

    deopValue(): void;
    setValue(value: any): void;
    clearValue(): void;

    /** Register a constant violation with the provided `path`. */
    reassign(path: NodePath): void;
    /** Increment the amount of references to this binding. */
    reference(path: NodePath): void;
    /** Decrement the amount of references to this binding. */
    dereference(): void;
}

export type Visitor<S = unknown> =
    & VisitNodeObject<S, Node>
    & {
        [N in Node as N["type"]]?: VisitNode<S, N extends { type: N["type"] } ? N : never>;
    }
    & {
        [K in keyof t.Aliases]?: VisitNode<S, t.Aliases[K]>;
    }
    & {
        [K in keyof VirtualTypeAliases]?: VisitNode<S, VirtualTypeAliases[K]>;
    }
    & {
        // Babel supports `NodeTypesWithoutComment | NodeTypesWithoutComment | ... ` but it is
        // too complex for TS. So we type it as a general visitor only if the key contains `|`
        // this is good enough for non-visitor traverse options e.g. `noScope`
        [k: `${string}|${string}`]: VisitNode<S, Node>;
    };

export type VisitNode<S, P extends Node> = VisitNodeFunction<S, P> | VisitNodeObject<S, P>;

export type VisitNodeFunction<S, P extends Node> = (this: S, path: NodePath<P>, state: S) => void;

type NodeType = Node["type"] | keyof t.Aliases;

export interface VisitNodeObject<S, P extends Node> {
    enter?: VisitNodeFunction<S, P>;
    exit?: VisitNodeFunction<S, P>;
}

export type NodeKeyOfArrays<T extends Node> = {
    [P in keyof T]-?: T[P] extends Array<Node | null | undefined> ? P : never;
}[keyof T];

export type NodeKeyOfNodes<T extends Node> = {
    [P in keyof T]-?: T[P] extends Node | null | undefined ? P : never;
}[keyof T];

export type NodePaths<T extends Node | readonly Node[]> = T extends readonly Node[]
    ? { -readonly [K in keyof T]: NodePath<Extract<T[K], Node>> }
    : T extends Node ? [NodePath<T>]
    : never;

type NodeListType<N, K extends keyof N> = N[K] extends Array<infer P> ? (P extends Node ? P : never) : never;

type NodesInsertionParam<T extends Node> = T | readonly T[] | [T, ...T[]];

export class NodePath<T = Node> {
    constructor(hub: HubInterface, parent: Node);
    parent: Node;
    hub: Hub;
    data: Record<string | symbol, unknown>;
    context: TraversalContext;
    scope: Scope;
    contexts: TraversalContext[];
    state: any;
    opts: any; // exploded TraverseOptions
    skipKeys: Record<string, boolean> | null;
    parentPath: T extends t.Program ? null : NodePath;
    container: Node | Node[] | null;
    listKey: string | null;
    key: string | number | null;
    node: T;
    type: T extends Node ? T["type"] : T extends null | undefined ? undefined : Node["type"] | undefine