# -*- coding: utf-8 -*-
#!/usr/bin/python
"""\n
Diffs to given formedtrees.  Note that currently no checks on external choice
lists are done.  Furter checks on Repeatgroups are very simple for now.
Warning: If a field with the same name is moved from master_tbl into a RG, it will not be
noticed, as we do not diff strutural changes.

Usage: diff_formed.py old new 

Options:
    -h / --help         Print this message and exit.
    -e / --elements     comma-seperated list of elements to check
    -s / --subtree      Define subtree as root for checks 
    -f / --fields       comma-seperated list of element-name to check
    --check-attributes  Check changes in attributes (changed descriptions, target...)

"""
# 20100810 Torsten Irlaender <torsten.irlaender@intevation.de>
#
# This program is free software under the GNU GPL (>=v2)
# Read the file COPYING coming with the software for details.


import sys 
import getopt
import logging

from lxml import etree

logging.basicConfig(level=logging.ERROR)
log = logging.getLogger('diff_formed')

class AttributeChangeException(Exception):
    def __init__(self, n, c, d):
        self.new = n
        self.changed = c
        self.deleted = d

        msg = []
        if self.new:
            msg.append('New: %s' % ",".join(self.new))
        if self.changed:
            msg.append('Changed: %s' % ",".join(self.changed))
        if self.deleted:
            msg.append('Deleted: %s' % ",".join(self.deleted))
        self.message = " | ".join(msg)

class Inspector():

    def __init__(self, filename_old, filename_new):

        self.old_xml_doc = None
        self.new_xml_doc = None
        self.elements_old = {}
        self.elements_new = {}
        self.elements = ['choice', 'text', 'textarea', 'repeat']

        # Open files and load xml
        try:
            f1 = None
            f2 = None
            log.debug('Old: %s'% filename_old)
            f1 = open(filename_old, "r")
            log.debug('New: %s'% filename_new)
            f2 = open(filename_new, "r")
            self.old_xml_doc = etree.parse(f1)
            self.new_xml_doc = etree.parse(f2)
        except:
            log.exception('Error while opening file')
        finally:
            if f1 is not None:
                f1.close()
            if f2 is not None:
                f2.close()

    def perform(self, elements=None, subtree=None, fields=None, check_attributes=False):
        log.info('Only checking %s elements' % ",".join(elements or ['all']))
        for element in self.elements:
            if elements is not None:
                if element not in elements: continue 
            self.elements_old[element] = []
            self.elements_new[element] = []
            if subtree:
                needle = '//*[@name="%s"]//%s' % (subtree, element)
            else:
                needle = '//%s' % (element)
            log.debug("Searching for %s" % needle)
            for e in self.old_xml_doc.findall(needle):
                if fields is not None:
                    name = e.attrib.get('name')
                    if str(name ) not in fields: 
                        log.debug('Old: Not found %s in %s' % (name, fields))
                        continue
                self.elements_old[element].append(e)
            for e in self.new_xml_doc.findall(needle):
                if fields is not None:
                    name = e.attrib.get('name')
                    if str(name )not in fields: 
                        log.debug('New: Not found %s in %s' % (name, fields))
                        continue
                self.elements_new[element].append(e)
            self.diff_element(element, check_attributes)

    def diff_element(self, element, check_attributes):
        log.debug('Checking %s elements' % element)
        out = []

        # Search for old field in new 
        for o in self.elements_old.get(element):
            error = []
            name = o.attrib.get('name')
            found = False
            for n in self.elements_new.get(element):
                if name == n.attrib.get('name'):
                    found = True
                    try:
                        if check_attributes:
                            self.diff_attributes(o, n)
                    except AttributeChangeException, e:
                        error.append("Attributes changed for '%s': %s" % (name, e.message))
                    if element == 'choice':
                        error.extend(self.diff_choicelist(o, n, check_attributes))
                    break
            if not found:
                error.append("Deleted")

            if error:
                out.append("Field: %s (%s)" % (name, element))
                out.append('---')
                out.append('Changes:')
                for num, err in enumerate(error):
                    out.append('%s. %s' % (num, err))
                out.append('===')

        # Searching for new fields in old
        for o in self.elements_new.get(element):
            error = []
            name = o.attrib.get('name')
            # Search for element in new
            found = False
            for n in self.elements_old.get(element):
                if name == n.attrib.get('name'):
                    found = True
                    break
            if not found:
                    error.append("New")
                    if element == 'choice':
                        error.extend(self.diff_choicelist(None, o, check_attributes))

            if error:
                out.append("Field: %s (%s)" % (name, element))
                out.append('---')
                out.append('Changes:')
                for num, err in enumerate(error):
                    out.append('%s. %s' % (num, err))
                out.append('===')

        for o in out:
            print unicode(o).encode("utf-8")
    
    def diff_choicelist(self, o, n, check_attributes):
        old = {}
        new = {}
        errors = []
        if o is not None:
            for c in o.findall('.//bool'):
                old[c.attrib.get('value')] = c
        if n is not None:
            for c in n.findall('.//bool'):
                new[c.attrib.get('value')] = c

        for key in old.keys():
            if new.has_key(key):
                try:
                    if check_attributes:
                        self.diff_attributes(old.get(key), new.get(key))
                except AttributeChangeException, e:
                    errors.append("Attributes changed for '%s': %s " % (key, e.message) )
            else:
                errors.append("Deleted option: '%s' value: '%s'" % (old.get(key).attrib.get('description'), key))
        for key in new.keys():
            if not old.has_key(key):
                errors.append("New option: '%s' value: '%s'" % (new.get(key).attrib.get('description'), key))

        return errors

    def diff_attributes(self, o, n):
        ok = False
        new = []
        changed = []
        deleted = []
        for key, value in o.attrib.iteritems():
            if n.attrib.has_key(key):
                if n.get(key) == value:
                    pass #Ok
                else:
                    changed.append(key)
            else:
                deleted.append(key)
        for key, value in n.attrib.iteritems():
            if not o.attrib.has_key(key):
                new.append(key)

        if len(new) > 0 or len(changed) > 0 or len(deleted) > 0:
            raise AttributeChangeException(new, changed, deleted)

def usage(code, msg=''):
    print >> sys.stderr, __doc__
    if msg:
        print >> sys.stderr, msg
    sys.exit(code)

def main():
    try:
        opts, args = getopt.getopt(sys.argv[1:], 'hvVe:s:f:',
        ['elements=', 'check-attributes', 'subtree=', 'fields='])
   
    except getopt.error, msg:
        usage(1, msg)
    
    elements = None
    subtree = None
    fields = None
    check_attributes = False
    for opt, arg in opts:
        if opt in ('-v'):
            log.setLevel(logging.INFO)
        if opt in ('-V'):
            log.setLevel(logging.DEBUG)
        if opt in ('-h', '--help'):
            usage(0)
        if opt in ('-e', '--elements'):
            elements = arg.split(',')
            log.debug('Elements: %s' % elements)
        if opt in ('-s', '--subtree'):
            subtree = arg
            log.debug('Subtree: %s' % subtree)
        if opt in ('-f', '--fields'):
            fields = arg.split(',')
            log.debug('Fields: %s' % fields)
        if opt in ('--check-attributes'):
            check_attributes = True
    if len(args) < 2:
        usage(1)

    ins = Inspector(args[0], args[1])
    ins.perform(elements, subtree, fields, check_attributes)

if __name__ == '__main__':
    main()
