# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

# This module contains functionality for adding formatted, opaque "code" blocks
# into the IPDL ast. These code objects follow IPDL C++ ast patterns, and
# perform lowering in much the same way.

# In general it is recommended to use these for blocks of code which would
# otherwise be specified by building a hardcoded IPDL-C++ AST, as users of this
# API are often easier to read than users of the AST APIs in these cases.

import re
import math
import textwrap

from ipdl.cxx.ast import Node, Whitespace, GroupNode, VerbatimNode


# -----------------------------------------------------------------------------
# Public API.


def StmtCode(tmpl, **kwargs):
    """Perform template substitution to build opaque C++ AST nodes. See the
    module documentation for more information on the templating syntax.

    StmtCode nodes should be used where Stmt* nodes are used. They are placed
    on their own line and indented."""
    return _code(tmpl, False, kwargs)


def ExprCode(tmpl, **kwargs):
    """Perform template substitution to build opaque C++ AST nodes. See the
    module documentation for more information on the templating syntax.

    ExprCode nodes should be used where Expr* nodes are used. They are placed
    inline, and no trailing newline is added."""
    return _code(tmpl, True, kwargs)


def StmtVerbatim(text):
    """Build an opaque C++ AST node which emits input text verbatim.

    StmtVerbatim nodes should be used where Stmt* nodes are used. They are placed
    on their own line and indented."""
    return _verbatim(text, False)


def ExprVerbatim(text):
    """Build an opaque C++ AST node which emits input text verbatim.

    ExprVerbatim nodes should be used where Expr* nodes are used. They are
    placed inline, and no trailing newline is added."""
    return _verbatim(text, True)


# -----------------------------------------------------------------------------
# Implementation


def _code(tmpl, inline, context):
    # Remove common indentation, and strip the preceding newline from
    # '''-quoting, because we usually don't want it.
    if tmpl.startswith("\n"):
        tmpl = tmpl[1:]
    tmpl = textwrap.dedent(tmpl)

    # Process each line in turn, building up a list of nodes.
    nodes = []
    for idx, line in enumerate(tmpl.splitlines()):
        # Place newline tokens between lines in the input.
        if idx > 0:
            nodes.append(Whitespace.NL)

        # Don't indent the first line if `inline` is set.
        skip_indent = inline and idx == 0
        nodes.append(_line(line.rstrip(), skip_indent, idx + 1, context))

    # If we're inline, don't add the final trailing newline.
    if not inline:
        nodes.append(Whitespace.NL)
    return GroupNode(nodes)


def _verbatim(text, inline):
    # For simplicitly, _verbatim is implemented using the same logic as _code,
    # but with '$' characters escaped. This ensures we only need to worry about
    # a single, albeit complex, codepath.
    return _code(text.replace("$", "$$"), inline, {})


# Pattern used to identify substitutions.
_substPat = re.compile(
    r"""
    \$(?:
        (?P<escaped>\$)                  | # '$$' is an escaped '$'
        (?P<list>[*,])?{(?P<expr>[^}]+)} | # ${expr}, $*{expr}, or $,{expr}
        (?P<invalid>)                      # For error reporting
    )
    """,
    re.IGNORECASE | re.VERBOSE,
)


def _line(raw, skip_indent, lineno, context):
    assert "\n" not in raw

    # Determine the level of indentation used for this line
    line = raw.lstrip()
    offset = int(math.ceil((len(raw) - len(line)) / 4))

    # If line starts with a directive, don't indent it.
    if line.startswith("#"):
        skip_indent = True

    column = 0
    children = []
    for match in _substPat.finditer(line):
        if match.group("invalid") is not None:
            raise ValueError("Invalid substitution on line %d" % lineno)

        # Any text from before the current entry should be written, and column
        # advanced.
        if match.start() > column:
            before = line[column : match.start()]
            children.append(VerbatimNode(before))
        column = match.end()

        # If we have an escaped group, emit a '$' node.
        if match.group("escaped") is not None:
            children.append(VerbatimNode("$"))
            continue

        # At this point we should have an expression.
        list_chr = match.group("list")
        expr = match.group("expr")
        assert expr is not None

        # Evaluate our expression in the context to get the values.
        try:
            values = eval(expr, context, {})
        except Exception as e:
            msg = "%s in substitution on line %d" % (repr(e), lineno)
            raise ValueError(msg) from e

        # If we aren't dealing with lists, wrap the result into a
        # single-element list.
        if list_chr is None:
            values = [values]

        # Check if this substitution is inline, or the entire line.
        inline = match.span() != (0, len(line))

        for idx, value in enumerate(values):
            # When using `,` as a list mode in a non-inline environment, put
            # each expression on its own line.
            multiline_list = list_chr == "," and not inline

            # If we're using ',' as list mode, put a comma between each node.
            if idx > 0 and list_chr == ",":
                children.append(VerbatimNode(",\n" if multiline_list else ", "))

            # Expr entries such as those used in multiline lists do not indent,
            # so add a node to handle indenting on each line.
            if multiline_list:
                children.append(VerbatimNode("", indent=True))

            # If our value isn't a node, turn it into one. Verbatim should be
            # inline unless indent isn't being skipped, and the match isn't
            # inline.
            if not isinstance(value, Node):
                value = _verbatim(str(value), skip_indent or inline)
            children.append(value)

        # If we were the entire line, indentation is handled by the added child
        # nodes. Do this after the above loop such that created verbatims have
        # the correct inline-ness.
        if not inline:
            skip_indent = True

    # Add any remaining text in the line.
    if len(line) > column:
        children.append(VerbatimNode(line[column:]))

    # If we have no children, just emit the empty string. This will become a
    # blank line.
    if len(children) == 0:
        return VerbatimNode("")

    # Add the initial indent if we aren't skipping it.
    if not skip_indent:
        children.insert(0, VerbatimNode("", indent=True))

    # Wrap ourselves into a group node with the correct indent offset
    return GroupNode(children, offset=offset)
