# SPDX-License-Identifier: GPL-2.0-or-later

# This is a quite stupid script which extracts bmesh api docs from
# 'bmesh_opdefines.c' in order to avoid having to add a lot of introspection
# data access into the api.
#
# The script is stupid because it makes assumptions about formatting...
# that each arg has its own line, that comments above or directly after will be __doc__ etc...
#
# We may want to replace this script with something else one day but for now its good enough.
# if it needs large updates it may be better to rewrite using a real parser or
# add introspection into bmesh.ops.
# - campbell

import os
import re

CURRENT_DIR = os.path.abspath(os.path.dirname(__file__))
SOURCE_DIR = os.path.normpath(os.path.abspath(os.path.normpath(os.path.join(CURRENT_DIR, "..", ".."))))
FILE_OP_DEFINES_C = os.path.join(SOURCE_DIR, "source", "blender", "bmesh", "intern", "bmesh_opdefines.c")
OUT_RST = os.path.join(CURRENT_DIR, "rst", "bmesh.ops.rst")

HEADER = r"""
BMesh Operators (bmesh.ops)
===========================

.. module:: bmesh.ops

This module gives access to low level bmesh operations.

Most operators take input and return output, they can be chained together
to perform useful operations.


Operator Example
++++++++++++++++
This script shows how operators can be used to model a link of a chain.

.. literalinclude:: __/examples/bmesh.ops.1.py
"""


def main():
    fsrc = open(FILE_OP_DEFINES_C, 'r', encoding="utf-8")

    blocks = []

    is_block = False
    is_comment = False  # /* global comments only */

    comment_ctx = None
    block_ctx = None

    for l in fsrc:
        l = l[:-1]
        # weak but ok
        if (
            (("BMOpDefine" in l and l.split()[1] == "BMOpDefine") and "bmo_opdefines[]" not in l) or
            ("static BMO_FlagSet " in l)
        ):
            is_block = True
            block_ctx = []
            blocks.append((comment_ctx, block_ctx))
        elif l.strip().startswith("/*"):
            is_comment = True
            comment_ctx = []

        if is_block:
            if l.strip().startswith("//"):
                pass
            else:
                # remove c++ comment if we have one
                cpp_comment = l.find("//")
                if cpp_comment != -1:
                    l = l[:cpp_comment]

                # remove sentinel from enums
                l = l.replace("{0, NULL}", "")

                block_ctx.append(l)

            if l.strip().endswith("};"):
                is_block = False
                comment_ctx = None

        if is_comment:
            c_comment_start = l.find("/*")
            if c_comment_start != -1:
                l = l[c_comment_start + 2:]

            c_comment_end = l.find("*/")
            if c_comment_end != -1:
                l = l[:c_comment_end]

                is_comment = False
            comment_ctx.append(l)

    fsrc.close()
    del fsrc

    # namespace hack
    vars = (
        "BMO_OP_SLOT_ELEMENT_BUF",
        "BMO_OP_SLOT_BOOL",
        "BMO_OP_SLOT_FLT",
        "BMO_OP_SLOT_INT",
        "BMO_OP_SLOT_MAT",
        "BMO_OP_SLOT_VEC",
        "BMO_OP_SLOT_PTR",
        "BMO_OP_SLOT_MAPPING",

        "BMO_OP_SLOT_SUBTYPE_MAP_ELEM",
        "BMO_OP_SLOT_SUBTYPE_MAP_BOOL",
        "BMO_OP_SLOT_SUBTYPE_MAP_INT",
        "BMO_OP_SLOT_SUBTYPE_MAP_FLT",
        "BMO_OP_SLOT_SUBTYPE_MAP_EMPTY",
        "BMO_OP_SLOT_SUBTYPE_MAP_INTERNAL",

        "BMO_OP_SLOT_SUBTYPE_PTR_BMESH",
        "BMO_OP_SLOT_SUBTYPE_PTR_SCENE",
        "BMO_OP_SLOT_SUBTYPE_PTR_OBJECT",
        "BMO_OP_SLOT_SUBTYPE_PTR_MESH",
        "BMO_OP_SLOT_SUBTYPE_PTR_STRUCT",

        "BMO_OP_SLOT_SUBTYPE_INT_ENUM",
        "BMO_OP_SLOT_SUBTYPE_INT_FLAG",

        "BMO_OP_SLOT_SUBTYPE_ELEM_IS_SINGLE",

        "BM_VERT",
        "BM_EDGE",
        "BM_FACE",

        "BMO_OPTYPE_FLAG_NORMALS_CALC",
        "BMO_OPTYPE_FLAG_UNTAN_MULTIRES",
        "BMO_OPTYPE_FLAG_SELECT_FLUSH",
        "BMO_OPTYPE_FLAG_SELECT_VALIDATE",
        "BMO_OPTYPE_FLAG_NOP",
    )
    vars_dict = {}
    for i, v in enumerate(vars):
        vars_dict[v] = (1 << i)
    globals().update(vars_dict)
    # reverse lookup
    vars_dict_reverse = {v: k for k, v in vars_dict.items()}
    # end namespace hack

    blocks_py = []
    for comment, b in blocks:
        # magic, translate into python
        b[0] = b[0].replace("static BMOpDefine ", "")
        is_enum = False

        for i, l in enumerate(b):
            l = l.strip()
            # casts
            l = l.replace("(int)", "")

            l = l.replace("{", "(")
            l = l.replace("}", ")")

            if l.startswith("/*"):
                l = l.replace("/*", "'''own <")
            else:
                l = l.replace("/*", "'''inline <")
            l = l.replace("*/", ">''',")

            # exec func. eg: bmo_rotate_edges_exec,
            if l.startswith("bmo_") and l.endswith("_exec,"):
                l = "None,"

            # enums
            if l.startswith("static BMO_FlagSet "):
                is_enum = True

            b[i] = l

        # for l in b:
        #     print(l)

        if is_enum:
            text = "".join(b)
            text = text.replace("static BMO_FlagSet ", "")
            text = text.replace("[]", "")
            text = text.strip(";")
            text = text.replace("(", "[").replace(")", "]")
            text = text.replace("\"", "'")

            k, v = text.split("=", 1)

            v = repr(re.findall(r"'([^']*)'", v))

            k = k.strip()
            v = v.strip()

            vars_dict[k] = v

            continue

        text = "\n".join(b)
        global_namespace = {
            "__file__": "generated",
            "__name__": "__main__",
        }

        global_namespace.update(vars_dict)

        text_a, text_b = text.split("=", 1)
        text = "result = " + text_b
        exec(compile(text, "generated", 'exec'), global_namespace)
        # print(global_namespace["result"])
        blocks_py.append((comment, global_namespace["result"]))

    # ---------------------
    # Now convert into rst.
    fout = open(OUT_RST, 'w', encoding="utf-8")
    fw = fout.write
    fw(HEADER)
    for comment, b in blocks_py:
        args_in = None
        args_out = None
        for member in b[1:]:
            if type(member) == tuple:
                if args_in is None:
                    args_in = member
                elif args_out is None:
                    args_out = member
                    break

        args_in_index = []
        args_out_index = []

        if args_in is not None:
            args_in_index[:] = [i for (i, a) in enumerate(args_in) if type(a) == tuple]
        if args_out is not None:
            args_out_index[:] = [i for (i, a) in enumerate(args_out) if type(a) == tuple]

        fw(".. function:: %s(bm, %s)\n\n" % (b[0], ", ".join([args_in[i][0] for i in args_in_index])))

        # -- wash the comment
        comment_washed = []
        comment = [] if comment is None else comment
        for i, l in enumerate(comment):
            assert ((l.strip() == "") or
                    (l in {"/*", " *"}) or
                    (l.startswith(("/* ", " * "))))

            l = l[3:]
            if i == 0 and not l.strip():
                continue
            if l.strip():
                l = "   " + l
            comment_washed.append(l)

        fw("\n".join(comment_washed))
        fw("\n")
        # -- done

        # get the args
        def get_args_wash(args, args_index, is_ret):
            args_wash = []
            for i in args_index:
                arg = args[i]
                if len(arg) == 4:
                    name, tp, tp_sub, enums = arg
                elif len(arg) == 3:
                    name, tp, tp_sub = arg
                elif len(arg) == 2:
                    name, tp = arg
                    tp_sub = None
                else:
                    print(arg)
                    assert 0

                tp_str = ""

                comment_prev = ""
                comment_next = ""
                if i != 0:
                    comment_prev = args[i + 1]
                    if type(comment_prev) == str and comment_prev.startswith("our <"):
                        comment_prev = comment_next[5:-1]  # strip inline <...>
                    else:
                        comment_prev = ""

                if i + 1 < len(args):
                    comment_next = args[i + 1]
                    if type(comment_next) == str and comment_next.startswith("inline <"):
                        comment_next = comment_next[8:-1]  # strip inline <...>
                    else:
                        comment_next = ""

                comment = ""
                if comment_prev:
                    comment += comment_prev.strip()
                if comment_next:
                    comment += ("\n" if comment_prev else "") + comment_next.strip()

                if tp == BMO_OP_SLOT_FLT:
                    tp_str = "float"
                elif tp == BMO_OP_SLOT_INT:
                    if tp_sub == BMO_OP_SLOT_SUBTYPE_INT_ENUM:
                        tp_str = "enum in " + enums + ", default " + enums.split(",", 1)[0].strip("[")
                    elif tp_sub == BMO_OP_SLOT_SUBTYPE_INT_FLAG:
                        tp_str = "set of flags from " + enums + ", default {}"
                    else:
                        tp_str = "int"
                elif tp == BMO_OP_SLOT_BOOL:
                    tp_str = "bool"
                elif tp == BMO_OP_SLOT_MAT:
                    tp_str = ":class:`mathutils.Matrix`"
                elif tp == BMO_OP_SLOT_VEC:
                    tp_str = ":class:`mathutils.Vector`"
                    if not is_ret:
                        tp_str += " or any sequence of 3 floats"
                elif tp == BMO_OP_SLOT_PTR:
                    tp_str = "dict"
                    assert tp_sub is not None
                    if tp_sub == BMO_OP_SLOT_SUBTYPE_PTR_BMESH:
                        tp_str = ":class:`bmesh.types.BMesh`"
                    elif tp_sub == BMO_OP_SLOT_SUBTYPE_PTR_SCENE:
                        tp_str = ":class:`bpy.types.Scene`"
                    elif tp_sub == BMO_OP_SLOT_SUBTYPE_PTR_OBJECT:
                        tp_str = ":class:`bpy.types.Object`"
                    elif tp_sub == BMO_OP_SLOT_SUBTYPE_PTR_MESH:
                        tp_str = ":class:`bpy.types.Mesh`"
                    elif tp_sub == BMO_OP_SLOT_SUBTYPE_PTR_STRUCT:
                        # XXX Used for CurveProfile only currently I think (bevel code),
                        #     but think the idea is that that pointer is for any type?
                        tp_str = ":class:`bpy.types.bpy_struct`"
                    else:
                        print("Can't find", vars_dict_reverse[tp_sub])
                        assert 0

                elif tp == BMO_OP_SLOT_ELEMENT_BUF:
                    assert tp_sub is not None

                    ls = []
                    if tp_sub & BM_VERT:
                        ls.append(":class:`bmesh.types.BMVert`")
                    if tp_sub & BM_EDGE:
                        ls.append(":class:`bmesh.types.BMEdge`")
                    if tp_sub & BM_FACE:
                        ls.append(":class:`bmesh.types.BMFace`")
                    assert ls  # Must be at least one.

                    if tp_sub & BMO_OP_SLOT_SUBTYPE_ELEM_IS_SINGLE:
                        tp_str = "/".join(ls)
                    else:
                        tp_str = ("list of (%s)" % ", ".join(ls))

                    del ls
                elif tp == BMO_OP_SLOT_MAPPING:
                    if tp_sub & BMO_OP_SLOT_SUBTYPE_MAP_EMPTY:
                        tp_str = "set of vert/edge/face type"
                    else:
                        tp_str = "dict mapping vert/edge/face types to "
                        if tp_sub == BMO_OP_SLOT_SUBTYPE_MAP_BOOL:
                            tp_str += "bool"
                        elif tp_sub == BMO_OP_SLOT_SUBTYPE_MAP_INT:
                            tp_str += "int"
                        elif tp_sub == BMO_OP_SLOT_SUBTYPE_MAP_FLT:
                            tp_str += "float"
                        elif tp_sub == BMO_OP_SLOT_SUBTYPE_MAP_ELEM:
                            tp_str += ":class:`bmesh.types.BMVert`/:class:`bmesh.types.BMEdge`/:class:`bmesh.types.BMFace`"
                        elif tp_sub == BMO_OP_SLOT_SUBTYPE_MAP_INTERNAL:
                            tp_str += "unknown internal data, not compatible with python"
                        else:
                            print("Can't find", vars_dict_reverse[tp_sub])
                            assert 0
                else:
                    print("Can't find", vars_dict_reverse[tp])
                    assert 0

                args_wash.append((name, tp_str, comment))
            return args_wash
        # end get_args_wash

        # all ops get this arg
        fw("   :arg bm: The bmesh to operate on.\n")
        fw("   :type bm: :class:`bmesh.types.BMesh`\n")

        args_in_wash = get_args_wash(args_in, args_in_index, False)
        args_out_wash = get_args_wash(args_out, args_out_index, True)

        for (name, tp, comment) in args_in_wash:
            if comment == "":
                comment = "Undocumented."

            fw("   :arg %s: %s\n" % (name, comment))
            fw("   :type %s: %s\n" % (name, tp))

        if args_out_wash:
            fw("   :return:\n\n")

            for (name, tp, comment) in args_out_wash:
                assert name.endswith(".out")
                name = name[:-4]
                fw("      - ``%s``: %s\n\n" % (name, comment))
                fw("        **type** %s\n" % tp)

            fw("\n")
            fw("   :rtype: dict with string keys\n")

        fw("\n\n")

    fout.close()
    del fout
    print(OUT_RST)


if __name__ == "__main__":
    main()
