# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
#
# Copyright (C) 2006-2009, 2012 Lukáš Lalinský
# Copyright (C) 2007 Javier Kohen
# Copyright (C) 2008-2011, 2014-2015, 2018-2020 Philipp Wolfer
# Copyright (C) 2009 Carlin Mangar
# Copyright (C) 2009 Nikolai Prokoschenko
# Copyright (C) 2011-2012 Michael Wiencek
# Copyright (C) 2012 Chad Wilson
# Copyright (C) 2012 stephen
# Copyright (C) 2012, 2014, 2017 Wieland Hoffmann
# Copyright (C) 2013-2014, 2017-2020 Laurent Monin
# Copyright (C) 2014, 2017 Sophist-UK
# Copyright (C) 2016-2017 Sambhav Kothari
# Copyright (C) 2016-2017 Ville Skyttä
# Copyright (C) 2017-2018 Antonio Larrosa
# Copyright (C) 2018 Calvin Walton
# Copyright (C) 2018 virusMac
# Copyright (C) 2020 Bob Swift
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.


from collections import namedtuple
from collections.abc import MutableSequence
import datetime
from functools import reduce
from inspect import getfullargspec
import operator
from queue import LifoQueue
import re
import unicodedata

from picard import config
from picard.metadata import (
    MULTI_VALUED_JOINER,
    Metadata,
)
from picard.plugin import ExtensionPoint
from picard.util import uniqify


class ScriptError(Exception):
    pass


class ScriptParseError(ScriptError):
    def __init__(self, stackitem, message):
        super().__init__(
            "{prefix:s}: {message:s}".format(
                prefix=str(stackitem),
                message=message
            )
        )


class ScriptEndOfFile(ScriptParseError):
    def __init__(self, stackitem):
        super().__init__(
            stackitem,
            "Unexpected end of script"
        )


class ScriptSyntaxError(ScriptParseError):
    pass


class ScriptUnknownFunction(ScriptParseError):
    def __init__(self, stackitem):
        super().__init__(
            stackitem,
            "Unknown function '{name}'".format(name=stackitem.name)
        )


class ScriptRuntimeError(ScriptError):
    def __init__(self, stackitem, message='Unknown error'):
        super().__init__(
            "{prefix:s}: {message:s}".format(
                prefix=str(stackitem),
                message=message
            )
        )


class StackItem:
    def __init__(self, line, column, name=None):
        self.line = line
        self.column = column
        if name is None:
            self.name = None
        else:
            self.name = '$' + name

    def __str__(self):
        if self.name is None:
            return '{line:d}:{column:d}'.format(
                line=self.line,
                column=self.column
            )
        else:
            return '{line:d}:{column:d}:{name}'.format(
                line=self.line,
                column=self.column,
                name=self.name
            )


class ScriptText(str):

    def eval(self, state):
        return self


def normalize_tagname(name):
    if name.startswith('_'):
        return "~" + name[1:]
    return name


class ScriptVariable(object):

    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return '<ScriptVariable %%%s%%>' % self.name

    def eval(self, state):
        return state.context.get(normalize_tagname(self.name), "")


FunctionRegistryItem = namedtuple("FunctionRegistryItem",
                                  ["function", "eval_args",
                                   "argcount"])
Bound = namedtuple("Bound", ["lower", "upper"])


class ScriptFunction(object):

    def __init__(self, name, args, parser, column=0, line=0):
        self.stackitem = StackItem(line, column, name)
        try:
            argnum_bound = parser.functions[name].argcount
            argcount = len(args)
            if argnum_bound:
                too_few_args = argcount < argnum_bound.lower
                if argnum_bound.upper is not None:
                    if argnum_bound.lower == argnum_bound.upper:
                        expected = "exactly %i" % argnum_bound.lower
                    else:
                        expected = "between %i and %i" % (argnum_bound.lower, argnum_bound.upper)
                    too_many_args = argcount > argnum_bound.upper
                else:
                    expected = "at least %i" % argnum_bound.lower
                    too_many_args = False

                if too_few_args or too_many_args:
                    raise ScriptSyntaxError(
                        self.stackitem,
                        "Wrong number of arguments for $%s: Expected %s, got %i"
                        % (name, expected, argcount)
                    )
        except KeyError:
            raise ScriptUnknownFunction(self.stackitem)

        self.name = name
        self.args = args

    def __repr__(self):
        return "<ScriptFunction $%s(%r)>" % (self.name, self.args)

    def eval(self, parser):
        try:
            function, eval_args, num_args = parser.functions[self.name]
        except KeyError:
            raise ScriptUnknownFunction(self.stackitem)

        if eval_args:
            args = [arg.eval(parser) for arg in self.args]
        else:
            args = self.args
        parser._function_stack.put(self.stackitem)
        # Save return value to allow removing function from the stack on successful completion
        return_value = function(parser, *args)
        parser._function_stack.get()
        return return_value


class ScriptExpression(list):

    def eval(self, state):
        return "".join([item.eval(state) for item in self])


def isidentif(ch):
    return ch.isalnum() or ch == '_'


class ScriptParser(object):

    r"""Tagger script parser.

Grammar:
  text       ::= [^$%] | '\$' | '\%' | '\(' | '\)' | '\,'
  argtext    ::= [^$%(),] | '\$' | '\%' | '\(' | '\)' | '\,'
  identifier ::= [a-zA-Z0-9_]
  variable   ::= '%' identifier '%'
  function   ::= '$' identifier '(' (argument (',' argument)*)? ')'
  expression ::= (variable | function | text)*
  argument   ::= (variable | function | argtext)*
"""

    _function_registry = ExtensionPoint(label='function_registry')
    _cache = {}

    def __init__(self):
        self._function_stack = LifoQueue()

    def __raise_eof(self):
        raise ScriptEndOfFile(StackItem(line=self._y, column=self._x))

    def __raise_char(self, ch):
        raise ScriptSyntaxError(StackItem(line=self._y, column=self._x), "Unexpected character '%s'" % ch)

    def read(self):
        try:
            ch = self._text[self._pos]
        except IndexError:
            return None
        else:
            self._pos += 1
            self._px = self._x
            self._py = self._y
            if ch == '\n':
                self._line = self._pos
                self._x = 1
                self._y += 1
            else:
                self._x += 1
        return ch

    def unread(self):
        self._pos -= 1
        self._x = self._px
        self._y = self._py

    def parse_arguments(self):
        results = []
        while True:
            result, ch = self.parse_expression(False)
            results.append(result)
            if ch == ')':
                # Only an empty expression as first argument
                # is the same as no argument given.
                if len(results) == 1 and results[0] == []:
                    return []
                return results

    def parse_function(self):
        start = self._pos
        column = self._x - 2     # Set x position to start of function name ($)
        line = self._y
        while True:
            ch = self.read()
            if ch == '(':
                name = self._text[start:self._pos-1]
                if name not in self.functions:
                    raise ScriptUnknownFunction(StackItem(line, column, name))
                return ScriptFunction(name, self.parse_arguments(), self, column, line)
            elif ch is None:
                self.__raise_eof()
            elif not isidentif(ch):
                self.__raise_char(ch)

    def parse_variable(self):
        begin = self._pos
        while True:
            ch = self.read()
            if ch == '%':
                return ScriptVariable(self._text[begin:self._pos-1])
            elif ch is None:
                self.__raise_eof()
            elif not isidentif(ch) and ch != ':':
                self.__raise_char(ch)

    def parse_text(self, top):
        text = []
        while True:
            ch = self.read()
            if ch == "\\":
                ch = self.read()
                if ch == 'n':
                    text.append('\n')
                elif ch == 't':
                    text.append('\t')
                elif ch not in "$%(),\\":
                    self.__raise_char(ch)
                else:
                    text.append(ch)
            elif ch is None:
                break
            elif not top and ch == '(':
                self.__raise_char(ch)
            elif ch in '$%' or (not top and ch in ',)'):
                self.unread()
                break
            else:
                text.append(ch)
        return ScriptText("".join(text))

    def parse_expression(self, top):
        tokens = ScriptExpression()
        while True:
            ch = self.read()
            if ch is None:
                if top:
                    break
                else:
                    self.__raise_eof()
            elif not top and ch in ',)':
                break
            elif ch == '$':
                tokens.append(self.parse_function())
            elif ch == '%':
                tokens.append(self.parse_variable())
            else:
                self.unread()
                tokens.append(self.parse_text(top))
        return (tokens, ch)

    def load_functions(self):
        self.functions = {}
        for name, item in ScriptParser._function_registry:
            self.functions[name] = item

    def parse(self, script, functions=False):
        """Parse the script."""
        self._text = script
        self._pos = 0
        self._px = self._x = 1
        self._py = self._y = 1
        self._line = 0
        if not functions:
            self.load_functions()
        return self.parse_expression(True)[0]

    def eval(self, script, context=None, file=None):
        """Parse and evaluate the script."""
        self.context = context if context is not None else Metadata()
        self.file = file
        self.load_functions()
        key = hash(script)
        if key not in ScriptParser._cache:
            ScriptParser._cache[key] = self.parse(script, True)
        return ScriptParser._cache[key].eval(self)


class MultiValue(MutableSequence):
    def __init__(self, parser, multi, separator):
        self.parser = parser
        if isinstance(separator, ScriptExpression):
            self.separator = separator.eval(self.parser)
        else:
            self.separator = separator
        if (self.separator == MULTI_VALUED_JOINER
            and len(multi) == 1
            and isinstance(multi[0], ScriptVariable)):
            # Convert ScriptExpression containing only a single variable into variable
            self._multi = self.parser.context.getall(normalize_tagname(multi[0].name))
        else:
            # Fall-back to converting to a string and splitting if haystack is an expression
            # or user has overridden the separator character.
            evaluated_multi = multi.eval(self.parser)
            if not evaluated_multi:
                self._multi = []
            elif self.separator:
                self._multi = evaluated_multi.split(self.separator)
            else:
                self._multi = [evaluated_multi]

    def __len__(self):
        return len(self._multi)

    def __getitem__(self, key):
        return self._multi[key]

    def __setitem__(self, key, value):
        self._multi[key] = value

    def __delitem__(self, key):
        del self._multi[key]

    def insert(self, index, value):
        return self._multi.insert(index, value)

    def __repr__(self):
        return '%s(%r, %r, %r)' % (self.__class__.__name__, self.parser, self._multi, self.separator)

    def __str__(self):
        return self.separator.join(self)


def enabled_tagger_scripts_texts():
    """Returns an iterator over the enabled tagger scripts.
    For each script, you'll get a tuple consisting of the script name and text"""
    if not config.setting["enable_tagger_scripts"]:
        return []
    return [(s_name, s_text) for _s_pos, s_name, s_enabled, s_text in config.setting["list_of_scripts"] if s_enabled and s_text]


def register_script_function(function, name=None, eval_args=True,
                             check_argcount=True):
    """Registers a script function. If ``name`` is ``None``,
    ``function.__name__`` will be used.
    If ``eval_args`` is ``False``, the arguments will not be evaluated before being
    passed to ``function``.
    If ``check_argcount`` is ``False`` the number of arguments passed to the
    function will not be verified."""

    args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations = getfullargspec(function)

    required_kwonlyargs = len(kwonlyargs)
    if kwonlydefaults is not None:
        required_kwonlyargs -= len(kwonlydefaults.keys())
    if required_kwonlyargs:
        raise TypeError("Functions with required keyword-only parameters are not supported")

    args = len(args) - 1  # -1 for the parser
    varargs = varargs is not None
    defaults = len(defaults) if defaults else 0

    argcount = Bound(args - defaults, args if not varargs else None)

    if name is None:
        name = function.__name__
    ScriptParser._function_registry.register(function.__module__,
        (name, FunctionRegistryItem(
            function, eval_args,
            argcount if argcount and check_argcount else False)
         )
    )


def script_function(name=None, eval_args=True, check_argcount=True, prefix='func_'):
    """Decorator helper to register script functions

    It calls ``register_script_function()`` and share same arguments
    Extra optional arguments:
        ``prefix``: define the prefix to be removed from defined function to name script function
                    By default, ``func_foo`` will create ``foo`` script function

    Example:
        @script_function(eval_args=False)
        def func_myscriptfunc():
            ...
    """
    def script_function_decorator(func):
        fname = func.__name__
        if name is None and prefix and fname.startswith(prefix):
            sname = fname[len(prefix):]
        else:
            sname = name
        register_script_function(func, name=sname, eval_args=eval_args, check_argcount=check_argcount)
        return func
    return script_function_decorator


def _compute_int(operation, *args):
    return str(reduce(operation, map(int, args)))


def _compute_logic(operation, *args):
    return operation(args)


@script_function(eval_args=False)
def func_if(parser, _if, _then, _else=None):
    """If ``if`` is not empty, it returns ``then``, otherwise it returns ``else``."""
    if _if.eval(parser):
        return _then.eval(parser)
    elif _else:
        return _else.eval(parser)
    return ''


@script_function(eval_args=False)
def func_if2(parser, *args):
    """Returns first non empty argument."""
    for arg in args:
        arg = arg.eval(parser)
        if arg:
            return arg
    return ''


@script_function(eval_args=False)
def func_noop(parser, *args):
    """Does nothing :)"""
    return ''


@script_function()
def func_left(parser, text, length):
    """Returns first ``num`` characters from ``text``."""
    try:
        return text[:int(length)]
    except ValueError:
        return ""


@script_function()
def func_right(parser, text, length):
    """Returns last ``num`` characters from ``text``."""
    try:
        return text[-int(length):]
    except ValueError:
        return ""


@script_function()
def func_lower(parser, text):
    """Returns ``text`` in lower case."""
    return text.lower()


@script_function()
def func_upper(parser, text):
    """Returns ``text`` in upper case."""
    return text.upper()


@script_function()
def func_pad(parser, text, length, char):
    try:
        return char * (int(length) - len(text)) + text
    except ValueError:
        return ""


@script_function()
def func_strip(parser, text):
    return re.sub(r"\s+", " ", text).strip()


@script_function()
def func_replace(parser, text, old, new):
    return text.replace(old, new)


@script_function()
def func_in(parser, text, needle):
    if needle in text:
        return "1"
    else:
        return ""


@script_function(eval_args=False)
def func_inmulti(parser, haystack, needle, separator=MULTI_VALUED_JOINER):
    """Searches for ``needle`` in ``haystack``, supporting a list variable for
       ``haystack``. If a string is used instead, then a ``separator`` can be
       used to split it. In both cases, it returns true if the resulting list
       contains exactly ``needle`` as a member."""

    needle = needle.eval(parser)
    return func_in(parser, MultiValue(parser, haystack, separator), needle)


@script_function()
def func_rreplace(parser, text, old, new):
    try:
        return re.sub(old, new, text)
    except re.error:
        return text


@script_function()
def func_rsearch(parser, text, pattern):
    try:
        match = re.search(pattern, text)
    except re.error:
        return ""
    if match:
        try:
            return match.group(1)
        except IndexError:
            return match.group(0)
    return ""


@script_function()
def func_num(parser, text, length):
    try:
        format_ = "%%0%dd" % max(0, min(int(length), 20))
    except ValueError:
        return ""
    try:
        value = int(text)
    except ValueError:
        value = 0
    return format_ % value


@script_function()
def func_unset(parser, name):
    """Unsets the variable ``name``."""
    name = normalize_tagname(name)
    # Allow wild-card unset for certain keys
    if name in ('performer:*', 'comment:*', 'lyrics:*'):
        name = name[:-1]
        for key in list(parser.context.keys()):
            if key.startswith(name):
                parser.context.unset(key)
        return ""
    try:
        parser.context.unset(name)
    except KeyError:
        pass
    return ""


@script_function()
def func_delete(parser, name):
    """
    Deletes the variable ``name``.
    This will unset the tag with the given name and also mark the tag for
    deletion on save.
    """
    parser.context.delete(normalize_tagname(name))
    return ""


@script_function()
def func_set(parser, name, value):
    """Sets the variable ``name`` to ``value``."""
    if value:
        parser.context[normalize_tagname(name)] = value
    else:
        func_unset(parser, name)
    return ""


@script_function()
def func_setmulti(parser, name, value, separator=MULTI_VALUED_JOINER):
    """Sets the variable ``name`` to ``value`` as a list; splitting by the passed string, or "; " otherwise."""
    return func_set(parser, name, value.split(separator) if value and separator else value)


@script_function()
def func_get(parser, name):
    """Returns the variable ``name`` (equivalent to ``%name%``)."""
    return parser.context.get(normalize_tagname(name), "")


@script_function()
def func_copy(parser, new, old):
    """Copies content of variable ``old`` to variable ``new``."""
    new = normalize_tagname(new)
    old = normalize_tagname(old)
    parser.context[new] = parser.context.getall(old)[:]
    return ""


@script_function()
def func_copymerge(parser, new, old):
    """Copies content of variable ``old`` and appends it into variable ``new``, removing duplicates. This is normally
    used to merge a multi-valued variable into another, existing multi-valued variable."""
    new = normalize_tagname(new)
    old = normalize_tagname(old)
    newvals = parser.context.getall(new)
    oldvals = parser.context.getall(old)
    parser.context[new] = uniqify(newvals + oldvals)
    return ""


@script_function()
def func_trim(parser, text, char=None):
    """Trims all leading and trailing whitespaces from ``text``. The optional
       second parameter specifies the character to trim."""
    if char:
        return text.strip(char)
    else:
        return text.strip()


@script_function()
def func_add(parser, x, y, *args):
    """Adds ``y`` to ``x``.
       Can be used with an arbitrary number of arguments.
       Eg: $add(x, y, z) = ((x + y) + z)
    """
    try:
        return _compute_int(operator.add, x, y, *args)
    except ValueError:
        return ""


@script_function()
def func_sub(parser, x, y, *args):
    """Subtracts ``y`` from ``x``.
       Can be used with an arbitrary number of arguments.
       Eg: $sub(x, y, z) = ((x - y) - z)
    """
    try:
        return _compute_int(operator.sub, x, y, *args)
    except ValueError:
        return ""


@script_function()
def func_div(parser, x, y, *args):
    """Divides ``x`` by ``y``.
       Can be used with an arbitrary number of arguments.
       Eg: $div(x, y, z) = ((x / y) / z)
    """
    try:
        return _compute_int(operator.floordiv, x, y, *args)
    except ValueError:
        return ""
    except ZeroDivisionError:
        return ""


@script_function()
def func_mod(parser, x, y, *args):
    """Returns the remainder of ``x`` divided by ``y``.
       Can be used with an arbitrary number of arguments.
       Eg: $mod(x, y, z) = ((x % y) % z)
    """
    try:
        return _compute_int(operator.mod, x, y, *args)
    except (ValueError, ZeroDivisionError):
        return ""


@script_function()
def func_mul(parser, x, y, *args):
    """Multiplies ``x`` by ``y``.
       Can be used with an arbitrary number of arguments.
       Eg: $mul(x, y, z) = ((x * y) * z)
    """
    try:
        return _compute_int(operator.mul, x, y, *args)
    except ValueError:
        return ""


@script_function()
def func_or(parser, x, y, *args):
    """Returns true, if either ``x`` or ``y`` not empty.
       Can be used with an arbitrary number of arguments. The result is
       true if ANY of the arguments is not empty.
    """
    if _compute_logic(any, x, y, *args):
        return "1"
    else:
        return ""


@script_function()
def func_and(parser, x, y, *args):
    """Returns true, if both ``x`` and ``y`` are not empty.
       Can be used with an arbitrary number of arguments. The result is
       true if ALL of the arguments are not empty.
    """
    if _compute_logic(all, x, y, *args):
        return "1"
    else:
        return ""


@script_function()
def func_not(parser, x):
    """Returns true, if ``x`` is empty."""
    if not x:
        return "1"
    else:
        return ""


@script_function()
def func_eq(parser, x, y):
    """Returns true, if ``x`` equals ``y``."""
    if x == y:
        return "1"
    else:
        return ""


@script_function()
def func_ne(parser, x, y):
    """Returns true, if ``x`` not equals ``y``."""
    if x != y:
        return "1"
    else:
        return ""


@script_function()
def func_lt(parser, x, y):
    """Returns true, if ``x`` is lower than ``y``."""
    try:
        if int(x) < int(y):
            return "1"
    except ValueError:
        pass
    return ""


@script_function()
def func_lte(parser, x, y):
    """Returns true, if ``x`` is lower than or equals ``y``."""
    try:
        if int(x) <= int(y):
            return "1"
    except ValueError:
        pass
    return ""


@script_function()
def func_gt(parser, x, y):
    """Returns true, if ``x`` is greater than ``y``."""
    try:
        if int(x) > int(y):
            return "1"
    except ValueError:
        pass
    return ""


@script_function()
def func_gte(parser, x, y):
    """Returns true, if ``x`` is greater than or equals ``y``."""
    try:
        if int(x) >= int(y):
            return "1"
    except ValueError:
        pass
    return ""


@script_function()
def func_len(parser, text=""):
    return str(len(text))


@script_function(eval_args=False)
def func_lenmulti(parser, multi, separator=MULTI_VALUED_JOINER):
    return str(len(MultiValue(parser, multi, separator)))


@script_function()
def func_performer(parser, pattern="", join=", "):
    values = []
    for name, value in parser.context.items():
        if name.startswith("performer:") and pattern in name:
            values.append(value)
    return join.join(values)


@script_function(eval_args=False)
def func_matchedtracks(parser, *args):
    # only works in file naming scripts, always returns zero in tagging scripts
    file = parser.file
    if file and file.parent and hasattr(file.parent, 'album'):
        return str(parser.file.parent.album.get_num_matched_tracks())
    return "0"


@script_function()
def func_is_complete(parser):
    # only works in file naming scripts, always returns zero in tagging scripts
    file = parser.file
    if (file and file.parent and hasattr(file.parent, 'album')
            and file.parent.album.is_complete()):
        return "1"
    return ""


@script_function()
def func_firstalphachar(parser, text="", nonalpha="#"):
    if len(text) == 0:
        return nonalpha
    firstchar = text[0]
    if firstchar.isalpha():
        return firstchar.upper()
    else:
        return nonalpha


@script_function()
def func_initials(parser, text=""):
    return "".join(a[:1] for a in text.split(" ") if a[:1].isalpha())


@script_function()
def func_firstwords(parser, text, length):
    try:
        length = int(length)
    except ValueError:
        length = 0
    if len(text) <= length:
        return text
    else:
        try:
            if text[length] == ' ':
                return text[:length]
            return text[:length].rsplit(' ', 1)[0]
        except IndexError:
            return ''


@script_function()
def func_startswith(parser, text, prefix):
    if text.startswith(prefix):
        return "1"
    return ""


@script_function()
def func_endswith(parser, text, suffix):
    if text.endswith(suffix):
        return "1"
    return ""


@script_function()
def func_truncate(parser, text, length):
    try:
        length = int(length)
    except ValueError:
        length = None
    return text[:length].rstrip()


@script_function(check_argcount=False)
def func_swapprefix(parser, text, *prefixes):
    """
    Moves the specified prefixes to the end of text.
    If no prefix is specified 'A' and 'The' are taken as default.
    """
    # Inspired by the swapprefix plugin by Philipp Wolfer.

    text, prefix = _delete_prefix(parser, text, *prefixes)
    if prefix != '':
        return text + ', ' + prefix
    return text


@script_function(check_argcount=False)
def func_delprefix(parser, text, *prefixes):
    """
    Deletes the specified prefixes.
    If no prefix is specified 'A' and 'The' are taken as default.
    """
    # Inspired by the swapprefix plugin by Philipp Wolfer.

    return _delete_prefix(parser, text, *prefixes)[0]


def _delete_prefix(parser, text, *prefixes):
    """
    Worker function to deletes the specified prefixes.
    Returns remaining string and deleted part separately.
    If no prefix is specified 'A' and 'The' used.
    """
    # Inspired by the swapprefix plugin by Philipp Wolfer.

    if not prefixes:
        prefixes = ('A', 'The')
    text = text.strip()
    rx = '(' + r'\s+)|('.join(map(re.escape, prefixes)) + r'\s+)'
    match = re.match(rx, text)
    if match:
        pref = match.group()
        return text[len(pref):], pref.strip()
    return text, ''


@script_function(check_argcount=False)
def func_eq_any(parser, x, *args):
    """
    Return True if one string matches any of one or more other strings.
    $eq_any(a,b,c ...) is functionally equivalent to $or($eq(a,b),$eq(a,c) ...)
    Example: $if($eq_any(%artist%,foo,bar,baz),$set(engineer,test))
    """
    # Inspired by the eq2 plugin by Brian Schweitzer.
    return '1' if x in args else ''


@script_function(check_argcount=False)
def func_ne_all(parser, x, *args):
    """
    Return True if one string doesn't match all of one or more other strings.
    $ne_all(a,b,c ...) is functionally equivalent to $and($ne(a,b),$ne(a,c) ...)
    Example: $if($ne_all(%artist%,foo,bar,baz),$set(engineer,test))
    """
    # Inspired by the ne2 plugin by Brian Schweitzer.
    return '1' if x not in args else ''


@script_function(check_argcount=False)
def func_eq_all(parser, x, *args):
    """
    Return True if all string are equal.
    $eq_all(a,b,c ...) is functionally equivalent to $and($eq(a,b),$eq(a,c) ...)
    Example: $if($eq_all(%albumartist%,%artist%,Justin Bieber),$set(engineer,Meat Loaf))
    """
    for i in args:
        if x != i:
            return ''
    return '1'


@script_function(check_argcount=False)
def func_ne_any(parser, x, *args):
    """
    Return True if all strings are not equal.
    $ne_any(a,b,c ...) is functionally equivalent to $or($ne(a,b),$ne(a,c) ...)
    Example: $if($ne_any(%albumartist%,%trackartist%,%composer%),$set(lyricist,%composer%))
    """
    return func_not(parser, func_eq_all(parser, x, *args))


@script_function()
def func_title(parser, text):
    # GPL 2.0 licensed code by Javier Kohen, Sambhav Kothari
    # from https://github.com/metabrainz/picard-plugins/blob/2.0/plugins/titlecase/titlecase.py
    """
    Title-case a text - capitalizes first letter of every word
    like: from "Lost in the Supermarket" to "Lost In The Supermarket"
    Example: $set(album,$title(%album%))
    """
    capitalized = text[0].capitalize()
    capital = False
    for i in range(1, len(text)):
        t = text[i]
        if t in "’'" and text[i-1].isalpha():
            capital = False
        elif iswbound(t):
            capital = True
        elif capital and t.isalpha():
            capital = False
            t = t.capitalize()
        else:
            capital = False
        capitalized += t
    return capitalized


def iswbound(char):
    # GPL 2.0 licensed code by Javier Kohen, Sambhav Kothari
    # from https://github.com/metabrainz/picard-plugins/blob/2.0/plugins/titlecase/titlecase.py
    """ Checks whether the given character is a word boundary """
    category = unicodedata.category(char)
    return "Zs" == category or "Sk" == category or "P" == category[0]


@script_function()
def func_is_audio(parser):
    """Returns true, if the file processed is an audio file."""
    if func_is_video(parser) == "1":
        return ""
    else:
        return "1"


@script_function()
def func_is_video(parser):
    """Returns true, if the file processed is a video file."""
    if parser.context['~video'] and parser.context['~video'] != '0':
        return "1"
    else:
        return ""


@script_function()
def func_find(parser, haystack, needle):
    """Find the location of the first occurrence of one string within another.

    Arguments:
        parser: The ScriptParser object used to parse the script.
        haystack: The string to search.
        needle: The substring to find.

    Returns:
        The zero-based index of the first occurrence of needle in haystack, or "" if needle was not found.
    """
    index = haystack.find(needle)
    if index < 0:
        return ''
    return str(index)


@script_function()
def func_reverse(parser, text):
    """Returns 'text' in reverse order.

    Arguments:
        parser: The ScriptParser object used to parse the script.
        text: String to be processed.

    Returns:
        Text in reverse order.
    """
    return text[::-1]


@script_function()
def func_substr(parser, text, start_index, end_index):
    """Extract a specified portion of a string.

    Arguments:
        parser: The ScriptParser object used to parse the script.
        text: The string from which the extract will be made.
        start_index: Integer index of the first character to extract.
        end_index: Integer index of the first character that will not be extracted.

    Returns:
        Returns the substring beginning with the character at the start index,
        up to (but not including) the character at the end index.  The first
        character is at index number 0.  If the start index is left blank, it
        defaults to the first character in the string.  If the end index is
        left blank, it defaults to the number of characters in the string.
        If either index is negative, it is subtracted from the total number of
        characters in the string to provide the index used.
    """
    try:
        start = int(start_index) if start_index else None
    except ValueError:
        start = None
    try:
        end = int(end_index) if end_index else None
    except ValueError:
        end = None
    return text[start:end]


@script_function(eval_args=False)
def func_getmulti(parser, multi, item_index, separator=MULTI_VALUED_JOINER):
    """Returns value of the item at the specified index in the multi-value variable.  Index values are zero-based.

    Arguments:
        parser: The ScriptParser object used to parse the script.
        multi: The multi-value from which the item is to be retrieved.
        item_index: The zero-based integer index of the item to be retrieved.
        separator: String used to separate the elements in the multi-value.

    Returns:
        Returns the value of the item at the specified index in the multi-value variable.
    """
    if not item_index:
        return ''
    try:
        index = int(item_index.eval(parser))
        multi_value = MultiValue(parser, multi, separator)
        return str(multi_value[index])
    except (ValueError, IndexError):
        return ''


@script_function(eval_args=False)
def func_foreach(parser, multi, loop_code, separator=MULTI_VALUED_JOINER):
    """Iterates over each element found in the specified multi-value variable.

    Iterates over each element found in the specified multi-value variable, executing the specified code.
    For each loop, the element value is first stored in the tag specified by _loop_value and the count is
    stored in the tag specified by _loop_count.  This allows the element or count value to be accessed within
    the code script.

    Arguments:
        parser: The ScriptParser object used to parse the script.
        multi: The multi-value to be iterated.
        loop_code: String of script code to be processed on each iteration.
        separator: String used to separate the elements in the multi-value.
    """
    multi_value = MultiValue(parser, multi, separator)
    for loop_count, value in enumerate(multi_value, 1):
        func_set(parser, '_loop_count', str(loop_count))
        func_set(parser, '_loop_value', str(value))
        loop_code.eval(parser)
    func_unset(parser, '_loop_count')
    func_unset(parser, '_loop_value')
    return ''


@script_function(eval_args=False)
def func_while(parser, condition, loop_code):
    """Standard 'while' loop.  Also includes a runaway check to limit the maximum number of iterations.

    Arguments:
        parser: The ScriptParser object used to parse the script.
        condition: String of script code to check before each iteration through the loop.
        loop_code: String of script code to be processed on each iteration.
    """
    if condition and loop_code:
        runaway_check = 1000
        loop_count = 0
        while condition.eval(parser) and loop_count < runaway_check:
            loop_count += 1
            func_set(parser, '_loop_count', str(loop_count))
            loop_code.eval(parser)
        func_unset(parser, '_loop_count')
    return ''


@script_function(eval_args=False)
def func_map(parser, multi, loop_code, separator=MULTI_VALUED_JOINER):
    """Iterates over each element found in the specified multi-value variable and updates the value.

    Iterates over each element found in the specified multi-value variable and updates the value of the
    element to the value returned by the specified code. For each loop, the element value is first stored in
    the tag specified by _loop_value and the count is stored in the tag specified by _loop_count.  This
    allows the element or count value to be accessed within the code script.

    Arguments:
        parser: The ScriptParser object used to parse the script.
        multi: The multi-value to be iterated.
        loop_code: String of script code to be processed on each iteration that yields the new value for
            the multi-value element.
        separator: String used to separate the elements in the multi-value.

    Returns the updated multi-value variable.
    """
    multi_value = MultiValue(parser, multi, separator)
    for loop_count, value in enumerate(multi_value, 1):
        func_set(parser, '_loop_count', str(loop_count))
        func_set(parser, '_loop_value', str(value))
        multi_value[loop_count - 1] = str(loop_code.eval(parser))
    func_unset(parser, '_loop_count')
    func_unset(parser, '_loop_value')
    return str(multi_value)


@script_function(eval_args=False)
def func_join(parser, multi, join_phrase, separator=MULTI_VALUED_JOINER):
    """Joins all elements in the specified multi-value variable, placing the join_phrase between each element.

    Arguments:
        parser: The ScriptParser object used to parse the script.
        multi: The ScriptVariable/Function that evaluates to a multi-value whose
            elements are to be joined.
        join_phrase: The ScriptVariable/Function that evaluates to a string which
            will be placed between each of the elements.
        separator: A string or the ScriptVariable/Function that evaluates to the
            string used to separate the elements in the multi-value.

    Returns a string with the elements joined.
    """
    join_phrase = str(join_phrase.eval(parser))
    multi_value = MultiValue(parser, multi, separator)
    return join_phrase.join(multi_value)


@script_function(eval_args=False)
def func_slice(parser, multi, start_index, end_index, separator=MULTI_VALUED_JOINER):
    """Returns a multi-value containing a slice of the supplied multi-value.  Index values are zero-based.

    Arguments:
        parser: The ScriptParser object used to parse the script.
        multi: The ScriptVariable/Function that evaluates to a multi-value from
            which the slice is to be retrieved.
        start_index: The ScriptVariable/Function that evaluates to a zero-based integer
            index of the first item included in the slice.
        end_index: The ScriptVariable/Function that evaluates to a zero-based integer
            index of the first item not included in the slice.
        separator: A string or the ScriptVariable/Function that evaluates to the
            string used to separate the elements in the multi-value.

    Returns:
        Returns a multi-value variable containing the specified slice.
    """
    try:
        start = int(start_index.eval(parser)) if start_index else None
    except ValueError:
        start = None
    try:
        end = int(end_index.eval(parser)) if end_index else None
    except ValueError:
        end = None
    multi_value = MultiValue(parser, multi, separator)
    return multi_value.separator.join(multi_value[start:end])


@script_function()
def func_datetime(parser, format=None):
    """Return the current date and time as a string.

    Arguments:
        parser: The ScriptParser object used to parse the script.
        format: A string or the ScriptVariable/Function that evaluates to the
            string used to format the output.  Default is '%Y-%m-%d %H:%M:%S'
            if blank.  Uses strftime() format.

    Returns:
        Returns the current date and time as a string.
    """
    # local_tz required for Python 3.5 which does not allow setting astimezone()
    # on a naive datetime.datetime object.  This provides timezone information to
    # allow the use of %Z and %z in the output format.
    local_tz = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo

    # Handle case where format evaluates to ''
    if not format:
        format = '%Y-%m-%d %H:%M:%S'
    try:
        return datetime.datetime.now(tz=local_tz).strftime(format)
    except ValueError:
        stackitem = parser._function_stack.get()
        raise ScriptRuntimeError(stackitem, "Unsupported format code")


@script_function(eval_args=False)
def func_sortmulti(parser, multi, separator=MULTI_VALUED_JOINER):
    """Returns the supplied multi-value sorted in ascending order.

        parser: The ScriptParser object used to parse the script.
        multi: The ScriptVariable/Function that evaluates to a multi-value to be
            sorted.
        separator: A string or the ScriptVariable/Function that evaluates to the
            string used to separate the elements in the multi-value.

    Returns:
        Returns the supplied multi-value sorted in ascending order.
    """
    multi_value = MultiValue(parser, multi, separator)
    return multi_value.separator.join(sorted(multi_value))


@script_function(eval_args=False)
def func_reversemulti(parser, multi, separator=MULTI_VALUED_JOINER):
    """Returns the supplied multi-value in reverse order.

        parser: The ScriptParser object used to parse the script.
        multi: The ScriptVariable/Function that evaluates to a multi-value to be
            reversed.
        separator: A string or the ScriptVariable/Function that evaluates to the
            string used to separate the elements in the multi-value.

    Returns:
        Returns the supplied multi-value in reverse order.
    """
    multi_value = MultiValue(parser, multi, separator)
    return multi_value.separator.join(reversed(multi_value))
