#!/usr/bin/env python3
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2018 Linaro Ltd.
# Copyright 2018 Arm Ltd.

import signal

def sigint_handler(signum, frame):
    sys.exit(-2)

signal.signal(signal.SIGINT, sigint_handler)

import sys
import os
import ruamel.yaml
import jsonschema
import argparse
import glob

import dtschema

verbose = False
show_unmatched = False
match_schema_file = None

class schema_group():
    def __init__(self, schema_file=""):
        if schema_file != "" and not os.path.exists(schema_file):
            exit(-1)

        self.schemas = dtschema.set_schemas([schema_file])

    def check_node(self, tree, node, disabled, nodename, fullname, filename):
        # Hack to save some time validating examples
        if 'example-0' in node or 'example-' in nodename:
            return

        node['$nodename'] = [ nodename ]
        node_matched = False
        matched_schemas = []
        for schema in self.schemas.values():
            if '$select_validator' in schema and schema['$select_validator'].is_valid(node):
                # We have a match if a conditional schema is selected
                if schema['select'] != True:
                    matched_schemas.append(schema['$id'])
                    node_matched = True
                try:
                    errors = sorted(dtschema.DTValidator(schema).iter_errors(node), key=lambda e: e.linecol)
                    for error in errors:

                        # Disabled nodes might not have all the required
                        # properties filled in, such as a regulator or a
                        # GPIO meant to be filled at the DTS level on
                        # boards using that particular node. Thus, if the
                        # node is marked as disabled, let's just ignore
                        # any error message reporting a missing property.
                        if disabled or (isinstance(error.instance, dict) and \
                           'status' in error.instance and \
                           'disabled' in error.instance['status']):
                            if 'required property' in error.message:
                                continue
                            elif error.context:
                                for e in error.context:
                                    if not 'required property' in e.message:
                                        break
                                else:
                                    continue

                        if schema['$id'] == 'generated-compatibles':
                            if show_unmatched < 1:
                                continue
                            if isinstance(node, ruamel.yaml.comments.CommentedBase):
                                line = node.lc.line
                                col = node.lc.col
                            else:
                                line = 0
                                col = 0
                            print("%s:%i:%i: %s: failed to match any schema with compatible: %s" %
                                  (filename, line, col, fullname, node['compatible']), file=sys.stderr)
                            continue

                        print(dtschema.format_error(filename, error, nodename=nodename, verbose=verbose) +
                            '\n\tFrom schema: ' + schema['$filename'],
                            file=sys.stderr)
                except RecursionError as e:
                    print(ap.prog + ": recursion error: Check for prior errors in a referenced schema", file=sys.stderr)

        if show_matched and matched_schemas:
            print("%s: %s: matched on schema(s)\n\t" % (filename, fullname) +
                '\n\t'.join(matched_schemas), file=sys.stderr)

        if show_unmatched >= 2 and not node_matched:
            print("%s: %s: failed to match any schema" % (filename, fullname), file=sys.stderr)

    def check_subtree(self, tree, subtree, disabled, nodename, fullname, filename):
        if nodename.startswith('__'):
            return

        try:
            disabled = ('disabled' in subtree['status'])
        except:
            pass

        self.check_node(tree, subtree, disabled, nodename, fullname, filename)
        if fullname != "/":
            fullname += "/"
        for name,value in subtree.items():
            if isinstance(value, dict):
                self.check_subtree(tree, value, disabled, name, fullname + name, filename)

    def check_trees(self, filename, dt):
        """Check the given DT against all schemas"""

        for schema in self.schemas.values():
            if match_schema_file and match_schema_file not in schema['$filename']:
                continue
            schema["$select_validator"] = dtschema.DTValidator(schema['select'])

        for subtree in dt:
            self.check_subtree(dt, subtree, False, "/", "/", filename)

if __name__ == "__main__":
    ap = argparse.ArgumentParser(fromfile_prefix_chars='@',
        epilog='Arguments can also be passed in a file prefixed with a "@" character.')
    ap.add_argument("yamldt", nargs='*',
                    help="Filename or directory of YAML encoded devicetree input file(s)")
    ap.add_argument('-s', '--schema', help="preparsed schema file or path to schema files")
    ap.add_argument('-p', '--preparse', help="preparsed schema file (deprecated, use '-s')")
    ap.add_argument('-l', '--limit', help="limit validation to schema files matching substring")
    ap.add_argument('-m', '--show-unmatched', default=0,
        help="Print out nodes which don't match any schema.\n" \
             "Once for only nodes with 'compatible'\n" \
             "Twice for all nodes", action="count")
    ap.add_argument('-M', '--show-matched',
        help="Print out matching schema for each node", action="store_true")
    ap.add_argument('-n', '--line-number', help="Print line and column numbers (slower)", action="store_true")
    ap.add_argument('-v', '--verbose', help="verbose mode", action="store_true")
    ap.add_argument('-u', '--url-path', help="Additional search path for references")
    ap.add_argument('-V', '--version', help="Print version number",
                    action="version", version=dtschema.__version__)
    args = ap.parse_args()

    verbose = args.verbose
    show_unmatched = args.show_unmatched
    show_matched = args.show_matched
    match_schema_file = args.limit

    if args.url_path:
        dtschema.add_schema_path(args.url_path)

    if args.preparse:
        sg = schema_group(args.preparse)
    elif args.schema:
        sg = schema_group(args.schema)
    else:
        sg = schema_group()

    for d in args.yamldt:
        if not os.path.isdir(d):
            continue
        for filename in glob.iglob(d + "/**/*.yaml", recursive=True):
            testtree = dtschema.load(filename, line_number=args.line_number)
            if verbose:
                print("Check:  " + filename)
            sg.check_trees(filename, testtree)

    for filename in args.yamldt:
        if not os.path.isfile(filename):
            continue
        testtree = dtschema.load(filename, line_number=args.line_number)
        if verbose:
            print("Check:  " + filename)
        sg.check_trees(filename, testtree)
