#!/usr/bin/env python3

'''\
Read and report upon a ViPR SRM alerting.xml file.

Tested with Python 3.4.3
'''

# Insure maximum compatibility between Python 2 and 3
from __future__ import absolute_import, division, print_function


__version__ = 1.1
__copyright__ = "Copyright 2015 Samuel T. Denton, III"
__author__ = "Samuel T. Denton, III <sam.denton@emc.com>"
__contributors__ = []

__license__ = """
Copyright (c) 2015 Samuel T. Denton, III, <sam.denton@emc.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

# Python standard libraries
from collections import defaultdict, namedtuple
from functools import partial
from hashlib import md5  # deprecated, but we want speed, not security
from operator import attrgetter
from string import Template
from sys import platform, version_info, stdout

import argparse, cgi, os, string, sys, textwrap

PY2 = version_info.major == 2
if PY2:
    from StringIO import StringIO
    zip_longest = partial(map, None)
else:
    from io import StringIO
    from itertools import zip_longest
    basestring = str
    unicode = str

# Python site libraries
try:
    from graphviz import Digraph
except:
    print("""
Can't import graphviz! You can use 'pip install graphviz' to fix that
but you probably don't have the software installed, either.  Go to
http://www.graphviz.org/download/ to get it.
""", file=sys.stderr)
    sys.exit(1)

# Python personal libraries
from AlertingConfig import AlertingConfig, getText
from htmltags import *

if 0:
    style = 'radial'
    fillcolor = [ '/pastel15/{0}:/set15/{0}'.format(i) for i in range(6) ]
else:
    style='filled'
    fillcolor = [ '/set15/{0:d}'.format(i) for i in range(6) ]

#
# Utilities
#

def parse_folders(path, sep='/', _cache={}):
    '''Split a path into flagged components.

>>> parse_folders('foo')
[(True, u'foo')]

>>> parse_folders('foo/bar')
[(False, u'foo'), (True, u'bar')]

>>> sorted('foo foo/bar foo/bar/baz bar bar/baz baz'.split(), key=parse_folders)
[u'bar/baz', u'foo/bar/baz', u'foo/bar', u'bar', u'baz', u'foo']
'''
    pieces = path.split(sep)
    n = len(pieces) - 1
    try:
        # Caching the flags speeds things up by about 27%
        flags = _cache[n]
    except KeyError:
        # Flag folders with False to get correct sort order
        flags = _cache[n] = [False] * n + [True]
    return list(zip(flags, pieces))


class MyFormatter(string.Formatter):
    def convert_field(self, value, conversion):
        if conversion == 'q':
            return cgi.escape(str(value), True)
        elif conversion == 'Q':
            return cgi.escape(str(value).title(), True)
        else:
            return super(MyFormatter, self).convert_field(value, conversion)
fmt = MyFormatter().format


pattern = r"""(
    \s+ |                                 # any whitespace
    [^\s\w]* \w+? (?:
        [-_] (?=\w) |           # hyphenated words
        [a-z] (?=[A-Z])         # CamelCase
        ) |
    (?<=[\w\!\"\'\&\.\,\?]) -{2,} (?=\w)  # em-dash
    )"""

textwrap.TextWrapper.wordsep_re = textwrap.re.compile(pattern, textwrap.re.X)
tw = textwrap.TextWrapper(width=16)
if isinstance('', bytes):
    tw.wordsep_re_uni = textwrap.re.compile(unicode(pattern), textwrap.re.X+textwrap.re.U)
else:
    tw.wordsep_re_uni = tw.wordsep_re
fill = tw.fill


LEAVING_CORNERS = {
    1: ['e'],
    2: ['ne', 'se'],
    3: ['ne', 'e', 'se'],
    4: ['n', 'ne', 'se', 's'],
    5: ['n', 'ne', 'e', 'se', 's'],
    }

ENTERING_CORNERS = {
    1: ['w'],
    2: ['nw', 'sw'],
    3: ['nw', 'w', 'sw'],
    4: ['n', 'nw', 'sw', 's'],
    5: ['n', 'nw', 'w', 'sw', 's'],
    }

rotations = {
    'BT': {
        'e': 'n',
        'n': 'w',
        'ne': 'nw',
        'nw': 'sw',
        's': 'e',
        'se': 'ne',
        'sw': 'se',
        'w': 's'},
    'LR': {
        'e': 'e',
        'n': 'n',
        'ne': 'ne',
        'nw': 'nw',
        's': 's',
        'se': 'se',
        'sw': 'sw',
        'w': 'w'},
    'RL': {
        'e': 'w',
        'n': 's',
        'ne': 'sw',
        'nw': 'se',
        's': 'n',
        'se': 'nw',
        'sw': 'ne',
        'w': 'e'},
    'TB': {
        'e': 's',
        'n': 'e',
        'ne': 'se',
        'nw': 'ne',
        's': 'w',
        'se': 'sw',
        'sw': 'nw',
        'w': 'n'},
    }

# UsefulInfo should really be a base class...
UsefulInfo = namedtuple('UsefulInfo', 'title, keyAttr, filter, key_is_path')


def always(*args, **keywords):
    return True


def DDTP(s):
    '''\
Dehyphenate a string,
Drop the last word,
Title-case the result,
Pluralize it.

>>> DDTP('adapter-list')
>>> DDTP('definition-list')
>>> DDTP('grouped-box-list')
>>> DDTP('component-template-list')
'''
    s = ' '.join(s.split('-')[:-1])
    s = s.title()
    return s + ('es' if s.endswith(('ch', 'x')) else 's')


#
# The main event...
#


class BaseAlertingConfig(AlertingConfig):
    '''\
The alerting.xml files define grouped boxes in a manner similar to the
alert definitions, enough so that we can use the AlertingConfig class to
handle both. This class encapsulates the common aspects.'''

    def get_basic_info(self, node, *tagNames):
        info = { '': tagNames }  # a bit of a hack
        for tagName in tagNames:
            child = node.getElementsByTagName(tagName)
            text = getText(child[0].childNodes) if child else ''
            info[tagName] = text
            if tagName == 'class':
                self.jclasses.add(text)
        return info

    def show_basic_info(self, info, **kwds):
        try:
            detail = self.detail
        except:
            detail = self.html
        bgcolor = kwds.pop('bgcolor', 'White')
        detail(fmt('<table bgcolor="{0!q}">', bgcolor))
        detail('<colgroup><col width=20%><col width=75%></colgroup>')
        detail('<tbody>')
        for tagName in info['']:
            detail(fmt('<tr><td>{0!q}</td><td>{1!q}</td></tr>', tagName, info[tagName]))
        detail('</tbody>')
        detail('</table>')
        return info

    def param_table(self, node, bgcolor='#F7F7FF'):
        try:
            detail = self.detail
        except:
            detail = self.html
        detail(fmt('<table bgcolor="{0!q}">', bgcolor))
        detail('<colgroup><col width=20%><col width=75%></colgroup>')
        detail('<thead><tr><th colspan="2">Parameters</th></tr></thead>')
        detail('<tbody>')
        for param in node.getElementsByTagName('param-list'):
            name = param.getAttribute('name')
            text = getText(param.childNodes)
            detail(fmt('<tr><td class="level{2}">{0!q}</td><td>{1!q}</td></tr>', name, text, self.depth))
        detail('</tbody>')
        detail('</table>')

    def prepare(self):
        self.digraph = Digraph(self.unique_name, format='svg')
        self.digraph.graph_attr.update(
            fontsize='30',
            labelloc='t',
            splines='True',
            overlap='False',
            rankdir=self.args.rankdir,
            id='${uniqueid}',  #fmt('graph-{0!q}', id(self)),
            )
        self.digraph.node_attr.update(
            style=style,
            fontcolor='white',)
        self.stringio = StringIO()
        self.detail = partial(print, file=self.stringio)
        self.node_keywords = {}
        self.edge_keywords = {}

    def cleanup(self):
        for node, keywords in sorted(self.node_keywords.items()):
            self.digraph.node(fmt("{0!q}", node), **keywords)

        leaving = defaultdict(set)
        entering = defaultdict(set)
        for from_node, from_port, to_node, to_port in self.edge_keywords:
            leaving[from_node].add(from_port)
            entering[to_node].add(to_port)
        leaving_mapping = {}
        leaving_corners = self.args.leaving_corners
        for node, ports in sorted(leaving.items()):
            leaving_mapping[node] = {port: corner for port, corner in zip(sorted(ports), leaving_corners[len(ports)])}
        entering_mapping = {}
        entering_corners = self.args.entering_corners
        for node, ports in sorted(entering.items()):
            entering_mapping[node] = {port: corner for port, corner in zip(sorted(ports), entering_corners[len(ports)])}
        # If multiple edges leave (or enter) via the same port, we only want to label the first one.
        from_seen = set()
        to_seen = set()