# Copyright (c) 2011-2020 Eric Froemling
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Plugins for pylint"""

from __future__ import annotations

from typing import TYPE_CHECKING

import astroid

if TYPE_CHECKING:
    from astroid import node_classes as nc
    from typing import Set, Dict, Any

VERBOSE = False


def register(linter: Any) -> None:
    """Unused here; we're modifying the ast; not linters."""
    del linter  # Unused.


failed_imports: Set[str] = set()


def failed_import_hook(modname: str) -> None:
    """Custom failed import callback."""

    # We don't actually do anything here except note in our log that
    # something couldn't be imported. (may help sanity-check our filtering)
    if VERBOSE:
        if modname not in failed_imports:
            failed_imports.add(modname)
            print('GOT FAILED IMPORT OF', modname)
    raise astroid.AstroidBuildingError(modname=modname)


def ignore_type_check_filter(node: nc.NodeNG) -> nc.NodeNG:
    """Ignore stuff under 'if TYPE_CHECKING:' block at module level."""

    # Look for a non-nested 'if TYPE_CHECKING:'
    if (isinstance(node.test, astroid.Name)
            and node.test.name == 'TYPE_CHECKING'
            and isinstance(node.parent, astroid.Module)):

        # Find the module node.
        mnode = node
        while mnode.parent is not None:
            mnode = mnode.parent

        # First off, remove any names that are getting defined
        # in this block from the module locals.
        for cnode in node.body:
            _strip_import(cnode, mnode)

        # Now replace the body with a simple 'pass'. This will
        # keep pylint from complaining about grouped imports/etc.
        passnode = astroid.Pass(parent=node,
                                lineno=node.lineno + 1,
                                col_offset=node.col_offset + 1)
        node.body = [passnode]
    return node


def ignore_reveal_type_call(node: nc.NodeNG) -> nc.NodeNG:
    """Make 'reveal_type()' not trigger an error.

    The 'reveal_type()' fake call is used for type debugging types with
    mypy and it is annoying having pylint errors pop up alongside the mypy
    info.
    """

    # Let's just replace any reveal_type(x) call with print(x)..
    if (isinstance(node.func, astroid.Name)
            and node.func.name == 'reveal_type'):
        node.func.name = 'print'
        return node
    return node


def _strip_import(cnode: nc.NodeNG, mnode: nc.NodeNG) -> None:
    if isinstance(cnode, (astroid.Import, astroid.ImportFrom)):
        for name, val in list(mnode.locals.items()):
            if cnode in val:

                # Pull us out of the list.
                valnew = [v for v in val if v is not cnode]
                if valnew:
                    mnode.locals[name] = valnew
                else:
                    del mnode.locals[name]


def using_future_annotations(node: nc.NodeNG) -> nc.NodeNG:
    """Return whether postponed annotation evaluation is enabled (PEP 563)."""

    # Find the module.
    mnode = node
    while mnode.parent is not None:
        mnode = mnode.parent

    # Look for 'from __future__ import annotations' to decide
    # if we should assume all annotations are defer-eval'ed.
    # NOTE: this will become default at some point within a few years..
    annotations_set = mnode.locals.get('annotations')
    if (annotations_set and isinstance(annotations_set[0], astroid.ImportFrom)
            and annotations_set[0].modname == '__future__'):
        return True
    return False


def func_annotations_filter(node: nc.NodeNG) -> nc.NodeNG:
    """Filter annotated function args/retvals.

    This accounts for deferred evaluation available in in Python 3.7+
    via 'from __future__ import annotations'. In this case we don't
    want Pylint to complain about missing symbols in annotations when
    they aren't actually needed at runtime.
    """
    # Only do this if deferred annotations are on.
    if not using_future_annotations(node):
        return node

    # Wipe out argument annotations.

    # Special-case: functools.singledispatch and ba.dispatchmethod *do*
    # evaluate annotations at runtime so we want to leave theirs intact.
    # Lets just look for a @XXX.register decorator used by both I guess.
    if node.decorators is not None:
        for dnode in node.decorators.nodes:
            if (isinstance(dnode, astroid.nodes.Name)
                    and dnode.name in ('dispatchmethod', 'singledispatch')):
                return node  # Leave annotations intact.

            if (isinstance(dnode, astroid.nodes.Attribute)
                    and dnode.attrname == 'register'):
                return node  # Leave annotations intact.

    node.args.annotations = [None for _ in node.args.args]
    node.args.varargannotation = None
    node.args.kwargannotation = None
    node.args.kwonlyargs_annotations = [None for _ in node.args.kwonlyargs]

    # Wipe out return-value annotation.
    if node.returns is not None:
        node.returns = None

    return node


def var_annotations_filter(node: nc.NodeNG) -> nc.NodeNG:
    """Filter annotated function variable assigns.

    This accounts for deferred evaluation.
    """
    if using_future_annotations(node):
        # Future behavior:
        # Annotations are never evaluated.
        willeval = False
    else:
        # Legacy behavior:
        # Annotated assigns under functions are not evaluated,
        # but class or module vars are.
        fnode = node
        willeval = True
        while fnode is not None:
            if isinstance(fnode,
                          (astroid.FunctionDef, astroid.AsyncFunctionDef)):
                willeval = False
                break
            if isinstance(fnode, astroid.ClassDef):
                willeval = True
                break
            fnode = fnode.parent

    # If this annotation won't be eval'ed, replace it with a dummy string.
    if not willeval:
        dummyval = astroid.Const(parent=node, value='dummyval')
        node.annotation = dummyval

    return node


def register_plugins(manager: astroid.Manager) -> None:
    """Apply our transforms to a given astroid manager object."""

    if VERBOSE:
        manager.register_failed_import_hook(failed_import_hook)

    # Completely ignore everything under an 'if TYPE_CHECKING' conditional.
    # That stuff only gets run for mypy, and in general we want to
    # check code as if it doesn't exist at all.
    manager.register_transform(astroid.If, ignore_type_check_filter)

    manager.register_transform(astroid.Call, ignore_reveal_type_call)

    # Annotations on variables within a function are defer-eval'ed
    # in some cases, so lets replace them with simple strings in those
    # cases to avoid type complaints.
    # (mypy will still properly alert us to type errors for them)
    manager.register_transform(astroid.AnnAssign, var_annotations_filter)
    manager.register_transform(astroid.FunctionDef, func_annotations_filter)
    manager.register_transform(astroid.AsyncFunctionDef,
                               func_annotations_filter)


register_plugins(astroid.MANAGER)
