#!/usr/bin/env python

# MIT License
#
# Copyright (c) 2018 Nithin Nellikunnu (nithin.nn@gmail.com)
#
# 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.

import logging
import argparse
import yaml
import re
import sys
import difflib
import os
import fnmatch
from clang.cindex import Index
from clang.cindex import CursorKind
from clang.cindex import StorageClass
from clang.cindex import TypeKind
from clang.cindex import Config


# Clang cursor kind to ncc Defined cursor map
default_rules_db = {}
clang_to_user_map = {}
special_kind = {CursorKind.STRUCT_DECL: 1, CursorKind.CLASS_DECL: 1}
file_extensions = [".c", ".cpp", ".h", ".hpp"]


class Rule(object):
    def __init__(self, name, clang_kind, parent_kind=None, pattern_str='^.*$'):
        self.name = name
        self.clang_kind = clang_kind
        self.parent_kind = parent_kind
        self.pattern_str = pattern_str
        self.pattern = re.compile(pattern_str)
        self.includes = []
        self.excludes = []

    def evaluate(self, node, scope=None):
        if not self.pattern.match(node.spelling):
            fmt = '{}:{}:{}: "{}" does not match "{}" associated with {}\n'
            msg = fmt.format(node.location.file.name, node.location.line, node.location.column,
                             node.displayname, self.pattern_str, self.name)
            sys.stderr.write(msg)
            return False
        return True


class ScopePrefixRule(object):
    def __init__(self, pattern_obj):
        self.name = "ScopePrefixRule"
        self.rule_names = ["Global", "Static", "ClassMember", "StructMember"]
        self.global_prefix = ""
        self.static_prefix = ""
        self.class_member_prefix = ""
        self.struct_member_prefix = ""

        try:
            for key, value in pattern_obj.items():
                if key == "Global":
                    self.global_prefix = value
                elif key == "Static":
                    self.static_prefix = value
                elif key == "ClassMember":
                    self.class_member_prefix = value
                elif key == "StructMember":
                    self.struct_member_prefix = value
                else:
                    raise ValueError(key)
        except ValueError as e:
            sys.stderr.write('{} is not a valid rule name\n'.format(e.message))
            fixit = difflib.get_close_matches(e.message, self.rule_names, n=1, cutoff=0.8)
            if fixit:
                sys.stderr.write('Did you mean rule name: {} ?\n'.format(fixit[0]))
            sys.exit(1)


class DataTypePrefixRule(object):
    def __init__(self, pattern_obj):
        self.name = "DataTypePrefix"
        self.rule_names = ["String", "Integer", "Bool", "Pointer"]
        self.string_prefix = ""

        try:
            for key, value in pattern_obj.items():
                if key == "String":
                    self.string_prefix = value
                elif key == "Integer":
                    self.integer_prefix = value
                elif key == "Bool":
                    self.bool_prefix = value
                elif key == "Pointer":
                    self.pointer_prefix = value
                else:
                    raise ValueError(key)
        except ValueError as e:
            sys.stderr.write('{} is not a valid rule name\n'.format(e.message))
            fixit = difflib.get_close_matches(e.message, self.rule_names, n=1, cutoff=0.8)
            if fixit:
                sys.stderr.write('Did you mean rule name: {} ?\n'.format(fixit[0]))
            sys.exit(1)


class VariableNameRule(object):
    def __init__(self, pattern_obj=None):
        self.name = "VariableName"
        self.pattern_str = "^.*$"
        self.rule_names = ["ScopePrefix", "DataTypePrefix", "Pattern"]
        self.scope_prefix_rule = None
        self.datatype_prefix_rule = None

        try:
            for key, value in pattern_obj.items():
                if key == "ScopePrefix":
                    self.scope_prefix_rule = ScopePrefixRule(value)
                elif key == "DataTypePrefix":
                    self.datatype_prefix_rule = DataTypePrefixRule(value)
                elif key == "Pattern":
                    self.pattern_str = value
                else:
                    raise ValueError(key)
        except ValueError as e:
            sys.stderr.write('{} is not a valid rule name\n'.format(e.message))
            fixit = difflib.get_close_matches(e.message, self.rule_names, n=1, cutoff=0.8)
            if fixit:
                sys.stderr.write('Did you mean rule name: {} ?\n'.format(fixit[0]))
            sys.exit(1)
        except re.error as e:
            sys.stderr.write('{} is not a valid pattern \n'.format(e.message))
            sys.exit(1)

    def get_scope_prefix(self, node, scope=None):
        if node.storage_class == StorageClass.STATIC:
            return self.scope_prefix_rule.static_prefix
        elif (scope is None) and (node.storage_class == StorageClass.EXTERN or
                                  node.storage_class == StorageClass.NONE):
            return self.scope_prefix_rule.global_prefix
        elif (scope is CursorKind.CLASS_DECL) or (scope is CursorKind.CLASS_TEMPLATE):
            return self.scope_prefix_rule.class_member_prefix
        elif (scope is CursorKind.STRUCT_DECL) or (scope is CursorKind.UNION_DECL):
            return self.scope_prefix_rule.struct_member_prefix
        return ""

    def get_datatype_prefix(self, node):
        if node.type.kind is TypeKind.ELABORATED:
            if node.type.spelling.startswith('std::string'):
                return self.datatype_prefix_rule.string_prefix
            elif (node.type.spelling.startswith('std::unique_ptr') or
                  node.type.spelling.startswith("std::shared_ptr")):
                return self.datatype_prefix_rule.pointer_prefix
        elif node.type.kind is TypeKind.POINTER:
            return self.datatype_prefix_rule.pointer_prefix
        else:
            if node.type.spelling == "int":
                return self.datatype_prefix_rule.integer_prefix
            elif node.type.spelling.startswith('bool'):
                return self.datatype_prefix_rule.bool_prefix
        return ""

    def evaluate(self, node, scope=None):
        pattern_str = self.pattern_str
        scope_prefix = self.get_scope_prefix(node, scope)
        datatype_prefix = self.get_datatype_prefix(node)

        pattern_str = pattern_str[0] + scope_prefix + datatype_prefix + pattern_str[1:]

        pattern = re.compile(pattern_str)
        if not pattern.match(node.spelling):
            fmt = '{}:{}:{}: "{}" does not have the pattern {} associated with Variable name\n'
            msg = fmt.format(node.location.file.name, node.location.line, node.location.column,
                             node.displayname, pattern_str)
            sys.stderr.write(msg)
            return False

        return True


# All supported rules
default_rules_db["StructName"] = Rule("StructName", CursorKind.STRUCT_DECL)
default_rules_db["UnionName"] = Rule("UnionName", CursorKind.UNION_DECL)
default_rules_db["ClassName"] = Rule("ClassName", CursorKind.CLASS_DECL)
default_rules_db["EnumName"] = Rule("EnumName", CursorKind.ENUM_DECL)
default_rules_db["EnumConstantName"] = Rule("EnumConstantName", CursorKind.ENUM_CONSTANT_DECL)
default_rules_db["FunctionName"] = Rule("FunctionName", CursorKind.FUNCTION_DECL)
default_rules_db["ParameterName"] = Rule("ParameterName", CursorKind.PARM_DECL)
default_rules_db["TypedefName"] = Rule("TypedefName", CursorKind.TYPEDEF_DECL)
default_rules_db["CppMethod"] = Rule("CppMethod", CursorKind.CXX_METHOD)
default_rules_db["Namespace"] = Rule("Namespace", CursorKind.NAMESPACE)
default_rules_db["ConversionFunction"] = Rule("ConversionFunction", CursorKind.CONVERSION_FUNCTION)
default_rules_db["TemplateTypeParameter"] = Rule(
    "TemplateTypeParameter", CursorKind.TEMPLATE_TYPE_PARAMETER)
default_rules_db["TemplateNonTypeParameter"] = Rule(
    "TemplateNonTypeParameter", CursorKind.TEMPLATE_NON_TYPE_PARAMETER)
default_rules_db["TemplateTemplateParameter"] = Rule(
    "TemplateTemplateParameter", CursorKind.TEMPLATE_TEMPLATE_PARAMETER)
default_rules_db["FunctionTemplate"] = Rule("FunctionTemplate", CursorKind.FUNCTION_TEMPLATE)
default_rules_db["ClassTemplate"] = Rule("ClassTemplate", CursorKind.CLASS_TEMPLATE)
default_rules_db["ClassTemplatePartialSpecialization"] = Rule(
    "ClassTemplatePartialSpecialization", CursorKind.CLASS_TEMPLATE_PARTIAL_SPECIALIZATION)
default_rules_db["NamespaceAlias"] = Rule("NamespaceAlias", CursorKind.NAMESPACE_ALIAS)
default_rules_db["UsingDirective"] = Rule("UsingDirective", CursorKind.USING_DIRECTIVE)
default_rules_db["UsingDeclaration"] = Rule("UsingDeclaration", CursorKind.USING_DECLARATION)
default_rules_db["TypeAliasName"] = Rule("TypeAliasName", CursorKind.TYPE_ALIAS_DECL)
default_rules_db["ClassAccessSpecifier"] = Rule(
    "ClassAccessSpecifier", CursorKind.CXX_ACCESS_SPEC_DECL)
default_rules_db["TypeReference"] = Rule("TypeReference", CursorKind.TYPE_REF)
default_rules_db["CxxBaseSpecifier"] = Rule("CxxBaseSpecifier", CursorKind.CXX_BASE_SPECIFIER)
default_rules_db["TemplateReference"] = Rule("TemplateReference", CursorKind.TEMPLATE_REF)
default_rules_db["NamespaceReference"] = Rule("NamespaceReference", CursorKind.NAMESPACE_REF)
default_rules_db["MemberReference"] = Rule("MemberReference", CursorKind.MEMBER_REF)
default_rules_db["LabelReference"] = Rule("LabelReference", CursorKind.LABEL_REF)
default_rules_db["OverloadedDeclarationReference"] = Rule(
    "OverloadedDeclarationReference", CursorKind.OVERLOADED_DECL_REF)
default_rules_db["VariableReference"] = Rule("VariableReference", CursorKind.VARIABLE_REF)
default_rules_db["InvalidFile"] = Rule("InvalidFile", CursorKind.INVALID_FILE)
default_rules_db["NoDeclarationFound"] = Rule("NoDeclarationFound", CursorKind.NO_DECL_FOUND)
default_rules_db["NotImplemented"] = Rule("NotImplemented", CursorKind.NOT_IMPLEMENTED)
default_rules_db["InvalidCode"] = Rule("InvalidCode", CursorKind.INVALID_CODE)
default_rules_db["UnexposedExpression"] = Rule("UnexposedExpression", CursorKind.UNEXPOSED_EXPR)
default_rules_db["DeclarationReferenceExpression"] = Rule(
    "DeclarationReferenceExpression", CursorKind.DECL_REF_EXPR)
default_rules_db["MemberReferenceExpression"] = Rule(
    "MemberReferenceExpression", CursorKind.MEMBER_REF_EXPR)
default_rules_db["CallExpression"] = Rule("CallExpression", CursorKind.CALL_EXPR)
default_rules_db["BlockExpression"] = Rule("BlockExpression", CursorKind.BLOCK_EXPR)
default_rules_db["IntegerLiteral"] = Rule("IntegerLiteral", CursorKind.INTEGER_LITERAL)
default_rules_db["FloatingLiteral"] = Rule("FloatingLiteral", CursorKind.FLOATING_LITERAL)
default_rules_db["ImaginaryLiteral"] = Rule("ImaginaryLiteral", CursorKind.IMAGINARY_LITERAL)
default_rules_db["StringLiteral"] = Rule("StringLiteral", CursorKind.STRING_LITERAL)
default_rules_db["CharacterLiteral"] = Rule("CharacterLiteral", CursorKind.CHARACTER_LITERAL)
default_rules_db["ParenExpression"] = Rule("ParenExpression", CursorKind.PAREN_EXPR)
default_rules_db["UnaryOperator"] = Rule("UnaryOperator", CursorKind.UNARY_OPERATOR)
default_rules_db["ArraySubscriptExpression"] = Rule(
    "ArraySubscriptExpression", CursorKind.ARRAY_SUBSCRIPT_EXPR)
default_rules_db["BinaryOperator"] = Rule("BinaryOperator", CursorKind.BINARY_OPERATOR)
default_rules_db["CompoundAssignmentOperator"] = Rule(
    "CompoundAssignmentOperator", CursorKind.COMPOUND_ASSIGNMENT_OPERATOR)
default_rules_db["ConditionalOperator"] = Rule(
    "ConditionalOperator", CursorKind.CONDITIONAL_OPERATOR)
default_rules_db["CstyleCastExpression"] = Rule(
    "CstyleCastExpression", CursorKind.CSTYLE_CAST_EXPR)
default_rules_db["CompoundLiteralExpression"] = Rule(
    "CompoundLiteralExpression", CursorKind.COMPOUND_LITERAL_EXPR)
default_rules_db["InitListExpression"] = Rule("InitListExpression", CursorKind.INIT_LIST_EXPR)
default_rules_db["AddrLabelExpression"] = Rule("AddrLabelExpression", CursorKind.ADDR_LABEL_EXPR)
default_rules_db["StatementExpression"] = Rule("StatementExpression", CursorKind.StmtExpr)
default_rules_db["GenericSelectionExpression"] = Rule(
    "GenericSelectionExpression", CursorKind.GENERIC_SELECTION_EXPR)
default_rules_db["GnuNullExpression"] = Rule("GnuNullExpression", CursorKind.GNU_NULL_EXPR)
default_rules_db["CxxStaticCastExpression"] = Rule(
    "CxxStaticCastExpression", CursorKind.CXX_STATIC_CAST_EXPR)
default_rules_db["CxxDynamicCastExpression"] = Rule(
    "CxxDynamicCastExpression", CursorKind.CXX_DYNAMIC_CAST_EXPR)
default_rules_db["CxxReinterpretCastExpression"] = Rule(
    "CxxReinterpretCastExpression", CursorKind.CXX_REINTERPRET_CAST_EXPR)
default_rules_db["CxxConstCastExpression"] = Rule(
    "CxxConstCastExpression", CursorKind.CXX_CONST_CAST_EXPR)
default_rules_db["CxxFunctionalCastExpression"] = Rule(
    "CxxFunctionalCastExpression", CursorKind.CXX_FUNCTIONAL_CAST_EXPR)
default_rules_db["CxxTypeidExpression"] = Rule("CxxTypeidExpression", CursorKind.CXX_TYPEID_EXPR)
default_rules_db["CxxBoolLiteralExpression"] = Rule(
    "CxxBoolLiteralExpression", CursorKind.CXX_BOOL_LITERAL_EXPR)
default_rules_db["CxxNullPointerLiteralExpression"] = Rule(
    "CxxNullPointerLiteralExpression", CursorKind.CXX_NULL_PTR_LITERAL_EXPR)
default_rules_db["CxxThisExpression"] = Rule("CxxThisExpression", CursorKind.CXX_THIS_EXPR)
default_rules_db["CxxThrowExpression"] = Rule("CxxThrowExpression", CursorKind.CXX_THROW_EXPR)
default_rules_db["CxxNewExpression"] = Rule("CxxNewExpression", CursorKind.CXX_NEW_EXPR)
default_rules_db["CxxDeleteExpression"] = Rule("CxxDeleteExpression", CursorKind.CXX_DELETE_EXPR)
default_rules_db["CxxUnaryExpression"] = Rule("CxxUnaryExpression", CursorKind.CXX_UNARY_EXPR)
default_rules_db["PackExpansionExpression"] = Rule(
    "PackExpansionExpression", CursorKind.PACK_EXPANSION_EXPR)
default_rules_db["SizeOfPackExpression"] = Rule(
    "SizeOfPackExpression", CursorKind.SIZE_OF_PACK_EXPR)
default_rules_db["LambdaExpression"] = Rule("LambdaExpression", CursorKind.LAMBDA_EXPR)
default_rules_db["ObjectBoolLiteralExpression"] = Rule(
    "ObjectBoolLiteralExpression", CursorKind.OBJ_BOOL_LITERAL_EXPR)
default_rules_db["ObjectSelfExpression"] = Rule("ObjectSelfExpression", CursorKind.OBJ_SELF_EXPR)
default_rules_db["UnexposedStatement"] = Rule("UnexposedStatement", CursorKind.UNEXPOSED_STMT)
default_rules_db["LabelStatement"] = Rule("LabelStatement", CursorKind.LABEL_STMT)
default_rules_db["CompoundStatement"] = Rule("CompoundStatement", CursorKind.COMPOUND_STMT)
default_rules_db["CaseStatement"] = Rule("CaseStatement", CursorKind.CASE_STMT)
default_rules_db["DefaultStatement"] = Rule("DefaultStatement", CursorKind.DEFAULT_STMT)
default_rules_db["IfStatement"] = Rule("IfStatement", CursorKind.IF_STMT)
default_rules_db["SwitchStatement"] = Rule("SwitchStatement", CursorKind.SWITCH_STMT)
default_rules_db["WhileStatement"] = Rule("WhileStatement", CursorKind.WHILE_STMT)
default_rules_db["DoStatement"] = Rule("DoStatement", CursorKind.DO_STMT)
default_rules_db["ForStatement"] = Rule("ForStatement", CursorKind.FOR_STMT)
default_rules_db["GotoStatement"] = Rule("GotoStatement", CursorKind.GOTO_STMT)
default_rules_db["IndirectGotoStatement"] = Rule(
    "IndirectGotoStatement", CursorKind.INDIRECT_GOTO_STMT)
default_rules_db["ContinueStatement"] = Rule("ContinueStatement", CursorKind.CONTINUE_STMT)
default_rules_db["BreakStatement"] = Rule("BreakStatement", CursorKind.BREAK_STMT)
default_rules_db["ReturnStatement"] = Rule("ReturnStatement", CursorKind.RETURN_STMT)
default_rules_db["AsmStatement"] = Rule("AsmStatement", CursorKind.ASM_STMT)
default_rules_db["CxxCatchStatement"] = Rule("CxxCatchStatement", CursorKind.CXX_CATCH_STMT)
default_rules_db["CxxTryStatement"] = Rule("CxxTryStatement", CursorKind.CXX_TRY_STMT)
default_rules_db["CxxForRangeStatement"] = Rule(
    "CxxForRangeStatement", CursorKind.CXX_FOR_RANGE_STMT)
default_rules_db["MsAsmStatement"] = Rule("MsAsmStatement", CursorKind.MS_ASM_STMT)
default_rules_db["NullStatement"] = Rule("NullStatement", CursorKind.NULL_STMT)
default_rules_db["DeclarationStatement"] = Rule("DeclarationStatement", CursorKind.DECL_STMT)
default_rules_db["TranslationUnit"] = Rule("TranslationUnit", CursorKind.TRANSLATION_UNIT)
default_rules_db["UnexposedAttribute"] = Rule("UnexposedAttribute", CursorKind.UNEXPOSED_ATTR)
default_rules_db["CxxFinalAttribute"] = Rule("CxxFinalAttribute", CursorKind.CXX_FINAL_ATTR)
default_rules_db["CxxOverrideAttribute"] = Rule(
    "CxxOverrideAttribute", CursorKind.CXX_OVERRIDE_ATTR)
default_rules_db["AnnotateAttribute"] = Rule("AnnotateAttribute", CursorKind.ANNOTATE_ATTR)
default_rules_db["AsmLabelAttribute"] = Rule("AsmLabelAttribute", CursorKind.ASM_LABEL_ATTR)
default_rules_db["PackedAttribute"] = Rule("PackedAttribute", CursorKind.PACKED_ATTR)
default_rules_db["PureAttribute"] = Rule("PureAttribute", CursorKind.PURE_ATTR)
default_rules_db["ConstAttribute"] = Rule("ConstAttribute", CursorKind.CONST_ATTR)
default_rules_db["NoduplicateAttribute"] = Rule(
    "NoduplicateAttribute", CursorKind.NODUPLICATE_ATTR)
default_rules_db["PreprocessingDirective"] = Rule(
    "PreprocessingDirective", CursorKind.PREPROCESSING_DIRECTIVE)
default_rules_db["MacroDefinition"] = Rule("MacroDefinition", CursorKind.MACRO_DEFINITION)
default_rules_db["MacroInstantiation"] = Rule("MacroInstantiation", CursorKind.MACRO_INSTANTIATION)
default_rules_db["InclusionDirective"] = Rule("InclusionDirective", CursorKind.INCLUSION_DIRECTIVE)
default_rules_db["TypeAliasTeplateDeclaration"] = Rule(
    "TypeAliasTeplateDeclaration", CursorKind.TYPE_ALIAS_TEMPLATE_DECL)

# Reverse lookup map. The parse identifies Clang cursor kinds, which must be mapped
# to user defined types
for key, value in default_rules_db.items():
    clang_to_user_map[value.clang_kind] = key
default_rules_db["VariableName"] = Rule("VariableName", CursorKind.VAR_DECL)
clang_to_user_map[CursorKind.FIELD_DECL] = "VariableName"
clang_to_user_map[CursorKind.VAR_DECL] = "VariableName"


class AstNodeStack(object):
    def __init__(self):
        self.stack = []

    def pop(self):
        return self.stack.pop()

    def push(self, kind):
        self.stack.append(kind)

    def peek(self):
        if len(self.stack) > 0:
            return self.stack[-1]
        return None


class Options:
    def __init__(self):
        self.args = None
        self._style_file = None
        self.file_exclusions = None
        self._skip_file = None

        self.parser = argparse.ArgumentParser(
            prog="ncc.py",
            description="ncc is a development tool to help programmers "
            "write C/C++ code that adheres to adhere some naming conventions. It automates the "
            "process of checking C code to spare humans of this boring "
            "(but important) task. This makes it ideal for projects that want "
            "to enforce a coding standard.")

        self.parser.add_argument('--recurse', action='store_true', dest="recurse",
                                 help="Read all files under each directory, recursively")

        self.parser.add_argument('--style', dest="style_file",
                                 help="Read rules from the specified file. If the user does not"
                                 "provide a style file ncc will use all style rules. To print"
                                 "all style rules use --dump option")

        self.parser.add_argument('--include', dest='include', nargs="+", help="User defined "
                                 "header file path, this is same as -I argument to the compiler")

        self.parser.add_argument('--definition', dest='definition', nargs="+", help="User specified "
                                 "definitions, this is same as -D argument to the compiler")

        self.parser.add_argument('--dump', dest='dump', action='store_true',
                                 help="Dump all available options")

        self.parser.add_argument('--output', dest='output', help="output file name where"
                                 "naming convenction vialoations will be stored")

        self.parser.add_argument('--filetype', dest='filetype', help="File extentions type"
                                 "that are applicable for naming convection validation")

        self.parser.add_argument('--clang-lib', dest='clang_lib',
                                 help="Custom location of clang library")

        self.parser.add_argument('--exclude', dest='exclude', nargs="+", help="Skip files "
                                 "matching the pattern specified from recursive searches. It "
                                 "matches a specified pattern according to the rules used by "
                                 "the Unix shell")

        self.parser.add_argument('--skip', '-s', dest="skip_file",
                                 help="Read list of items to ignore during the check. "
                                 "User can use the skip file to specify character sequences that should "
                                 "be ignored by ncc")

        # self.parser.add_argument('--exclude-dir', dest='exclude_dir', help="Skip the directories"
        #                          "matching the pattern specified")

        self.parser.add_argument('--path', dest='path', nargs="+",
                                 help="Path of file or directory")

    def parse_cmd_line(self):
        self.args = self.parser.parse_args()

        if self.args.dump:
            self.dump_all_rules()

        if self.args.style_file:
            self._style_file = self.args.style_file
            if not os.path.exists(self._style_file):
                sys.stderr.write("Style file '{}' not found!\n".format(self._style_file))
                sys.exit(1)

        if self.args.skip_file:
            self._skip_file = self.args.skip_file
            if not os.path.exists(self._skip_file):
                sys.stderr.write("Skip file '{}' not found!\n".format(self._skip_file))

    def dump_all_rules(self):
        print("----------------------------------------------------------")
        print("{:<35} | {}".format("Rule Name", "Pattern"))
        print("----------------------------------------------------------")
        for (key, value) in default_rules_db.items():
            print("{:<35} : {}".format(key, value.pattern_str))

class SkipDb(object):
    def __init__(self, skip_file=None):
        self.__skip_db = {}

        if skip_file:
            self.build_skip_db(skip_file)

    def build_skip_db(self, skip_file):
        with open(skip_file) as stylefile:
            style_rules = yaml.safe_load(stylefile)
            for (skip_string, skip_comment) in style_rules.items():
                self.__skip_db[skip_string] = skip_comment

    def check_skip_db(self, input_query):
        if input_query in self.__skip_db.keys():
            return 1
        else:
            return 0

class RulesDb(object):
    def __init__(self, style_file=None):
        self.__rule_db = {}
        self.__clang_db = {}

        if style_file:
            self.build_rules_db(style_file)
        else:
            self.__rule_db = default_rules_db
            self.__clang_db = clang_to_user_map

    def build_rules_db(self, style_file):
        with open(style_file) as stylefile:
            style_rules = yaml.safe_load(stylefile)

        for (rule_name, pattern_str) in style_rules.items():
            try:
                clang_kind = default_rules_db[rule_name].clang_kind
                if clang_kind:
                    if rule_name == "VariableName":
                        self.__rule_db[rule_name] = VariableNameRule(pattern_str)
                        self.__clang_db[CursorKind.FIELD_DECL] = rule_name
                        self.__clang_db[CursorKind.VAR_DECL] = rule_name
                    else:
                        self.__rule_db[rule_name] = default_rules_db[rule_name]
                        self.__rule_db[rule_name].pattern_str = pattern_str
                        self.__rule_db[rule_name].pattern = re.compile(pattern_str)
                        self.__clang_db[clang_kind] = rule_name

            except KeyError as e:
                sys.stderr.write('{} is not a valid C/C++ construct name\n'.format(e.message))
                fixit = difflib.get_close_matches(e.message, default_rules_db.keys(),
                                                  n=1, cutoff=0.8)
                if fixit:
                    sys.stderr.write('Did you mean rule name: {} ?\n'.format(fixit[0]))
                sys.exit(1)
            except re.error as e:
                sys.stderr.write('"{}" pattern {} has {} \n'.
                                 format(rule_name, pattern_str, e.message))
                sys.exit(1)

    def is_rule_enabled(self, kind):
        if self.__clang_db.get(kind):
            return True
        return False

    def get_rule_names(self, kind):
        """
        Multiple user defined rules can be configured against one type of ClangKind
        For e.g. ClassMemberVariable, StructMemberVariable are types of FIELD_DECL
        """
        return self.__clang_db.get(kind)

    def get_rule(self, rule_name):
        return self.__rule_db.get(rule_name)


class Validator(object):
    def __init__(self, rule_db, filename, options, skip_db=None):
        self.filename = filename
        self.rule_db = rule_db
        self.skip_db = skip_db
        self.options = options
        self.node_stack = AstNodeStack()

        index = Index.create()
        args = []
        args.append('-x')
        args.append('c++')
        args.append('-D_GLIBCXX_USE_CXX11_ABI=0')
        if self.options.args.definition:
            for item in self.options.args.definition:
                defintion = r'-D' + item
                args.append(defintion)
        if self.options.args.include:
            for item in self.options.args.include:
                inc = r'-I' + item
                args.append(inc)
        self.cursor = index.parse(filename, args).cursor

    def validate(self):
        return self.check(self.cursor)

    def check(self, node):
        """
        Recursively visit all nodes of the AST and match against the patter provided by
        the user. Return the total number of errors caught in the file
        """
        errors = 0
        for child in node.get_children():
            if self.is_local(child, self.filename):

                # This is the case when typedef of struct is causing double reporting of error
                # TODO: Find a better way to handle it
                parent = self.node_stack.peek()
                if (parent and parent == CursorKind.TYPEDEF_DECL and
                        child.kind == CursorKind.STRUCT_DECL):
                    return 0

                errors += self.evaluate(child)

                # Members struct, class, and unions must be treated differently.
                # So parent ast node information is pushed in to the stack.
                # Once all its children are validated pop it out of the stack
                self.node_stack.push(child.kind)
                errors += self.check(child)
                self.node_stack.pop()

        return errors

    def evaluate(self, node):
        """
        get the node's rule and match the pattern. Report and error if pattern
        matching fails
        """
        if not self.rule_db.is_rule_enabled(node.kind):
            return 0

        # If the pattern is in the skip list, ignore it
        if self.skip_db.check_skip_db(node.displayname):
            return 0

        rule_name = self.rule_db.get_rule_names(node.kind)
        rule = self.rule_db.get_rule(rule_name)
        if rule.evaluate(node, self.node_stack.peek()) is False:
            return 1
        return 0

    def is_local(self, node, filename):
        """ Returns True is node belongs to the file being validated and not an include file """
        if node.location.file and node.location.file.name in filename:
            return True
        return False


def do_validate(options, filename):
    """
    Returns true if the file should be validated
    - Check if its a c/c++ file
    - Check if the file is not excluded
    """
    path, extension = os.path.splitext(filename)
    if extension not in file_extensions:
        return False

    if options.args.exclude:
        for item in options.args.exclude:
            if fnmatch.fnmatch(filename, item):
                return False

    return True


if __name__ == "__main__":
    logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s',
                        filemode='w')

    """ Parse all command line arguments and validate """
    op = Options()
    op.parse_cmd_line()

    if op.args.path is None:
        sys.exit(1)

    if op.args.clang_lib:
        Config.set_library_file(op.args.clang_lib)

    """ Creating the rules database """
    rules_db = RulesDb(op._style_file)

    """ Creating the skip database """
    skip_db = SkipDb(op._skip_file)

    """ Check the source code against the configured rules """
    errors = 0
    for path in op.args.path:
        if os.path.isfile(path):
            if do_validate(op, path):
                v = Validator(rules_db, path, op, skip_db)
                errors += v.validate()
        elif os.path.isdir(path):
            for (root, subdirs, files) in os.walk(path):
                for filename in files:
                    path = root + '/' + filename
                    if do_validate(op, path):
                        v = Validator(rules_db, path, op, skip_db)
                        errors += v.validate()

                if not op.args.recurse:
                    break
        else:
            sys.stderr.write("File '{}' not found!\n".format(path))
            sys.exit(1)

    if errors:
        print("Total number of errors = {}".format(errors))
        sys.exit(1)
