#
# Copyright (c), 2018-2020, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present
# distribution, or http://opensource.org/licenses/MIT.
#
# @author Davide Brunato <brunato@sissa.it>
#
"""
XPath 3.0 implementation - part 3 (functions)
"""
import sys
import decimal
import os
import re
import codecs
import math
from copy import copy
from itertools import zip_longest
from typing import cast, Any, Dict, List, Optional, Tuple, Union, Type, Set
from urllib.parse import urlsplit
from urllib.request import urlopen
from urllib.error import URLError

from elementpath._typing import Iterator
from elementpath.aliases import Emptiable
from elementpath.protocols import ElementProtocol
from elementpath.exceptions import ElementPathError
from elementpath.tdop import MultiLabel
from elementpath.helpers import OCCURRENCE_INDICATORS, Patterns, \
    is_xml_codepoint, node_position
from elementpath.namespaces import get_expanded_name, split_expanded_name, \
    XPATH_FUNCTIONS_NAMESPACE, XSD_NAMESPACE
from elementpath.datatypes import xsd10_atomic_types, NumericProxy, QName, Date10, \
    DateTime10, Time, AnyURI, UntypedAtomic, AtomicType, NumericType, NMToken, \
    Idref, Entity
from elementpath.sequence_types import is_sequence_type, match_sequence_type
from elementpath.etree import defuse_xml
from elementpath.xpath_nodes import XPathNode, ElementNode, NamespaceNode, \
    DocumentNode, EtreeElementNode, SchemaElementNode
from elementpath.tree_builders import get_node_tree
from elementpath.xpath_tokens import XPathToken, ValueToken, XPathFunction, XPathConstructor
from elementpath.serialization import get_serialization_params, serialize_to_xml, \
    serialize_to_json
from elementpath.xpath_context import ContextType, ItemType, FunctionArgType, \
    XPathContext, XPathSchemaContext
from elementpath.regex import translate_pattern, RegexError

from ._xpath30_operators import XPath30Parser
from .xpath30_helpers import UNICODE_DIGIT_PATTERN, DECIMAL_DIGIT_PATTERN, \
    MODIFIER_PATTERN, decimal_to_string, int_to_roman, int_to_alphabetic, \
    format_digits, int_to_words, parse_datetime_picture, parse_datetime_marker, \
    ordinal_suffix

if sys.version_info < (3, 9):
    zoneinfo = None
else:
    import zoneinfo

FORMAT_INTEGER_TOKENS = {'A', 'a', 'i', 'I', 'w', 'W', 'Ww'}

DECL_PARAM_PATTERN = re.compile(r'([^\d\W][\w.\-\u00B7\u0300-\u036F\u203F\u2040]*)\s*=\s*')
EXPONENT_PIC = re.compile(r'\d[eE]\d')

register = XPath30Parser.register
method = XPath30Parser.method
function = XPath30Parser.function


###
# 'inline function' expression or 'function test'

class _InlineFunction(XPathFunction):

    symbol = lookup_name = 'function'
    lbp = 90
    rbp = 90
    label: Union[str, MultiLabel] = MultiLabel('inline function', 'function test')

    body: Optional[XPathToken] = None
    "Body of anonymous inline function."

    variables: Optional[Dict[str, Any]] = None
    "In-scope variables linked by let and for expressions and arguments."

    varnames: Optional[List[str]] = None
    "Inline function arguments varnames."

    def __str__(self) -> str:
        return str(self.label)

    @property
    def source(self) -> str:
        if self.label == 'function test':
            if len(self.sequence_types) == 1 and self.sequence_types[0] == '*':
                return 'function(*)'
            else:
                return 'function(%s) as %s' % (
                    ', '.join(self.sequence_types[:-1]), self.sequence_types[-1]
                )

        arguments = []
        return_type = ''
        for var, sq in zip_longest(self, self.sequence_types):
            if var is None:
                if sq != 'item()*':
                    return_type = f' as {sq}'
            elif sq is None or sq == 'item()*':
                arguments.append(var.source)
            else:
                arguments.append(f'{var.source} as {sq}')

        return '%s(%s)%s {%s}' % (
            self.symbol,
            ', '.join(arguments),
            return_type,
            getattr(self.body, 'source', '')
        )

    def __call__(self, *args: FunctionArgType,
                 context: Optional[XPathContext] = None) -> Any:

        def get_argument(v: Any) -> Any:
            if isinstance(v, XPathToken) and not isinstance(v, XPathFunction):
                v = v.evaluate(context)

            if isinstance(v, XPathFunction) and sequence_type.startswith('function('):
                if not v.match_function_test(sequence_type, as_argument=True):
                    msg = "argument {!r}: {} does not match sequence type {}"
                    raise self.error('XPTY0004', msg.format(varname, v, sequence_type))

            elif not match_sequence_type(v, sequence_type, self.parser):
                _v = self.cast_to_primitive_type(v, sequence_type)
                if not match_sequence_type(_v, sequence_type, self.parser):
                    msg = "argument '${}': {} does not match sequence type {}"
                    raise self.error('XPTY0004', msg.format(varname, v, sequence_type))
                return _v
            return v

        sequence_type: str
        self.check_arguments_number(len(args))

        context = copy(context)
        if self.variables and context is not None:
            context.variables.update(self.variables)

        if self.varnames is None:
            self.varnames = []

        assert self.body is not None
        if self.label == 'inline partial function':
            k = 0
            for varname, sequence_type, tk in zip(self.varnames, self.sequence_types, self):
                if context is None:
                    raise self.missing_context()

                if tk.symbol != '?' or tk:
                    context.variables[varname] = tk.evaluate(context)
                else:
                    context.variables[varname] = get_argument(args[k])
                    k += 1

            result = self.body.evaluate(context)
        else:
            if context is None:
                raise self.missing_context()
            elif not args and self:
                if isinstance(context.item, DocumentNode):
                    if isinstance(context.root, DocumentNode):
                        context.item = context.root.getroot()
                    elif context.root is not None:
                        context.item = context.root

                args = cast(Tuple[FunctionArgType], (context.item,))

            partial_function = False
            if self.variables is None:
                self.variables = {}

            for varname, sequence_type, value in zip(self.varnames, self.sequence_types, args):
                if isinstance(value, XPathToken) and value.symbol == '?':
                    partial_function = True
                else:
                    context.variables[varname] = get_argument(value)

            if partial_function:
                self.to_partial_function()
                return self

            result = self.body.evaluate(context)

        return self.validated_result(result)

    def nud(self) -> Union[XPathFunction, XPathToken]:  # type: ignore[override]
        def append_sequence_type(tk: XPathToken) -> None:
            if tk.symbol == '(' and len(tk) == 1:
                tk = tk[0]

            sequence_type = tk.source
            next_symbol = self.parser.next_token.symbol
            if sequence_type != 'empty-sequence()' and next_symbol in OCCURRENCE_INDICATORS:
                self.parser.advance()
                sequence_type += next_symbol
                tk.occurrence = next_symbol

            if not is_sequence_type(sequence_type, self.parser):
                if 'xs:NMTOKENS' in sequence_type \
                        or 'xs:ENTITIES' in sequence_type \
                        or 'xs:IDREFS' in sequence_type:
                    msg = "a list type cannot be used in a function signature"
                    raise self.error('XPST0051', msg)
                raise self.error('XPST0003', "a sequence type expected")

            assert isinstance(self.sequence_types, list)
            self.sequence_types.append(sequence_type)

        if self.parser.next_token.symbol != '(':
            return self.as_name()

        self.parser.advance('(')
        self.sequence_types = []

        if self.parser.next_token.symbol in ('$', ')'):
            self.label = 'inline function'
            self.varnames = []

            while self.parser.next_token.symbol != ')':
                self.parser.next_token.expected('$')
                variable = self.parser.expression(5)
                varname = variable[0].value
                assert isinstance(varname, str)
                if varname in self.varnames:
                    raise self.error('XQST0039')

                self.append(variable)
                self.varnames.append(varname)

                if self.parser.next_token.symbol == 'as':
                    self.parser.advance('as')
                    token = self.parser.expression(90)
                    append_sequence_type(token)
                else:
                    self.sequence_types.append('item()*')

                self.parser.next_token.expected(')', ',')
                if self.parser.next_token.symbol == ',':
                    self.parser.advance()
                    self.parser.next_token.unexpected(')')

            self.parser.advance(')')

        elif self.parser.next_token.symbol == '*':
            self.label = 'function test'
            self.append(self.parser.advance('*'))
            self.sequence_types.append('*')
            self.parser.advance(')')
            return self
        else:
            self.label = 'function test'
            while True:
                token = self.parse_sequence_type()
                append_sequence_type(token)
                self.append(token)
                if self.parser.next_token.symbol != ',':
                    break
                self.parser.advance(',')
            self.parser.advance(')')

        # Add function return sequence type
        if self.parser.next_token.symbol != 'as':
            self.sequence_types.append('item()*')
        else:
            self.parser.advance('as')
            if self.parser.next_token.label not in ('kind test', 'sequence type', 'function test'):
                self.parser.expected_next('(name)', ':')

            token = self.parser.expression(rbp=90)
            append_sequence_type(token)

        if self.label == 'inline function':
            if self.parser.next_token.symbol != '{' and not self:
                self.label = 'function test'
            else:
                self.parser.advance('{')
                if self.parser.next_token.symbol != '}':
                    self.body = self.parser.expression()
                elif self.parser.version >= '3.1':
                    self.body = ValueToken(self.parser, value=[])
                else:
                    raise self.wrong_syntax("inline function has an empty body")
                self.parser.advance('}')

        return self

    def evaluate(self, context: ContextType = None) -> Union[ItemType, List[ItemType]]:
        if context is None:
            raise self.missing_context()
        elif self.label.endswith('function'):
            self.variables = context.variables.copy()  # like a closure
            return self

        # A function test
        if not isinstance(context.item, XPathFunction):
            return []
        elif self.source == 'function(*)':
            return context.item
        elif context.item.match_function_test(self.sequence_types):
            return context.item
        else:
            return []

    def to_partial_function(self) -> None:
        assert self.label != 'function test', "an effective inline function required"

        nargs = len([tk and not tk for tk in self._items if tk.symbol == '?'])
        assert nargs, "a partial function requires at least a placeholder token"

        self._name = None
        self.label = 'inline partial function'
        self.nargs = nargs


XPath30Parser.symbol_table['function'] = _InlineFunction


###
# Mathematical functions
@method(function('pi', prefix='math', nargs=0,
                 sequence_types=('xs:double',)))
def evaluate_pi_function(self: XPathFunction, context: ContextType = None) -> float:
    return math.pi


@method(function('exp', prefix='math', nargs=1,
                 sequence_types=('xs:double?', 'xs:double?')))
def evaluate_exp_function(self: XPathFunction, context: ContextType = None) -> Emptiable[float]:
    arg: NumericType = self.get_argument(self.context or context, cls=NumericProxy)
    if arg is None:
        return []
    return math.exp(arg)


@method(function('exp10', prefix='math', nargs=1,
                 sequence_types=('xs:double?', 'xs:double?')))
def evaluate_exp10_function(self: XPathFunction, context: ContextType = None) -> Emptiable[float]:
    arg: NumericType = self.get_argument(self.context or context, cls=NumericProxy)
    if arg is None:
        return []
    return float(10 ** arg)


@method(function('log', prefix='math', nargs=1,
                 sequence_types=('xs:double?', 'xs:double?')))
def evaluate_log_function(self: XPathFunction, context: ContextType = None) -> Emptiable[float]:
    arg: Optional[NumericType] = self.get_argument(self.context or context, cls=NumericProxy)
    if arg is None:
        return []
    return float('-inf') if not arg else math.nan if arg <= -1 else math.log(arg)


@method(function('log10', prefix='math', nargs=1,
                 sequence_types=('xs:double?', 'xs:double?')))
def evaluate_log10_function(self: XPathFunction, context: ContextType = None) -> Emptiable[float]:
    arg: Optional[NumericType] = self.get_argument(self.context or context, cls=NumericProxy)
    if arg is None:
        return []
    return float('-inf') if not arg else math.nan if arg <= -1 else math.log10(arg)


@method(function('pow', prefix='math', nargs=2,
                 sequence_types=('xs:double?', 'xs:numeric', 'xs:double?')))
def evaluate_pow_function(self: XPathFunction, context: ContextType = None) -> Emptiable[float]:
    if self.context is not None:
        context = self.context

    x = self.get_argument(context, cls=NumericProxy)
    y = self.get_argument(context, index=1, required=True, cls=NumericProxy)
    if x is None:
        return []
    elif not x and y < 0:
        return math.copysign(float('inf'), x) if (y % 2) == 1 else float('inf')

    try:
        return float(x ** y)
    except TypeError:
        return math.nan


@method(function('sqrt', prefix='math', nargs=1,
                 sequence_types=('xs:double?', 'xs:double?')))
def evaluate_sqrt_function(self: XPathFunction, context: ContextType = None) -> Emptiable[float]:
    arg: Optional[NumericType] = self.get_argument(self.context or context, cls=NumericProxy)
    if arg is None:
        return []
    elif arg < 0:
        return math.nan
    return math.sqrt(arg)


@method(function('sin', prefix='math', nargs=1,
                 sequence_types=('xs:double?', 'xs:double?')))
def evaluate_sin_function(self: XPathFunction, context: ContextType = None) -> Emptiable[float]:
    arg: Optional[NumericType] = self.get_argument(self.context or context, cls=NumericProxy)
    if arg is None:
        return []
    elif math.isinf(arg):
        return math.nan
    return math.sin(arg)


@method(function('cos', prefix='math', nargs=1,
                 sequence_types=('xs:double?', 'xs:double?')))
def evaluate_cos_function(self: XPathFunction, context: ContextType = None) -> Emptiable[float]:
    arg: Optional[NumericType] = self.get_argument(self.context or context, cls=NumericProxy)
    if arg is None:
        return []
    elif math.isinf(arg):
        return math.nan
    return math.cos(arg)


@method(function('tan', prefix='math', nargs=1,
                 sequence_types=('xs:double?', 'xs:double?')))
def evaluate_tan_function(self: XPathFunction, context: ContextType = None) -> Emptiable[float]:
    arg: Optional[NumericType] = self.get_argument(self.context or context, cls=NumericProxy)
    if arg is None:
        return []
    elif math.isinf(arg):
        return math.nan
    return math.tan(arg)


@method(function('asin', prefix='math', nargs=1,
                 sequence_types=('xs:double?', 'xs:double?')))
def evaluate_asin_function(self: XPathFunction, context: ContextType = None) -> Emptiable[float]:
    arg: Optional[NumericType] = self.get_argument(self.context or context, cls=NumericProxy)
    if arg is None:
        return []
    elif arg < -1 or arg > 1:
        return math.nan
    return math.asin(arg)


@method(function('acos', prefix='math', nargs=1,
                 sequence_types=('xs:double?', 'xs:double?')))
def evaluate_acos_function(self: XPathFunction, context: ContextType = None) -> Emptiable[float]:
    arg: Optional[NumericType] = self.get_argument(self.context or context, cls=NumericProxy)
    if arg is None:
        return []
    elif arg < -1 or arg > 1:
        return math.nan
    return math.acos(arg)


@method(function('atan', prefix='math', nargs=1,
                 sequence_types=('xs:double?', 'xs:double?')))
def evaluate_atan_function(self: XPathFunction, context: ContextType = None) -> Emptiable[float]:
    arg: Optional[NumericType] = self.get_argument(self.context or context, cls=NumericProxy)
    if arg is None:
        return []
    return math.atan(arg)


@method(function('atan2', prefix='math', nargs=2,
                 sequence_types=('xs:double', 'xs:double', 'xs:double')))
def evaluate_atan2_function(self: XPathFunction, context: ContextType = None) -> Emptiable[float]:
    if self.context is not None:
        context = self.context

    x = self.get_argument(context, cls=NumericProxy)
    y = self.get_argument(context, index=1, required=True, cls=NumericProxy)
    return math.atan2(x, y)


###
# Formatting functions
@method(function('format-integer', nargs=(2, 3),
                 sequence_types=('xs:integer?', 'xs:string', 'xs:string?', 'xs:string')))
def evaluate_format_integer_function(self: XPathFunction, context: ContextType = None) -> str:
    if self.context is not None:
        context = self.context

    value = self.get_argument(context, cls=NumericProxy)
    picture = self.get_argument(context, index=1, required=True, cls=str)
    lang = self.get_argument(context, index=2, cls=str)
    if value is None:
        return ''

    if ';' not in picture:
        fmt_token, fmt_modifier = picture, ''
    else:
        fmt_token, fmt_modifier = picture.rsplit(';', 1)

    if MODIFIER_PATTERN.match(fmt_modifier) is None:
        raise self.error('FODF1310')

    if not fmt_token:
        raise self.error('FODF1310')
    elif fmt_token in FORMAT_INTEGER_TOKENS:
        if fmt_token == 'a':
            result = int_to_alphabetic(value, lang)
        elif fmt_token == 'A':
            result = int_to_alphabetic(value, lang).upper()
        elif fmt_token == 'i':
            result = int_to_roman(value).lower()
        elif fmt_token == 'I':
            result = int_to_roman(value)
        elif fmt_token == 'w':
            return int_to_words(value, lang, fmt_modifier)
        elif fmt_token == 'W':
            return int_to_words(value, lang, fmt_modifier).upper()
        else:
            return int_to_words(value, lang, fmt_modifier).title()

    else:
        if UNICODE_DIGIT_PATTERN.search(fmt_token) is None:
            if any(not x.isalpha() and not x.isdigit() for x in fmt_token):
                result = str(value)  # fallback for invalid pictures
            else:
                base_char = '1'
                for base_char in fmt_token:
                    if base_char.isalpha():
                        break
                if base_char.islower():
                    result = int_to_alphabetic(value, base_char)
                else:
                    result = int_to_alphabetic(value, base_char.lower()).upper()

        elif DECIMAL_DIGIT_PATTERN.search(fmt_token) is None or ',,' in fmt_token:
            msg = 'picture argument has an invalid primary format token'
            raise self.error('FODF1310', msg)
        else:
            digits = UNICODE_DIGIT_PATTERN.findall(fmt_token)
            cp = ord(digits[0])
            if any((ord(ch) - cp) > 10 for ch in digits[1:]):
                msg = "picture argument mixes digits from different digit families"
                raise self.error('FODF1310', msg)
            elif fmt_token[0].isdigit():
                if '#' in fmt_token:
                    msg = 'picture argument has an invalid primary format token'
                    raise self.error('FODF1310', msg)
            elif fmt_token[0] != '#':
                raise self.error('FODF1310', "invalid grouping in picture argument")

            if digits[0].isdigit():
                cp = ord(digits[0])
                while chr(cp - 1).isdigit():
                    cp -= 1
                digits_family = ''.join(chr(cp + k) for k in range(10))
            else:
                raise ValueError()

            if value < 0:
                result = '-' + format_digits(str(abs(value)), fmt_token, digits_family)
            else:
                result = format_digits(str(abs(value)), fmt_token, digits_family)

    if fmt_modifier.startswith('o'):
        return f'{result}{ordinal_suffix(value)}'
    return result


@method(function('format-number', nargs=(2, 3),
                 sequence_types=('xs:numeric?', 'xs:string', 'xs:string?', 'xs:string')))
def evaluate_format_number_function(self: XPathFunction, context: ContextType = None) -> str:
    if self.context is not None:
        context = self.context

    value = self.get_argument(context, cls=NumericProxy)
    picture = self.get_argument(context, index=1, required=True, cls=str)
    decimal_format_name = self.get_argument(context, index=2, cls=str)

    # Check and adapt decimal format name
    if decimal_format_name is not None:
        decimal_format_name = decimal_format_name.strip()
        if decimal_format_name.startswith('Q{'):
            if decimal_format_name.startswith('Q{}'):
                decimal_format_name = decimal_format_name[3:]
            else:
                decimal_format_name = decimal_format_name[1:]
        elif ':' in decimal_format_name:
            try:
                decimal_format_name = get_expanded_name(
                    name=decimal_format_name,
                    namespaces=self.parser.namespaces
                )
            except (KeyError, ValueError):
                raise self.error('FODF1280') from None

    try:
        decimal_format = self.parser.decimal_formats[decimal_format_name]
    except KeyError:
        raise self.error('FODF1280') from None

    pattern_separator = decimal_format['pattern-separator']
    sub_pictures = picture.split(pattern_separator)
    if len(sub_pictures) > 2:
        raise self.error('FODF1310')

    decimal_separator = decimal_format['decimal-separator']
    if any(p.count(decimal_separator) > 1 for p in sub_pictures):
        raise self.error('FODF1310')

    percent_sign = decimal_format['percent']
    per_mille_sign = decimal_format['per-mille']
    if any(p.count(percent_sign) + p.count(per_mille_sign) > 1 for p in sub_pictures):
        raise self.error('FODF1310')

    zero_digit = decimal_format['zero-digit']
    optional_digit = decimal_format['digit']
    digits_family = ''.join(chr(cp + ord(zero_digit)) for cp in range(10))
    if any(optional_digit not in p and all(x not in p for x in digits_family)
           for p in sub_pictures):
        raise self.error('FODF1310')

    grouping_separator = decimal_format['grouping-separator']
    adjacent_pattern = re.compile(r'[\\%s\\%s]{2}' % (grouping_separator, decimal_separator))
    if any(adjacent_pattern.search(p) for p in sub_pictures):
        raise self.error('FODF1310')

    if any(x.endswith(grouping_separator)
           for s in sub_pictures for x in s.split(decimal_separator)):
        raise self.error('FODF1310')

    active_characters = digits_family + ''.join([
        decimal_separator, grouping_separator, pattern_separator, optional_digit
    ])

    exponent_pattern = None
    exponent_separator = 'e'
    if self.parser.version > '3.0':
        # Check optional exponent spec correctness in each sub-picture
        exponent_separator = decimal_format['exponent-separator']
        _pattern = re.compile(r'(?<=[{0}]){1}[{0}]'.format(
            re.escape(active_characters), exponent_separator
        ))
        for p in sub_pictures:
            for match in _pattern.finditer(p):
                if percent_sign in p or per_mille_sign in p:
                    raise self.error('FODF1310')
                elif any(c not in digits_family for c in p[match.span()[1]-1:]):
                    # detailed check to consider suffix
                    has_suffix = False
                    for ch in p[match.span()[1]-1:]:
                        if ch in digits_family:
                            if has_suffix:
                                raise self.error('FODF1310')
                        elif ch in active_characters:
                            raise self.error('FODF1310')
                        else:
                            has_suffix = True

                exponent_pattern = _pattern

    if exponent_pattern is None:
        if any(EXPONENT_PIC.search(s) for s in sub_pictures):
            raise self.error('FODF1310')

    if value is None or math.isnan(value):
        return f"{decimal_format['NaN']}"
    elif isinstance(value, float):
        value = decimal.Decimal.from_float(value)
    elif not isinstance(value, decimal.Decimal):
        value = decimal.Decimal(value)

    minus_sign = decimal_format['minus-sign']

    prefix = ''
    if value >= 0:
        subpic = sub_pictures[0]
    else:
        subpic = sub_pictures[-1]
        if len(sub_pictures) == 1:
            prefix = minus_sign

    for k, ch in enumerate(subpic):
        if ch in active_characters:
            prefix += subpic[:k]
            subpic = subpic[k:]
            break
    else:
        prefix += subpic
        subpic = ''

    if not subpic:
        suffix = ''
    elif subpic[-1] == percent_sign:
        suffix = percent_sign
        subpic = subpic[:-1]

        if value.as_tuple().exponent < 0:
            value *= 100
        else:
            value = decimal.Decimal(int(value) * 100)

    elif subpic[-1] == per_mille_sign:
        suffix = per_mille_sign
        subpic = subpic[:-1]

        if value.as_tuple().exponent < 0:
            value *= 1000
        else:
            value = decimal.Decimal(int(value) * 1000)

    else:
        for k, ch in enumerate(reversed(subpic)):
            if ch in active_characters:
                idx = len(subpic) - k
                suffix = subpic[idx:]
                subpic = subpic[:idx]
                break
        else:
            suffix = subpic
            subpic = ''

    exp_fmt = None
    if exponent_pattern is not None:
        exp_match = exponent_pattern.search(subpic)
        if exp_match is not None:
            exp_fmt = subpic[exp_match.span()[0]+1:]
            subpic = subpic[:exp_match.span()[0]]

    fmt_tokens = subpic.split(decimal_separator)
    if all(not fmt for fmt in fmt_tokens):
        raise self.error('FODF1310')

    if math.isinf(value):
        return f"{prefix}{decimal_format['infinity']}{suffix}"

    # Calculate the exponent value if it's in the sub-picture
    exp_value = 0
    if exp_fmt and value:
        num_digits = 0
        for ch in fmt_tokens[0]:
            if ch in digits_family:
                num_digits += 1

        if abs(value) > 1:
            v = abs(value)
            while v > 10 ** num_digits:
                exp_value += 1
                v /= 10

            # modify empty fractional part to store a digit
            if not num_digits:
                if len(fmt_tokens) == 1:
                    fmt_tokens.append(zero_digit)
                elif not fmt_tokens[-1]:
                    fmt_tokens[-1] = zero_digit

        elif len(fmt_tokens) > 1 and fmt_tokens[-1] and value >= 0:
            v = abs(value) * 10
            while v < 10 ** num_digits:
                exp_value -= 1
                v *= 10
        else:
            v = abs(value) * 10
            while v < 10:
                exp_value -= 1
                v *= 10

        if exp_value:
            value = value * decimal.Decimal(10) ** -exp_value

    # round the value by fractional part
    if len(fmt_tokens) == 1 or not fmt_tokens[-1]:
        exp = decimal.Decimal('1')
    else:
        k = -1
        for ch in fmt_tokens[-1]:
            if ch in digits_family or ch == optional_digit:
                k += 1
        exp = decimal.Decimal('.' + '0' * k + '1')

    try:
        if value > 0:
            value = value.quantize(exp, rounding='ROUND_HALF_UP')
        else:
            value = value.quantize(exp, rounding='ROUND_HALF_DOWN')
    except decimal.InvalidOperation:
        pass  # number too large, don't round elementpath..

    chunks = decimal_to_string(value).lstrip('-').split('.')
    kwargs = {
        'digits_family': digits_family,
        'optional_digit': optional_digit,
        'grouping_separator': grouping_separator,
    }
    result = format_digits(chunks[0], fmt_tokens[0], **kwargs)

    if len(fmt_tokens) > 1 and fmt_tokens[-1]:
        has_optional_digit = False
        for ch in fmt_tokens[-1]:
            if ch == optional_digit:
                has_optional_digit = True
            elif ch.isdigit() and has_optional_digit:
                raise self.error('FODF1310')

        if len(chunks) == 1:
            chunks.append(zero_digit)

        decimal_part = format_digits(chunks[1], fmt_tokens[-1], **kwargs)

        for ch in reversed(fmt_tokens[-1]):
            if ch == optional_digit:
                if decimal_part and decimal_part[-1] == zero_digit:
                    decimal_part = decimal_part[:-1]
            else:
                if not decimal_part:
                    decimal_part = zero_digit
                break

        if decimal_part:
            result += decimal_separator + decimal_part

            if not fmt_tokens[0] and result.startswith(zero_digit):
                result = result.lstrip(zero_digit)

    if exp_fmt:
        exp_digits = format_digits(str(abs(exp_value)), exp_fmt, **kwargs)
        if exp_value >= 0:
            result += f'{exponent_separator}{exp_digits}'
        else:
            result += f'{exponent_separator}-{exp_digits}'

    return prefix + result + suffix


function('format-dateTime', nargs=(2, 5),
         sequence_types=('xs:dateTime?', 'xs:string', 'xs:string?',
                         'xs:string?', 'xs:string?', 'xs:string?'))
function('format-date', nargs=(2, 5),
         sequence_types=('xs:date?', 'xs:string', 'xs:string?',
                         'xs:string?', 'xs:string?', 'xs:string?'))
function('format-time', nargs=(2, 5),
         sequence_types=('xs:time?', 'xs:string', 'xs:string?',
                         'xs:string?', 'xs:string?', 'xs:string?'))


@method('format-dateTime')
@method('format-date')
@method('format-time')
def evaluate_format_date_time_functions(self: XPathFunction, context: ContextType = None) \
        -> Emptiable[str]:
    cls: Type[Union[DateTime10, Date10, Time]]
    if self.symbol == 'format-dateTime':
        cls = DateTime10
        invalid_markers = ''
    elif self.symbol == 'format-date':
        cls = Date10
        invalid_markers = 'HhPmsf'
    else:
        cls = Time
        invalid_markers = 'YMDdFWwCE'

    if self.context is not None:
        context = self.context

    value = self.get_argument(context, cls=cls)
    picture = self.get_argument(context, index=1, required=True, cls=str)
    if len(self) not in [2, 5]:
        raise self.error('XPST0017')
    language = self.get_argument(context, index=2, cls=str)
    calendar = self.get_argument(context, index=3, cls=str)
    place = self.get_argument(context, index=4, cls=str)

    if value is None:
        return ''

    try:
        literals, markers = parse_datetime_picture(picture)
    except ElementPathError as err:
        err.token = self
        raise

    if invalid_markers:
        for mrk in markers:
            if mrk[1] in invalid_markers:
                msg = 'Invalid date formatting component {!r}'.format(mrk)
                raise self.error('FOFD1350', msg)

    result = []
    if language not in ('en', 'it', None):
        language = 'en'
        result.append('[Language: en')

    if calendar is not None:
        if calendar.startswith('Q{}'):
            calendar = calendar[3:]

        if calendar not in ('AD', 'ISO', 'OS'):
            if context is None or calendar != context.default_calendar:
                if QName.is_valid(calendar):
                    if ':' not in calendar:
                        msg = f'unknown calendar in no namespace {calendar!r}'
                        raise self.error('FOFD1340', msg)

                    try:
                        _ = get_expanded_name(calendar, self.parser.namespaces)
                    except (KeyError, ValueError) as err:
                        raise self.error('FOFD1340', str(err)) from None

                elif Patterns.extended_qname.search(calendar) is None:
                    raise self.error('FOFD1340', f'Invalid calendar argument {calendar!r}')
            else:
                result.append('[' if not result else ', ')
                result.append('Calendar: AD')

    if place is not None and zoneinfo is not None:
        try:
            zone = zoneinfo.ZoneInfo(place.strip())
        except zoneinfo.ZoneInfoNotFoundError:
            if not isinstance(context, XPathSchemaContext):
                raise self.error('FOFD1340', f'Invalid place argument {place!r}')
        else:
            value = value.astimezone(zone)

    if result:
        result.append(']')

    for k in range(len(markers)):
        result.append(literals[k])
        try:
            result.append(parse_datetime_marker(markers[k], value, language))
        except ElementPathError as err:
            err.token = self
            raise

    result.append(literals[-1])
    return ''.join(result)


###
# String functions that use regular expressions
@method(function('analyze-string', nargs=(2, 3),
                 sequence_types=('xs:string?', 'xs:string', 'xs:string',
                                 'element(fn:analyze-string-result)')))
def evaluate_analyze_string_function(self: XPathFunction, context: ContextType = None) \
        -> ElementProtocol:
    if self.context is not None:
        context = self.context

    input_string = self.get_argument(context, default='', cls=str)
    pattern = self.get_argument(context, 1, required=True, cls=str)
    flags = 0
    if len(self) > 2:
        for c in self.get_argument(context, 2, required=True, cls=str):
            if c in 'smix':
                flags |= getattr(re, c.upper())
            elif c == 'q' and self.parser.version > '2':
                pattern = re.escape(pattern)
            else:
                raise self.error('FORX0001', "Invalid regular expression flag %r" % c)

    try:
        python_pattern = translate_pattern(pattern, flags, self.parser.xsd_version)
        compiled_pattern = re.compile(python_pattern, flags=flags)
    except (re.error, RegexError) as err:
        msg = "Invalid regular expression: {}"
        raise self.error('FORX0002', msg.format(str(err))) from None
    except OverflowError as err:
        raise self.error('FORX0002', err) from None

    if compiled_pattern.match('') is not None:
        raise self.error('FORX0003', "pattern matches a zero-length string")

    if context is None:
        raise self.missing_context()

    level = 0
    escaped = False
    char_class = False
    group_levels = [0]
    for s in compiled_pattern.pattern:
        if escaped:
            escaped = False
        elif s == '\\':
            escaped = True
        elif char_class:
            if s == ']':
                char_class = False
        elif s == '[':
            char_class = True
        elif s == '(':
            group_levels.append(level)
            level += 1
        elif s == ')':
            level -= 1

    lines = ['<analyze-string-result xmlns="{}">'.format(XPATH_FUNCTIONS_NAMESPACE)]
    k = 0

    while k < len(input_string):
        match = compiled_pattern.search(input_string, k)
        if match is None:
            lines.append('<non-match>{}</non-match>'.format(input_string[k:]))
            break
        elif not match.groups():
            start, stop = match.span()
            if start > k:
                lines.append('<non-match>{}</non-match>'.format(input_string[k:start]))
            lines.append('<match>{}</match>'.format(input_string[start:stop]))
            k = stop
        else:
            start, stop = match.span()
            if start > k:
                lines.append('<non-match>{}</non-match>'.format(input_string[k:start]))
                k = start

            match_items = []
            group_tmpl = '<group nr="{}">{}'
            empty_group_tmpl = '<group nr="{}"/>'
            unclosed_groups = 0

            for idx in range(1, compiled_pattern.groups + 1):
                _start, _stop = match.span(idx)
                if _start < 0:
                    continue
                elif _start > k:
                    if unclosed_groups:
                        for _ in range(unclosed_groups):
                            match_items.append('</group>')
                        unclosed_groups = 0

                    match_items.append(input_string[k:_start])

                if _start == _stop:
                    if group_levels[idx] <= group_levels[idx - 1]:
                        for _ in range(unclosed_groups):
                            match_items.append('</group>')
                        unclosed_groups = 0
                    match_items.append(empty_group_tmpl.format(idx))
                    k = _stop
                elif idx == compiled_pattern.groups:
                    k = _stop
                    match_items.append(group_tmpl.format(idx, input_string[_start:k]))
                    match_items.append('</group>')
                else:
                    next_start = match.span(idx + 1)[0]
                    if next_start < 0 or _stop < next_start or _stop == next_start \
                            and group_levels[idx + 1] <= group_levels[idx]:
                        k = _stop
                        match_items.append(group_tmpl.format(idx, input_string[_start:k]))
                        match_items.append('</group>')
                    else:
                        k = next_start
                        match_items.append(group_tmpl.format(idx, input_string[_start:k]))
                        unclosed_groups += 1

            for _ in range(unclosed_groups):
                match_items.append('</group>')

            match_items.append(input_string[k:stop])
            k = stop
            lines.append('<match>{}</match>'.format(''.join(match_items)))

    lines.append('</analyze-string-result>')
    if self.parser.defuse_xml:
        root = context.etree.XML(defuse_xml(''.join(lines)))
    else:
        root = context.etree.XML(''.join(lines))

    return cast(ElementProtocol, get_node_tree(root=root, namespaces=self.parser.namespaces))


###
# Functions and operators on nodes
@method(function('path', nargs=(0, 1), sequence_types=('node()?', 'xs:string?')))
def evaluate_path_function(self: XPathFunction, context: ContextType = None) -> Emptiable[str]:
    if self.context is not None:
        context = self.context
    elif context is None:
        raise self.missing_context()

    if not self:
        item = context.item
    else:
        item = self.get_argument(context)
        if item is None:
            return []

    if not isinstance(item, XPathNode):
        return []
    elif context.root is None or (root_node := item.root_node) is not context.root:
        # The context has no root or the root is not the root of the item node
        return []
    elif not isinstance(root_node, (DocumentNode, SchemaElementNode)):
        # It's a fragment: add fn:root() to select the root position
        path = item.path[len(root_node.path):]
        return f"Q{{{XPATH_FUNCTIONS_NAMESPACE}}}root(){path}"
    else:
        return item.path or []


@method(function('has-children', nargs=(0, 1), sequence_types=('node()?', 'xs:boolean')))
def evaluate_has_children_function(self: XPathFunction, context: ContextType = None) -> bool:
    if self.context is not None:
        context = self.context
    elif context is None:
        raise self.missing_context()

    if not self:
        item = context.item
        if not isinstance(item, XPathNode):
            raise self.error('XPTY0004', 'context item must be a node')
    else:
        item = self.get_argument(context)
        if item is None:
            return False
        elif not isinstance(item, XPathNode):
            raise self.error('XPTY0004', 'argument must be a node')

    return isinstance(item, DocumentNode) or \
        isinstance(item, EtreeElementNode) and (len(item.obj) > 0 or item.obj.text is not None)


@method(function('innermost', nargs=1, sequence_types=('node()*', 'node()*')))
def select_innermost_function(self: XPathFunction, context: ContextType = None) \
        -> Iterator[XPathNode]:
    if context is None:
        raise self.missing_context()

    context = copy(context)
    nodes = [e for e in self[0].select(context)]
    if any(not isinstance(x, XPathNode) for x in nodes):
        raise self.error('XPTY0004', 'argument must contain only nodes')

    ancestors = {x for context.item in nodes for x in context.iter_ancestors(axis='ancestor')}
    results = {x for x in nodes if x not in ancestors}
    yield from cast(List[XPathNode], sorted(results, key=node_position))


@method(function('outermost', nargs=1, sequence_types=('node()*', 'node()*')))
def select_outermost_function(self: XPathFunction, context: ContextType = None) \
        -> Iterator[XPathNode]:
    if context is None:
        raise self.missing_context()

    context = copy(context)

    nodes: Union[List[ItemType], Set[ItemType]]
    nodes = [e for e in self[0].select(context)]
    if any(not isinstance(x, XPathNode) for x in nodes):
        raise self.error('XPTY0004', 'argument must contain only nodes')

    results = set()
    if len(nodes) > 10:
        nodes = set(nodes)

    for item in nodes:
        context.item = item
        ancestors = {x for x in context.iter_ancestors(axis='ancestor')}
        if any(x in nodes for x in ancestors):
            continue
        results.add(item)

    yield from cast(List[XPathNode], sorted(results, key=node_position))


##
# Functions and operators on sequences
@method(function('head', nargs=1, sequence_types=('item()*', 'item()?')))
def evaluate_head_function(self: XPathFunction, context: ContextType = None) \
        -> Emptiable[ItemType]:
    for item in self[0].select(self.context or context):
        return item
    else:
        return []


@method(function('tail', nargs=1, sequence_types=('item()*', 'item()*')))
def select_tail_function(self: XPathFunction, context: ContextType = None) \
        -> Iterator[ItemType]:
    for k, item in enumerate(self[0].select(self.context or context)):
        if k:
            yield item


@method(function('generate-id', nargs=(0, 1), sequence_types=('node()?', 'xs:string')))
def evaluate_generate_id_function(self: XPathFunction, context: ContextType = None) -> str:
    arg: Optional[NumericType]
    arg = self.get_argument(self.context or context, default_to_context=True)
    if arg is None:
        return ''
    elif not isinstance(arg, XPathNode):
        if self:
            raise self.error('XPTY0004', "argument is not a node")
        raise self.error('XPTY0004', "context item is not a node")
    else:
        return f'ID{id(arg)}'


@method(function('uri-collection', nargs=(0, 1),
                 sequence_types=('xs:string?', 'xs:anyURI*')))
def evaluate_uri_collection_function(self: XPathFunction, context: ContextType = None) \
        -> List[AnyURI]:
    if self.context is not None:
        context = self.context

    uri = self.get_argument(context)
    if context is None:
        raise self.missing_context()
    elif isinstance(context, XPathSchemaContext):
        return []
    elif not self or uri is None:
        if context.default_resource_collection is None:
            raise self.error('FODC0002', 'no default resource collection has been defined')
        resource_collection = [AnyURI(context.default_resource_collection)]
    else:
        try:
            AnyURI(uri)
        except ValueError:
            raise self.error('FODC0004', 'invalid argument to fn:uri-collection') from None

        if not context.resource_collections:
            resource_collection = []
        else:
            uri = self.get_absolute_uri(uri)
            try:
                resource_collection = [AnyURI(x) for x in context.resource_collections[uri]]
            except (KeyError, TypeError):
                url_parts = urlsplit(uri)
                if url_parts.scheme in ('', 'file') and \
                        not url_parts.path.startswith(':') and url_parts.path.endswith('/'):
                    raise self.error('FODC0003', 'collection URI is a directory')
                raise self.error('FODC0002', '{!r} collection not found'.format(uri)) from None

    if not match_sequence_type(resource_collection, 'xs:anyURI*', self.parser):
        raise self.error('XPDY0050', "Type does not match sequence type xs:anyURI*")

    return resource_collection


@method(function('unparsed-text', nargs=(1, 2),
                 sequence_types=('xs:string?', 'xs:string', 'xs:string?')))
@method(function('unparsed-text-lines', nargs=(1, 2),
                 sequence_types=('xs:string?', 'xs:string', 'xs:string*')))
def evaluate_unparsed_text_functions(self: XPathFunction, context: ContextType = None) \
        -> Union[Emptiable[str], List[str]]:
    if self.context is not None:
        context = self.context

    href: Optional[str] = self.get_argument(context, cls=str)
    if href is None:
        return []
    elif urlsplit(href).fragment:
        raise self.error('FOUT1170')

    encoding: str
    if len(self) > 1:
        encoding = self.get_argument(context, index=1, required=True, cls=str)
    else:
        encoding = 'UTF-8'

    try:
        uri = self.get_absolute_uri(href)
    except ValueError:
        raise self.error('FOUT1170') from None

    try:
        codecs.lookup(encoding)
    except LookupError:
        raise self.error('FOUT1190') from None

    if context is not None and uri in context.text_resources:
        text = context.text_resources[uri]
    else:
        try:
            with urlopen(uri) as rp:
                stream_reader = codecs.getreader(encoding)(rp)
                text = stream_reader.read()
        except URLError as err:
            raise self.error('FOUT1170', err) from None
        except ValueError as err:
            if len(self) > 1:
                raise self.error('FOUT1190', err) from None

            try:
                with urlopen(uri) as rp:
                    stream_reader = codecs.getreader('UTF-16')(rp)
                    text = stream_reader.read()
            except URLError as err:
                raise self.error('FOUT1170', err) from None
            except ValueError as err:
                raise self.error('FOUT1190', err) from None

        if context is not None:
            context.text_resources[uri] = text

    if not all(is_xml_codepoint(ord(s)) for s in text):
        raise self.error('FOUT1190')

    text = text.lstrip('\ufeff')

    if self.symbol == 'unparsed-text-lines':
        lines = Patterns.xml_newlines.split(text)
        return lines[:-1] if lines[-1] == '' else lines

    return text


@method(function('unparsed-text-available', nargs=(1, 2),
                 sequence_types=('xs:string?', 'xs:string', 'xs:boolean')))
def evaluate_unparsed_text_available_function(self: XPathFunction, context: ContextType = None) \
        -> bool:
    if self.context is not None:
        context = self.context

    href = self.get_argument(context, cls=str)
    if href is None:
        return False
    elif urlsplit(href).fragment:
        return False

    if len(self) > 1:
        encoding = self.get_argument(context, index=1, required=True, cls=str)
    else:
        encoding = 'UTF-8'

    try:
        uri = self.get_absolute_uri(href)
    except ValueError:
        return False

    try:
        codecs.lookup(encoding)
    except LookupError:
        return False

    try:
        with urlopen(uri) as rp:
            stream_reader = codecs.getreader(encoding)(rp)
            for line in stream_reader:
                if any(not is_xml_codepoint(ord(s)) for s in line):
                    return False
    except URLError:
        return False
    except ValueError:
        if len(self) > 1:
            return False
    else:
        return True

    # Fallback auto-detection with utf-16
    try:
        with urlopen(uri) as rp:
            stream_reader = codecs.getreader('UTF-16')(rp)
            for line in stream_reader:
                if any(not is_xml_codepoint(ord(s)) for s in line):
                    return False
    except (ValueError, URLError):
        return False
    else:
        return True


@method(function('environment-variable', nargs=1,
                 sequence_types=('xs:string', 'xs:string?')))
def evaluate_environment_variable_function(self: XPathFunction, context: ContextType = None) \
        -> Emptiable[str]:
    if self.context is not None:
        context = self.context

    name: str = self.get_argument(context, required=True, cls=str)
    if context is None:
        raise self.missing_context()
    elif not context.allow_environment:
        return []
    else:
        value = os.environ.get(name)
        return value if value is not None else []


@method(function('available-environment-variables', nargs=0,
                 sequence_types=('xs:string*',)))
def evaluate_available_environment_variables_function(
        self: XPathFunction, context: ContextType = None) -> List[str]:
    if self.context is not None:
        context = self.context
    elif context is None:
        raise self.missing_context()

    if not context.allow_environment:
        return []
    else:
        return list(os.environ)


###
# Parsing and serializing
@method(function('parse-xml', nargs=1,
                 sequence_types=('xs:string?', 'document-node(element(*))?')))
def evaluate_parse_xml_function(self: XPathFunction, context: ContextType = None) \
        -> Emptiable[DocumentNode]:
    # TODO: resolve relative entity references with static base URI
    if self.context is not None:
        context = self.context

    arg: Optional[str] = self.get_argument(context, cls=str)
    if arg is None:
        return []
    elif context is None:
        raise self.missing_context()

    etree = context.etree
    try:
        if self.parser.defuse_xml:
            root = etree.XML(defuse_xml(arg.encode('utf-8')))
        else:
            root = etree.XML(arg.encode('utf-8'))
    except etree.ParseError:
        raise self.error('FODC0006')
    else:
        return cast(DocumentNode, get_node_tree(etree.ElementTree(root), self.parser.namespaces))


@method(function('parse-xml-fragment', nargs=1,
                 sequence_types=('xs:string?', 'document-node()?')))
def evaluate_parse_xml_fragment_function(self: XPathFunction, context: ContextType = None) \
        -> Emptiable[DocumentNode]:
    if self.context is not None:
        context = self.context

    arg: Optional[str] = self.get_argument(context, cls=str)
    if arg is None or isinstance(context, XPathSchemaContext):
        return []
    elif context is None:
        raise self.missing_context()

    # Wrap argument in a fake document because an
    # XML document can have only one root element
    if arg.startswith('<?xml '):
        xml_declaration, _, arg = arg[6:].partition('?>')
        xml_params = DECL_PARAM_PATTERN.findall(xml_declaration)
        if 'encoding' not in xml_params:
            raise self.error('FODC0006', "'encoding' argument is mandatory")

        for param in xml_params:
            if param not in ('version', 'encoding'):
                msg = f'unexpected parameter {param!r} in XML declaration'
                raise self.error('FODC0006', msg)

    if arg.lstrip().startswith('<!DOCTYPE'):
        raise self.error('FODC0006', "<!DOCTYPE is not allowed")

    etree = context.etree
    try:
        if self.parser.defuse_xml:
            root = etree.XML(defuse_xml(arg))
        else:
            root = etree.XML(arg)
    except etree.ParseError as err:
        # A not parsable fragment: try to parse including XML data in a dummy element.
        try:
            dummy_element_node = get_node_tree(
                root=etree.XML(f'<document>{arg}</document>'),
                namespaces=self.parser.namespaces
            )
        except etree.ParseError:
            raise self.error('FODC0006', str(err)) from None
        else:
            assert isinstance(dummy_element_node, ElementNode)
            return dummy_element_node.get_document_node(replace=True)
    else:
        return cast(DocumentNode, get_node_tree(
            root=etree.ElementTree(root),
            namespaces=self.parser.namespaces
        ))


@method(function('serialize', nargs=(1, 2), sequence_types=(
        'item()*', 'element(output:serialization-parameters)?', 'xs:string')))
def evaluate_serialize_function(self: XPathFunction, context: ContextType = None) -> str:
    # TODO full implementation of serialization with
    #  https://www.w3.org/TR/xpath-functions-30/#xslt-xquery-serialization-30
    if self.context is not None:
        context = self.context

    params = self.get_argument(context, index=1) if len(self) == 2 else None
    kwargs = get_serialization_params(params, token=self)

    if context is None:
        raise self.missing_context()
    elif isinstance(context, XPathSchemaContext):
        return ''  # not applicable to schemas

    method_ = kwargs.get('method', 'xml')
    if method_ in ('xml', 'html', 'text'):
        etree_module = context.etree
        if context.namespaces:
            for pfx, uri in context.namespaces.items():
                etree_module.register_namespace(pfx, uri)
        else:
            for pfx, uri in self.parser.namespaces.items():
                etree_module.register_namespace(pfx, uri)

        return serialize_to_xml(self[0].select(context), etree_module, **kwargs)

    elif method_ == 'json':
        return serialize_to_json(self[0].select(context), token=self, **kwargs)
    else:
        return ''


###
# Higher-order functions

@method(function('function-lookup', nargs=2,
                 sequence_types=('xs:QName', 'xs:integer', 'function(*)?')))
def evaluate_function_lookup_function(self: XPathFunction, context: ContextType = None) \
        -> Emptiable[XPathFunction]:
    if self.context is not None:
        context = self.context

    qname: QName = self.get_argument(context, cls=QName, required=True)
    arity: int = self.get_argument(context, index=1, cls=int, required=True)
    if qname.namespace == '':
        return []

    try:
        cls = self.parser.symbol_table[qname.expanded_name]
    except KeyError:
        try:
            cls = self.parser.symbol_table[qname.local_name]
        except KeyError:
            return []

    assert issubclass(cls, XPathFunction)
    try:
        func = cls(self.parser, nargs=arity)
    except TypeError:
        return []

    func.namespace = qname.namespace
    func.context = copy(context)
    return func


@method(function('function-name', nargs=1, sequence_types=('function(*)', 'xs:QName?')))
def evaluate_function_name_function(self: XPathFunction, context: ContextType = None) \
        -> Emptiable[QName]:
    if self.context is not None:
        context = self.context

    if isinstance(self[0], XPathFunction):
        func = self[0]
    else:
        func = self.get_argument(context)

    if not isinstance(func, XPathFunction):
        raise self.error('XPTY0004', "argument is not a function")
    else:
        name = func.qname
        return [] if name is None else name


@method(function('function-arity', nargs=1, sequence_types=('function(*)', 'xs:integer')))
def evaluate_function_arity_function(self: XPathFunction, context: ContextType = None) -> int:
    if isinstance(self[0], XPathFunction):
        return self[0].arity

    func: XPathFunction
    func = self.get_argument(self.context or context, cls=XPathFunction, required=True)
    return func.arity


@method(function('for-each', nargs=2,
                 sequence_types=('item()*', 'function(item()) as item()*', 'item()*')))
def select_for_each_function(self: XPathFunction, context: ContextType = None) \
        -> Iterator[ItemType]:
    if self.context is not None:
        context = self.context

    func = self[1][1] if self[1].symbol == ':' else self[1]
    if not isinstance(func, XPathFunction):
        func = self.get_argument(context, index=1, cls=XPathFunction, required=True)
    assert isinstance(func, XPathFunction)

    for item in self[0].select(copy(context)):
        result = func(item, context=context)
        if isinstance(result, list):
            yield from result
        else:
            yield result


@method(function('filter', nargs=2,
                 sequence_types=('item()*', 'function(item()) as xs:boolean', 'item()*')))
def select_filter_function(self: XPathFunction, context: ContextType = None)\
        -> Iterator[ItemType]:
    func = self[1][1] if self[1].symbol == ':' else self[1]
    if not isinstance(func, XPathFunction):
        func = self.get_argument(context, index=1, cls=XPathFunction, required=True)
    assert isinstance(func, XPathFunction)

    if func.nargs == 0:
        raise self.error('XPTY0004', f'invalid number of arguments {func.nargs}')

    for item in self[0].select(copy(context)):
        cond = func(item, context=context)
        if not isinstance(cond, bool):
            raise self.error('XPTY0004', 'a single boolean value required')
        if cond:
            yield item


@method(function('fold-left', nargs=3,
                 sequence_types=('item()*', 'item()*',
                                 'function(item()*, item()) as item()*', 'item()*')))
def select_fold_left_function(self: XPathFunction, context: ContextType = None) \
        -> Iterator[ItemType]:
    func = self[2][1] if self[2].symbol == ':' else self[2]
    if not isinstance(func, XPathFunction):
        func = self.get_argument(context, index=2, cls=XPathFunction, required=True)
    assert isinstance(func, XPathFunction)

    if func.arity != 2:
        raise self.error('XPTY0004', "function arity must be 2")

    zero = self.get_argument(context, index=1)

    result = zero
    for item in self[0].select(copy(context)):
        result = func(result, item, context=context)

    if isinstance(result, list):
        yield from result
    else:
        yield result


@method(function('fold-right', nargs=3,
                 sequence_types=('item()*', 'item()*',
                                 'function(item()*, item()) as item()*', 'item()*')))
def select_fold_right_function(self: XPathFunction, context: ContextType = None) \
        -> Iterator[ItemType]:
    func = self[2][1] if self[2].symbol == ':' else self[2]
    if not isinstance(func, XPathFunction):
        func = self.get_argument(context, index=2, cls=XPathFunction, required=True)
    assert isinstance(func, XPathFunction)

    if func.arity != 2:
        raise self.error('XPTY0004', "function arity must be 2")

    zero = self.get_argument(context, index=1)

    result = zero
    sequence = [x for x in self[0].select(copy(context))]

    for item in reversed(sequence):
        result = func(item, result, context=context)

    if isinstance(result, list):
        yield from result
    else:
        yield result


@method(function('for-each-pair', nargs=3,
                 sequence_types=('item()*', 'item()*',
                                 'function(item(), item()) as item()*', 'item()*')))
def select_for_each_pair_function(self: XPathFunction, context: ContextType = None) \
        -> Iterator[ItemType]:
    func = self[2][1] if self[2].symbol == ':' else self[2]
    if not isinstance(func, XPathFunction):
        func = self.get_argument(context, index=2, cls=XPathFunction, required=True)

    if not isinstance(func, XPathFunction):
        raise self.error('XPTY0004', "invalid type for 3rd argument {!r}".format(func))
    elif func.arity != 2:
        raise self.error('XPTY0004', "function arity of 3rd argument must be 2")

    for item1, item2 in zip(self[0].select(copy(context)), self[1].select(copy(context))):
        result = func(item1, item2, context=context)
        if isinstance(result, list):
            yield from result
        else:
            yield result


@method(function('namespace-node', nargs=0, label='kind test'))
def select_namespace_node_kind_test(self: XPathFunction, context: ContextType = None) \
        -> Iterator[NamespaceNode]:
    if context is None:
        raise self.missing_context()
    elif isinstance(context.item, NamespaceNode):
        yield context.item
    elif isinstance(context, XPathSchemaContext):
        return  # deprecated for XP20+ and not needed for schema analysis
    elif isinstance(context.item, ElementNode):
        elem = context.item
        for context.item in elem.namespace_nodes:
            yield context.item


###
# Redefined or extended functions
XPath30Parser.unregister('data')
XPath30Parser.unregister('document-uri')
XPath30Parser.unregister('nilled')
XPath30Parser.unregister('node-name')
XPath30Parser.unregister('string-join')
XPath30Parser.unregister('round')


@method(function('data', nargs=(0, 1), sequence_types=('item()*', 'xs:anyAtomicType*')))
def select_data_function(self: XPathFunction, context: ContextType = None) \
        -> Iterator[AtomicType]:
    if self.context is not None:
        context = self.context

    if self:
        yield from self[0].atomization(context)
    elif context is None:
        raise self.missing_context()
    else:
        yield from self.atomize_item(context.item)


@method(function('document-uri', nargs=(0, 1), sequence_types=('node()?', 'xs:anyURI?')))
def evaluate_document_uri_function(self: XPathFunction, context: ContextType = None) \
        -> Emptiable[AnyURI]:
    if self.context is not None:
        context = self.context
    elif context is None:
        raise self.missing_context()

    arg: Optional[NumericType] = self.get_argument(context, default_to_context=True)
    if isinstance(arg, DocumentNode):
        uri = arg.document_uri
        if uri is not None:
            return AnyURI(uri)
        elif isinstance(context.root, DocumentNode):
            if context.documents:
                for uri, doc in context.documents.items():
                    if doc and doc.document is context.root.document:
                        return AnyURI(uri)
    return []


@method(function('nilled', nargs=(0, 1), sequence_types=('node()?', 'xs:boolean?')))
def evaluate_nilled_function(self: XPathFunction, context: ContextType = None) \
        -> Emptiable[bool]:
    arg: Optional[NumericType] = self.get_argument(self.context or context, default_to_context=True)
    if arg is None:
        return []
    elif isinstance(arg, XPathNode):
        result = arg.nilled
        return result if result is not None else []
    else:
        raise self.error('XPTY0004', 'an XPath node required')


@method(function('node-name', nargs=(0, 1), sequence_types=('node()?', 'xs:QName?')))
def evaluate_node_name_function(self: XPathFunction, context: ContextType = None) \
        -> Emptiable[QName]:
    arg: Optional[NumericType]
    arg = self.get_argument(self.context or context, default_to_context=True)
    if arg is None:
        return []
    elif isinstance(arg, XPathNode):
        name = arg.name
        if name is None:
            return []
        elif name.startswith('{'):
            # name is a QName in extended format
            namespace, local_name = split_expanded_name(name)
            for pfx, uri in self.parser.namespaces.items():
                if uri == namespace:
                    if not pfx:
                        return QName(uri, local_name)
                    return QName(uri, '{}:{}'.format(pfx, local_name))
            raise self.error('FONS0004', 'no prefix found for namespace {}'.format(namespace))
        else:
            # name is a local name
            return QName(self.parser.namespaces.get('', ''), name)
    else:
        raise self.error('XPTY0004', 'an XPath node required')


@method(function('string-join', nargs=(1, 2),
                 sequence_types=('xs:string*', 'xs:string', 'xs:string')))
def evaluate_string_join_function(self: XPathFunction, context: ContextType = None) -> str:
    if self.context is not None:
        context = self.context

    items = [
        self.validated_value(s, cls=str, promote=AnyURI, index=k)
        for k, s in enumerate(self[0].atomization(context))
    ]

    if len(self) == 1:
        return ''.join(items)
    separator: str = self.get_argument(context, 1, required=True, cls=str)
    return separator.join(items)


@method(function('round', nargs=(1, 2),
                 sequence_types=('xs:numeric?', 'xs:integer', 'xs:numeric?')))
def evaluate_round_function(self: XPathFunction, context: ContextType = None) \
        -> Emptiable[NumericType]:
    if self.context is not None:
        context = self.context

    arg: Optional[NumericType] = self.get_argument(context)
    if arg is None:
        return []
    elif isinstance(arg, XPathNode) or self.parser.compatibility_mode:
        arg = self.number_value(arg)

    if isinstance(arg, float) and (math.isnan(arg) or math.isinf(arg)):
        return arg

    precision: int = self.get_argument(context, index=1, default=0, cls=int)
    try:
        if precision < 0:
            return type(arg)(round(arg, precision))  # type: ignore[call-overload, arg-type]

        number = decimal.Decimal(arg)
        exponent = decimal.Decimal('1') / 10 ** precision
        if number > 0:
            return type(arg)(number.quantize(exponent, rounding='ROUND_HALF_UP'))
        else:
            return type(arg)(number.quantize(exponent, rounding='ROUND_HALF_DOWN'))
    except TypeError as err:
        if isinstance(context, XPathSchemaContext):
            return []
        raise self.error('FORG0006', err) from None
    except decimal.InvalidOperation:
        if isinstance(arg, str):
            if isinstance(context, XPathSchemaContext):
                return []
            raise self.error('XPTY0004') from None
        return round(arg)
    except decimal.DecimalException as err:
        if isinstance(context, XPathSchemaContext):
            return []
        raise self.error('FOCA0002', err) from None


#
# XSD list-based constructors

@XPath30Parser.constructor('NMTOKENS', sequence_types=('xs:NMTOKEN*',))
def cast_nmtokens_list_type(self: XPathConstructor, value: AtomicType) \
        -> List[NMToken]:
    cast_func = xsd10_atomic_types['NMTOKEN']
    if isinstance(value, UntypedAtomic):
        values = value.value.split() or [value.value]
    elif hasattr(value, 'split'):
        values = value.split() or [value]
    else:
        raise self.error('FORG0001')

    try:
        return [cast_func(x) for x in values]
    except ValueError as err:
        raise self.error('FORG0001', err) from None


@XPath30Parser.constructor('IDREFS', sequence_types=('xs:IDREF*',))
def cast_idrefs_list_type(self: XPathConstructor, value: AtomicType) \
        -> List[Idref]:
    cast_func = xsd10_atomic_types['IDREF']
    if isinstance(value, UntypedAtomic):
        values = value.value.split() or [value.value]
    elif hasattr(value, 'split'):
        values = value.split() or [value]
    else:
        raise self.error('FORG0001')

    try:
        return [cast_func(x) for x in values]
    except ValueError as err:
        raise self.error('FORG0001', err) from None


@XPath30Parser.constructor('ENTITIES', sequence_types=('xs:ENTITY*',))
def cast_entities_list_type(self: XPathConstructor, value: AtomicType) \
        -> List[Entity]:
    cast_func = xsd10_atomic_types['ENTITY']
    if isinstance(value, UntypedAtomic):
        values = value.value.split() or [value.value]
    elif hasattr(value, 'split'):
        values = value.split() or [value]
    else:
        raise self.error('FORG0001')

    try:
        return [cast_func(x) for x in values]
    except ValueError as err:
        raise self.error('FORG0001', err) from None


###
# In XPath 3.0+ the 'error' keyword has to be used both for fn:error() and xs:error()
XPath30Parser.unregister('error')


# TODO: apply sequence_types=('xs:anyAtomicType?', 'xs:error?') for xs:error
@XPath30Parser.constructor('error', bp=90, label=('function', 'constructor function'),
                           nargs=(0, 3),
                           sequence_types=('xs:QName?', 'xs:string', 'item()*', 'none'))
def cast_error_type(self: XPathConstructor, value: AtomicType) -> Emptiable[None]:
    if value is None or value == []:
        return []
    msg = f"Cast {value!r} to xs:error is not possible"
    raise self.error('FORG0001', msg)


@method('error')
def nud_error_type_and_function(self: XPathConstructor) -> XPathConstructor:
    self.clear()
    if not self.parser.parse_arguments:
        return self

    try:
        self.parser.advance('(')
        if self.namespace == XSD_NAMESPACE:
            self.label = 'constructor function'
            self.nargs = 1
            if self.parser.xsd_version == '1.0':
                raise self.error('XPST0051', 'xs:error is not defined with XSD 1.0')
            self.append(self.parser.expression(5))
        else:
            self.label = 'function'
            for k in range(3):
                if self.parser.next_token.symbol == ')':
                    break
                self.append(self.parser.expression(5))
                if self.parser.next_token.symbol == ')':
                    break
                self.parser.advance(',')
        self.parser.advance(')')
    except SyntaxError:
        raise self.error('XPST0017') from None
    else:
        return self


@method('error')
def evaluate_error_type_and_function(self: XPathConstructor, context: ContextType = None) \
        -> Emptiable[ElementPathError]:
    if self.context is not None:
        context = self.context

    error: Optional[QName]
    if self.label == 'constructor function':
        return cast(ElementPathError, self.cast(self.get_argument(context)))
    elif not self:
        raise self.error('FOER0000')
    elif len(self) == 1:
        error = self.get_argument(context, cls=QName)
        if error is None and self.parser.version == '3.0':
            raise self.error('XPTY0004', "an xs:QName expected")
        raise self.error(error or 'FOER0000')
    else:
        error = self.get_argument(context, cls=QName)
        description: Optional[str] = self.get_argument(context, index=1, cls=str)
        raise self.error(error or 'FOER0000', description)
