#!/usr/bin/env python2
"""
wmltest -- tool to validate the syntax and semantics of WML.

Use --help to see usage.
"""
#TODO:
#-define verbosity levels better

from __future__ import print_function
import wesnoth.wmldata as wmldata
import wesnoth.wmlparser as wmlparser
import wesnoth.wmlgrammar as wmlgrammar
import re

def print_indent(string, depth, char=' '):
    print("%s%s" % (depth * char, string))

class Validator:
    """
    The class that takes a wmlgrammar object to validate wml trees with
    """
    def __init__(self, schema, verbosity=0):
        self.schema = wmlgrammar.Grammar(schema)
        self.verbosity = verbosity
        self.validate_result = {}

    def validate_result_add(self, from_file, line, origin, message):
        if not from_file in self.validate_result:
            self.validate_result[from_file] = []

        self.validate_result[from_file].append({'line': line, 'origin': origin, 'message': message})

    def validate_result_print(self):
        normal = '\033[0m'
        bold = '\033[1m'
        underline = '\033[4m'
        for k, v in self.validate_result.iteritems():
            print("%s%s%s" % (bold, k, normal))
            for i in v:
                print("%s#%d: %s%s %s" % (underline, i['line'], i['origin'], normal, i['message']))

    def validate(self, node, depth=0, name=None):
        """
        Validate the given DataSub node.
        depth indicates how deep we've recursed into the tree,
        name is a mechanism for overwriting the node name to look up in the schema, used for overloaded names.
        """
        if not name or name == node.name:
            name = node.name
            verbosename = name
        else:
            verbosename = "%s (%s)" % (node.name, name)

        if self.verbosity > 1:
            print_indent(node.name, depth)

        try:
            schema = self.schema.get_element(name)
        except KeyError:
            print("No valid schema found for %s" % verbosename)
            return

        # Validate the attributes
        for attribute in schema.get_attributes():
            matches = node.get_texts(attribute.name)

            # Check frequency
            nummatches = len(matches)
            if attribute.freq == wmlgrammar.REQUIRED and nummatches != 1:
                self.validate_result_add(node.file, node.line, "Attribute [%s] %s" % (verbosename, attribute.name), "Should appear exactly once, not %d times" % nummatches)
            elif attribute.freq == wmlgrammar.OPTIONAL and nummatches > 1:
                self.validate_result_add(node.file, node.line, "Attribute [%s] %s" % (verbosename, attribute.name), "Should appear at most once, not %d times" % nummatches)
            elif attribute.freq == wmlgrammar.FORBIDDEN and nummatches > 0:
                self.validate_result_add(node.file, node.line, "Attribute [%s] %s" % (verbosename, attribute.name), "Should not appear. It appears %d times" % nummatches)

            # Check use
            for match in matches:
                if 'translatable' in attribute.optionals and match.is_translatable() == False:
                    self.validate_result_add(node.file, node.line, "Attribute [%s] %s" % (verbosename, attribute.name), "Value is translatable, but haven't _ at the beginning")
                elif 'translatable' not in attribute.optionals and 'optional-translatable' not in attribute.optionals and match.is_translatable() == True:
                    self.validate_result_add(node.file, node.line, "Attribute [%s] %s" % (verbosename, attribute.name), "Value isn't translatable, but have a _ at the beginning")

                def check_attribute_value(value, pos=None):
                    def gerate_message_with_pos():
                        if pos is None:
                            return ""
                        else:
                            return " (At position %d)" % pos

                    if not attribute.validate(value):
                        self.validate_result_add(node.file, node.line, "Attribute [%s] %s%s" % (verbosename, attribute.name, gerate_message_with_pos()), "Value should be %s, found: %s" % (attribute.type, value))

                    regex_limit = re.compile(ur'^limit\((\d+.\d+|\d+),(\d+.\d+|\d+)\)$')
                    check_limit = [i for i in attribute.optionals if regex_limit.search(i)]
                    if len(check_limit):
                        check_limit = check_limit[0]
                        number_min, number_max = regex_limit.search(check_limit).groups()

                        if float(value) > float(number_max) or float(value) < float(number_min):
                            self.validate_result_add(node.file, node.line, "Attribute [%s] %s%s" % (verbosename, attribute.name, gerate_message_with_pos()), "Value must be between %s and %s, found : %s" % (number_min, number_max, value))

                    regex_limit_lower = re.compile(ur'^limit-lower\((\d+.\d+|\d+)\)$')
                    check_limit_lower = [i for i in attribute.optionals if regex_limit_lower.search(i)]
                    if len(check_limit_lower):
                        check_limit_lower = check_limit_lower[0]
                        number = regex_limit_lower.search(check_limit_lower).group(1)

                        if float(value) < float(number):
                            self.validate_result_add(node.file, node.line, "Attribute [%s] %s%s" % (verbosename, attribute.name, gerate_message_with_pos()), "Value needs to be at least %s, found : %s" % (number, value))

                    regex_limit_max = re.compile(ur'^limit-max\((\d+.\d+|\d+)\)$')
                    check_limit_max = [i for i in attribute.optionals if regex_limit_max.search(i)]
                    if len(check_limit_max):
                        check_limit_max = check_limit_max[0]
                        number = regex_limit_max.search(check_limit_max).group(1)

                        if float(value) > float(number):
                            self.validate_result_add(node.file, node.line, "Attribute [%s] %s%s" % (verbosename, attribute.name, gerate_message_with_pos()), "Value needs to be at max %s, found : %s" % (number, value))

                    regex_file_exist = re.compile(ur'^need-file-in\(([\w.\-\/]+)\)$')
                    check_file_exist = [i for i in attribute.optionals if regex_file_exist.search(i)]
                    if len(check_file_exist):
                        check_file_exist = check_file_exist[0]
                        directory = regex_file_exist.search(check_file_exist).group(1)

                        value_directory, value_file = re.search(re.compile(ur'(?:(.*)?\/)?(.+)'), value).groups()

                        import glob
                        if directory == '.':
                            sub_directory = os.path.dirname(node.file) + '/'
                        else:
                            sub_directory = os.path.dirname(node.file) + '/' + directory + '/'

                        if not value_directory is None:
                            sub_directory += value_directory + '/'

                        files_from_sub_directory = glob.glob(sub_directory + '*')

                        # We just want the names of the files from directory, but...
                        if os.path.splitext(value_file)[1] == '':
                            # ... if without extension in the value_file, then it is implied. In this case, we do want extensions
                            files_from_sub_directory = [re.sub(re.compile(r'^.*\/(.*)\..*'), r'\1', i) for i in files_from_sub_directory]
                        else:
                            files_from_sub_directory = [re.sub(re.compile(r'^.*\/(.*)'), r'\1', i) for i in files_from_sub_directory]

                        if not value_file in files_from_sub_directory:
                            self.validate_result_add(node.file, node.line, "Attribute [%s] %s%s" % (verbosename, attribute.name, gerate_message_with_pos()), "The file %s not exist in directory %s" % (value_file, sub_directory))

                if 'list' in attribute.optionals:
                    pos = 1
                    for i in match.data.split(","):
                        if i[0] == ' ': i = i[1:]
                        check_attribute_value(i, pos=pos)
                        pos += 1
                else:
                    check_attribute_value(match.data)
                node.remove(match) # Get rid of these so we can see what's left
        for attribute in node.get_all_text():
            self.validate_result_add(node.file, node.line, "Attribute [%s] %s" % (verbosename, attribute.name), "Found, which has no meaning there")

        # Validate the elements
        for element in schema.get_elements():
            matches = node.get_subs(element.name)

            # Check frequency
            nummatches = len(matches)
            if element.freq == wmlgrammar.REQUIRED and nummatches != 1:
                self.validate_result_add(node.file, node.line, "Element [%s] [%s]" % (verbosename, element.name), "Should appear exactly once, not %d times" % nummatches)
            elif element.freq == wmlgrammar.OPTIONAL and nummatches > 1:
                self.validate_result_add(node.file, node.line, "Element [%s] [%s]" % (verbosename, element.name), "Should appear at most once, not %d times" % nummatches)
            elif element.freq == wmlgrammar.FORBIDDEN and nummatches > 0:
                self.validate_result_add(node.file, node.line, "Element [%s] [%s]" % (verbosename, element.name), "Should not appear. It appears %d times" % nummatches)

            # Check sub
            for match in matches:
                self.validate(match, depth+1, element.subname)
                node.remove(match)
        for element in node.get_all_subs():
            self.validate_result_add(node.file, node.line, "Element [%s] [%s]" % (verbosename, element.name), "Found, which has no meaning there")

if __name__ == '__main__':
    import argparse, subprocess, os, codecs, sys

    # Ugly hack to force the output of UTF-8.
    # This prevents us from crashing when we're being verbose
    #  and encounter a non-ascii character.
    sys.stdout = codecs.getwriter('utf-8')(sys.stdout)

    ap = argparse.ArgumentParser("Usage: %(prog)s [options]")
    ap.add_argument("-p", "--path",
        help = "Specify Wesnoth's data dir",
        dest = "path")
    ap.add_argument("-u", "--userpath",
        help = "Specify user data dir",
        dest = "userpath")
    ap.add_argument("-s", "--schema",
        help = "Specify WML schema",
        dest = "schema")
    ap.add_argument("-v", "--verbose",
        action = "count",
        dest = "verbose",
        help = "Increase verbosity, 4 is the maximum.")
    ap.add_argument("-D", "--define",
        action = "append",
        dest = "defines",
        default = [],
        help = "Define (empty) preprocessor macros, for campaign/multiplayer inclusion.")
    ap.add_argument("filename",
        nargs = "*",
        help = "Files to validate or directory. If it is a directory, get all the cfg files from directory")
    args = ap.parse_args()
    if args.path:
        args.path = os.path.expanduser(args.path)
    if args.userpath:
        args.userpath = os.path.expanduser(args.userpath)
    if args.filename:
        args.filename = [os.path.expanduser(i) for i in args.filename]

    if not args.path:
        try:
            p = subprocess.Popen(["wesnoth", "--path"], stdout = subprocess.PIPE)
            path = p.stdout.read().strip()
            args.path = os.path.join(path, "data")
            sys.stderr.write("No Wesnoth path given.\nAutomatically found '%s'\n" % (args.path, ) )
        except OSError:
            args.path = '.'
            sys.stderr.write("Could not determine Wesnoth path.\nAssuming '%s'\n" % (args.path, ) )

    list_files_analyze = []
    if len(args.filename) < 1:
        list_files_analyze.append(os.path.join(args.path, '_main.cfg'))

    for i in args.filename:
        if os.path.isdir(i):
            if i[-1] != '/':
                i += '/'
            cfg_from_dir = [i + cfg for cfg in os.listdir(i) if cfg[-3:] == 'cfg']
            list_files_analyze += cfg_from_dir
        else:
            list_files_analyze.append(i)

    if args.verbose > 1:
        print("Args: %s\n"% (args, ))

    if not args.schema:
        args.schema = os.path.join(args.path, 'schema.cfg')

    # Parse the schema
    parser = wmlparser.Parser(args.path)

    if args.verbose > 3:
        parser.verbose = True
    parser.parse_file(args.schema)

    schema = wmldata.DataSub("schema")
    parser.parse_top(schema)

    # Construct the validator
    validator = Validator(schema, args.verbose)

    # Parse the WML
    parser = wmlparser.Parser(args.path, args.userpath)

    if args.verbose > 3:
        parser.verbose = True

    if args.userpath:
        parser.parse_text("{~add-ons}")
    for file in list_files_analyze:
        parser.parse_file(file)
    for macro in args.defines:
        parser.parse_text("#define %s \n#enddef" % (macro, ) )

    data = wmldata.DataSub("root")
    parser.parse_top(data)

    # Validate
    validator.validate(data)
    validator.validate_result_print()

# vim: tabstop=4: shiftwidth=4: expandtab: softtabstop=4: autoindent:
