# -*- coding: utf-8; -*-
"""AST markers for internal communication.

*Internal* here means they are to be never passed to Python's `compile`;
macros may use them to work together.
"""

__all__ = ["ASTMarker", "get_markers", "delete_markers", "check_no_markers_remaining"]

import ast

from . import core, utils, walkers


class ASTMarker(ast.AST):
    """Base class for AST markers.

    Markers are AST-node-like objects meant for communication between
    co-operating, related macros. They are also used by the macro expander
    to talk with itself during expansion.

    We inherit from `ast.AST`, so that during macro expansion, a marker
    behaves like a single AST node.

    It is a postcondition of a completed macro expansion that no markers
    remain in the AST.

    To help fail-fast, if you define your own marker types, use `get_markers`
    to check (at an appropriate point) that the expanded AST has no instances
    of your own markers remaining. (You'll want a base class for your own markers.)

    A typical usage example is in the quasiquote system, where the unquote
    operators (some of which expand to markers) may only appear inside a quoted
    section. So just before the quote operator exits, it checks that all
    quasiquote markers within that section have been compiled away.

    Subclasses should define additional fields at the class level::

        class MyMarker(ASTMarker):
            _fields = ["body", "kind"]

            def __init__(self, body, kind):
                super().__init__(body)
                self.kind = kind

    The ``body`` field must always be included. ``__init__`` creates a
    per-instance copy of ``_fields`` as a safety net.
    """
    _fields = ["body"]

    # Silly default `None`, because `copy` and `deepcopy` call `__init__` without arguments,
    # though the docs say they behave like `pickle` (and wouldn't thus need to call __init__ at all!).
    def __init__(self, body=None):
        """body: the actual AST that is annotated by this marker"""
        self.body = body
        # Per-instance copy so that any accidental `self._fields += [...]`
        # in downstream code doesn't mutate the class-level list.
        self._fields = list(type(self)._fields)


def get_markers(tree, cls=ASTMarker):
    """Return a `list` of any `cls` instances found in `tree`. For output validation."""
    class ASTMarkerCollector(walkers.ASTVisitor):
        def examine(self, tree):
            if isinstance(tree, cls):
                self.collect(tree)
            self.generic_visit(tree)
    w = ASTMarkerCollector()
    w.visit(tree)
    return w.collected


def delete_markers(tree, cls=ASTMarker):
    """Delete any `cls` ASTMarker instances found in `tree`.

    The deletion takes place by replacing each marker node with
    the actual AST node stored in its `body` attribute.
    """
    class ASTMarkerDeleter(walkers.ASTTransformer):
        def transform(self, tree):
            if isinstance(tree, cls):
                return self.visit(tree.body)
            return self.generic_visit(tree)
    return ASTMarkerDeleter().visit(tree)


def check_no_markers_remaining(tree, *, filename, cls=None):
    """Check that `tree` has no AST markers remaining.

    If a class `cls` is provided, only check for markers that `isinstance(cls)`.

    If there are any, raise `MacroExpansionError`.
    No return value.

    `filename` is the full path to the `.py` file, for error reporting.

    Convenience function.
    """
    cls = cls or ASTMarker
    remaining_markers = get_markers(tree, cls)
    if remaining_markers:
        codes = [utils.format_context(node, n=5) for node in remaining_markers]
        locations = [utils.format_location(filename, node, code) for node, code in zip(remaining_markers, codes)]
        report = "\n\n".join(locations)
        raise core.MacroExpansionError(f"{filename}: AST markers remaining after expansion:\n{report}")
