import sys
from inspect import Parameter, getdoc, signature

from napari.utils.misc import camel_to_snake
from napari.utils.translations import trans

template = """def {name}{signature}:
    kwargs = locals()
    kwargs.pop('self', None)
    layer = {cls_name}(**kwargs)
    self.layers.append(layer)
    return layer
"""


def create_func(cls, name=None, doc=None, filename: str = '<string>'):
    cls_name = cls.__name__

    if name is None:
        name = camel_to_snake(cls_name)

    if 'layer' in name:
        raise ValueError(
            trans._(
                "name {name} should not include 'layer'",
                deferred=True,
                name=name,
            )
        )

    name = 'add_' + name

    if doc is None:
        doc = getdoc(cls)
        cutoff = doc.find('\n\nParameters\n----------\n')
        if cutoff > 0:
            doc = doc[cutoff:]

        n = 'n' if cls_name[0].lower() in 'aeiou' else ''
        doc = f'Add a{n} {cls_name} layer to the layer list. ' + doc
        doc += '\n\nReturns\n-------\n'
        doc += f'layer : :class:`napari.layers.{cls_name}`'
        doc += f'\n\tThe newly-created {cls_name.lower()} layer.'
        doc = doc.expandtabs(4)

    sig = signature(cls)
    new_sig = sig.replace(
        parameters=[
            Parameter("self", Parameter.POSITIONAL_OR_KEYWORD),
            *list(sig.parameters.values()),
        ],
        return_annotation=cls,
    )
    src = template.format(
        name=name,
        signature=new_sig,
        cls_name=cls_name,
    )

    execdict = {cls_name: cls, 'napari': sys.modules.get('napari')}
    code = compile(src, filename=filename, mode='exec')
    exec(code, execdict)
    func = execdict[name]

    func.__doc__ = doc

    return func


def _register(cls, *, name=None, doc=None):
    from napari.components import ViewerModel

    func = create_func(cls, name=name, doc=doc)
    setattr(ViewerModel, func.__name__, func)
    return cls


def add_to_viewer(cls=None, *, name=None, doc=None):
    """Adds a layer creation convenience method under viewers
    as ``add_{name}``.

    Parameters
    ----------
    cls : type, optional
        Class to register.
        If None, this function is treated as a decorator.
    name : string, keyword-only
        Name in snake-case of the layer name.
        If None, is autogenerated from the class name.
    doc : string, keyword-only
        Docstring to use in the method.
        If None, is autogenerated from the existing docstring.
    """
    if cls is not None:
        return _register(cls, name=name, doc=doc)

    def inner(cls):
        return _register(cls, name=name, doc=doc)

    return inner
