#!/usr/bin/env python

# This test ensures that every ID in the produced json actually resolves to an item either in
# `index` or `paths`. It DOES NOT check that the structure of the produced json is actually in
# any way correct, for example an empty map would pass.

# FIXME: Better error output

import sys
import json

crate = json.load(open(sys.argv[1], encoding="utf-8"))


def get_local_item(item_id):
    if item_id in crate["index"]:
        return crate["index"][item_id]
    print("Missing local ID:", item_id)
    sys.exit(1)


# local IDs have to be in `index`, external ones can sometimes be in `index` but otherwise have
# to be in `paths`
def valid_id(item_id):
    return item_id in crate["index"] or item_id[0] != "0" and item_id in crate["paths"]


def check_generics(generics):
    for param in generics["params"]:
        check_generic_param(param)
    for where_predicate in generics["where_predicates"]:
        if "bound_predicate" in where_predicate:
            pred = where_predicate["bound_predicate"]
            check_type(pred["type"])
            for bound in pred["bounds"]:
                check_generic_bound(bound)
        elif "region_predicate" in where_predicate:
            pred = where_predicate["region_predicate"]
            for bound in pred["bounds"]:
                check_generic_bound(bound)
        elif "eq_predicate" in where_predicate:
            pred = where_predicate["eq_predicate"]
            check_type(pred["rhs"])
            check_type(pred["lhs"])


def check_generic_param(param):
    if "type" in param["kind"]:
        ty = param["kind"]["type"]
        if ty["default"]:
            check_type(ty["default"])
        for bound in ty["bounds"]:
            check_generic_bound(bound)
    elif "const" in param["kind"]:
        check_type(param["kind"]["const"])


def check_generic_bound(bound):
    if "trait_bound" in bound:
        for param in bound["trait_bound"]["generic_params"]:
            check_generic_param(param)
        check_type(bound["trait_bound"]["trait"])


def check_decl(decl):
    for (_name, ty) in decl["inputs"]:
        check_type(ty)
    if decl["output"]:
        check_type(decl["output"])


def check_type(ty):
    if ty["kind"] == "resolved_path":
        for bound in ty["inner"]["param_names"]:
            check_generic_bound(bound)
        args = ty["inner"]["args"]
        if args:
            if "angle_bracketed" in args:
                for arg in args["angle_bracketed"]["args"]:
                    if "type" in arg:
                        check_type(arg["type"])
                    elif "const" in arg:
                        check_type(arg["const"]["type"])
                for binding in args["angle_bracketed"]["bindings"]:
                    if "equality" in binding["binding"]:
                        term = binding["binding"]["equality"]
                        if "type" in term: check_type(term["type"])
                        elif "const" in term: check_type(term["const"])
                    elif "constraint" in binding["binding"]:
                        for bound in binding["binding"]["constraint"]:
                            check_generic_bound(bound)
            elif "parenthesized" in args:
                for ty in args["parenthesized"]["inputs"]:
                    check_type(ty)
                if args["parenthesized"]["output"]:
                    check_type(args["parenthesized"]["output"])
        if not valid_id(ty["inner"]["id"]):
            print("Type contained an invalid ID:", ty["inner"]["id"])
            sys.exit(1)
    elif ty["kind"] == "tuple":
        for ty in ty["inner"]:
            check_type(ty)
    elif ty["kind"] == "slice":
        check_type(ty["inner"])
    elif ty["kind"] == "impl_trait":
        for bound in ty["inner"]:
            check_generic_bound(bound)
    elif ty["kind"] in ("raw_pointer", "borrowed_ref", "array"):
        check_type(ty["inner"]["type"])
    elif ty["kind"] == "function_pointer":
        for param in ty["inner"]["generic_params"]:
            check_generic_param(param)
        check_decl(ty["inner"]["decl"])
    elif ty["kind"] == "qualified_path":
        check_type(ty["inner"]["self_type"])
        check_type(ty["inner"]["trait"])


work_list = set([crate["root"]])
visited = work_list.copy()

while work_list:
    current = work_list.pop()
    visited.add(current)
    item = get_local_item(current)
    # check intradoc links
    for (_name, link) in item["links"].items():
        if not valid_id(link):
            print("Intra-doc link contains invalid ID:", link)

    # check all fields that reference types such as generics as well as nested items
    # (modules, structs, traits, and enums)
    if item["kind"] == "module":
        work_list |= set(item["inner"]["items"]) - visited
    elif item["kind"] == "struct":
        check_generics(item["inner"]["generics"])
        work_list |= (
            set(item["inner"]["fields"]) | set(item["inner"]["impls"])
        ) - visited
    elif item["kind"] == "struct_field":
        check_type(item["inner"])
    elif item["kind"] == "enum":
        check_generics(item["inner"]["generics"])
        work_list |= (
            set(item["inner"]["variants"]) | set(item["inner"]["impls"])
        ) - visited
    elif item["kind"] == "variant":
        if item["inner"]["variant_kind"] == "tuple":
            for ty in item["inner"]["variant_inner"]:
                check_type(ty)
        elif item["inner"]["variant_kind"] == "struct":
            work_list |= set(item["inner"]["variant_inner"]) - visited
    elif item["kind"] in ("function", "method"):
        check_generics(item["inner"]["generics"])
        check_decl(item["inner"]["decl"])
    elif item["kind"] in ("static", "constant", "assoc_const"):
        check_type(item["inner"]["type"])
    elif item["kind"] == "typedef":
        check_type(item["inner"]["type"])
        check_generics(item["inner"]["generics"])
    elif item["kind"] == "opaque_ty":
        check_generics(item["inner"]["generics"])
        for bound in item["inner"]["bounds"]:
            check_generic_bound(bound)
    elif item["kind"] == "trait_alias":
        check_generics(item["inner"]["params"])
        for bound in item["inner"]["bounds"]:
            check_generic_bound(bound)
    elif item["kind"] == "trait":
        check_generics(item["inner"]["generics"])
        for bound in item["inner"]["bounds"]:
            check_generic_bound(bound)
        work_list |= (
            set(item["inner"]["items"]) | set(item["inner"]["implementations"])
        ) - visited
    elif item["kind"] == "impl":
        check_generics(item["inner"]["generics"])
        if item["inner"]["trait"]:
            check_type(item["inner"]["trait"])
        if item["inner"]["blanket_impl"]:
            check_type(item["inner"]["blanket_impl"])
        check_type(item["inner"]["for"])
        for assoc_item in item["inner"]["items"]:
            if not valid_id(assoc_item):
                print("Impl block referenced a missing ID:", assoc_item)
                sys.exit(1)
    elif item["kind"] == "assoc_type":
        for bound in item["inner"]["bounds"]:
            check_generic_bound(bound)
        if item["inner"]["default"]:
            check_type(item["inner"]["default"])
