"""Allows inline path template customization code in the config file."""

import itertools
import traceback

from beets import config
from beets.plugins import BeetsPlugin

FUNC_NAME = "__INLINE_FUNC__"


class InlineError(Exception):
    """Raised when a runtime error occurs in an inline expression."""

    def __init__(self, code, exc):
        super().__init__(
            f"error in inline path field code:\n{code}\n{type(exc).__name__}: {exc}"
        )


def _compile_func(body, args=""):
    """Given Python code for a function body, return a compiled
    callable that invokes that code.
    """
    body = body.replace("\n", "\n    ")
    body = f"def {FUNC_NAME}({args}):\n    {body}"
    code = compile(body, "inline", "exec")
    env = {}
    eval(code, env)
    return env[FUNC_NAME]


class InlinePlugin(BeetsPlugin):
    def __init__(self):
        super().__init__()

        config.add(
            {
                "pathfields": {},  # Legacy name.
                "item_fields": {},
                "album_fields": {},
            }
        )

        # Item fields.
        for key, view in itertools.chain(
            config["item_fields"].items(), config["pathfields"].items()
        ):
            self._log.debug("adding item field {}", key)
            func = self.compile_inline(view.as_str(), False, key)
            if func is not None:
                self.template_fields[key] = func

        # Album fields.
        for key, view in config["album_fields"].items():
            self._log.debug("adding album field {}", key)
            func = self.compile_inline(view.as_str(), True, key)
            if func is not None:
                self.album_template_fields[key] = func

    def compile_inline(self, python_code, album, field_name):
        """Given a Python expression or function body, compile it as a path
        field function. The returned function takes a single argument, an
        Item, and returns a Unicode string. If the expression cannot be
        compiled, then an error is logged and this function returns None.
        """
        # First, try compiling as a single function.
        try:
            code = compile(f"({python_code})", "inline", "eval")
        except SyntaxError:
            # Fall back to a function body.
            try:
                func = _compile_func(python_code, args="db_obj")
            except SyntaxError:
                self._log.error(
                    "syntax error in inline field definition:\n{}",
                    traceback.format_exc(),
                )
                return None
            else:
                is_expr = False
        else:
            is_expr = True

        def _dict_for(obj):
            out = {}
            for key in obj.keys(computed=False):
                if key == field_name:
                    continue
                out[key] = obj._get(key)

            if album:
                out["items"] = list(obj.items())
            return out

        if is_expr:
            # For expressions, just evaluate and return the result.
            def _expr_func(obj):
                values = _dict_for(obj)
                values["db_obj"] = obj
                try:
                    return eval(code, values)
                except Exception as exc:
                    raise InlineError(python_code, exc)

            return _expr_func

        # For function bodies, invoke the function with values as global
        # variables.
        def _func_func(obj):
            old_globals = dict(func.__globals__)
            func.__globals__.update(_dict_for(obj))
            try:
                return func(obj)
            except Exception as exc:
                raise InlineError(python_code, exc)
            finally:
                func.__globals__.clear()
                func.__globals__.update(old_globals)

        return _func_func
