"""
Generic function parser for functions embedded in a string, on the form
`$funcname(*args, **kwargs)`, for example:

```
"A string $foo() with $bar(a, b, c, $moo(), d=23) etc."
```

Each arg/kwarg can also be another nested function. These will be executed
inside-out and their return will used as arguments for the enclosing function
(so the same as for regular Python function execution).

This is the base for all forms of embedded func-parsing, like inlinefuncs and
protfuncs. Each function available to use must be registered as a 'safe'
function for the parser to accept it. This is usually done in a module with
regular Python functions on the form:

```python
# in a module whose path is passed to the parser

def _helper(x):
    # use underscore to NOT make the function available as a callable

def funcname(*args, **kwargs):
    # this can be accessed as $funcname(*args, **kwargs)
    # it must always accept *args and **kwargs.
    ...
    return something
```

Usage:

```python
from evennia.utils.funcparser import FuncParser

parser = FuncParser("path.to.module_with_callables")
result = parser.parse("String with $funcname() in it")

```

The `FuncParser` also accepts a direct dict mapping of `{'name': callable, ...}`.

---

"""

import dataclasses
import inspect
import random

from django.conf import settings

from evennia.utils import logger, search
from evennia.utils.utils import (
    callables_from_module,
    crop,
    int2str,
    justify,
    make_iter,
    pad,
    safe_convert_to_types,
    variable_from_module,
)
from evennia.utils.verb_conjugation.conjugate import verb_actor_stance_components
from evennia.utils.verb_conjugation.pronouns import pronoun_to_viewpoints

# setup

_CLIENT_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
_MAX_NESTING = settings.FUNCPARSER_MAX_NESTING
_START_CHAR = settings.FUNCPARSER_START_CHAR
_ESCAPE_CHAR = settings.FUNCPARSER_ESCAPE_CHAR


@dataclasses.dataclass
class _ParsedFunc:
    """
    Represents a function parsed from the string

    """

    prefix: str = _START_CHAR
    funcname: str = ""
    args: list = dataclasses.field(default_factory=list)
    kwargs: dict = dataclasses.field(default_factory=dict)

    # state storage
    fullstr: str = ""
    infuncstr: str = ""
    rawstr: str = ""
    double_quoted: int = -1
    current_kwarg: str = ""
    open_lparens: int = 0
    open_lsquate: int = 0
    open_lcurly: int = 0
    exec_return = ""

    def get(self):
        return self.funcname, self.args, self.kwargs

    def __str__(self):
        return self.prefix + self.rawstr + self.infuncstr


class ParsingError(RuntimeError):
    """
    Failed to parse for some reason.
    """

    pass


class FuncParser:
    """
    Sets up a parser for strings containing `$funcname(*args, **kwargs)`
    substrings.

    """

    def __init__(
        self,
        callables,
        start_char=_START_CHAR,
        escape_char=_ESCAPE_CHAR,
        max_nesting=_MAX_NESTING,
        **default_kwargs,
    ):
        """
        Initialize the parser.

        Args:
            callables (str, module, list or dict): Where to find
                'safe' functions to make available in the parser. If a `dict`,
                it should be a direct mapping `{"funcname": callable, ...}`. If
                one or mode modules or module-paths, the module(s) are first checked
                for a dict `FUNCPARSER_CALLABLES = {"funcname", callable, ...}`. If
                no such variable exists, all callables in the module (whose name does
                not start with an underscore) will be made available to the parser.
            start_char (str, optional): A character used to identify the beginning
                of a parseable function. Default is `$`.
            escape_char (str, optional): Prepend characters with this to have
                them not count as a function. Default is the backtick, `\\\\`.
            max_nesting (int, optional): How many levels of nested function calls
                are allowed, to avoid exploitation. Default is 20.
            **default_kwargs: These kwargs will be passed into all callables. These
                kwargs can be overridden both by kwargs passed direcetly to `.parse` *and*
                by kwargs given directly in the string `$funcname` call. They are
                suitable for global defaults that is intended to be changed by the
                user. To guarantee a call always gets a particular kwarg, pass it
                into `.parse` as `**reserved_kwargs` instead.

        """
        if isinstance(callables, dict):
            loaded_callables = {**callables}
        else:
            # load all modules/paths in sequence. Later-added will override
            # earlier same-named callables (allows for overriding evennia defaults)
            loaded_callables = {}
            for module_or_path in make_iter(callables):
                callables_mapping = variable_from_module(
                    module_or_path, variable="FUNCPARSER_CALLABLES"
                )
                if callables_mapping:
                    try:
                        # mapping supplied in variable
                        loaded_callables.update(callables_mapping)
                    except ValueError:
                        raise ParsingError(
                            f"Failure to parse - {module_or_path}.FUNCPARSER_CALLABLES "
                            "(must be a dict {'funcname': callable, ...})"
                        )
                else:
                    # use all top-level variables
                    # (handles both paths and module instances
                    loaded_callables.update(callables_from_module(module_or_path))
        self.validate_callables(loaded_callables)
        self.callables = loaded_callables
        self.escape_char = escape_char
        self.start_char = start_char
        self.default_kwargs = default_kwargs

    def validate_callables(self, callables):
        """
        Validate the loaded callables. Each callable must support at least
        `funcname(*args, **kwargs)`.
        property.

        Args:
            callables (dict): A mapping `{"funcname": callable, ...}` to validate

        Raise:
            AssertionError: If invalid callable was found.

        Notes:
            This is also a good method to override for individual parsers
            needing to run any particular pre-checks.

        """
        for funcname, clble in callables.items():
            try:
                mapping = inspect.getfullargspec(clble)
            except TypeError:
                logger.log_trace(f"Could not run getfullargspec on {funcname}: {clble}")
            else:
                assert mapping.varargs, f"Parse-func callable '{funcname}' does not support *args."
                assert mapping.varkw, f"Parse-func callable '{funcname}' does not support **kwargs."

    def execute(self, parsedfunc, raise_errors=False, **reserved_kwargs):
        """
        Execute a parsed function

        Args:
            parsedfunc (_ParsedFunc): This dataclass holds the parsed details
                of the function.
            raise_errors (bool, optional): Raise errors. Otherwise return the
                string with the function unparsed.
            **reserved_kwargs: These kwargs are _guaranteed_ to always be passed into
                the callable on every call. It will override any default kwargs
                _and_ also a same-named kwarg given manually in the $funcname
                call. This is often used by Evennia to pass required data into
                the callable, for example the current Session for inlinefuncs.
        Returns:
            any: The result of the execution. If this is a nested function, it
                can be anything, otherwise it will be converted to a string later.
                Always a string on un-raised error (the unparsed function string).

        Raises:
            ParsingError, any: A `ParsingError` if the function could not be
            found, otherwise error from function definition. Only raised if
            `raise_errors` is `True`

        Notes:
            The kwargs passed into the callable will be a mixture of the
            `default_kwargs` passed into `FuncParser.__init__`, kwargs given
            directly in the `$funcdef` string, and the `reserved_kwargs` this
            function gets from `.parse()`. For colliding keys, funcdef-defined
            kwargs will override default kwargs while reserved kwargs will always
            override the other two.

        """
        funcname, args, kwargs = parsedfunc.get()
        func = self.callables.get(funcname)

        if not func:
            if raise_errors:
                available = ", ".join(f"'{key}'" for key in self.callables)
                raise ParsingError(
                    f"Unknown parsed function '{str(parsedfunc)}' (available: {available})"
                )
            return str(parsedfunc)

        # build kwargs in the proper priority order
        kwargs = {
            **self.default_kwargs,
            **kwargs,
            **reserved_kwargs,
            **{"funcparser": self, "raise_errors": raise_errors},
        }

        try:
            ret = func(*args, **kwargs)
            return ret
        except ParsingError:
            if raise_errors:
                raise
            return str(parsedfunc)
        except Exception:
            logger.log_trace()
            if raise_errors:
                raise
            return str(parsedfunc)

    def parse(
        self,
        string,
        raise_errors=False,
        escape=False,
        strip=False,
        return_str=True,
        **reserved_kwargs,
    ):
        """
        Use parser to parse a string that may or may not have
        `$funcname(*args, **kwargs)` - style tokens in it. Only the callables
        used to initiate the parser will be eligible for parsing.

        Args:
            string (str): The string to parse.
            raise_errors (bool, optional): By default, a failing parse just
                means not parsing the string but leaving it as-is. If this is
                `True`, errors (like not closing brackets) will lead to an
                ParsingError.
            escape (bool, optional): If set, escape all found functions so they
                are not executed by later parsing.
            strip (bool, optional): If set, strip any inline funcs from string
                as if they were not there.
            return_str (bool, optional): If set (default), always convert the
                parse result to a string, otherwise return the result of the
                latest called inlinefunc (if called separately).
            **reserved_kwargs: If given, these are guaranteed to _always_ pass
                as part of each parsed callable's **kwargs. These  override
                same-named default options given in `__init__` as well as any
                same-named kwarg given in the string function. This is because
                it is often used by Evennia to pass necessary kwargs into each
                callable (like the current Session object for inlinefuncs).

        Returns:
            str or any: The parsed string, or the same string on error (if
                `raise_errors` is `False`). This is always a string

        Raises:
            ParsingError: If a problem is encountered and `raise_errors` is True.

        """
        start_char = self.start_char
        escape_char = self.escape_char

        # replace e.g. $$ with \$ so we only need to handle one escape method
        string = string.replace(start_char + start_char, escape_char + start_char)

        # parsing state
        callstack = []

        double_quoted = -1
        open_lparens = 0  # open (
        open_lsquare = 0  # open [
        open_lcurly = 0  # open {
        escaped = False
        current_kwarg = ""
        exec_return = ""

        curr_func = None
        fullstr = ""  # final string
        infuncstr = ""  # string parts inside the current level of $funcdef (including $)
        literal_infuncstr = False

        for char in string:
            if escaped:
                # always store escaped characters verbatim
                if curr_func:
                    infuncstr += char
                else:
                    fullstr += char
                escaped = False
                continue

            if char == escape_char:
                # don't store the escape-char itself
                escaped = True
                continue

            if char == start_char:
                # start a new function definition (not escaped as $$)

                if curr_func:
                    # we are starting a nested funcdef
                    if len(callstack) >= _MAX_NESTING - 1:
                        # stack full - ignore this function
                        if raise_errors:
                            raise ParsingError(
                                "Only allows for parsing nesting function defs "
                                f"to a max depth of {_MAX_NESTING}."
                            )
                        infuncstr += char
                        continue
                    else:
                        # store state for the current func and stack it
                        curr_func.current_kwarg = current_kwarg
                        curr_func.infuncstr = infuncstr
                        curr_func.double_quoted = double_quoted
                        curr_func.open_lparens = open_lparens
                        curr_func.open_lsquare = open_lsquare
                        curr_func.open_lcurly = open_lcurly
                        # we must strip the remaining funcstr so it's not counted twice
                        curr_func.rawstr = curr_func.rawstr[: -len(infuncstr)]
                        current_kwarg = ""
                        infuncstr = ""
                        double_quoted = -1
                        open_lparens = 0
                        open_lsquare = 0
                        open_lcurly = 0
                        exec_return = ""
                        literal_infuncstr = False
                        callstack.append(curr_func)

                # start a new func
                curr_func = _ParsedFunc(prefix=char, fullstr=char)
                continue

            if not curr_func:
                # a normal piece of string
                fullstr += char
                # this must always be a string
                return_str = True
                continue

            # in a function def (can be nested)

            curr_func.rawstr += char

            if exec_return != "" and char not in (",=)"):
                # if exec_return is followed by any other character
                # than one demarking an arg,kwarg or function-end
                # it must immediately merge as a string
                infuncstr += str(exec_return)
                exec_return = ""

            if char == '"':  # note that this is the same as '\"'
                # a double quote = flip status
                if double_quoted == 0:
                    infuncstr = infuncstr[1:]
                    double_quoted = -1
                elif double_quoted > 0:
                    prefix = infuncstr[0:double_quoted]
                    infuncstr = prefix + infuncstr[double_quoted + 1 :]
                    double_quoted = -1
                else:
                    infuncstr += char
                    infuncstr = infuncstr.strip()
                    double_quoted = len(infuncstr) - 1
                    literal_infuncstr = True

                continue

            if double_quoted >= 0:
                # inside a string definition - this escapes everything else
                infuncstr += char
                continue

            # special characters detected inside function def
            if char == "(":
                if not curr_func.funcname:
                    # end of a funcdef name
                    curr_func.funcname = infuncstr
                    curr_func.fullstr += infuncstr + char
                    infuncstr = ""
                else:
                    # just a random left-parenthesis
                    infuncstr += char
                # track the open left-parenthesis
                open_lparens += 1
                continue

            if char in "[]":
                # a square bracket - start/end of a list?
                infuncstr += char
                open_lsquare += -1 if char == "]" else 1
                continue

            if char in "{}":
                # a curly bracket - start/end of dict/set?
                infuncstr += char
                open_lcurly += -1 if char == "}" else 1
                continue

            if char == "=":
                # beginning of a keyword argument
                if exec_return != "":
                    infuncstr = exec_return
                current_kwarg = infuncstr.strip()
                curr_func.kwargs[current_kwarg] = ""
                curr_func.fullstr += infuncstr + char
                infuncstr = ""
                continue

            if char in (",)"):
                # commas and right-parens may indicate arguments ending

                if open_lparens > 1:
                    # one open left-parens is ok (beginning of arglist), more
                    # indicate we are inside an unclosed, nested (, so
                    # we need to not count this as a new arg or end of funcdef.
                    infuncstr += char
                    open_lparens -= 1 if char == ")" else 0
                    continue

                if open_lcurly > 0 or open_lsquare > 0:
                    # also escape inside an open [... or {... structure
                    infuncstr += char
                    continue

                if exec_return != "":
                    # store the execution return as-received
                    if current_kwarg:
                        curr_func.kwargs[current_kwarg] = exec_return
                    else:
                        curr_func.args.append(exec_return)
                else:
                    if not literal_infuncstr:
                        infuncstr = infuncstr.strip()

                    # store a string instead
                    if current_kwarg:
                        curr_func.kwargs[current_kwarg] = infuncstr
                    elif literal_infuncstr or infuncstr.strip():
                        # don't store the empty string
                        curr_func.args.append(infuncstr)

                # note that at this point either exec_return or infuncstr will
                # be empty. We need to store the full string so we can print
                # it 'raw' in case this funcdef turns out to e.g. lack an
                # ending paranthesis
                curr_func.fullstr += str(exec_return) + infuncstr + char

                current_kwarg = ""
                exec_return = ""
                infuncstr = ""
                literal_infuncstr = False

                if char == ")":
                    # closing the function list - this means we have a
                    # ready function-def to run.
                    open_lparens = 0

                    if strip:
                        # remove function as if it returned empty
                        exec_return = ""
                    elif escape:
                        # get function and set it as escaped
                        exec_return = escape_char + curr_func.fullstr
                    else:
                        # execute the function - the result may be a string or
                        # something else
                        exec_return = self.execute(
                            curr_func, raise_errors=raise_errors, **reserved_kwargs
                        )

                    if callstack:
                        # unnest the higher-level funcdef from stack
                        # and continue where we were
                        curr_func = callstack.pop()
                        current_kwarg = curr_func.current_kwarg
                        if curr_func.infuncstr:
                            # if we have an ongoing string, we must merge the
                            # exec into this as a part of that string
                            infuncstr = curr_func.infuncstr + str(exec_return)
                            exec_return = ""
                        curr_func.infuncstr = ""
                        double_quoted = curr_func.double_quoted
                        open_lparens = curr_func.open_lparens
                        open_lsquare = curr_func.open_lsquare
                        open_lcurly = curr_func.open_lcurly
                    else:
                        # back to the top-level string - this means the
                        # exec_return should always be converted to a string.
                        curr_func = None
                        fullstr += str(exec_return)
                        if return_str:
                            exec_return = ""
                        infuncstr = ""
                        literal_infuncstr = False
                continue

            infuncstr += char

        if curr_func:
            # if there is a still open funcdef or defs remaining in callstack,
            # these are malformed (no closing bracket) and we should get their
            # strings as-is.
            callstack.append(curr_func)
            for inum, _ in enumerate(range(len(callstack))):
                funcstr = str(callstack.pop())
                if inum == 0 and funcstr.endswith(infuncstr):
                    # avoid double-echo of nested function calls. This should
                    # produce a good result most of the time, but it's not 100%
                    # guaranteed to, since it can ignore genuine duplicates
                    infuncstr = funcstr
                else:
                    infuncstr = funcstr + infuncstr

        if not return_str and exec_return != "":
            # return explicit return
            return exec_return

        # add the last bit to the finished string
        fullstr += infuncstr

        return fullstr

    def parse_to_any(
        self, string, raise_errors=False, escape=False, strip=False, **reserved_kwargs
    ):
        """
        This parses a string and if the string only contains a "$func(...)",
        the return will be the return value of that function, even if it's not
        a string. If mixed in with other strings, the result will still always
        be a string.

        Args:
            string (str): The string to parse.
            raise_errors (bool, optional): If unset, leave a failing (or
                unrecognized) inline function as unparsed in the string. If set,
                raise an ParsingError.
            escape (bool, optional): If set, escape all found functions so they
                are not executed by later parsing.
            strip (bool, optional): If set, strip any inline funcs from string
                as if they were not there.
            **reserved_kwargs: If given, these are guaranteed to _always_ pass
                as part of each parsed callable's **kwargs. These  override
                same-named default options given in `__init__` as well as any
                same-named kwarg given in the string function. This is because
                it is often used by Evennia to pass necessary kwargs into each
                callable (like the current Session object for inlinefuncs).

        Returns:
            any: The return from the callable. Or string if the callable is not
                given alone in the string.

        Raises:
            ParsingError: If a problem is encountered and `raise_errors` is True.

        Notes:
            This is a convenience wrapper for `self.parse(..., return_str=False)` which
            accomplishes the same thing.

        Examples:
            ::

                from ast import literal_eval
                from evennia.utils.funcparser import FuncParser


                def ret1(*args, **kwargs):
                    return 1

                parser = FuncParser({"lit": lit})

                assert parser.parse_to_any("$ret1()" == 1
                assert parser.parse_to_any("$ret1() and text" == '1 and text'

        """
        return self.parse(
            string,
            raise_errors=raise_errors,
            escape=escape,
            strip=strip,
            return_str=False,
            **reserved_kwargs,
        )


#
# Default funcparser callables. These are made available from this module's
# FUNCPARSER_CALLABLES.
#


def funcparser_callable_eval(*args, **kwargs):
    """
    Funcparser callable. This will combine safe evaluations to try to parse the
    incoming string into a python object. If it fails, the return will be same
    as the input.

    Args:
        string (str): The string to parse. Only simple literals or operators are allowed.

    Returns:
        any: The string parsed into its Python form, or the same as input.

    Examples:
        - `$py(1) -> 1`
        - `$py([1,2,3,4] -> [1, 2, 3]`
        - `$py(3 + 4) -> 7`

    """
    args, kwargs = safe_convert_to_types(("py", {}), *args, **kwargs)
    return args[0] if args else ""


def funcparser_callable_toint(*args, **kwargs):
    """Usage: $toint(43.0) -> 43"""
    inp = funcparser_callable_eval(*args, **kwargs)
    try:
        return int(inp)
    except TypeError:
        return inp
    except ValueError:
        return inp


def funcparser_callable_int2str(*args, **kwargs):
    """
    Usage: $int2str(1) -> 'one' etc, up to 12->twelve.

    Args:
        number (int): The number. If not an int, will be converted.

    Uses the int2str utility function.
    """
    if not args:
        return ""
    try:
        number = int(args[0])
    except ValueError:
        return args[0]
    return int2str(number)


def funcparser_callable_an(*args, **kwargs):
    """
    Usage: $an(thing) -> a thing

    Adds a/an depending on if the first letter of the given word is a consonant or not.

    """
    if not args:
        return ""
    item = str(args[0])
    if item and item[0] in "aeiouyAEIOUY":
        return f"an {item}"
    return f"a {item}"


def _apply_operation_two_elements(*args, operator="+", **kwargs):
    """
    Helper operating on two arguments

    Args:
        val1 (any): First value to operate on.
        val2 (any): Second value to operate on.

    Return:
        any: The result of val1 + val2. Values must be
            valid simple Python structures possible to add,
            such as numbers, lists etc. The $eval is usually
            better for non-list arithmetic.

    """
    args, kwargs = safe_convert_to_types((("py", "py"), {}), *args, **kwargs)
    if not len(args) > 1:
        return ""
    val1, val2 = args[0], args[1]
    try:
        if operator == "+":
            return val1 + val2
        elif operator == "-":
            return val1 - val2
        elif operator == "*":
            return val1 * val2
        elif operator == "/":
            return val1 / val2
    except Exception:
        if kwargs.get("raise_errors"):
            raise
        return ""


def funcparser_callable_add(*args, **kwargs):
    """Usage: `$add(val1, val2) -> val1 + val2`"""
    return _apply_operation_two_elements(*args, operator="+", **kwargs)


def funcparser_callable_sub(*args, **kwargs):
    """Usage: ``$sub(val1, val2) -> val1 - val2`"""
    return _apply_operation_two_elements(*args, operator="-", **kwargs)


def funcparser_callable_mult(*args, **kwargs):
    """Usage: `$mult(val1, val2) -> val1 * val2`"""
    return _apply_operation_two_elements(*args, operator="*", **kwargs)


def funcparser_callable_div(*args, **kwargs):
    """Usage: `$mult(val1, val2) -> val1 / val2`"""
    return _apply_operation_two_elements(*args, operator="/", **kwargs)


def funcparser_callable_round(*args, **kwargs):
    """
    Funcparser callable. Rounds an incoming float to a
    certain number of significant digits.

    Args:
        inp (str or number): If a string, it will attempt
            to be converted to a number first.
        significant (int): The number of significant digits.  Default is None -
            this will turn the result into an int.

    Returns:
        any: The rounded value or inp if inp was not a number.

    Examples:
        - `$round(3.5434343, 3) -> 3.543`
        - `$round($random(), 2)` - rounds random result, e.g `0.22`

    """
    if not args:
        return ""
    args, _ = safe_convert_to_types(((float, int), {}), *args, **kwargs)

    num, *significant = args
    significant = significant[0] if significant else 0
    try:
        return round(num, significant)
    except Exception:
        if kwargs.get("raise_errors"):
            raise
        return ""


def funcparser_callable_random(*args, **kwargs):
    """
    Funcparser callable. Returns a random number between 0 and 1, from 0 to a
    maximum value, or within a given range (inclusive).

    Args:
        minval (str, optional): Minimum value. If not given, assumed 0.
        maxval (str, optional): Maximum value.

    Notes:
        If either of the min/maxvalue has a '.' in it, a floating-point random
        value will be returned. Otherwise it will be an
        integer value in the given range.

    Examples:
        - `$random()` - random value [0 .. 1) (float).
        - `$random(5)` - random value [0..5] (int)
        - `$random(5.0)` - random value [0..5] (float)
        - `$random(5, 10)` - random value [5..10] (int)
        - `$random(5, 10.0)` - random value [5..10] (float)

    """
    args, _ = safe_convert_to_types((("py", "py"), {}), *args, **kwargs)

    nargs = len(args)
    if nargs == 1:
        # only maxval given
        minval, maxval = 0, args[0]
    elif nargs > 1:
        minval, maxval = args[:2]
    else:
        minval, maxval = 0, 1

    try:
        if isinstance(minval, float) or isinstance(maxval, float):
            return minval + ((maxval - minval) * random.random())
        else:
            return random.randint(minval, maxval)
    except Exception:
        if kwargs.get("raise_errors"):
            raise
        return ""


def funcparser_callable_randint(*args, **kwargs):
    """
    Usage: $randint(start, end):

    Legacy alias - always returns integers.

    """
    return int(funcparser_callable_random(*args, **kwargs))


def funcparser_callable_choice(*args, **kwargs):
    """
    FuncParser callable. Picks a random choice from a list.

    Args:
        listing (list): A list of items to randomly choose between.
            This will be converted from a string to a real list.
        *args: If multiple args are given, will pick one randomly from them.

    Returns:
        any: The randomly chosen element.

    Example:
        - `$choice(key, flower, house)`
        - `$choice([1, 2, 3, 4])`

    """
    if not args:
        return ""

    nargs = len(args)
    if nargs == 1:
        # this needs to be a list/tuple for this to make sense
        args, _ = safe_convert_to_types(("py", {}), args[0], **kwargs)
        args = make_iter(args[0]) if args else None
    else:
        # separate arg per entry
        converters = ["py" for _ in range(nargs)]
        args, _ = safe_convert_to_types((converters, {}), *args, **kwargs)

    if not args:
        return ""
    try:
        return random.choice(args)
    except Exception:
        if kwargs.get("raise_errors"):
            raise
        return ""


def funcparser_callable_pad(*args, **kwargs):
    """
    FuncParser callable. Pads text to given width, optionally with fill-characters

    Args:
        text (str): Text to pad.
        width (int): Width of padding.
        align (str, optional): Alignment of padding; one of 'c', 'l' or 'r'.
        fillchar (str, optional): Character used for padding. Defaults to a space.

    Example:
        - `$pad(text, 12, r, ' ') -> "        text"`
        - `$pad(text, width=12, align=c, fillchar=-) -> "----text----"`

    """
    if not args:
        return ""
    args, kwargs = safe_convert_to_types(
        ((str, int, str, str), {"width": int, "align": str, "fillchar": str}), *args, **kwargs
    )

    text, *rest = args
    nrest = len(rest)
    try:
        width = int(kwargs.get("width", rest[0] if nrest > 0 else _CLIENT_DEFAULT_WIDTH))
    except (TypeError, ValueError):
        width = _CLIENT_DEFAULT_WIDTH

    align = kwargs.get("align", rest[1] if nrest > 1 else "c")
    fillchar = kwargs.get("fillchar", rest[2] if nrest > 2 else " ")
    if align not in ("c", "l", "r"):
        align = "c"
    return pad(str(text), width=width, align=align, fillchar=fillchar)


def funcparser_callable_crop(*args, **kwargs):
    """
    FuncParser callable. Crops ingoing text to given widths.

    Args:
        text (str, optional): Text to crop.
        width (str, optional): Will be converted to an integer. Width of
            crop in characters.
        suffix (str, optional): End string to mark the fact that a part
            of the string was cropped. Defaults to `[...]`.

    Example:
        - `$crop(A long text, 10, [...]) -> "A lon[...]"`
        - `$crop(text, width=11, suffix='[...]) -> "A long[...]"`

    """
    if not args:
        return ""
    text, *rest = args
    nrest = len(rest)
    try:
        width = int(kwargs.get("width", rest[0] if nrest > 0 else _CLIENT_DEFAULT_WIDTH))
    except (TypeError, ValueError):
        width = _CLIENT_DEFAULT_WIDTH
    suffix = kwargs.get("suffix", rest[1] if nrest > 1 else "[...]")
    return crop(str(text), width=width, suffix=str(suffix))


def funcparser_callable_space(*args, **kwarg):
    """
    Usage: $space(43)

    Insert a length of space.

    """
    if not args:
        return ""
    try:
        width = int(args[0])
    except ValueError:
        width = 1
    return " " * width


def funcparser_callable_justify(*args, **kwargs):
    """
    Justify text across a width, default across screen width.

    Args:
        text (str): Text to justify.
        width (int, optional): Defaults to default screen width.
        align (str, optional): One of 'l', 'c', 'r' or 'f' for 'full'.
        indent (int, optional): Intendation of text block, if any.

    Returns:
        str: The justified text.

    Examples:
        - `$just(text, width=40)`
        - `$just(text, align=r, indent=2)`

    """
    if not args:
        return ""
    text, *rest = args
    lrest = len(rest)
    try:
        width = int(kwargs.get("width", rest[0] if lrest > 0 else _CLIENT_DEFAULT_WIDTH))
    except (TypeError, ValueError):
        width = _CLIENT_DEFAULT_WIDTH
    align = str(kwargs.get("align", rest[1] if lrest > 1 else "f"))
    try:
        indent = int(kwargs.get("indent", rest[2] if lrest > 2 else 0))
    except (TypeError, ValueError):
        indent = 0
    return justify(str(text), width=width, align=align, indent=indent)


# legacy for backwards compatibility
def funcparser_callable_left_justify(*args, **kwargs):
    "Usage: $ljust(text)"
    return funcparser_callable_justify(*args, align="l", **kwargs)


def funcparser_callable_right_justify(*args, **kwargs):
    "Usage: $rjust(text)"
    return funcparser_callable_justify(*args, align="r", **kwargs)


def funcparser_callable_center_justify(*args, **kwargs):
    "Usage: $cjust(text)"
    return funcparser_callable_justify(*args, align="c", **kwargs)


def funcparser_callable_clr(*args, **kwargs):
    """
    FuncParser callable. Colorizes nested text.

    Args:
        startclr (str, optional): An ANSI color abbreviation without the
            prefix `|`, such as `r` (red foreground) or `[r` (red background).
        text (str, optional): Text
        endclr (str, optional): The color to use at the end of the string. Defaults
            to `|n` (reset-color).
    Kwargs:
        color (str, optional): If given,

    Example:
        - `$clr(r, text, n) -> "|rtext|n"`
        - `$clr(r, text) -> "|rtext|n`
        - `$clr(text, start=r, end=n) -> "|rtext|n"`

    """
    if not args:
        return ""

    startclr, text, endclr = "", "", ""
    if len(args) > 1:
        # $clr(pre, text, post))
        startclr, *rest = args
        if rest:
            text, *endclr = rest
            if endclr:
                endclr = endclr[0]
    else:
        # $clr(text, start=pre, end=post)
        text = args[0]
        startclr = kwargs.get("start", "")
        endclr = kwargs.get("end", "")

    startclr = "|" + startclr if startclr else ""
    endclr = "|" + endclr if endclr else ("|n" if startclr else "")
    return f"{startclr}{text}{endclr}"


def funcparser_callable_pluralize(*args, **kwargs):
    """
    FuncParser callable. Handles pluralization of a word.

    Args:
        singular_word (str): The base (singular) word to optionally pluralize
        number (int): The number of elements; if 1 (or 0), use `singular_word` as-is,
            otherwise use plural form.
        plural_word (str, optional): If given, this will be used if `number`
            is greater than one. If not given, we simply add 's' to the end of
            `singular_word`.

    Example:
        - `$pluralize(thing, 2)` -> "things"
        - `$pluralize(goose, 18, geese)` -> "geese"

    """
    if not args:
        return ""
    nargs = len(args)
    if nargs > 2:
        singular_word, number, plural_word = args[:3]
    elif nargs > 1:
        singular_word, number = args[:2]
        plural_word = f"{singular_word}s"
    else:
        singular_word, number = args[0], 1
    return singular_word if abs(int(number)) in (0, 1) else plural_word


def funcparser_callable_search(*args, caller=None, access="control", **kwargs):
    """
    FuncParser callable. Finds an object based on name or #dbref. Note that
    this requries the parser be called with the caller's Session for proper
    security. If called without session, the call is aborted.

    Args:
        query (str): The key or dbref to search for. This can consist of any args used
            for one of the regular search methods. Also kwargs will be passed into
            the search (except the kwargs given below)

    Keyword Args:
        return_list (bool): If set, return a list of objects with
            0, 1 or more matches to `query`. Defaults to False.
        type (str): One of 'obj', 'account', 'script'
        caller (Entity): Supplied to Parser. This is required and will
            be passed into the access check for the entity being searched for.
            The 'control' permission is required.
        access (str): Which locktype access to check. Unset to disable the
            security check.
        **kwargs: Will be passed into the main search.

    Returns:
        any: An entity match or None if no match or a list if `return_list` is set.

    Raise:
        ParsingError: If zero/multimatch and `return_list` is False, or caller was not
            passed into parser.

    Examples:
        - "$search(#233)"
        - "$search(Tom, type=account)"
        - "$search(meadow, return_list=True)"
        - "$search(beach, category=outdoors, type=tag)

    """
    # clean out funcparser-specific kwargs so we can use the kwargs for
    # searching
    search_kwargs = {
        key: value
        for key, value in kwargs.items()
        if key not in ("funcparser", "raise_errors", "type", "return_list")
    }
    return_list = str(kwargs.pop("return_list", "false")).lower() == "true"

    if not args:
        return [] if return_list else None
    if not caller:
        raise ParsingError("$search requires a `caller` passed to the parser.")

    typ = kwargs.get("type", "obj")
    targets = []
    if typ == "obj":
        targets = search.search_object(*args, **search_kwargs)
    elif typ == "account":
        targets = search.search_account(*args, **search_kwargs)
    elif typ == "script":
        targets = search.search_script(*args, **search_kwargs)
    elif typ == "tag":
        targets = search.search_object_by_tag(*args, **search_kwargs)

    if not targets:
        if return_list:
            return []
        raise ParsingError(f"$search: Query '{args[0]}' gave no matches.")

    if len(targets) > 1 and not return_list:
        raise ParsingError(
            "$search: Query '{query}' found {num} matches. "
            "Set return_list=True to accept a list".format(query=query, num=len(targets))
        )

    for target in targets:
        if not target.access(caller, access):
            raise ParsingError("$search Cannot add found entity - access failure.")

    return list(targets) if return_list else targets[0]


def funcparser_callable_search_list(*args, caller=None, access="control", **kwargs):
    """
    Usage: $objlist(#123)

    Legacy alias for search with a return_list=True kwarg preset.

    """
    return funcparser_callable_search(
        *args, caller=caller, access=access, return_list=True, **kwargs
    )


def funcparser_callable_you(
    *args, caller=None, receiver=None, mapping=None, capitalize=False, **kwargs
):
    """
    Usage: $you() or $you(key)

    Replaces with you for the caller of the string, with the display_name
    of the caller for others.

    Keyword Args:
        caller (Object): The 'you' in the string. This is used unless another
            you-key is passed to the callable in combination with `mapping`.
        receiver (Object): The recipient of the string.
        mapping (dict, optional): This is a mapping `{key:Object, ...}` and is
            used to find which object `$you(key)` refers to. If not given, the
            `caller` kwarg is used.
        capitalize (bool): Passed by the You helper, to capitalize you.

    Returns:
        str: The parsed string.

    Raises:
        ParsingError: If `caller` and `receiver` were not supplied.

    Notes:
        The kwargs should be passed the to parser directly.

    Examples:
        This can be used by the say or emote hooks to pass actor stance
        strings. This should usually be combined with the $conj() callable.

        - `With a grin, $you() $conj(jump) at $you(tommy).`

        The caller-object will see "With a grin, you jump at Tommy."
        Tommy will see "With a grin, CharName jumps at you."
        Others will see "With a grin, CharName jumps at Tommy."

    """
    if args and mapping:
        # this would mean a $you(key) form
        caller = mapping.get(args[0], None)

    if not (caller and receiver):
        raise ParsingError("No caller or receiver supplied to $you callable.")

    capitalize = bool(capitalize)
    if caller == receiver:
        return "You" if capitalize else "you"
    return (
        caller.get_display_name(looker=receiver)
        if hasattr(caller, "get_display_name")
        else str(caller)
    )


def funcparser_callable_you_capitalize(
    *args, you=None, receiver=None, mapping=None, capitalize=True, **kwargs
):
    """
    Usage: $You() - capitalizes the 'you' output.

    """
    return funcparser_callable_you(
        *args, you=you, receiver=receiver, mapping=mapping, capitalize=capitalize, **kwargs
    )


def funcparser_callable_your(
    *args, caller=None, receiver=None, mapping=None, capitalize=False, **kwargs
):
    """
    Usage: $your() or $your(key)

    Replaces with your for the caller of the string, with the display_name +'s
    of the caller for others.

    Keyword Args:
        caller (Object): The 'your' in the string. This is used unless another
            your-key is passed to the callable in combination with `mapping`.
        receiver (Object): The recipient of the string.
        mapping (dict, optional): This is a mapping `{key:Object, ...}` and is
            used to find which object `$you(key)` refers to. If not given, the
            `caller` kwarg is used.
        capitalize (bool): Passed by the You helper, to capitalize you.

    Returns:
        str: The parsed string.

    Raises:
        ParsingError: If `caller` and `receiver` were not supplied.

    Notes:
        The kwargs should be passed the to parser directly.

    Examples:
        This can be used by the say or emote hooks to pass actor stance
        strings.

        - `$your() pet jumps at $you(tommy).`

        The caller-object will see "Your pet jumps Tommy."
        Tommy will see "CharName's pet jumps at you."
        Others will see "CharName's pet jumps at Tommy."

    """
    if args and mapping:
        # this would mean a $your(key) form
        caller = mapping.get(args[0], None)

    if not (caller and receiver):
        raise ParsingError("No caller or receiver supplied to $your callable.")

    capitalize = bool(capitalize)
    if caller == receiver:
        return "Your" if capitalize else "your"

    name = (
        caller.get_display_name(looker=receiver)
        if hasattr(caller, "get_display_name")
        else str(caller)
    )

    return name + "'s"


def funcparser_callable_your_capitalize(
    *args, you=None, receiver=None, mapping=None, capitalize=True, **kwargs
):
    """
    Usage: $Your() - capitalizes the 'your' output.

    """
    return funcparser_callable_your(
        *args, you=you, receiver=receiver, mapping=mapping, capitalize=capitalize, **kwargs
    )


def funcparser_callable_conjugate(*args, caller=None, receiver=None, mapping=None, **kwargs):
    """
    Usage: $conj(word, [key])

    Conjugate a verb according to if it should be 2nd or third person.

    Keyword Args:
        caller (Object): The object who represents 'you' in the string.
        receiver (Object): The recipient of the string.
        mapping (dict, optional): This is a mapping `{key:Object, ...}` and is
            used to find which object the optional `key` argument refers to. If not given,
            the `caller` kwarg is used.

    Returns:
        str: The parsed string.

    Raises:
        ParsingError: If `you` and `recipient` were not both supplied.

    Notes:
        Note that the verb will not be capitalized.

    Examples:
        This is often used in combination with the $you/You callables.

        - `With a grin, $you() $conj(jump)`

        You will see "With a grin, you jump."
        Others will see "With a grin, CharName jumps."

    """
    if not args:
        return ""
    if not (caller and receiver):
        raise ParsingError("No caller/receiver supplied to $conj callable")

    verb, *options = args
    obj = caller
    if mapping and options:
        # get the correct referenced object from the mapping, or fall back to caller
        obj = mapping.get(options[0], caller)

    second_person_str, third_person_str = verb_actor_stance_components(verb)
    return second_person_str if obj == receiver else third_person_str


def funcparser_callable_conjugate_for_pronouns(
    *args, caller=None, receiver=None, mapping=None, **kwargs
):
    """
    Usage: $pconj(word, [key])

    Conjugate a verb according to if it should be 2nd or third person, respecting the
    singular/plural gendering for third person.

    Keyword Args:
        caller (Object): The object who represents 'you' in the string.
        receiver (Object): The recipient of the string.
        mapping (dict, optional): This is a mapping `{key:Object, ...}` and is
            used to find which object the optional `key` argument refers to. If not given,
            the `caller` kwarg is used.

    Returns:
        str: The parsed string.

    Raises:
        ParsingError: If `you` and `recipient` were not both supplied.

    Notes:
        Note that the verb will not be capitalized.

    Examples:
        This is often used in combination with the $pron/Pron callables.

        - `With a grin, $pron(you) $pconj(jump)`

        You will see "With a grin, you jump."
        With your gender as "male", others will see "With a grin, he jumps."
        With your gender as "plural", others will see "With a grin, they jump."

    """
    if not args:
        return ""
    if not (caller and receiver):
        raise ParsingError("No caller/receiver supplied to $conj callable")

    verb, *options = args
    obj = caller
    if mapping and options:
        # get the correct referenced object from the mapping, or fall back to caller
        obj = mapping.get(options[0], caller)

    # identify whether the 3rd person form should be singular or plural
    plural = False
    if hasattr(obj, "gender"):
        if callable(obj.gender):
            plural = obj.gender() == "plural"
        else:
            plural = obj.gender == "plural"

    second_person_str, third_person_str = verb_actor_stance_components(verb, plural=plural)
    return second_person_str if obj == receiver else third_person_str


def funcparser_callable_pronoun(
    *args, caller=None, receiver=None, mapping=None, capitalize=False, **kwargs
):
    """

    Usage: $pron(word, [options], [key])

    Adjust pronouns to the expected form. Pronouns are words you use instead of a
    proper name, such as 'him', 'herself', 'theirs' etc. These look different
    depending on who sees the outgoing string.

    The parser maps between this table ...

    ====================  =======  =======  ==========  ==========  ===========
    1st/2nd person        Subject  Object   Possessive  Possessive  Reflexive
                          Pronoun  Pronoun  Adjective   Pronoun     Pronoun
    ====================  =======  =======  ==========  ==========  ===========
    1st person               I        me        my        mine       myself
    1st person plural       we       us        our        ours       ourselves
    2nd person              you      you       your       yours      yourself
    2nd person plural       you      you       your       yours      yourselves
    ====================  =======  =======  ==========  ==========  ===========

    ... and this table (and vice versa).

    ====================  =======  =======  ==========  ==========  ===========
    3rd person            Subject  Object   Possessive  Possessive  Reflexive
                          Pronoun  Pronoun  Adjective   Pronoun     Pronoun
    ====================  =======  =======  ==========  ==========  ===========
    3rd person male         he       him       his        his        himself
    3rd person female       she      her       her        hers       herself
    3rd person neutral      it       it        its                   itself
    3rd person plural       they    them       their      theirs     themselves
    ====================  =======  =======  ==========  ==========  ===========

    This system will examine `caller` for either a property or a callable `.gender` to
    get a default gender fallback (if not specified in the call). If a callable,
    `.gender` will be called without arguments and should return a string
    `male`/`female`/`neutral`/`plural` (plural is considered a gender for this purpose).
    If no `gender` property/callable is found, `neutral` is used as a fallback.

    The pronoun-type default (if not specified in call) is `subject pronoun`.

    Args:
        pronoun (str): Input argument to parsed call. This can be any of the pronouns
            in the table above. If given in 1st/second form, they will be mappped to
            3rd-person form for others viewing the message (but will need extra input
            via the `gender`, see below). If given on 3rd person form, this will be
            mapped to 2nd person form for `caller` unless `viewpoint` is specified
            in options.
        options (str, optional): A space- or comma-separated string detailing `pronoun_type`,
            `gender`/`plural` and/or `viewpoint` to help the mapper differentiate between
            non-unique cases (such as if `you` should become `him` or `they`).
            Allowed values are:

            - `subject pronoun`/`subject`/`sp` (I, you, he, they)
            - `object pronoun`/`object/`/`op`  (me, you, him, them)
            - `possessive adjective`/`adjective`/`pa` (my, your, his, their)
            - `possessive pronoun`/`pronoun`/`pp`  (mine, yours, his, theirs)
            - `male`/`m`
            - `female`/`f`
            - `neutral`/`n`
            - `plural`/`p`
            - `1st person`/`1st`/`1`
            - `2nd person`/`2nd`/`2`
            - `3rd person`/`3rd`/`3`
        key (str, optional): If a mapping is provided, a string defining which object to
            reference when finding the correct pronoun. If not provided, it defaults
            to `caller`

    Keyword Args:

        caller (Object): The object creating the string. If this has a property 'gender',
            it will be checked for a string 'male/female/neutral' to determine
            the 3rd person gender (but if `pronoun_type` contains a gender
            component, that takes precedence). Provided automatically to the
            funcparser.
        receiver (Object): The recipient of the string. This being the same as
            `caller` or not helps determine 2nd vs 3rd-person forms. This is
            provided automatically by the funcparser.
        mapping (dict, optional): This is a mapping `{key:Object, ...}` and is
            used to find which object the optional `key` argument refers to. If not given,
            the `caller` kwarg is used.
        capitalize (bool): The input retains its capitalization. If this is set the output is
            always capitalized.

    Examples:

        ======================  =============    ===========
        Input                   caller sees      others see
        ======================  =============    ===========
        $pron(I, m)             I                he
        $pron(you,fo)           you              her
        $pron(yourself)         yourself         itself
        $pron(its)              your             its
        $pron(you,op,p)         you              them
        ======================  =============    ===========

    Notes:
        There is no option to specify reflexive pronouns since they are all unique
        and the mapping can always be auto-detected.

    """
    if not args:
        return ""
    # by default, we use the caller as the object being referred to
    obj = caller

    pronoun, *options = args
    if options and mapping:
        # check if the last argument is a valid mapping key
        if options[-1] in mapping:
            # get the object and remove the key from options
            obj = mapping[options[-1]]
            options = options[:-1]

    # options is either multiple args or a space-separated string
    if len(options) == 1:
        options = options[0]

    # default differentiators
    default_pronoun_type = "subject pronoun"
    default_gender = "neutral"
    default_viewpoint = "2nd person"

    if hasattr(obj, "gender"):
        if callable(obj.gender):
            default_gender = obj.gender()
        else:
            default_gender = obj.gender

    if "viewpoint" in kwargs:
        # passed into FuncParser initialization
        default_viewpoint = kwargs["viewpoint"]

    pronoun_1st_or_2nd_person, pronoun_3rd_person = pronoun_to_viewpoints(
        pronoun,
        options,
        pronoun_type=default_pronoun_type,
        gender=default_gender,
        viewpoint=default_viewpoint,
    )

    if capitalize:
        pronoun_1st_or_2nd_person = pronoun_1st_or_2nd_person.capitalize()
        pronoun_3rd_person = pronoun_3rd_person.capitalize()

    return pronoun_1st_or_2nd_person if obj == receiver else pronoun_3rd_person


def funcparser_callable_pronoun_capitalize(
    *args, caller=None, receiver=None, capitalize=True, **kwargs
):
    """
    Usage: $Pron(word, [options]) - always maps to a capitalized word.

    """
    return funcparser_callable_pronoun(
        *args, caller=caller, receiver=receiver, capitalize=capitalize, **kwargs
    )


# these are made available as callables by adding 'evennia.utils.funcparser' as
# a callable-path when initializing the FuncParser.

FUNCPARSER_CALLABLES = {
    # 'standard' callables
    # eval and arithmetic
    "eval": funcparser_callable_eval,
    "add": funcparser_callable_add,
    "sub": funcparser_callable_sub,
    "mult": funcparser_callable_mult,
    "div": funcparser_callable_div,
    "round": funcparser_callable_round,
    "toint": funcparser_callable_toint,
    # randomizers
    "random": funcparser_callable_random,
    "randint": funcparser_callable_randint,
    "choice": funcparser_callable_choice,
    # string manip
    "pad": funcparser_callable_pad,
    "crop": funcparser_callable_crop,
    "just": funcparser_callable_justify,
    "ljust": funcparser_callable_left_justify,
    "rjust": funcparser_callable_right_justify,
    "cjust": funcparser_callable_center_justify,
    "justify": funcparser_callable_justify,  # aliases for backwards compat
    "justify_left": funcparser_callable_left_justify,
    "justify_right": funcparser_callable_right_justify,
    "justify_center": funcparser_callable_center_justify,
    "space": funcparser_callable_space,
    "clr": funcparser_callable_clr,
    "pluralize": funcparser_callable_pluralize,
    "int2str": funcparser_callable_int2str,
    "an": funcparser_callable_an,
}

SEARCHING_CALLABLES = {
    # requires `caller` and optionally `access` to be passed into parser
    "search": funcparser_callable_search,
    "obj": funcparser_callable_search,  # aliases for backwards compat
    "objlist": funcparser_callable_search_list,
    "dbref": funcparser_callable_search,
}

ACTOR_STANCE_CALLABLES = {
    # requires `you`, `receiver` and `mapping` to be passed into parser
    "you": funcparser_callable_you,
    "You": funcparser_callable_you_capitalize,
    "your": funcparser_callable_your,
    "Your": funcparser_callable_your_capitalize,
    "obj": funcparser_callable_you,
    "Obj": funcparser_callable_you_capitalize,
    "conj": funcparser_callable_conjugate,
    "pconj": funcparser_callable_conjugate_for_pronouns,
    "pron": funcparser_callable_pronoun,
    "Pron": funcparser_callable_pronoun_capitalize,
    **FUNCPARSER_CALLABLES,
}
