"""
Generate address space code from xml file specification
"""
from __future__ import annotations
from xml.etree import ElementTree
from logging import getLogger
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
import re
from pathlib import Path

_logger = getLogger(__name__)

IgnoredEnums: List[str] = []
IgnoredStructs: List[str] = []

# by default, we split requests and responses in header and parameters, but some are so simple we do not split them
NoSplitStruct = [
    "GetEndpointsResponse",
    "CloseSessionRequest",
    "AddNodesResponse",
    "DeleteNodesResponse",
    "BrowseResponse",
    "HistoryReadResponse",
    "HistoryUpdateResponse",
    "RegisterServerResponse",
    "CloseSecureChannelRequest",
    "CloseSecureChannelResponse",
    "CloseSessionRequest",
    "CloseSessionResponse",
    "UnregisterNodesResponse",
    "MonitoredItemModifyRequest",
    "MonitoredItemsCreateRequest",
    "ReadResponse",
    "WriteResponse",
    "TranslateBrowsePathsToNodeIdsResponse",
    "DeleteSubscriptionsResponse",
    "DeleteMonitoredItemsResponse",
    "CreateMonitoredItemsResponse",
    "ServiceFault",
    "AddReferencesResponse",
    "ModifyMonitoredItemsResponse",
    "RepublishResponse",
    "CallResponse",
    "FindServersResponse",
    "RegisterServerRequest",
    "RegisterServer2Response",
]

buildin_types = [
    'Char',
    'SByte',
    'Int16',
    'Int32',
    'Int64',
    'UInt16',
    'UInt32',
    'UInt64',
    'Boolean',
    'Double',
    'Float',
    'Byte',
    'String',
    'CharArray',
    'ByteString',
    'DateTime',
    "Guid",
]

# structs that end with Request or Response but are not
NotRequest = ["MonitoredItemCreateRequest", "MonitoredItemModifyRequest", "CallMethodRequest"]
OverrideTypes = {}


@dataclass
class EnumField:
    name: str = None
    value: int = None


@dataclass
class Field:
    name: str = None
    data_type: str = "i=24"  # i=24 means anything
    value_rank: int = -1
    array_dimensions: List[int] = None
    max_string_length: int = None
    value: Any = None
    is_optional: bool = False
    allow_subtypes: bool = False
    is_nullable: bool = False

    def is_array(self):
        return self.value_rank != -1 or self.array_dimensions


@dataclass
class Struct:
    name: str = None
    basetype: Optional[str] = None
    node_id: str = None
    doc: str = ""
    fields: List[Field] = field(default_factory=list)
    has_optional: bool = False
    needoverride = False
    children: List[Any] = field(default_factory=list)
    parents: List[Any] = field(default_factory=list)
    # we splt some structs, they must not be registered as extension objects
    do_not_register: bool = False
    is_data_type: bool = False

    def __hash__(self):
        return hash(self.name)

    def get_field(self, name):
        for f in self.fields:
            if f.name == name:
                return f
        raise Exception(f'field not found: {name}')


@dataclass
class Enum:
    name: str = None
    data_type: str = None
    fields: List[Field] = field(default_factory=list)
    doc: str = ""
    is_option_set: bool = False
    base_type: str = None


@dataclass
class Alias:
    name: str
    data_type: str
    real_type: str


class Model:
    structs: List[Struct]
    enums: List[Enum]
    struct_list: List[str]
    enum_list: List[str]
    known_structs: List[str]
    aliases: Dict[str, Alias]

    def __init__(self):
        self.structs = []
        self.enums = []
        self.struct_list = []
        self.enum_list = []
        self.known_structs = []
        self.aliases = {}

    def get_struct(self, name: str) -> Struct:
        for struct in self.structs:
            if name == struct.name:
                return struct
        raise Exception("No struct named: " + name)

    def get_struct_by_nodeid(self, nodeid: str) -> Struct:
        for struct in self.structs:
            if nodeid == struct.node_id:
                return struct
        raise Exception("No struct with node id: " + nodeid)

    def get_enum(self, name: str) -> Enum:
        for s in self.enums:
            if name == s.name:
                return s
        raise Exception("No enum named: " + str(name))

    def get_alias(self, name: str) -> Optional[Alias]:
        for alias in self.aliases.values():
            if alias.name == name:
                return alias
        return None


def _add_struct(struct: Struct, newstructs: List[Struct], waiting_structs, known_structs: List[str]):
    newstructs.append(struct)
    known_structs.append(struct.name)
    # now seeing if some struct where waiting for this one
    waitings = waiting_structs.pop(struct.name, None)
    if waitings:
        for s in waitings:
            s.waitingfor.remove(struct.name)
            if not s.waitingfor:
                _add_struct(s, newstructs, waiting_structs, known_structs)


def reorder_structs(model: Model):
    types: List[str] = IgnoredStructs + IgnoredEnums + buildin_types + [
        'StatusCode',
        'DiagnosticInfo',
        "ExtensionObject",
        "QualifiedName",
        "ResponseHeader",
        "RequestHeader",
        'AttributeID',
        "ExpandedNodeId",
        "NodeId",
        "Variant",
        "DataValue",
        "LocalizedText",
    ] + [enum.name for enum in model.enums] + ['VariableAccessLevel'] + [alias.name for alias in model.aliases.values()]
    waiting_structs: Dict[str, List[Struct]] = {}
    newstructs: List[Struct] = []
    for s in model.structs:
        s.waitingfor = []
        ok = True
        for f in s.fields:
            if f.data_type not in types:
                if f.data_type in waiting_structs:
                    waiting_structs[f.data_type].append(s)
                else:
                    waiting_structs[f.data_type] = [s]
                s.waitingfor.append(f.data_type)
                ok = False
        if ok:
            _add_struct(s, newstructs, waiting_structs, types)

    if len(model.structs) != len(newstructs):
        _logger.warning('Error while reordering structs, some structs could not be reinserted: had %s structs, we now have %s structs', len(model.structs), len(newstructs))
        s1 = set(model.structs)
        s2 = set(newstructs)
        _logger.debug('Variant' in types)
        for s in s1 - s2:
            _logger.warning('%s is waiting_structs for: %s', s.name, s.waitingfor)
    model.structs = newstructs


def nodeid_to_names(model: Model):
    ids = {}
    with open(Path.cwd() / "UA-Nodeset-master" / "Schema" / "NodeIds.csv") as f:
        for line in f:
            name, nb, datatype = line.split(",")
            ids[nb] = name
    ids["24"] = "Variant"
    ids["22"] = "ExtensionObject"

    for struct in model.structs:
        if struct.basetype is not None:
            if struct.basetype.startswith("i="):
                struct.basetype = ids[struct.basetype[2:]]
        for sfield in struct.fields:
            if sfield.data_type.startswith("i="):
                sfield.data_type = ids[sfield.data_type[2:]]
    for alias in model.aliases.values():
        alias.data_type = ids[alias.data_type[2:]]
        alias.real_type = ids[alias.real_type[2:]]
    for enum in model.enums:
        if enum.base_type.startswith("i="):
            enum.base_type = ids[enum.base_type[2:]]


def split_requests(model: Model):
    structs = []
    for struct in model.structs:
        structtype = None
        if struct.name.endswith('Request') and struct.name not in NotRequest:
            structtype = 'Request'
        elif struct.name.endswith('Response') or struct.name == 'ServiceFault':
            structtype = 'Response'
        if structtype:
            struct.needconstructor = True
            sfield = Field(name="TypeId", data_type="NodeId")
            struct.fields.insert(0, sfield)

        if structtype and struct.name not in NoSplitStruct:
            paramstruct = Struct(do_not_register=True)
            if structtype == 'Request':
                basename = struct.name.replace('Request', '') + 'Parameters'
                paramstruct.name = basename
            else:
                basename = struct.name.replace('Response', '') + 'Result'
                paramstruct.name = basename
            paramstruct.fields = struct.fields[2:]

            struct.fields = struct.fields[:2]
            structs.append(paramstruct)

            typeid = Field(name="Parameters", data_type=paramstruct.name)
            struct.fields.append(typeid)
        structs.append(struct)
    model.structs = structs


def get_basetypes(el: ElementTree.Element) -> List[str]:
    # return all basetypes
    basetypes = []
    for ref in el.findall("./{*}References/{*}Reference"):
        if ref.get("ReferenceType") == "HasSubtype" and \
           ref.get("IsForward", "true") == "false" and \
           ref.text != "i=22":
            basetypes.append(ref.text)
    return basetypes


class Parser:
    model: Optional[Model]

    def __init__(self, path: Path):
        self.path = path
        self.model = None
        self._tag_re = re.compile(r"\{.*\}(.*)")

    def parse(self) -> Model:
        _logger.debug("Parsing: %s", self.path)
        self.model = Model()
        tree = ElementTree.parse(self.path)
        root = tree.getroot()
        for child in root.findall("{*}UADataType"):
            self._add_data_type(child)

        return self.model

    def _add_data_type(self, el: ElementTree.Element):
        if self.model is None:
            raise RuntimeError("Model not initialized")

        name = el.get("BrowseName")
        if name is None:
            raise RuntimeError(f'No BrowseName for element: {el}')

        for ref in el.findall("./{*}References/{*}Reference"):
            if ref.get("ReferenceType") == "HasSubtype" and ref.get("IsForward", "true") == "false":
                if ref.text == "i=29":
                    enum = self.parse_enum(name, el, "Int32")  # Enumeration Values are always Int32 type
                    self.model.enums.append(enum)
                    self.model.enum_list.append(enum.name)
                    return

                if ref.text in ("i=2", "i=3", "i=4", "i=5", "i=6", "i=7", "i=8", "i=9"):
                    # looks like some enums are defined there too
                    enum = self.parse_enum(name, el, ref.text)
                    if not enum.fields:
                        alias = Alias(name, el.get("NodeId"), ref.text)
                        self.model.aliases[alias.data_type] = alias
                        return
                    self.model.enums.append(enum)
                    self.model.enum_list.append(enum.name)
                    return

                if ref.text == "i=22" or ref.text in self.model.known_structs:
                    struct = self.parse_struct(name, el)
                    struct.is_data_type = True
                    if ref.text in self.model.known_structs:
                        parent = self.model.get_struct_by_nodeid(ref.text)
                        for sfield in reversed(parent.fields):
                            struct.fields.insert(0, sfield)
                    self.model.structs.append(struct)
                    self.model.known_structs.append(struct.node_id)
                    self.model.struct_list.append(struct.name)
                    return

                if 0 < int(ref.text[2:]) < 21:
                    alias = Alias(name, el.get("NodeId"), ref.text)
                    self.model.aliases[alias.data_type] = alias
                    return

                if ref.text in self.model.aliases:
                    alias = Alias(name, el.get("NodeId"), self.model.aliases[ref.text].real_type)
                    self.model.aliases[alias.data_type] = alias
                    return

                if ref.text in ("i=24"):
                    return

                _logger.warning(" %s is of unknown type %s", name, ref.text)

    @staticmethod
    def parse_struct(name: str, el: ElementTree.Element) -> Struct:
        """Parse the XML element into a Struct."""
        doc_el = el.find("{*}Documentation")
        if doc_el is not None:
            doc = doc_el.text
        else:
            doc = ""
        struct = Struct(
            name=name,
            doc=doc,
            node_id=el.get("NodeId"),
        )
        basetypes = get_basetypes(el)
        if basetypes:
            struct.basetype = basetypes[0]
            if len(basetypes) > 1:
                print(f'Error found mutliple basetypes for {struct} {basetypes}')
        for sfield in el.findall("./{*}Definition/{*}Field"):
            opt = sfield.get("IsOptional", "false")
            allow_subtypes = True if sfield.get("AllowSubTypes", "false") == 'true' else False
            is_optional = True if opt == "true" else False
            f = Field(
                name=sfield.get("Name"),
                data_type=sfield.get("DataType", "i=24"),
                value_rank=sfield.get("ValueRank", -1),
                array_dimensions=sfield.get("ArayDimensions"),
                value=sfield.get("Value"),
                is_optional=is_optional,
                allow_subtypes=allow_subtypes
            )
            if is_optional:
                struct.has_optional = True
            struct.fields.append(f)
        return struct

    @staticmethod
    def parse_enum(name: str, el: ElementTree.Element, base_type: str) -> Enum:
        """Parse the XML element into a Enum."""
        doc_el = el.find("{*}Documentation")
        if doc_el is not None:
            doc = doc_el.text
        else:
            doc = ""
        is_option_set = el.find("./{*}Definition/[@IsOptionSet]")
        enum = Enum(
            name=name,
            data_type=el.get("NodeId"),
            doc=doc,
            is_option_set=bool(is_option_set and is_option_set.text and is_option_set.text != 'false'),
            base_type=base_type
        )
        for f in el.findall("./{*}Definition/{*}Field"):
            efield = EnumField(name=f.get("Name"), value=int(f.get("Value")))
            enum.fields.append(efield)
        return enum


def fix_names(model: Model):
    for s in model.enums:
        for f in s.fields:
            if f.name == 'None':
                f.name = 'None_'
    for s in model.structs:
        if s.name[0] == "3":
            s.name = "Three" + s.name[1:]
            for f in s.fields:
                if f.data_type[0] == "3":
                    f.data_type = "Three" + s.name[1:]
        # Next code might be better but the only case is the "3" above and
        # at many places the structs are call Three instead of 3 so the
        # code over ie better for now

        # if s.name[0].isdigit():
        # s.name = "_" + s.name
        # for f in s.fields:
        # if f.data_type[0].isdigit():
        # f.data_type = "_" + s.name


if __name__ == "__main__":
    # this is jus debug code
    BASE_DIR = Path.cwd()
    xml_path = BASE_DIR / 'UA-Nodeset-master' / 'Schema' / 'Opc.Ua.NodeSet2.Services.xml'
    p = Parser(xml_path)
    model = p.parse()
    nodeid_to_names(model)
    split_requests(model)
    fix_names(model)
    reorder_structs(model)
