#!/usr/bin/python
#    Copyright (c) 2013 Mirantis, Inc.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import argparse
import os
import re
import yaml


# Workaround for unknown yaml tags
def yaml_default_ctor(loader, tag_suffix, node):
    return tag_suffix + ' ' + node.value

yaml.add_multi_constructor('', yaml_default_ctor)


class PlantUmlNode():
    def __init__(self, node_dn):
        self._dn = node_dn
        self.attributes = []
        self.operations = []
        self.extras = []

    def write(self, f):
        f.write('class {0} {{\n'.format(self._dn))
        f.write('-- Properties --\n')
        for item in self.attributes:
            f.write('<{type}> {name}: {contract}\n'.format(**item))
        f.write('-- Workflows --\n')
        for item in self.operations:
            f.write('{name}()\n'.format(**item))
        f.write('-- Namespaces --\n')
        for item in self.extras:
            if item['key'] != '=':
                f.write('{key}: {value}\n'.format(**item))
        f.write('}\n')

    def add_attribute(self, name, type, contract):
        d = {'type': type, 'name': name, 'contract': contract}
        self.attributes.append(d)

    def add_operation(self, name):
        d = {'name': name}
        self.operations.append(d)

    def add_extra(self, key, value):
        d = {'key': key, 'value': value}
        self.extras.append(d)


class DslSpec():
    def __init__(self, class_dn, basepath='.'):
        self._dn = class_dn
        self._id = self._dn.replace('.', '_')
        self._name = self._dn.split('.')[-1]
        self._ns = ('=', self._dn.split('.')[:-1])
        self._graph = None
        self.is_virtual = True
        self.parent_dn = None
        self.manifest = None

        manifest_path = os.path.join(basepath, self._dn, 'manifest.yaml')
        if os.path.exists(manifest_path):
            self.manifest = yaml.load(open(manifest_path))

        if self.manifest:
            self.is_virtual = False
            self.name = self.manifest['Name']
            if 'Extends' in self.manifest:
                self.parent_dn = self.get_dn(self.manifest['Extends'])

    def split_ns(self, name=None):
        if name:
            parts = name.split(':')
            if len(parts) == 1:
                parts.insert(0, '=')
        else:
            parts = ['=', self._name]
        return parts

    def get_ns(self, name=None):
        parts = self.split_ns(name)
        return {
            'key': parts[0],
            'value': self.manifest['Namespaces'][parts[0]]
        }

    def get_dn(self, name=None):
        parts = self.split_ns(name)
        parts[0] = self.get_ns(parts[0] + ':')['value']
        return '.'.join(parts)

    def get_name(self):
        return self._name


class DslPlantUmlNode(DslSpec):
    def write(self, file):
        uml_class = PlantUmlNode(self._dn)
        ext_classes = []
        if not self.is_virtual:
            namespaces = [self.get_ns()]
            for name, item in self.manifest.get('Properties', {}).iteritems():
                item_type = item.get('Type', 'In')
                item_contract = str(item.get('Contract', 'UNDEFINED'))
                match = re.search('class\((.*?)\)', item_contract)
                if match:
                    ns = self.get_ns(match.group(1))
                    ext_classes.append(self.get_dn(match.group(1)))
                    if not ns in namespaces:
                        namespaces.append(ns)
                uml_class.add_attribute(
                    name,
                    type=item_type,
                    contract=item_contract
                )
            for m in self.manifest.get('Workflow', []):
                uml_class.add_operation(m)
            for ns in namespaces:
                uml_class.add_extra(ns['key'], ns['value'])
        uml_class.write(file)
        return ext_classes


class DslPlantUmlGraph():
    def __init__(self):
        self._nodes = []
        self._edges = []
        self._options = {}
        self._file = None

    def write(self, classname, level=0):
        if level == 0:
            self._file.write('@startuml\n')
            if self.get_option('NoNamespaces', False):
                self._file.write('set namespaceSeparator none\n')

        if self.node_exists(classname):
            return

        self.add_node(classname)

        node = DslPlantUmlNode(classname)
        ext_classes = node.write(self._file)

        if not self.get_option('ParentsOnly', False):
            for ext_class in ext_classes:
                self.add_edge(
                    from_node=ext_class,
                    to_node=classname,
                    edge_type='<..'
                )
                self.write(ext_class, level + 1)

        if node.is_virtual:
            return

        if node.parent_dn:
            self.add_edge(
                from_node=node.parent_dn,
                to_node=classname,
                edge_type='<|--'
            )
            self.write(node.parent_dn, level + 1)

        if level == 0:
            for edge in self._edges:
                self._file.write('{from_node} {type} {to_node}\n'
                    .format(**edge))
            self._file.write('@enduml\n')
            self._file.close()

    def add_node(self, classname):
        self._nodes.append(classname)

    def node_exists(self, dn):
        return dn in self._nodes

    def add_edge(self, from_node, to_node, edge_type):
        edge = {'from_node': from_node, 'to_node': to_node, 'type': edge_type}
        if not edge in self._edges:
            self._edges.append(edge)

    def set_option(self, key, value):
        self._options[key] = value

    def get_option(self, key, default=None):
        return self._options.get(key, default)

    def open_file(self, file_name):
        self._file = open(file_name, 'w')

    def close_file(self):
        self._file.close()


parser = argparse.ArgumentParser(description='Qwerty')
parser.add_argument('classname',
                    default='com.mirantis.murano.demoApp.DemoHost',
                    help='Dsl Class Name to draw.', nargs='?')
parser.add_argument('-n', '--no-namespaces', action='store_true')
parser.add_argument('-p', '--parents-only', action='store_true')
args = parser.parse_args()

graph = DslPlantUmlGraph()
graph.set_option('NoNamespaces', args.no_namespaces)
graph.set_option('ParentsOnly', args.parents_only)
graph.open_file('plantuml.txt')
graph.write(args.classname)
graph.close_file()

os.system('java -jar plantuml.jar plantuml.txt')
os.system('xdg-open plantuml.png')