# -*- 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 K
eyError:
            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:
      