# tsx components don't have the `.propTypes` property set
# Generate it instead with the provided metadata.json
# for them to be able to report invalid prop

import json
import os
import re

from dash.development._py_prop_typing import get_custom_ignore


init_check_re = re.compile("proptypes.js")

missing_init_msg = """
{warning_box}
{title}
{warning_box}

Add the following to `{namespace}/__init__.py` to enable
runtime prop types validation with tsx components:

_js_dist.append(dict(
    dev_package_path="proptypes.js",
    dev_only=True,
    namespace="{namespace}"
))

"""

prop_type_file_template = """// AUTOGENERATED FILE - DO NOT EDIT

var pt = window.PropTypes;
var pk = window['{package_name}'];

{components_prop_types}
"""

component_prop_types_template = "pk.{component_name}.propTypes = {prop_types};"


def generate_type(type_name):
    def wrap(*_):
        return f"pt.{type_name}"

    return wrap


def generate_union(prop_info):
    types = [generate_prop_type(t) for t in prop_info["value"]]
    return f"pt.oneOfType([{','.join(types)}])"


def generate_shape(prop_info):
    props = []
    for key, value in prop_info["value"].items():
        props.append(f"{key}:{generate_prop_type(value)}")
    inner = "{" + ",".join(props) + "}"
    return f"pt.shape({inner})"


def generate_array_of(prop_info):
    inner_type = generate_prop_type(prop_info["value"])
    return f"pt.arrayOf({inner_type})"


def generate_any(*_):
    return "pt.any"


def generate_enum(prop_info):
    values = [v["value"] for v in prop_info["value"] if v.get("value") is not None]
    csv = ",".join(str(v) for v in values)
    return f"pt.oneOf([{csv}])"


def generate_object_of(prop_info):
    return f"pt.objectOf({generate_prop_type(prop_info['value'])})"


def generate_tuple(*_):
    # PropTypes don't have a tuple... just generate an array.
    return "pt.array"


def generate_literal(prop_info):
    return f"pt.oneOf([{json.dumps(prop_info['value'])}])"


prop_types = {
    "array": generate_type("array"),
    "arrayOf": generate_array_of,
    "object": generate_type("object"),
    "shape": generate_shape,
    "exact": generate_shape,
    "string": generate_type("string"),
    "bool": generate_type("bool"),
    "number": generate_type("number"),
    "node": generate_type("node"),
    "func": generate_any,
    "element": generate_type("element"),
    "union": generate_union,
    "any": generate_any,
    "custom": generate_any,
    "enum": generate_enum,
    "objectOf": generate_object_of,
    "tuple": generate_tuple,
    "literal": generate_literal,
}


def generate_prop_type(prop_info):
    return prop_types[prop_info["name"]](prop_info)


def check_init(namespace):
    path = os.path.join(namespace, "__init__.py")
    if os.path.exists(path):
        with open(path, encoding="utf-8", mode="r") as f:
            if not init_check_re.search(f.read()):
                title = f"! Missing proptypes.js in `{namespace}/__init__.py` !"
                print(
                    missing_init_msg.format(
                        namespace=namespace,
                        warning_box="!" * len(title),
                        title=title,
                    )
                )


def generate_prop_types(
    metadata,
    package_name,
    custom_typing_module,
):
    patched = []

    custom_ignore = get_custom_ignore(custom_typing_module)

    for component_path, data in metadata.items():
        filename = component_path.split("/")[-1]
        extension = filename.split("/")[-1].split(".")[-1]
        if extension != "tsx":
            continue

        component_name = filename.split(".")[0]

        props = []
        for prop_name, prop_data in data.get("props", {}).items():
            if prop_name in custom_ignore:
                prop_type = "pt.any"
            else:
                prop_type = generate_prop_type(prop_data["type"])
            props.append(f"{prop_name}:{prop_type}")

        patched.append(
            component_prop_types_template.format(
                package_name=package_name,
                component_name=component_name,
                prop_types="{" + ",\n ".join(props) + "}",
            )
        )

    if patched:
        with open(
            os.path.join(package_name, "proptypes.js"), encoding="utf-8", mode="w"
        ) as f:
            f.write(
                prop_type_file_template.format(
                    package_name=package_name, components_prop_types="\n".join(patched)
                )
            )

        check_init(package_name)
