# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

# <pep8-80 compliant>

# for slightly faster access
from _bpy import ops as _ops_module

# op_add = _ops_module.add
_op_dir = _ops_module.dir
_op_poll = _ops_module.poll
_op_call = _ops_module.call
_op_as_string = _ops_module.as_string
_op_get_rna_type = _ops_module.get_rna_type
_op_get_bl_options = _ops_module.get_bl_options

_ModuleType = type(_ops_module)


# -----------------------------------------------------------------------------
# Callable Operator Wrapper

class _BPyOpsSubModOp:
    """
    Utility class to fake submodule operators.

    eg. bpy.ops.object.somefunc
    """

    __slots__ = ("_module", "_func")

    def _get_doc(self):
        idname = self.idname()
        sig = _op_as_string(self.idname())
        # XXX You never quite know what you get from bpy.types,
        # with operators... Operator and OperatorProperties
        # are shadowing each other, and not in the same way for
        # native ops and py ones! See T39158.
        # op_class = getattr(bpy.types, idname)
        op_class = _op_get_rna_type(idname)
        descr = op_class.description
        return "%s\n%s" % (sig, descr)

    @staticmethod
    def _parse_args(args):
        C_dict = None
        C_exec = 'EXEC_DEFAULT'
        C_undo = False

        is_dict = is_exec = is_undo = False

        for arg in args:
            if is_dict is False and isinstance(arg, dict):
                if is_exec is True or is_undo is True:
                    raise ValueError("dict arg must come first")
                C_dict = arg
                is_dict = True
            elif is_exec is False and isinstance(arg, str):
                if is_undo is True:
                    raise ValueError("string arg must come before the boolean")
                C_exec = arg
                is_exec = True
            elif is_undo is False and isinstance(arg, int):
                C_undo = arg
                is_undo = True
            else:
                raise ValueError("1-3 args execution context is supported")

        return C_dict, C_exec, C_undo

    @staticmethod
    def _view_layer_update(context):
        view_layer = context.view_layer
        if view_layer:  # None in background mode
            view_layer.update()
        else:
            import bpy
            for scene in bpy.data.scenes:
                for view_layer in scene.view_layers:
                    view_layer.update()

    __doc__ = property(_get_doc)

    def __init__(self, module, func):
        self._module = module
        self._func = func

    def poll(self, *args):
        C_dict, C_exec, _C_undo = _BPyOpsSubModOp._parse_args(args)
        return _op_poll(self.idname_py(), C_dict, C_exec)

    def idname(self):
        # submod.foo -> SUBMOD_OT_foo
        return self._module.upper() + "_OT_" + self._func

    def idname_py(self):
        # submod.foo -> SUBMOD_OT_foo
        return self._module + "." + self._func

    def __call__(self, *args, **kw):
        import bpy
        context = bpy.context

        # Get the operator from blender
        wm = context.window_manager

        # Run to account for any RNA values the user changes.
        # NOTE: We only update active view-layer, since that's what
        # operators are supposed to operate on. There might be some
        # corner cases when operator need a full scene update though.
        _BPyOpsSubModOp._view_layer_update(context)

        if args:
            C_dict, C_exec, C_undo = _BPyOpsSubModOp._parse_args(args)
            ret = _op_call(self.idname_py(), C_dict, kw, C_exec, C_undo)
        else:
            ret = _op_call(self.idname_py(), None, kw)

        if 'FINISHED' in ret and context.window_manager == wm:
            _BPyOpsSubModOp._view_layer_update(context)

        return ret

    def get_rna_type(self):
        """Internal function for introspection"""
        return _op_get_rna_type(self.idname())

    @property
    def bl_options(self):
        return _op_get_bl_options(self.idname())

    def __repr__(self):  # useful display, repr(op)
        return _op_as_string(self.idname())

    def __str__(self):  # used for print(...)
        return ("<function bpy.ops.%s.%s at 0x%x'>" %
                (self._module, self._func, id(self)))


# -----------------------------------------------------------------------------
# Sub-Module Access

def _bpy_ops_submodule__getattr__(module, func):
    # Return a value from `bpy.ops.{module}.{func}`
    if func.startswith("__"):
        raise AttributeError(func)
    return _BPyOpsSubModOp(module, func)


def _bpy_ops_submodule__dir__(module):
    functions = set()
    module_upper = module.upper()

    for id_name in _op_dir():
        id_split = id_name.split("_OT_", 1)
        if len(id_split) == 2 and module_upper == id_split[0]:
            functions.add(id_split[1])

    return list(functions)


def _bpy_ops_submodule(module):
    result = _ModuleType("bpy.ops." + module)
    result.__getattr__ = lambda func: _bpy_ops_submodule__getattr__(module, func)
    result.__dir__ = lambda: _bpy_ops_submodule__dir__(module)
    return result


# -----------------------------------------------------------------------------
# Module Access

def __getattr__(module):
    # Return a value from `bpy.ops.{module}`.
    if module.startswith("__"):
        raise AttributeError(module)
    return _bpy_ops_submodule(module)


def __dir__():
    submodules = set()
    for id_name in _op_dir():
        id_split = id_name.split("_OT_", 1)

        if len(id_split) == 2:
            submodules.add(id_split[0].lower())
        else:
            submodules.add(id_split[0])

    return list(submodules)
