# Copyright (c) 2015 SUSE Linux GmbH.  All rights reserved.
#
# This file is part of kiwi.
#
# kiwi is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# kiwi is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with kiwi.  If not, see <http://www.gnu.org/licenses/>
#
from xml.dom import minidom
from lxml import (
    etree,
    isoschematron
)
from tempfile import NamedTemporaryFile
import os
from six import BytesIO

# In python2 bytes is string which is different from
# the bytes type in python3. The bytes type from the
# builtins generalizes this type to be bytes always
from builtins import bytes

# project
from .defaults import Defaults
from . import xml_parse
from .command import Command
from .logger import log

from .exceptions import (
    KiwiSchemaImportError,
    KiwiValidationError,
    KiwiDescriptionInvalid,
    KiwiDataStructureError,
    KiwiDescriptionConflict,
    KiwiExtensionError,
    KiwiCommandNotFound
)


class XMLDescription(object):
    """
    **Implements data management for the XML description**

    * XSLT Style Sheet processing to apply on this version of kiwi
    * Schema Validation based on RelaxNG schema
    * Loading XML data into internal data structures

    Attributes

    :param string description: path to XML description file
    :param string derived_from: path to base XML description file
    :param string xml_content: XML description data as content string
    """
    def __init__(self, description=None, derived_from=None, xml_content=None):
        if description and xml_content:
            raise KiwiDescriptionConflict(
                'description and xml_content are mutually exclusive'
            )
        self.description_xslt_processed = NamedTemporaryFile()
        self.derived_from = derived_from

        self.description = description
        self.xml_content = xml_content
        self.extension_data = {}

    def load(self):                                         # noqa: C901
        """
        Read XML description, pass it along to the XSLT processor,
        validate it against the schema and finally pass it to the
        autogenerated(generateDS) parser.

        :return: instance of XML toplevel domain (image)

        :rtype: object
        """
        try:
            self._xsltproc()
            schema_doc = etree.parse(Defaults.get_schema_file())
            relaxng = etree.RelaxNG(schema_doc)
            schematron = isoschematron.Schematron(
                schema_doc, store_report=True
            )
        except Exception as e:
            raise KiwiSchemaImportError(
                '%s: %s' % (type(e).__name__, format(e))
            )
        try:
            description = etree.parse(self.description_xslt_processed.name)
            validation_rng = relaxng.validate(description)
            validation_sch = schematron.validate(description)
        except Exception as e:
            raise KiwiValidationError(
                '%s: %s' % (type(e).__name__, format(e))
            )
        if not validation_rng:
            self._get_relaxng_validation_details(
                Defaults.get_schema_file(),
                self.description_xslt_processed.name
            )
        if not validation_sch:
            self._get_schematron_validation_details(
                schematron.validation_report
            )
        if not validation_rng or not validation_sch:
            if self.description:
                message = 'Schema validation for {description} failed'.format(
                    description=self.description
                )
            else:
                message = 'Schema validation for XML content failed'
            raise KiwiDescriptionInvalid(message)

        parse_result = self._parse()

        if parse_result.get_extension():
            extension_namespace_map = \
                description.getroot().xpath('extension')[0].nsmap

            for namespace_name in extension_namespace_map:
                extensions_for_namespace = description.getroot().xpath(
                    'extension/{namespace}:*'.format(namespace=namespace_name),
                    namespaces=extension_namespace_map
                )
                if extensions_for_namespace:
                    # one toplevel entry point per extension via xmlns
                    if len(extensions_for_namespace) > 1:
                        raise KiwiExtensionError(
                            'Multiple toplevel sections for "{0}" found'.format(
                                namespace_name
                            )
                        )

                    # store extension xml data parse tree for this namespace
                    self.extension_data[namespace_name] = \
                        etree.ElementTree(extensions_for_namespace[0])

                    # validate extension xml data
                    try:
                        xml_catalog = Command.run(
                            [
                                'xmlcatalog', '/etc/xml/catalog',
                                extension_namespace_map[namespace_name]
                            ]
                        )
                        extension_schema = xml_catalog.output.rstrip().replace(
                            'file://', ''
                        )
                        extension_relaxng = etree.RelaxNG(
                            etree.parse(extension_schema)
                        )
                    except Exception as e:
                        raise KiwiExtensionError(
                            'Extension schema error: {0}: {1}'.format(
                                type(e).__name__, e
                            )
                        )
                    validation_result = extension_relaxng.validate(
                        self.extension_data[namespace_name]
                    )
                    if not validation_result:
                        xml_data_unformatted = etree.tostring(
                            self.extension_data[namespace_name],
                            encoding='utf-8'
                        )
                        xml_data_domtree = minidom.parseString(
                            xml_data_unformatted
                        )
                        extension_file = NamedTemporaryFile()
                        with open(extension_file.name, 'w') as xml_data:
                            xml_data.write(xml_data_domtree.toprettyxml())
                        self._get_relaxng_validation_details(
                            extension_schema, extension_file.name
                        )
                        raise KiwiExtensionError(
                            'Schema validation for extension XML data failed'
                        )

        return parse_result

    def get_extension_xml_data(self, namespace_name):
        """
        Return the xml etree parse result for the specified extension namespace

        :param string namespace_name: name of the extension namespace

        :return: result of etree.parse

        :rtype: object
        """
        if namespace_name in self.extension_data:
            return self.extension_data[namespace_name]

    def _get_relaxng_validation_details(self, schema_file, description_file):
        """
        Run jing program to validate description against the schema

        Jing provides detailed error information in case of a schema
        validation failure
        """
        try:
            cmd = Command.run(
                ['jing', schema_file, description_file],
                raise_on_error=False
            )
        except KiwiCommandNotFound as e:
            log.info(
                'For detailed schema validation report, please install: jing'
            )
            log.info(
                '%s: %s: %s', 'jing', type(e).__name__, format(e)
            )
            return
        log.info('RelaxNG validation failed. See jing report:')
        log.info('--> %s', cmd.output)

    def _get_schematron_validation_details(self, validation_report):
        """
        Extract error message form the schematron validation report

        :param etree validation_report: the schematron validation report
        """
        nspaces = validation_report.getroot().nsmap
        log.info('Schematron validation failed:')
        for msg in validation_report.xpath(
            '//svrl:failed-assert/svrl:text', namespaces=nspaces
        ):
            log.info('--> %s', msg.text)

    def _parse(self):
        try:
            parse = xml_parse.parse(
                self.description_xslt_processed.name, True
            )
            parse.description_dir = self.description and os.path.dirname(
                self.description
            )
            parse.derived_description_dir = self.derived_from
            return parse
        except Exception as e:
            raise KiwiDataStructureError(
                '%s: %s' % (type(e).__name__, format(e))
            )

    def _xsltproc(self):
        """
        Apply XSLT style sheet rules to the XML data

        The result of the XSLT processing is stored in a named
        temporary file and used for further schema validation
        and parsing into the data structure classes
        """
        xslt_transform = etree.XSLT(
            etree.parse(Defaults.get_xsl_stylesheet_file())
        )

        xml_source = self.description if self.description else \
            BytesIO(bytes(self.xml_content))

        with open(self.description_xslt_processed.name, "wb") as xsltout:
            xsltout.write(
                etree.tostring(xslt_transform(etree.parse(xml_source)))
            )
