# coding: utf-8
#
# Copyright 2018 The Oppia Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS-IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Implements additional custom Pylint checkers to be used as part of
presubmit checks. Next message id would be C0029.
"""

from __future__ import absolute_import  # pylint: disable=import-only-modules
from __future__ import unicode_literals  # pylint: disable=import-only-modules

import linecache
import os
import re
import sys
import tokenize

import python_utils
from .. import docstrings_checker

_PARENT_DIR = os.path.abspath(os.path.join(os.getcwd(), os.pardir))
_PYLINT_PATH = os.path.join(_PARENT_DIR, 'oppia_tools', 'pylint-1.9.4')
sys.path.insert(0, _PYLINT_PATH)

# List of punctuation symbols that can be used at the end of
# comments and docstrings.
ALLOWED_TERMINATING_PUNCTUATIONS = ['.', '?', '}', ']', ')']

# If any of these phrases are found inside a docstring or comment,
# the punctuation and capital letter checks will be skipped for that
# comment or docstring.
EXCLUDED_PHRASES = [
    'coding:', 'pylint:', 'http://', 'https://', 'scripts/', 'extract_node']

import astroid  # isort:skip  pylint: disable=wrong-import-order, wrong-import-position
from pylint import checkers  # isort:skip  pylint: disable=wrong-import-order, wrong-import-position
from pylint import interfaces  # isort:skip  pylint: disable=wrong-import-order, wrong-import-position
from pylint.checkers import typecheck  # isort:skip  pylint: disable=wrong-import-order, wrong-import-position
from pylint.checkers import utils as checker_utils  # isort:skip  pylint: disable=wrong-import-order, wrong-import-position
from pylint.extensions import _check_docs_utils # isort:skip  pylint: disable=wrong-import-order, wrong-import-position


def read_from_node(node):
    """Returns the data read from the ast node in unicode form.

    Args:
        node: astroid.scoped_nodes.Function. Node to access module content.

    Returns:
        list(str). The data read from the ast node.
    """
    return list([line.decode('utf-8') for line in node.stream().readlines()])


class ExplicitKeywordArgsChecker(checkers.BaseChecker):
    """Custom pylint checker which checks for explicit keyword arguments
    in any function call.
    """

    __implements__ = interfaces.IAstroidChecker

    name = 'explicit-keyword-args'
    priority = -1
    msgs = {
        'C0001': (
            'Keyword argument %s should be named explicitly in %s call of %s.',
            'non-explicit-keyword-args',
            'All keyword arguments should be explicitly named in function call.'
        ),
        'C0027': (
            'Keyword argument %s used for a non keyword argument in %s '
            'call of %s.',
            'arg-name-for-non-keyword-arg',
            'Position arguments should not be used as keyword arguments '
            'in function call.'
        ),
    }

    def _check_non_explicit_keyword_args(
            self, node, name, callable_name, keyword_args,
            num_positional_args_unused, num_mandatory_parameters):
        """Custom pylint check to ensure that position arguments should not
        be used as keyword arguments.

        Args:
            node: astroid.node.Function. The current function call node.
            name: str. Name of the keyword argument.
            callable_name: str. Name of method type.
            keyword_args: list(str). Name of all keyword arguments in function
                call.
            num_positional_args_unused: int. Number of unused positional
                arguments.
            num_mandatory_parameters: int. Number of mandatory parameters.

        Returns:
            int. Number of unused positional arguments.
        """
        display_name = repr(name)

        if name not in keyword_args and (
                num_positional_args_unused > (
                    num_mandatory_parameters)) and (
                        callable_name != 'constructor'):
            # This try/except block tries to get the function
            # name. Since each node may differ, multiple
            # blocks have been used.
            try:
                func_name = node.func.attrname
            except AttributeError:
                func_name = node.func.name

            self.add_message(
                'non-explicit-keyword-args', node=node,
                args=(
                    display_name,
                    callable_name,
                    func_name))
            num_positional_args_unused -= 1
        return num_positional_args_unused

    def _check_argname_for_nonkeyword_arg(
            self, node, called, callable_name, keyword_args,
            keyword_args_in_funcdef):
        """Custom pylint check to ensure that position arguments should not
        be used as keyword arguments.

        Args:
            node: astroid.node.Function. The current function call node.
            called: astroid.Call. The function call object.
            keyword_args: list(str). Name of all keyword arguments in function
                call.
    
        callable_name: str. Name of method type.
   
         keyword_args_in_funcdef: list(str). Name of all keyword arguments in
                function definition.
        """
        for arg in keyword_args:
            # TODO(#10038): Fix the check to cover below case as well.
            # If there is *args and **kwargs in the function definition skip the
            # check because we can use keywords arguments in function call even
            # if **kwargs is present in the function definition. See Example:
            # Function def -> def func(entity_id, *args, **kwargs):
            # Function call -> func(entity_id='1', a=1, b=2, c=3)
            # By parsing calling method we get
            # keyword_arguments = entity_id, a, b, c.
            # From the function definition, we will get keyword_arguments = []
            # Now we do not have a way to identify which one is a keyword
            # argument and which one is not.
            if not called.args.kwarg and callable_name != 'constructor':
                if not arg in keyword_args_in_funcdef:
                    # This try/except block tries to get the function
                    # name.
                    try:
                        func_name = node.func.attrname
                    except AttributeError:
                        func_name = node.func.name

                    self.add_message(
                        'arg-name-for-non-keyword-arg', node=node,
                        args=(repr(arg), callable_name, func_name))

    def visit_call(self, node):
        """Visits each function call in a lint check.

        Args:
            node: Call. The current function call node.
        """
        called = checker_utils.safe_infer(node.func)

        try:
            # For the rationale behind the Pylint pragma below,
            # see https://stackoverflow.com/a/35701863/8115428
            called, implicit_args, callable_name = (
                typecheck._determine_callable(called))  # pylint: disable=protected-access
        except ValueError:
            return

        if called.args.args is None:
            # Built-in functions have no argument information.
            return

        if len(called.argnames()) != len(set(called.argnames())):
            return

        # Build the set of keyword arguments and count the positional arguments.
        call_site = astroid.arguments.CallSite.from_call(node)

        num_positional_args = len(call_site.positional_arguments)
        keyword_args = list(call_site.keyword_arguments.keys())

        already_filled_positionals = getattr(calle