# Copyright 2019-     DNB
#
# 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.


from __future__ import print_function

import os
import sys
import glob
import argparse
import imp

from .common import SuiteRule, ResourceRule, TestRule, KeywordRule, GeneralRule, Rule
from .common import ERROR, WARNING, IGNORE
from .version import __version__
from .parser import RobotFactory, SuiteFile, ResourceFile
from .exceptions import UnknownRuleException

from robot.utils.argumentparser import ArgFileParser


class RfLint(object):
    """Robot Framework Linter"""

    def __init__(self):
        here = os.path.abspath(os.path.dirname(__file__))
        builtin_rules = os.path.join(here, "rules")
        site_rules = os.path.join(here, "site-rules")

        # mapping of class names to instances, to enable us to
        # instantiate each rule exactly once
        self._rules = {}

        for path in (builtin_rules, site_rules):
            for filename in glob.glob(path+"/*.py"):
                if filename.endswith(".__init__.py"):
                    continue
                self._load_rule_file(filename)

    @property
    def suite_rules(self):
        return self._get_rules(SuiteRule)

    @property 
    def resource_rules(self):
        return self._get_rules(ResourceRule)

    @property
    def testcase_rules(self):
        return self._get_rules(TestRule)

    @property
    def keyword_rules(self):
        return self._get_rules(KeywordRule)

    @property
    def general_rules(self):
        return self._get_rules(GeneralRule)

    @property
    def all_rules(self):
        all = self.suite_rules + self.resource_rules + self.testcase_rules + self.keyword_rules + self.general_rules
        return all

    def run(self, args):
        """Parse command line arguments, and run rflint"""

        self.args = self.parse_and_process_args(args)

        if self.args.version:
            print(__version__)
            return 0
            
        if self.args.rulefile:
            for filename in self.args.rulefile:
                self._load_rule_file(filename)

        if self.args.list:
            self.list_rules()
            return 0
        
        if self.args.describe:
            self._describe_rules(self.args.args)
            return 0

        self.counts = { ERROR: 0, WARNING: 0, "other": 0}
            
        for filename in self.args.args:
            if not (os.path.exists(filename)):
                sys.stderr.write("rflint: %s: No such file or directory\n" % filename)
                continue
            if os.path.isdir(filename):
                self._process_folder(filename)
            else:
                self._process_file(filename)

        if self.counts[ERROR] > 0:
            return self.counts[ERROR] if self.counts[ERROR] < 254 else 255

        return 0

    def _is_valid_rule(self, rule_name):
        for rule in self.all_rules:
            if rule_name.lower() == rule.name.lower():
                return True
        return False

    def _describe_rules(self, rule_names):
        for rulename in rule_names:
            if not self._is_valid_rule(rulename):
                raise UnknownRuleException(rulename)

        requested_rules = [rule.strip().lower() for rule in rule_names]
        for rule in sorted(self.all_rules, key=lambda rule: rule.name):
            if rule.name.lower() in requested_rules or len(requested_rules) == 0:
                print(rule.name)
                for line in rule.doc.split("\n"):
                    print("    " + line)

    def _process_folder(self, path):
        if self.args.recursive:
            for root, subdirs, filenames in os.walk(path):
                self._process_files(root, filenames)
        else:
            self._process_files(path, os.listdir(path))

    def _process_files(self, folder, filenames):
        for filename in filenames:
            name, ext = os.path.splitext(filename)
            if ext.lower() in (".robot", ".txt", ".tsv"):
                self._process_file(os.path.join(folder, filename))
 
    def _process_file(self, filename):
        # this is used by the reporting mechanism to know if it
        # should print the filename. Once it has been printed it
        # will be reset so that it won't get printed again until 
        # we process the next file.
        self._print_filename = filename if self.args.print_filenames else None

        robot_file = RobotFactory(filename)
        for rule in self.general_rules:
            if rule.severity != IGNORE:
                rule.apply(robot_file)

        if isinstance(robot_file, SuiteFile):
            for rule in self.suite_rules:
                if rule.severity != IGNORE:
                    rule.apply(robot_file)
            for testcase in robot_file.testcases:
                for rule in self.testcase_rules:
                    if rule.severity != IGNORE:
                        rule.apply(testcase)

        if isinstance(robot_file, ResourceFile):
            for rule in self.resource_rules:
                if rule.severity != IGNORE:
                    rule.apply(robot_file)

        for keyword in robot_file.keywords:
            for rule in self.keyword_rules:
                if rule.severity != IGNORE:
                    rule.apply(keyword)

    def list_rules(self):
        """Print a list of all rules"""
        for rule in sorted(self.all_rules, key=lambda rule: rule.name):
            print(rule)
            if self.args.verbose:
                for line in rule.doc.split("\n"):
                    print("    ", line)

    def report(self, linenumber, filename, severity, message, rulename, char):
        """Report a rule violation"""

        if self._print_filename is not None:
            # we print the filename only once. self._print_filename
            # will get reset each time a new file is processed.
            print("+ " + self._print_filename)
            self._print_filename = None

        if severity in (WARNING, ERROR):
            self.counts[severity] += 1
        else:
            self.counts["other"] += 1

        print(self.args.format.format(linenumber=linenumber, filename=filename,
                                      severity=severity, message=message.encode('utf-8'),
                                      rulename=rulename, char=char))

    def _get_rules(self, cls):
        """Returns a list of rules of a given class
        
        Rules are treated as singletons - we only instantiate each
        rule once. 
        """

        result = []
        for rule_class in cls.__subclasses__():
            rule_name = rule_class.__name__.lower()
            if rule_name not in self._rules:
                rule = rule_class(self)
                self._rules[rule_name] = rule
            result.append(self._rules[rule_name])
        return result

    def _load_rule_file(self, filename):
        """Import the given rule file"""
        if not (os.path.exists(filename)):
            sys.stderr.write("rflint: %s: No such file or directory\n" % filename)
            return
        try:
            basename = os.path.basename(filename)
            (name, ext) = os.path.splitext(basename)
            imp.load_source(name, filename)
        except Exception as e:
            sys.stderr.write("rflint: %s: exception while loading: %s\n" % (filename, str(e)))

    def parse_and_process_args(self, args):
        """Handle the parsing of command line arguments."""

        parser = argparse.ArgumentParser(
            prog="python -m rflint",
            description="A style checker for robot framework plain text files.",
            formatter_class=argparse.RawDescriptionHelpFormatter,
            epilog = (
                "You can use 'all' in place of RULENAME to refer to all rules. \n"
                "\n"
                "For example: '--ignore all --warn DuplicateTestNames' will ignore all\n"
                "rules except DuplicateTestNames.\n"
                "\n"
                "FORMAT is a string that performs a substitution on the following \n"
                "patterns: {severity}, {linenumber}, {char}, {message}, and {rulename}.\n"
                "\n"
                "For example: --format 'line: {linenumber}: message: {message}'. \n"
                "\n"
                "ARGUMENTFILE is a filename with contents that match the format of \n"
                "standard robot framework argument files\n"
                "\n"
                "If you give a directory as an argument, all files in the directory\n"
                "with the suffix .txt, .robot or .tsv will be processed. With the \n"
                "--recursive option, subfolders within the directory will also be\n"
                "processed."
                )
            )
        parser.add_argument("--error", "-e", metavar="RULENAME", action=SetErrorAction,
                            help="Assign a severity of ERROR to the given RULENAME")
        parser.add_argument("--ignore", "-i", metavar="RULENAME", action=SetIgnoreAction,
                            help="Ignore the given RULENAME")
        parser.add_argument("--warning", "-w", metavar="RULENAME", action=SetWarningAction,
                            help="Assign a severity of WARNING for the given RULENAME")
        parser.add_argument("--list", "-l", action="store_true",
                            help="show a list of known rules and exit")
        parser.add_argument("--describe", "-d", action="store_true",
                            help="describe the given rules")
        parser.add_argument("--no-filenames", action="store_false", dest="print_filenames", 
                            default=True,
                            help="suppress the printing of filenames")
        parser.add_argument("--format", "-f", 
                            help="Define the output format",
                            default='{severity}: {linenumber}, {char}: {message} ({rulename})')
        parser.add_argument("--version", action="store_true", default=False,
                            help="Display version number and exit")
        parser.add_argument("--verbose", "-v", action="store_true", default=False,
                            help="Give verbose output")
        parser.add_argument("--configure", "-c", action=ConfigureAction,
                            help="Configure a rule")
        parser.add_argument("--recursive", "-r", action="store_true", default=False,
                            help="Recursively scan subfolders in a directory")
        parser.add_argument("--rulefile", "-R", action=RulefileAction,
                            help="import additional rules from the given RULEFILE")
        parser.add_argument("--argumentfile", "-A", action=ArgfileLoader,
                            help="read arguments from the given file")
        parser.add_argument('args', metavar="file", nargs=argparse.REMAINDER)

        # create a custom namespace, in which we can store a reference to
        # our rules. This lets the custom argument actions access the list
        # of rules
        ns = argparse.Namespace()
        setattr(ns, "app", self)
        args = parser.parse_args(args, ns)

        Rule.output_format = args.format

        return args


class RulefileAction(argparse.Action):
    def __call__(self, parser, namespace, arg, option_string=None):
        app = getattr(namespace, "app")
        app._load_rule_file(arg)


class ConfigureAction(argparse.Action):
    def __call__(self, parser, namespace, arg, option_string=None):
        rulename, argstring = arg.split(":", 1)
        args = argstring.split(":")
        app = getattr(namespace, "app")

        for rule in app.all_rules:
            if rulename == rule.name:
                rule.configure(*args)
                return
        raise UnknownRuleException(rulename)


class SetStatusAction(argparse.Action):
    """Abstract class which provides a method for checking the rule name"""
    def check_rule_name(self, rulename, rules):
        if (rulename != "all" and
            rulename.lower() not in [rule.name.lower() for rule in rules]):
            raise UnknownRuleException(rulename)


class SetWarningAction(SetStatusAction):
    """Called when the argument parser encounters --warning"""
    def __call__(self, parser, namespace, rulename, option_string = None):

        app = getattr(namespace, "app")
        self.check_rule_name(rulename, app.all_rules)

        for rule in app.all_rules:
            if rulename == rule.name or rulename == "all":
                rule.severity = WARNING


class SetErrorAction(SetStatusAction):
    """Called when the argument parser encounters --error"""
    def __call__(self, parser, namespace, rulename, option_string = None):

        app = getattr(namespace, "app")
        self.check_rule_name(rulename, app.all_rules)

        for rule in app.all_rules:
            if rulename == rule.name or rulename == "all":
                rule.severity = ERROR


class SetIgnoreAction(SetStatusAction):
    """Called when the argument parser encounters --ignore"""
    def __call__(self, parser, namespace, rulename, option_string = None):

        app = getattr(namespace, "app")
        self.check_rule_name(rulename, app.all_rules)

        for rule in app.all_rules:
            if rulename == rule.name or rulename == "all":
                rule.severity = IGNORE


class ArgfileLoader(argparse.Action):
    """Called when the argument parser encounters --argumentfile"""
    def __call__ (self, parser, namespace, values, option_string = None):
        ap = ArgFileParser(["--argumentfile","-A"])
        args = ap.process(["-A", values])
        parser.parse_args(args, namespace)
