#!/usr/bin/python
# Copyright (c) 2013, Red Hat, Inc
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the
#    distribution.
# 3. Neither the name of Red Hat nor the names of its
#    contributors may be used to endorse or promote products derived
#    from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# Authors:  Stanislav Ochotnicky <sochotnicky@redhat.com>

import codecs
import os
import errno
import re
from StringIO import StringIO
import xml.dom.minidom

import lxml
import lxml.etree as ET
from lxml.etree import ElementTree, Element, SubElement

from javapackages.artifact import ArtifactValidationException

class XMvnConfigException(Exception):
    pass

class XMvnConfig(object):
    """
    Class for modifying XMvn configuration
    """

    INDEX_PATH = os.path.join(".xmvn", "javapackages-rule-index")
    CONFIG_DIR = os.path.join(".xmvn", "config.d")
    XMLNS = "http://fedorahosted.org/xmvn/CONFIG/0.6.0"

    def __init__(self):
        self.templateXML ="""<?xml version='1.0' encoding='utf-8'?>
<configuration xmlns="http://fedorahosted.org/xmvn/CONFIG/0.6.0">
{content}
</configuration>
"""
        try:
            os.makedirs(XMvnConfig.CONFIG_DIR, 0755)
        except OSError:
            # If directory already exists then it's OK.  If it doesn't
            # then creating the index file below will fail anyways.
            pass

        try:
            with open(XMvnConfig.INDEX_PATH) as index:
                self.index = int(index.read()) + 1
        except IOError as e:
            if e.errno == errno.ENOENT:
                self.index = 1
            else:
                raise e

    def __write_index(self):
        with open(XMvnConfig.INDEX_PATH, 'w') as index:
            index.write(str(self.index))
            self.index = self.index + 1

    def __get_current_config(self):
        fname = 'javapackages-config-{index:05d}.xml'.format(index=self.index)
        return os.path.join(XMvnConfig.CONFIG_DIR,
                            fname)

    def __init_xml(self, content=""):
        self.__write_index()

        xmlbuf = StringIO()
        xmlbuf.write(self.templateXML.format(content=content))

        root = ET.fromstring(xmlbuf.getvalue())
        root.append(ET.Comment("XMvn configuration file generated by "
                               "javapackages.xmvn_config  (part of "
                               "javapackages-tools)"))
        return root

    def __prettify_element(self, elem):
        xmlbuf = StringIO()
        et = ElementTree()
        et._setroot(elem)
        et.write(xmlbuf,
                 xml_declaration=True,
                 encoding = 'utf-8',
                 method = "xml",
                 pretty_print=True)
        return  xmlbuf.getvalue()


    def __write_xml(self, path, root):
        xmlstr = self.__prettify_element(root)
        with codecs.open(path, 'w+', "utf-8") as fout:
            fout.write(xmlstr)


    def __add_config(self, level1, level2, level3=None, content=None):
        if not content:
            raise Exception("Provide content as keyword argument")

        confpath = self.__get_current_config()
        root = self.__init_xml()

        level1 = SubElement(root, level1)
        level2 = SubElement(level1, level2)
        cont_level = level2
        if level3:
            cont_level = SubElement(level2, level3)

        if isinstance(content, basestring):
            cont_level.text = content
        elif isinstance(content, list):
            for elem in content:
                cont_level.append(elem)
        else:
            cont_level.append(content)

        self.__write_xml(confpath, root)

    def __count_backreferences(self, s):
        """
        Return maximum number of backreference used in string s
        """
        backref_re = re.compile('@(\d+)')
        backref_nos = [int(x) for x in backref_re.findall(s)]
        if backref_nos:
            return max((int(x) for x in backref_re.findall(s)))
        return 0

    def __count_wildcard_groups(self, s):
        """
        Return number of wildcard groups used in string s
        """
        left = s.count('{')
        right = s.count('}')

        if left != right:
            raise Exception("Number of opening and closing "
                            "parenthesis for groups of wildcard "
                            "matching is different.")
        return left
    def add_aliases(self, artifact, aliases):
        """
        Adds alias artifacts for given main artifact

        artifact -- main Artifact for which aliases are being provided
        aliases -- list of alternate Artifact representations
        """
        artifact.validate(allow_backref=False)
        wild_groups = self.__count_wildcard_groups(artifact.get_rpm_str())
        main = artifact.get_xml_element(root="artifactGlob")
        elems = [main]
        aelem = Element("aliases")
        for alias in aliases:
            alias.validate(allow_empty=False, allow_wildcards=False)
            backrefs = self.__count_backreferences(alias.get_rpm_str())
            if backrefs > wild_groups:
                raise ArtifactValidationException("Number of backrefenreces "
                                                  "is higher than wildcard "
                                                  "groups.")
            aelem.append(alias.get_xml_element(root="alias"))
        elems.append(aelem)
        self.__add_config("artifactManagement", "rule", content=elems)

    def add_compat_versions(self, artifact, versions):
        """
        Change where on filesystem given artifact is installed

        artifact -- Artifact to be modified
        versions -- list of compat versions for given artifact
        """
        artifact.validate(allow_backref=False)
        wild_groups = self.__count_wildcard_groups(artifact.get_rpm_str(artifact.version))
        main = artifact.get_xml_element(root="artifactGlob")
        elems = [main]
        velem = Element("versions")
        for version in versions:
            backrefs = self.__count_backreferences(version)
            if backrefs > wild_groups:
                raise ArtifactValidationException("Number of backrefenreces "
                                                  "is higher than wildcard "
                                                  "groups.")
            ve = SubElement(velem, "version")
            ve.text = version
        elems.append(velem)
        self.__add_config("artifactManagement", "rule", content=elems)

    def add_file_mapping(self, artifact, paths):
        """
        Change where on filesystem given artifact is installed

        artifact -- Artifact to be modified
        paths -- list of paths for given artifact
        """
        artifact.validate(allow_backref=False)
        wild_groups = self.__count_wildcard_groups(artifact.get_rpm_str())
        main = artifact.get_xml_element(root="artifactGlob")
        elems = [main]
        felem = Element("files")
        if not [path for path in paths if not os.path.isabs(path)]:
            raise XMvnConfigException("At least one path must be relative")
        for path in paths:
            backrefs = self.__count_backreferences(path)
            if backrefs > wild_groups:
                raise ArtifactValidationException("Number of backrefenreces "
                                                  "is higher than wildcard "
                                                  "groups.")
            pe = SubElement(felem, "file")
            pe.text = path
        elems.append(felem)
        self.__add_config("artifactManagement", "rule", content=elems)

    def add_package_mapping(self, artifact, package, optional=False):
        """
        Change which package given artifact belongs to

        artifact -- Artifact to be modified
        package -- subpackage name where artifact belongs
        """
        artifact.validate(allow_backref=False)
        wild_groups = self.__count_wildcard_groups(artifact.get_rpm_str())
        main = artifact.get_xml_element(root="artifactGlob")
        backrefs = self.__count_backreferences(package)
        if backrefs > wild_groups:
            raise ArtifactValidationException("Number of backrefenreces "
                                              "is higher than wildcard "
                                              "groups.")
        elems = [main]
        if optional:
            opt = Element("optional")
            opt.text = "true"
            elems.append(opt)
        target = Element("targetPackage")
        target.text = package
        elems.append(target)
        self.__add_config("artifactManagement", "rule", content=elems)

    def add_custom_option(self, optionstr, content):
        """
        Add custom configuration option

        optionstr -- XPath-like expression for specifying XMvn configuration
                     option location with '/' used as delimiter

                     example: buildSettings/compilerSource
        content -- XML content to be added to specified node. Can be just text, XML node or multiple nodes.

                   examples:
                   someText
                   <someNode>someText</someNode><someOtherNode/>
        """
        node_names = optionstr.split("/")
        confpath = self.__get_current_config()
        root = self.__init_xml()
        par = root
        for node in node_names:
            par = SubElement(par, node)

        try:
            #wrap content into something to allow text content
            inserted = "<root>{0}</root>".format(content);
            contentRoot = ET.fromstring(inserted);
            par.text = contentRoot.text
            for element in contentRoot:
                par.append(element)
        except lxml.etree.XMLSyntaxError:
            raise Exception("content is not valid content for XML node")
        self.__write_xml(confpath, root)
