"""
Created on 2020-11-12

@author: wf
"""

import textwrap
from typing import Dict
from sidif.sidif import DataInterchange


class PlantUml(object):
    """
    Plant UML support
    """

    # redundant to skinparams in pylodstorage.uml
    skinparams = """
' BITPlan Corporate identity skin params
' Copyright (c) 2015-2025 BITPlan GmbH
' see http://wiki.bitplan.com/PlantUmlSkinParams#BITPlanCI
' skinparams generated by com.bitplan.restmodelmanager
skinparam note {
  BackGroundColor #FFFFFF
  FontSize 12
  ArrowColor #FF8000
  BorderColor #FF8000
  FontColor black
  FontName Technical
}
skinparam component {
  BackGroundColor #FFFFFF
  FontSize 12
  ArrowColor #FF8000
  BorderColor #FF8000
  FontColor black
  FontName Technical
}
skinparam package {
  BackGroundColor #FFFFFF
  FontSize 12
  ArrowColor #FF8000
  BorderColor #FF8000
  FontColor black
  FontName Technical
}
skinparam usecase {
  BackGroundColor #FFFFFF
  FontSize 12
  ArrowColor #FF8000
  BorderColor #FF8000
  FontColor black
  FontName Technical
}
skinparam activity {
  BackGroundColor #FFFFFF
  FontSize 12
  ArrowColor #FF8000
  BorderColor #FF8000
  FontColor black
  FontName Technical
}
skinparam classAttribute {
  BackGroundColor #FFFFFF
  FontSize 12
  ArrowColor #FF8000
  BorderColor #FF8000
  FontColor black
  FontName Technical
}
skinparam interface {
  BackGroundColor #FFFFFF
  FontSize 12
  ArrowColor #FF8000
  BorderColor #FF8000
  FontColor black
  FontName Technical
}
skinparam class {
  BackGroundColor #FFFFFF
  FontSize 12
  ArrowColor #FF8000
  BorderColor #FF8000
  FontColor black
  FontName Technical
}
skinparam object {
  BackGroundColor #FFFFFF
  FontSize 12
  ArrowColor #FF8000
  BorderColor #FF8000
  FontColor black
  FontName Technical
}
hide Circle
' end of skinparams '
"""

    def __init__(
        self,
        copyRight=None,
        title=None,
        debug=False,
        withSkin: bool = True,
        withAt: bool = False,
        doc_width: int = 40,
    ):
        """
        Constructor
        """
        self.debug = debug
        self.withSkin = withSkin
        self.withAt = withAt
        self.uml = ""
        self.title = title
        self.copyRight = copyRight
        self.doc_width = doc_width

    def __str__(self):
        return self.atIt(self.uml)

    def atIt(self, markup: str):
        """
        Args:
            markup (str): the markup
        Returns:
            str: markup with @startuml and @enduml
        """
        if not self.withAt:
            return markup
        at_markup = f"""@startuml
{markup}
@enduml"""
        return at_markup

    def multi_line_doc(self, doc: str, width: int) -> str:
        """
        Returns the given documentation as a multiline string with a limited length per line.
        Lines are broken at whitespace.

        Args:
            doc (str): the documentation to wrap
            width (int): The maximum length of each line.
        Returns:
            str: the  Multiline string.
        """
        text = "\n".join(textwrap.wrap(doc, width=width))
        return text

    def asUmlDict(self, dif):
        """
        return the given DataInterchange as an UML Dict
        """
        uml = {"packages": {}, "topiclinks": {}}
        classKey = None
        classes = {}
        for _i, triple in enumerate(dif.triples):
            if self.debug:
                print(triple)
            if triple.p == "isA":
                itkey = triple.s
                if triple.o == "Context":
                    packageKey = itkey
                    packages = uml["packages"]
                    packages[itkey] = {"classes": {}}
                    it = packages[itkey]
                elif triple.o == "TopicLink":
                    links = uml["topiclinks"]
                    links[itkey] = {}
                    it = links[itkey]
                elif triple.o == "Property":
                    propKey = itkey
                    if not classKey:
                        print(f"invalid property {propKey} declared out of class scope")
                        continue
                    properties = classes[classKey]["properties"]
                    properties[propKey] = {}
                    it = properties[propKey]
                else:
                    isA = triple.o
                    classKey = itkey
                    classes = packages[packageKey]["classes"]
                    clazz_dict={"isA": isA, "properties": {}}
                    classes[itkey] = clazz_dict
                    it = clazz_dict
            elif triple.p == "extends":
                # inheritance relation e.g. Computer extends Device
                special_class = triple.s # Computer
                general_class = triple.o # Device
                clazz_dict={"extends": general_class, "properties": {}}
                classes[special_class]= clazz_dict
                it=clazz_dict
                pass
            elif triple.o == "it":
                if triple.p == "addsTo":
                    # @TODO might note be redundant but
                    # declaring a default
                    # redundant forward declaration
                    pass
                elif triple.p == "context":
                    parentKey = triple.s
                    packages[parentKey]["classes"][classKey] = classes[classKey]
                    pass
                elif triple.p == "topic":
                    parentKey = triple.s
                    classes[parentKey]["properties"][propKey] = properties[propKey]
                    pass
                else:
                    it[triple.p] = triple.s
        return uml

    def fromDIF(self, dif:DataInterchange) -> str:
        """
        create a UML from a Data Interchange

        Args:
            dif (DataInterchange): - the data interchange

        Returns:
            str: the planuml markup

        """
        umlDict = self.asUmlDict(dif)
        self.uml = self.fromUmlDict(umlDict)
        return

    def fromUmlDict(self, umlDict: Dict) -> str:
        """
        convert the given umlDict Dict to a plantuml string

        Args:
            umlDict (Dict): the dictionary of packages,classes and properties

        Returns:
            str: the planuml markup
        """
        uml = ""
        if self.title is not None:
            if self.copyRight is None:
                copyRight = ""
            else:
                copyRight = "\n%s" % self.copyRight
            uml += f"title\n{self.title}{copyRight}\nend title\n"
        packages = umlDict["packages"]
        for packageKey in packages.keys():
            package = packages[packageKey]
            package_name=package.get('name',packageKey)
            uml += f"package {package_name} {{\n"
            for classKey in package["classes"]:
                uclass = package["classes"][classKey]
                className = uclass.get("name", classKey)
                extends_class = uclass.get("extends")
                if extends_class:
                    extends = f"extends {extends_class} "
                else:
                    extends = ""
                uml += f"  class {className} {extends}{{\n"
                for propKey in uclass["properties"]:
                    prop = uclass["properties"][propKey]
                    uml += f"    {prop['name']}:{prop['type']}\n"
                uml += "  }\n"
                if "documentation" in uclass:
                    doc = uclass["documentation"]
                    doc = self.multi_line_doc(doc, self.doc_width)
                    uml += f"""Note top of {className}
{doc}
End note
"""
            uml += "}\n"

        links = umlDict["topiclinks"]
        for linkKey in links.keys():
            link = links[linkKey]
            sourceMany = "*" if link["sourceMultiple"] else "1"
            targetMany = "*" if link["targetMultiple"] else "1"
            sourceRole = link["sourceRole"] if "sourceRole" in link else ""
            targetRole = link["targetRole"] if "targetRole" in link else ""
            source = link["source"]
            target = link["target"]
            uml += f'{source} "{sourceRole} {sourceMany}" -- "{targetRole} {targetMany}" {target}\n'

        if self.withSkin:
            uml += PlantUml.skinparams
        return uml
