#
# Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
# All rights reserved.
# This component and the accompanying materials are made available
# under the terms of "Eclipse Public License v1.0"
# which accompanies this distribution, and is available
# at the URL "http://www.eclipse.org/legal/epl-v10.html".
#
# Initial Contributors:
# Nokia Corporation - initial contribution.
#
# Contributors:
#
# Description: 
#

import sys, os, shutil
import logging
from optparse import OptionParser, OptionGroup
import cone_common
from cone.report import report_util
from cone.public import api, exceptions, utils, plugin, parsecontext
import cone.validation.parsecontext
import cone.validation.schemavalidation
import cone.validation.implmlvalidation
import cone.validation.confmlvalidation
from cone.validation.problem_type_filter import ProblemTypeFilter

ROOT_PATH = os.path.abspath(os.path.dirname(__file__))

VERSION     = '1.0'

logger    = logging.getLogger('cone')

REPORT_SHORTCUTS = {
    'xml': report_util.ReportShortcut(
        os.path.join(ROOT_PATH, 'validation_report_template.xml'),
        'validation_report.xml',
        "Create a validation report of xml type."),
    
    'html': report_util.ReportShortcut(
        os.path.join(ROOT_PATH, 'validation_report_template.html'),
        'validation_report.html',
        "Create a validation report of html type."),
}

def main():
    """ Validate a configuration, or individual confml/implml files. """
    shortcut_container = report_util.ReportShortcutContainer(REPORT_SHORTCUTS,
                                                             'html')

    parser = OptionParser(version="ConE validate %s" % VERSION)
    
    parser.add_options(cone_common.COMMON_OPTIONS)
    
    parser.add_option("-c", "--configuration",
                        dest="configuration",
                        help="Defines the name of the configuration for the action",
                        metavar="CONFIG")

    parser.add_option("-p", "--project",
                       dest="project",
                       default=".",
                       help="defines the location of current project. Default is the "\
                            "current working directory.",
                       metavar="STORAGE")
    
    group = OptionGroup(parser, 'Validate options',
                        'The validate action is intended for performing validation on a     '\
                        'configuration or individual files.                                 ')
    
    group.add_option('--confml-file',
                     action="append",
                     help='Validate only the given single ConfML file.',
                     metavar="FILE",
                     default=None)
    
    group.add_option('--implml-file',
                     action="append",
                     help='Validate only the given single ImplML file.',
                     metavar="FILE",
                     default=None)
        
    group.add_option("--template",
                     help="Template used in report generation. "\
                          "Example: --template=report_template.html.",
                     metavar="FILE",
                     default=None)
        
    group.add_option("--report-type",
                   help="The type of the report to generate. This is a convenience "\
                        "switch for setting the used template. If --template is given, this option has no effect. "\
                        "Possible values:                                        "\
                        + shortcut_container.get_shortcut_help_text(),
                   metavar="TYPE",\
                   default=None)    
    
    group.add_option("--report",
                   help="Specifies the location of the validation report. "\
                        "Example --report=report.html.",
                   metavar="FILE",
                   default=None)
    
    group.add_option("--dump-schema-files",
                     help="Dump the XML schema files used for validation into the specified directory.",
                     metavar="DIR",
                     default=None)

    group.add_option("--exclude-filter",
                     action="append",
                     help="Exclude validation problems by given filter. "\
                          "Examples: --exclude-filter=schema, --exclude-filter=schema.implml, --exclude-filter=schema.confml, --exclude-filter=schema.implml.ruleml",
                     default=None)

    group.add_option("--include-filter",
                     action="append",
                     help="Include validation problems by given filter."\
                          "Examples: --include-filter=schema.implml, --include-filter=schema.implml.ruleml",
                     default=None)

    parser.add_option_group(group)
    (options, _) = parser.parse_args()
    
    cone_common.handle_common_options(options)
    
    if not shortcut_container.is_valid_shortcut(options.report_type):
        parser.error("Invalid report type: %s" % options.report_type)
            
    if options.dump_schema_files:
        dump_dir = options.dump_schema_files
        print "Dumping XML schema files to '%s'" % dump_dir
        
        cone.validation.schemavalidation.dump_schema_files(dump_dir)
        return
    
    pt_filter = ProblemTypeFilter(includes = options.include_filter or [],
                               excludes = options.exclude_filter or [])
    
    problems = []
    if options.confml_file or options.implml_file:
        if options.confml_file:
            
            func = cone.validation.schemavalidation.validate_confml_data
            for file in options.confml_file:
                problems.extend(validate_file(file, func))
        if options.implml_file:
            func = cone.validation.schemavalidation.validate_implml_data
            for file in options.implml_file:
                problems.extend(validate_file(file, func))
    else:
        problems = validate_configuration(options.project, options.configuration, pt_filter)
    
    if problems is not None:
        filters = {'filter_by_severity':filter_by_severity}
        problems = pt_filter.filter(problems)
        
        print "Total %d problem(s) after filtering" % len(problems)
        
        print "Generating report..."
        problems.sort(key=lambda p: (p.severity, p.file, p.line))
        template, report = shortcut_container.determine_template_and_report(
            options.report_type,
            options.template,
            options.report,
            'validation_report')
        report_util.generate_report(template, report, {'problems': problems},extra_filters=filters)

def extend_without_duplicates(source, target):
    for item in source:
        if item not in target:
            target.append(item)

def validate_file(filename, validator_func):
    if not os.path.isfile(filename):
        print "'%s' does not exist or is not a file!" % filename
        return
    else:
        print "Validating file '%s'" % filename
    
    f = open(filename, 'rb')
    try:        data = f.read()
    finally:    f.close()
    
    problems = []
    try:
        validator_func(data)
    except exceptions.ParseError, e:
        problems.append(api.Problem.from_exception(e))
    print "Found %d problem(s)" % len(problems)
    
    for p in problems:
        p.file = filename
    return problems

def validate_configuration(project_path, config_path, problem_type_filter):
    print "Project:       %s" % project_path
    try:
        project = api.Project(api.Storage.open(project_path, 'r'))
    except exceptions.StorageException:
        print "No such project."
        return
    
    confml_parse_context = cone.validation.parsecontext.ValidationParseContext()
    parsecontext.set_confml_context(confml_parse_context)
    
    if config_path is None:
        config_path = project.get_storage().get_active_configuration()
        if config_path is None:
            print "Project does not have an active configuration, please specify the configuration using --configuration."
            return
    print "Configuration: %s" % config_path
    
    try:
        config  = project.get_configuration(config_path)
    except exceptions.NotFound:
        print "No such configuration in project."
        return
    
    result = []
    
    
    print "Finding ConfML files in configuration..."
    configs = config._traverse(type=api.Configuration)
    print "%d ConfML file(s)" % len(configs)
    print "%d problem(s) while parsing" % len(confml_parse_context.problems)
    result.extend(confml_parse_context.problems)
    
    # Schema-validate ConfML files if not filtered out
    if problem_type_filter.match('schema.confml'):
        print "Performing XML schema validation on ConfML files..."
        schema_problems = cone.validation.schemavalidation.validate_confml_file(config, config_path)
        for conf in configs:
            path = conf.get_full_path()
            problems = cone.validation.schemavalidation.validate_confml_file(config, path)
            schema_problems.extend(problems)
        print "%d problem(s)" % len(schema_problems)
        
        # Add the schema problems with duplicates removed, since XML parse
        # errors might have already been recorded in the ConfML parsing phase
        extend_without_duplicates(source=schema_problems, target=result)
    
    
    # Validate the ConfML model if not filtered out
    validator_classes = cone.validation.confmlvalidation.get_validator_classes(problem_type_filter)
    if validator_classes:
        print "Validating ConfML model..."
        model_problems = cone.validation.confmlvalidation.validate_configuration(config, validator_classes).problems
        print "%d problem(s)" % len(model_problems)
        result.extend(model_problems)
        
    
    print "Finding ImplML files in configuration..."
    impls = []
    for file in config.get_layer().list_implml():
        if plugin.ImplFactory.is_supported_impl_file(file):
            impls.append(file)
    print "Found %d supported files" % len(impls)
    
    # Schema-validate ImplML files if not filtered out
    if problem_type_filter.match('schema.implml'):
        print "Performing XML schema validation on ImplML files..."
        schema_problems = []
        for impl in impls:
            probs = cone.validation.schemavalidation.validate_implml_file(config, impl)
            schema_problems.extend(probs)
        print "%d problem(s)" % len(schema_problems)
        result.extend(schema_problems)
    
    # Validate the ImplML model if not filtered out
    if problem_type_filter.match('model.implml'):
        print "Parsing implementations..."
        implml_parse_context = cone.validation.parsecontext.ValidationParseContext()
        parsecontext.set_implml_context(implml_parse_context)
        impl_set = plugin.create_impl_set(impls, config)
        
        # Add the model-level problems with duplicates removed, since XML parse
        # errors might have already been recorded in the schema validation phase
        extend_without_duplicates(source=implml_parse_context.problems, target=result)
        
        
        validator_classes = cone.validation.implmlvalidation.get_validator_classes(problem_type_filter)
        if validator_classes:
            print "Validating implementations..."
            impl_problems = cone.validation.implmlvalidation.validate_impl_set(impl_set, config, validator_classes)
            print "%d problem(s)" % len(impl_problems)
            result.extend(impl_problems)
    
    return result


def filter_by_severity(problems, severity):
    """
    Filter problems by severity
    """
    return [p for p in problems if p.severity == severity]

if __name__ == "__main__":
    main()
