# Copyright Contributors to the DNF5 project.
# Copyright (C) 2008-2011 Red Hat, Inc.
# Author: David Zeuthen <davidz@redhat.com>
# SPDX-License-Identifier: LGPL-2.1-or-later
# Based from "GDBus - GLib D-Bus Library":
#

#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General
# Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
#

import xml.parsers.expat


class Annotation:
    def __init__(self, key, value):
        self.key = key
        self.value = value
        self.annotations = []
        self.since = ""


class Arg:
    def __init__(self, name, signature):
        self.name = name
        self.signature = signature
        self.annotations = []
        self.doc_string = ""
        self.since = ""


class Method:
    def __init__(self, name, h_type_implies_unix_fd=True):
        self.name = name
        self.h_type_implies_unix_fd = h_type_implies_unix_fd
        self.in_args = []
        self.out_args = []
        self.annotations = []
        self.doc_string = ""
        self.since = ""
        self.deprecated = False
        self.unix_fd = False


class Signal:
    def __init__(self, name):
        self.name = name
        self.args = []
        self.annotations = []
        self.doc_string = ""
        self.since = ""
        self.deprecated = False


class Property:
    def __init__(self, name, signature, access):
        self.name = name
        self.signature = signature
        self.access = access
        self.annotations = []
        self.arg = Arg("value", self.signature)
        self.arg.annotations = self.annotations
        self.readable = False
        self.writable = False
        if self.access == "readwrite":
            self.readable = True
            self.writable = True
        elif self.access == "read":
            self.readable = True
        elif self.access == "write":
            self.writable = True
        else:
            raise ValueError('Invalid access type "{}"'.format(self.access))
        self.doc_string = ""
        self.since = ""
        self.deprecated = False
        self.emits_changed_signal = True


class Interface:
    def __init__(self, name):
        self.name = name
        self.methods = []
        self.signals = []
        self.properties = []
        self.annotations = []
        self.doc_string = ""
        self.doc_string_brief = ""
        self.since = ""
        self.deprecated = False


class DBusXMLParser:
    STATE_TOP = "top"
    STATE_NODE = "node"
    STATE_INTERFACE = "interface"
    STATE_METHOD = "method"
    STATE_SIGNAL = "signal"
    STATE_PROPERTY = "property"
    STATE_ARG = "arg"
    STATE_ANNOTATION = "annotation"
    STATE_IGNORED = "ignored"

    def __init__(self, xml_data, h_type_implies_unix_fd=True):
        self._parser = xml.parsers.expat.ParserCreate()
        self._parser.CommentHandler = self.handle_comment
        self._parser.CharacterDataHandler = self.handle_char_data
        self._parser.StartElementHandler = self.handle_start_element
        self._parser.EndElementHandler = self.handle_end_element

        self.parsed_interfaces = []
        self._cur_object = None

        self.state = DBusXMLParser.STATE_TOP
        self.state_stack = []
        self._cur_object = None
        self._cur_object_stack = []

        self.doc_comment_last_symbol = ""

        self._h_type_implies_unix_fd = h_type_implies_unix_fd

        self._parser.Parse(xml_data)

    COMMENT_STATE_BEGIN = "begin"
    COMMENT_STATE_PARAMS = "params"
    COMMENT_STATE_BODY = "body"
    COMMENT_STATE_SKIP = "skip"

    def handle_comment(self, data):
        comment_state = DBusXMLParser.COMMENT_STATE_BEGIN
        lines = data.split("\n")
        symbol = ""
        body = ""
        in_para = False
        params = {}
        for line in lines:
            orig_line = line
            line = line.lstrip()
            if comment_state == DBusXMLParser.COMMENT_STATE_BEGIN:
                if len(line) > 0:
                    colon_index = line.find(": ")
                    if colon_index == -1:
                        if line.endswith(":"):
                            symbol = line[0: len(line) - 1]
                            comment_state = DBusXMLParser.COMMENT_STATE_PARAMS
                        else:
                            comment_state = DBusXMLParser.COMMENT_STATE_SKIP
                    else:
                        symbol = line[0:colon_index]
                        rest_of_line = line[colon_index + 2:].strip()
                        if len(rest_of_line) > 0:
                            body += rest_of_line + "\n"
                        comment_state = DBusXMLParser.COMMENT_STATE_PARAMS
            elif comment_state == DBusXMLParser.COMMENT_STATE_PARAMS:
                if line.startswith("@"):
                    colon_index = line.find(": ")
                    if colon_index == -1:
                        comment_state = DBusXMLParser.COMMENT_STATE_BODY
                        if not in_para:
                            in_para = True
                        body += orig_line + "\n"
                    else:
                        param = line[1:colon_index]
                        docs = line[colon_index + 2:]
                        params[param] = docs
                else:
                    comment_state = DBusXMLParser.COMMENT_STATE_BODY
                    if len(line) > 0:
                        if not in_para:
                            in_para = True
                        body += orig_line + "\n"
            elif comment_state == DBusXMLParser.COMMENT_STATE_BODY:
                if len(line) > 0:
                    if not in_para:
                        in_para = True
                    body += orig_line + "\n"
                else:
                    if in_para:
                        body += "\n"
                        in_para = False
        if in_para:
            body += "\n"

        if symbol != "":
            self.doc_comment_last_symbol = symbol
            self.doc_comment_params = params
            self.doc_comment_body = body

    def handle_char_data(self, data):
        # print 'char_data=%s'%data
        pass

    def handle_start_element(self, name, attrs):
        old_state = self.state
        old_cur_object = self._cur_object
        if self.state == DBusXMLParser.STATE_IGNORED:
            self.state = DBusXMLParser.STATE_IGNORED
        elif self.state == DBusXMLParser.STATE_TOP:
            if name == DBusXMLParser.STATE_NODE:
                self.state = DBusXMLParser.STATE_NODE
            else:
                self.state = DBusXMLParser.STATE_IGNORED
        elif self.state == DBusXMLParser.STATE_NODE:
            if name == DBusXMLParser.STATE_INTERFACE:
                self.state = DBusXMLParser.STATE_INTERFACE
                iface = Interface(attrs["name"])
                self._cur_object = iface
                self.parsed_interfaces.append(iface)
            elif name == DBusXMLParser.STATE_ANNOTATION:
                self.state = DBusXMLParser.STATE_ANNOTATION
                anno = Annotation(attrs["name"], attrs["value"])
                self._cur_object.annotations.append(anno)
                self._cur_object = anno
            else:
                self.state = DBusXMLParser.STATE_IGNORED

            # assign docs, if any
            if "name" in attrs and self.doc_comment_last_symbol == attrs["name"]:
                self._cur_object.doc_string = self.doc_comment_body
                if "short_description" in self.doc_comment_params:
                    short_description = self.doc_comment_params["short_description"]
                    self._cur_object.doc_string_brief = short_description
                if "since" in self.doc_comment_params:
                    self._cur_object.since = self.doc_comment_params["since"].strip(
                    )

        elif self.state == DBusXMLParser.STATE_INTERFACE:
            if name == DBusXMLParser.STATE_METHOD:
                self.state = DBusXMLParser.STATE_METHOD
                method = Method(
                    attrs["name"], h_type_implies_unix_fd=self._h_type_implies_unix_fd
                )
                self._cur_object.methods.append(method)
                self._cur_object = method
            elif name == DBusXMLParser.STATE_SIGNAL:
                self.state = DBusXMLParser.STATE_SIGNAL
                signal = Signal(attrs["name"])
                self._cur_object.signals.append(signal)
                self._cur_object = signal
            elif name == DBusXMLParser.STATE_PROPERTY:
                self.state = DBusXMLParser.STATE_PROPERTY
                prop = Property(attrs["name"], attrs["type"], attrs["access"])
                self._cur_object.properties.append(prop)
                self._cur_object = prop
            elif name == DBusXMLParser.STATE_ANNOTATION:
                self.state = DBusXMLParser.STATE_ANNOTATION
                anno = Annotation(attrs["name"], attrs["value"])
                self._cur_object.annotations.append(anno)
                self._cur_object = anno
            else:
                self.state = DBusXMLParser.STATE_IGNORED

            # assign docs, if any
            if "name" in attrs and self.doc_comment_last_symbol == attrs["name"]:
                self._cur_object.doc_string = self.doc_comment_body
                if "since" in self.doc_comment_params:
                    self._cur_object.since = self.doc_comment_params["since"].strip(
                    )

        elif self.state == DBusXMLParser.STATE_METHOD:
            if name == DBusXMLParser.STATE_ARG:
                self.state = DBusXMLParser.STATE_ARG
                arg_name = None
                if "name" in attrs:
                    arg_name = attrs["name"]
                arg = Arg(arg_name, attrs["type"])
                direction = attrs.get("direction", "in")
                if direction == "in":
                    self._cur_object.in_args.append(arg)
                elif direction == "out":
                    self._cur_object.out_args.append(arg)
                else:
                    raise ValueError(
                        'Invalid direction "{}"'.format(direction))
                self._cur_object = arg
            elif name == DBusXMLParser.STATE_ANNOTATION:
                self.state = DBusXMLParser.STATE_ANNOTATION
                anno = Annotation(attrs["name"], attrs["value"])
                self._cur_object.annotations.append(anno)
                self._cur_object = anno
            else:
                self.state = DBusXMLParser.STATE_IGNORED

            # assign docs, if any
            if self.doc_comment_last_symbol == old_cur_object.name:
                if "name" in attrs and attrs["name"] in self.doc_comment_params:
                    doc_string = self.doc_comment_params[attrs["name"]]
                    if doc_string is not None:
                        self._cur_object.doc_string = doc_string
                    if "since" in self.doc_comment_params:
                        self._cur_object.since = self.doc_comment_params[
                            "since"
                        ].strip()

        elif self.state == DBusXMLParser.STATE_SIGNAL:
            if name == DBusXMLParser.STATE_ARG:
                self.state = DBusXMLParser.STATE_ARG
                arg_name = None
                if "name" in attrs:
                    arg_name = attrs["name"]
                arg = Arg(arg_name, attrs["type"])
                self._cur_object.args.append(arg)
                self._cur_object = arg
            elif name == DBusXMLParser.STATE_ANNOTATION:
                self.state = DBusXMLParser.STATE_ANNOTATION
                anno = Annotation(attrs["name"], attrs["value"])
                self._cur_object.annotations.append(anno)
                self._cur_object = anno
            else:
                self.state = DBusXMLParser.STATE_IGNORED

            # assign docs, if any
            if self.doc_comment_last_symbol == old_cur_object.name:
                if "name" in attrs and attrs["name"] in self.doc_comment_params:
                    doc_string = self.doc_comment_params[attrs["name"]]
                    if doc_string is not None:
                        self._cur_object.doc_string = doc_string
                    if "since" in self.doc_comment_params:
                        self._cur_object.since = self.doc_comment_params[
                            "since"
                        ].strip()

        elif self.state == DBusXMLParser.STATE_PROPERTY:
            if name == DBusXMLParser.STATE_ANNOTATION:
                self.state = DBusXMLParser.STATE_ANNOTATION
                anno = Annotation(attrs["name"], attrs["value"])
                self._cur_object.annotations.append(anno)
                self._cur_object = anno
            else:
                self.state = DBusXMLParser.STATE_IGNORED

        elif self.state == DBusXMLParser.STATE_ARG:
            if name == DBusXMLParser.STATE_ANNOTATION:
                self.state = DBusXMLParser.STATE_ANNOTATION
                anno = Annotation(attrs["name"], attrs["value"])
                self._cur_object.annotations.append(anno)
                self._cur_object = anno
            else:
                self.state = DBusXMLParser.STATE_IGNORED

        elif self.state == DBusXMLParser.STATE_ANNOTATION:
            if name == DBusXMLParser.STATE_ANNOTATION:
                self.state = DBusXMLParser.STATE_ANNOTATION
                anno = Annotation(attrs["name"], attrs["value"])
                self._cur_object.annotations.append(anno)
                self._cur_object = anno
            else:
                self.state = DBusXMLParser.STATE_IGNORED

        else:
            raise ValueError(
                'Unhandled state "{}" while entering element with name "{}"'.format(
                    self.state, name
                )
            )

        self.state_stack.append(old_state)
        self._cur_object_stack.append(old_cur_object)

    def handle_end_element(self, name):
        self.state = self.state_stack.pop()
        self._cur_object = self._cur_object_stack.pop()


def parse_dbus_xml(xml_data):
    parser = DBusXMLParser(xml_data, True)
    return parser.parsed_interfaces
