# -*- test-case-name: twistedchecker.test.test_runner -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

import sys
import os
import re

from astroid.modutils import file_from_modpath
from pylint.checkers.base import NameChecker
from pylint.lint import PyLinter

from twisted.python.compat import NativeStringIO

import twistedchecker
from twistedchecker.checkers import patch_pylint_format
from twistedchecker.core.exceptionfinder import findAllExceptions
from twistedchecker.reporters.limited import LimitedReporter


class Runner():
    """
    Run and control the checking process.
    """
    outputStream = None
    linter = None
    allowOptions = None
    # Customized checkers.
    checkers = ("header.HeaderChecker",
                "names.TwistedNamesChecker",
                "docstring.DocstringChecker",
                "formattingoperation.FormattingOperationChecker",
                "comment.CommentChecker",
                "testclassname.TestClassNameChecker")
    allowedMessagesFromPylint = ("F0001",
                                 "C0103",
                                 "C0301",
                                 "W0311",
                                 "W0312")
    diffOption = None
    errorResultRead = "Error: Failed to read result file '%s'.\n"
    prefixModuleName = "************* Module "
    regexLineStart = "^[WCEFR]\d{4}\:"

    def __init__(self):
        """
        Initialize C{PyLinter} object, and load configuration file.
        """
        self.allowOptions = True
        self.linter = PyLinter(self._makeOptions())
        # register standard checkers.
        self.linter.load_default_plugins()
        # read configuration.
        pathConfig = os.path.join(twistedchecker.abspath,
                                  "configuration", "pylintrc")
        self.linter.read_config_file(pathConfig)
        # now we can load file config and command line, plugins (which can
        # provide options) have been registered.
        self.linter.load_config_file()
        allowedMessages = self.registerCheckers()
        # disable messages
        disabledMessages = set(self.linter
                           .cfgfile_parser.get("TWISTEDCHECKER", "disable")
                           .replace(" ", "").split(","))
        if disabledMessages != {""}:
            for msg in disabledMessages:
                self.linter.disable(msg)
            allowedMessages -= disabledMessages
        # set default output stream to stdout
        self.setOutput(sys.stdout)
        # set default reporter to limited reporter
        self.linter.set_reporter(LimitedReporter(allowedMessages))


    def _makeOptions(self):
        """
        Return options for twistedchecker.
        """
        return (
            ("diff",
             {"type": "string",
              "metavar": "<result-file>",
              "help": "Set comparing result file to automatically "
                      "generate a diff."}
            ),
            ('pep8',
             {'type': 'yn', 'metavar': '<y_or_n>',
              'default': False,
              'help': 'Show pep8 warnings.'}
            ),
            ('strict-epydoc',
             {'type': 'yn', 'metavar': '<y_or_n>',
              'default': False,
              'help': "Check '@type' and '@rtype' in epydoc."}
            ),
          )


    def setOutput(self, stream):
        """
        Set the stream to output result of checking.

        @param stream: output stream, defaultly it should be stdout
        """
        self.outputStream = stream
        sys.stdout = stream


    def displayHelp(self):
        """
        Output help message of twistedchecker.
        """
        self.outputStream.write(self.linter.help())
        sys.exit(32)


    def registerCheckers(self):
        """
        Register all checkers of TwistedChecker to C{PyLinter}.

        @return: a list of allowed messages
        """
        # We patch the default pylint format checker.
        patch_pylint_format.patch()

        # register checkers
        allowedMessages = list(self.allowedMessagesFromPylint)
        for strChecker in self.checkers:
            modname, classname = strChecker.split(".")
            strModule = "twistedchecker.checkers.%s" % modname
            checker = getattr(__import__(strModule,
                                        fromlist=["twistedchecker.checkers"]),
                             classname)
            instanceChecker = checker(self.linter)
            allowedMessages += list(instanceChecker.msgs.keys())
            self.linter.register_checker(instanceChecker)

        self.restrictCheckers(allowedMessages)
        return set(allowedMessages)


    def unregisterChecker(self, checker):
        """
        Remove a checker from the list of registered checkers.

        @param checker: the checker to remove
        """
        self.linter._checkers[checker.name].remove(checker)
        if checker in self.linter._reports:
            del self.linter._reports[checker]
        if checker in self.linter.options_providers:
            self.linter.options_providers.remove(checker)


    def findUselessCheckers(self, allowedMessages):
        """
        Find checkers which generate no allowed messages.

        @param allowedMessages: allowed messages
        @return: useless checkers, remove them from pylint
        """
        uselessCheckers = []
        for checkerName in self.linter._checkers:
            for checker in list(self.linter._checkers[checkerName]):
                messagesOfChecker = set(checker.msgs)
                if not messagesOfChecker.intersection(allowedMessages):
                    uselessCheckers.append(checker)
        return uselessCheckers


    def restrictCheckers(self, allowedMessages):
        """
        Unregister useless checkers to speed up twistedchecker.

        @param allowedMessages: output messages allowed in twistedchecker
        """
        uselessCheckers = self.findUselessCheckers(allowedMessages)
        # Unregister these checkers
        for checker in uselessCheckers:
            self.unregisterChecker(checker)


    def getCheckerByName(self, checkerType):
        """
        Get checker by given name.

        @checkerType: type of the checker
        """
        for checker in sum(list(self.linter._checkers.values()), []):
            if isinstance(checker, checkerType):
                return checker
        return None


    def allowPatternsForNameChecking(self, patternsFunc, patternsClass):
        """
        Allow name exceptions by given patterns.

        @param patternsFunc: patterns of special function names
        @param patternsClass: patterns of special class names
        """
        cfgParser = self.linter.cfgfile_parser
        nameChecker = self.getCheckerByName(NameChecker)
        if not nameChecker:
            return
        if patternsFunc:
            regexFuncAdd = "|((%s).+)$" % "|".join(patternsFunc)
        else:
            regexFuncAdd = ""
        if patternsClass:
            regexClassAdd = "|((%s).+)$" % "|".join(patternsClass)
        else:
            regexClassAdd = ""
        # Modify regex for function, method and class name.
        regexMethod = cfgParser.get("BASIC", "method-rgx") + regexFuncAdd
        regexFunction = cfgParser.get("BASIC", "function-rgx") + regexFuncAdd
        regexClass = cfgParser.get("BASIC", "class-rgx") + regexClassAdd
        # Save to config parser.
        cfgParser.set("BASIC", "method-rgx", regexMethod)
        cfgParser.set("BASIC", "function-rgx", regexFunction)
        cfgParser.set("BASIC", "class-rgx", regexClass)
        # Save to name checker.
        nameChecker.config.method_rgx = re.compile(regexMethod)
        nameChecker.config.function_rgx = re.compile(regexFunction)
        nameChecker.config.class_rgx = re.compile(regexClass)


    def getPathList(self, filesOrModules):
        """
        Transform a list of modules to path.

        @param filesOrModules: a list of modules (may be foo/bar.py or
        foo.bar)
        """
        pathList = []
        for fileOrMod in filesOrModules:
            if not os.path.exists(fileOrMod):
                # May be given module is not not a path,
                # then transform it to a path.
                try:
                    filepath = file_from_modpath(fileOrMod.split('.'))
                except (ImportError, SyntaxError):
                    # Could not load this module.
                    continue
                if not os.path.exists(filepath):
                    # Could not find this module in file system.
                    continue
                if os.path.basename(filepath) == "__init__.py":
                    filepath = os.path.dirname(filepath)
            else:
                filepath = fileOrMod
            pathList.append(filepath)
        return pathList


    def setNameExceptions(self, filesOrModules):
        """
        Find name exceptions in codes and allow them to be ignored
        in checking.

        @param filesOrModules: a list of modules (may be foo/bar.py or
        foo.bar)
        """
        pathList = self.getPathList(filesOrModules)
        for path in pathList:
            patternsFunc, patternsClass = findAllExceptions(path)
            self.allowPatternsForNameChecking(patternsFunc, patternsClass)


    def run(self, args):
        """
        Setup the environment, and run pylint.

        @param args: arguments will be passed to pylint
        @type args: list of string