from typing import TypeVar
from collections import namedtuple
from Aspidites._vendor._compat import IS_CPYTHON
from Aspidites._vendor._compat import IS_PY2
from Aspidites._vendor.RestrictedPython.transformer import RestrictingNodeTransformer

import ast
import warnings

T = TypeVar("T")
CompileResult: T = namedtuple("CompileResult", "code, errors, warnings, used_names")
syntax_error_template = "Line {lineno}: {type}: {msg} at statement: {statement!r}"

NOT_CPYTHON_WARNING = (
    "RestrictedPython is only supported on CPython: use on other Python "
    "implementations may create security issues."
)


def _compile_restricted_mode(
    source,
    filename="<string>",
    mode="exec",
    flags=0,
    dont_inherit=False,
    policy=RestrictingNodeTransformer,
):

    if not IS_CPYTHON:
        warnings.warn_explicit(
            NOT_CPYTHON_WARNING, RuntimeWarning, "RestrictedPython", 0
        )

    byte_code = None
    collected_errors = []
    collected_warnings = []
    used_names = {}
    if policy is None:
        # Unrestricted Source Checks
        byte_code = compile(
            source, filename, mode=mode, flags=flags, dont_inherit=dont_inherit
        )
    elif issubclass(policy, RestrictingNodeTransformer):
        c_ast = None
        allowed_source_types = [str, ast.Module]
        if IS_PY2:  # pragma: no cover
            allowed_source_types.append(
                unicode
            )  # NOQA: F821,E501  # PY2 only statement, in Python 2 only module
        if not issubclass(type(source), tuple(allowed_source_types)):
            raise TypeError(
                "Not allowed source type: " '"{0.__class__.__name__}".'.format(source)
            )
        c_ast = None
        # workaround for pypy issue https://bitbucket.org/pypy/pypy/issues/2552
        if isinstance(source, ast.Module):
            c_ast = source
        else:
            try:
                c_ast = ast.parse(source, filename, mode)
            except (TypeError, ValueError) as e:
                collected_errors.append(str(e))
            except SyntaxError as v:
                collected_errors.append(
                    syntax_error_template.format(
                        lineno=v.lineno,
                        type=v.__class__.__name__,
                        msg=v.msg,
                        statement=v.text.strip() if v.text else None,
                    )
                )
        if c_ast:
            policy_instance = policy(collected_errors, collected_warnings, used_names)
            policy_instance.visit(c_ast)
            if not collected_errors:
                byte_code = compile(
                    c_ast,
                    filename,
                    mode=mode  # ,
                    # flags=flags,
                    # dont_inherit=dont_inherit
                )
    else:
        raise TypeError("Unallowed policy provided for RestrictedPython")
    return CompileResult(
        byte_code, tuple(collected_errors), collected_warnings, used_names
    )


def compile_restricted_exec(
    source,
    filename="<string>",
    flags=0,
    dont_inherit=False,
    policy=RestrictingNodeTransformer,
):
    """Compile restricted for the mode `exec`."""
    return _compile_restricted_mode(
        source,
        filename=filename,
        mode="exec",
        flags=flags,
        dont_inherit=dont_inherit,
        policy=policy,
    )


def compile_restricted_eval(
    source,
    filename="<string>",
    flags=0,
    dont_inherit=False,
    policy=RestrictingNodeTransformer,
):
    """Compile restricted for the mode `eval`."""
    return _compile_restricted_mode(
        source,
        filename=filename,
        mode="eval",
        flags=flags,
        dont_inherit=dont_inherit,
        policy=policy,
    )


def compile_restricted_single(
    source,
    filename="<string>",
    flags=0,
    dont_inherit=False,
    policy=RestrictingNodeTransformer,
):
    """Compile restricted for the mode `single`."""
    return _compile_restricted_mode(
        source,
        filename=filename,
        mode="single",
        flags=flags,
        dont_inherit=dont_inherit,
        policy=policy,
    )


def compile_restricted_function(
    p,  # parameters
    body,
    name,
    filename="<string>",
    globalize=None,  # List of globals (e.g. ['here', 'context', ...])
    flags=0,
    dont_inherit=False,
    policy=RestrictingNodeTransformer,
):
    """Compile a restricted code object for a function.

    Documentation see:
    http://restrictedpython.readthedocs.io/en/latest/usage/index.html#RestrictedPython.compile_restricted_function
    """
    # Parse the parameters and body, then combine them.
    try:
        body_ast = ast.parse(body, "<func code>", "exec")
    except SyntaxError as v:
        error = syntax_error_template.format(
            lineno=v.lineno,
            type=v.__class__.__name__,
            msg=v.msg,
            statement=v.text.strip() if v.text else None,
        )
        return CompileResult(code=None, errors=(error,), warnings=(), used_names=())

    # The compiled code is actually executed inside a function
    # (that is called when the code is called) so reading and assigning to a
    # global variable like this`printed += 'foo'` would throw an
    # UnboundLocalError.
    # We don't want the user to need to understand this.
    if globalize:
        body_ast.body.insert(0, ast.Global(globalize))
    wrapper_ast = ast.parse(
        "def masked_function_name(%s): pass" % p, "<func wrapper>", "exec"
    )
    # In case the name you chose for your generated function is not a
    # valid python identifier we set it after the fact
    function_ast = wrapper_ast.body[0]
    assert isinstance(function_ast, ast.FunctionDef)
    function_ast.name = name

    wrapper_ast.body[0].body = body_ast.body
    wrapper_ast = ast.fix_missing_locations(wrapper_ast)

    result = _compile_restricted_mode(
        wrapper_ast,
        filename=filename,
        mode="exec",
        flags=flags,
        dont_inherit=dont_inherit,
        policy=policy,
    )

    return result


def compile_restricted(
    source,
    filename="<unknown>",
    mode="exec",
    flags=0,
    dont_inherit=False,
    policy=RestrictingNodeTransformer,
):
    """Replacement for the built-in compile() function.

    policy ... `ast.NodeTransformer` class defining the restrictions.

    """
    if mode in ["exec", "eval", "single", "function"]:
        result = _compile_restricted_mode(
            source,
            filename=filename,
            mode=mode,
            flags=flags,
            dont_inherit=dont_inherit,
            policy=policy,
        )
    else:
        raise TypeError("unknown mode %s", mode)
    for warning in result.warnings:
        warnings.warn(warning, SyntaxWarning)
    if result.errors:
        raise SyntaxError(result.errors)
    return result.code
