#! /usr/bin/env python3
#
# Generate Sphinx documentation from JSON schema

import argparse
import sys
import json


def errprint(*args, **kwargs):
    print(*args, file=sys.stderr, **kwargs)


def find_ref(schema: dict, ref: str) -> dict:
    parts = ref.split("/")

    root = parts.pop(0)
    if root != "#":
        raise Exception("Unsupported reference: {}".format(ref))

    while parts:
        schema = schema[parts.pop(0)]

    return schema


def get_type(props: dict, name: str) -> str:
    prop_type = props["type"]
    if prop_type == "array":
        try:
            array_type = props["items"]["type"]
        except KeyError:
            errprint("warning: array property without items: {}".format(name))
            array_type = "unknown"
        prop_type = "array of {}s".format(array_type)
    return prop_type


def render_flat(schema: dict):
    stack = [(schema, [])]

    while stack:
        (current, path) = stack.pop(0)

        for name, props in current["properties"].items():
            if "$ref" in props:
                ref = find_ref(schema, props["$ref"])
                if not ref:
                    raise Exception("$ref not found: {}".format(props["$ref"]))
                props = ref
            if props["type"] in ["string", "integer", "boolean", "number"]:
                # End of the line...
                print("{}: {}".format(".".join(path + [name]), props["type"]))
            elif props["type"] == "object":
                print("{}: object".format(".".join(path + [name])))
                if "properties" in props:
                    stack.insert(0, (props, path + [name]))
                else:
                    errprint(
                        "warning: object without properties: {}".format(
                            ".".join(path + [name])
                        )
                    )
            elif props["type"] == "array":
                if "items" in props and "type" in props["items"]:
                    print(
                        "{}: {}[]".format(
                            ".".join(path + [name]), props["items"]["type"]
                        )
                    )
                    if "properties" in props["items"]:
                        stack.insert(
                            0,
                            (
                                props["items"],
                                path + ["{}[]".format(name)],
                            ),
                        )
                else:
                    errprint(
                        "warning: undocumented array: {}".format(
                            ".".join(path + [name])
                        )
                    )
                    print("{}: array".format(".".join(path + [name])))
            else:
                raise Exception("Unsupported type: {}".format(props["type"]))


def render_rst(schema: dict):
    stack = [(schema, [], "object")]

    while stack:
        (current, path, type) = stack.pop(0)

        items = []

        for name, props in current["properties"].items():
            if "$ref" in props:
                ref = find_ref(schema, props["$ref"])
                if not ref:
                    raise Exception(
                        "Reference not found: {}".format(props["$ref"])
                    )
                props = ref
            prop_type = get_type(props, name)
            description = props.get("description", "")

            items.append(
                {"name": name, "type": prop_type, "description": description}
            )

            if props["type"] == "object" and "properties" in props:
                stack.insert(0, (props, path + [name], "object"))
            elif (
                props["type"] == "array"
                and "items" in props
                and "properties" in props["items"]
            ):
                array_type = props["items"]["type"]
                stack.insert(
                    0,
                    (
                        props["items"],
                        path + ["{}".format(name)],
                        "array of {}s".format(array_type),
                    ),
                )

        render_rst_table(items, path, type)


def render_rst_table(items: list, path: list, type: str):
    if not path:
        title = "Top Level"
    else:
        title = ".".join(path)
    title = "{} ({})".format(title, type)
    print(title)
    print("^" * len(title))

    name_len = max([len(item["name"]) for item in items] + [len("Name")])
    desc_len = max(
        [len(item["description"]) for item in items] + [len("Description")]
    )
    type_len = max([len(item["type"]) for item in items])

    print(".. table::")
    print("   :width: 100%")
    print("   :widths: 30 25 45")
    print("")

    print("   {} {} {}".format("=" * name_len, "=" * type_len, "=" * desc_len))
    print(
        "   {} {} {}".format(
            "Name".ljust(name_len),
            "Type".ljust(type_len),
            "Description".ljust(desc_len),
        )
    )
    print("   {} {} {}".format("=" * name_len, "=" * type_len, "=" * desc_len))
    for item in items:
        print(
            "   {} {} {}".format(
                item["name"].ljust(name_len),
                item["type"].ljust(type_len),
                item["description"].ljust(desc_len),
            )
        )
    print("   {} {} {}".format("=" * name_len, "=" * type_len, "=" * desc_len))
    print("")


epilog = """

By default, the EVE schema is rendered as Sphinx documentation. To
create "flat" or "dot" separated output, use the --flat option.

"""


def main():
    parser = argparse.ArgumentParser(
        description="Generate documentation from JSON schema",
        epilog=epilog,
        formatter_class=argparse.RawTextHelpFormatter,
    )
    parser.add_argument("--object", help="Object name")
    parser.add_argument("--output", help="Output file")
    parser.add_argument("--flat", help="Flatten output", action="store_true")
    parser.add_argument("filename", help="JSON schema file")

    args = parser.parse_args()

    root = json.load(open(args.filename))
    schema = root

    if args.object:
        schema = schema["properties"][args.object]

    if args.output:
        sys.stdout = open(args.output, "w")

    if args.flat:
        render_flat(schema)
    else:
        render_rst(schema)


if __name__ == "__main__":
    sys.exit(main())
