# decompiler.py
#
# Copyright 2021 James Westman <james@jwestman.net>
#
# This file 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 3 of the
# License, or (at your option) any later version.
#
# This file 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 program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later

import typing as T
from collections import defaultdict
from dataclasses import dataclass
from enum import Enum

from . import formatter
from .errors import MultipleErrors
from .gir import *
from .tokenizer import tokenize
from .utils import Colors, TextEdit, escape_quote
from .xml_reader import Element, parse, parse_string

__all__ = ["decompile"]


_DECOMPILERS: dict[str, list] = defaultdict(list)
_CLOSING = {
    "{": "}",
    "[": "]",
}
_NAMESPACES = [
    ("GLib", "2.0"),
    ("GObject", "2.0"),
    ("Gio", "2.0"),
    ("Adw", "1"),
]


class LineType(Enum):
    NONE = 1
    STMT = 2
    BLOCK_START = 3
    BLOCK_END = 4


class DecompileCtx:
    def __init__(self, parent_gir: T.Optional[GirContext] = None) -> None:
        self.sub_decompiler = parent_gir is not None
        self._result: str = ""
        self.gir = parent_gir or GirContext()
        self._blocks_need_end: T.List[str] = []
        self._last_line_type: LineType = LineType.NONE
        self._obj_type_stack: list[T.Optional[GirType]] = []
        self._node_stack: list[Element] = []

        self.gir.add_namespace(get_namespace("Gtk", "4.0"))

    @property
    def result(self) -> str:
        imports = ""

        if not self.sub_decompiler:
            import_lines = sorted(
                [
                    f"using {ns} {namespace.version};"
                    for ns, namespace in self.gir.namespaces.items()
                    if ns != "Gtk"
                ]
            )
            imports += "\n".join(["using Gtk 4.0;", *import_lines])

        return formatter.format(imports + self._result)

    def type_by_cname(self, cname: str) -> T.Optional[GirType]:
        if type := self.gir.get_type_by_cname(cname):
            return type

        for ns, version in _NAMESPACES:
            try:
                namespace = get_namespace(ns, version)
                if type := namespace.get_type_by_cname(cname):
                    self.gir.add_namespace(namespace)
                    return type
            except:
                pass

        return None

    def start_block(self) -> None:
        self._blocks_need_end.append("")
        self._obj_type_stack.append(None)

    def end_block(self) -> None:
        if close := self._blocks_need_end.pop():
            self.print(close)
        self._obj_type_stack.pop()

    @property
    def current_obj_type(self) -> T.Optional[GirType]:
        return next((x for x in reversed(self._obj_type_stack) if x is not None), None)

    def push_obj_type(self, type: T.Optional[GirType]) -> None:
        self._obj_type_stack[-1] = type

    @property
    def current_node(self) -> T.Optional[Element]:
        if len(self._node_stack) == 0:
            return None
        else:
            return self._node_stack[-1]

    @property
    def parent_node(self) -> T.Optional[Element]:
        if len(self._node_stack) < 2:
            return None
        else:
            return self._node_stack[-2]

    @property
    def root_node(self) -> T.Optional[Element]:
        if len(self._node_stack) == 0:
            return None
        else:
            return self._node_stack[0]

    @property
    def template_class(self) -> T.Optional[str]:
        assert self.root_node is not None
        for child in self.root_node.children:
            if child.tag == "template":
                return child["class"]

        return None

    def find_object(self, id: str) -> T.Optional[Element]:
        assert self.root_node is not None
        for child in self.root_node.children:
            if child.tag == "template" and child["class"] == id:
                return child

        def find_in_children(node: Element) -> T.Optional[Element]:
            if node.tag in ["object", "menu"] and node["id"] == id:
                return node
            else:
                for child in node.children:
                    if result := find_in_children(child):
                        return result
            return None

        return find_in_children(self.root_node)

    def end_block_with(self, text: str) -> None:
        self._blocks_need_end[-1] = text

    def print(self, line: str, newline: bool = True) -> None:
        self._result += line

        if line.endswith("{") or line.endswith("["):
            if len(self._blocks_need_end):
                self._blocks_need_end[-1] = _CLOSING[line[-1]]

    # Converts a value from an XML element to a blueprint string
    # based on the given type. Returns a tuple of translator comments
    # (if any) and the decompiled syntax.
    def decompile_value(
        self,
        value: str,
        type: T.Optional[GirType],
        translatable: T.Optional[T.Tuple[str, str, str]] = None,
    ) -> T.Tuple[str, str]:
        def get_enum_name(value):
            for member in type.members.values():
                if (
                    member.nick == value
                    or member.c_ident == value
                    or str(member.value) == value
                ):
                    return member.name
            return value.replace("-", "_")

        def assignable_to(ns: str, cls: str):
            assert type is not None
            return type.assignable_to(self.gir.namespaces[ns].lookup_type(cls))

        if translatable is not None and truthy(translatable[0]):
            return decompile_translatable(value, *translatable)
        elif type is None:
            return "", f"{escape_quote(value)}"
        elif type.assignable_to(FloatType()):
            return "", str(value)
        elif type.assignable_to(BoolType()):
            val = truthy(value)
            return "", ("true" if val else "false")
        elif type.assignable_to(ArrayType(StringType())):
            if value == "":
                return "", "[]"
            else:
                items = ", ".join([escape_quote(x) for x in value.split("\n")])
                return "", f"[{items}]"
        elif (
            assignable_to("Gtk", "Gdk.Pixbuf")
            or assignable_to("Gtk", "Gdk.Texture")
            or assignable_to("Gtk", "Gdk.Paintable")
            or assignable_to("Gtk", "Gtk.ShortcutAction")
            or assignable_to("Gtk", "Gtk.ShortcutTrigger")
            or assignable_to("Gtk", "Gio.File")
        ):
            return "", escape_quote(value)
        elif value == self.template_class:
            return "", "template"
        elif type.assignable_to(
            self.gir.namespaces["Gtk"].lookup_type("GObject.Object")
        ) or isinstance(type, Interface):
            return "", ("null" if value == "" else value)
        elif isinstance(type, Bitfield):
            flags = [get_enum_name(flag) for flag in value.split("|")]
            return "", " | ".join(flags)
        elif isinstance(type, Enumeration):
            return "", get_enum_name(value)
        elif isinstance(type, TypeType):
            if t := self.type_by_cname(value):
                return "", f"typeof<{full_name(t)}>"
            else:
                return "", f"typeof<${value}>"
        else:
            return "", escape_quote(value)


def decompile_element(
    ctx: DecompileCtx, gir: T.Optional[GirContext], xml: Element
) -> None:
    try:
        decompilers = [d for d in _DECOMPILERS[xml.tag] if d._filter(ctx)]
        if len(decompilers) == 0:
            raise UnsupportedError(f"unsupported XML tag: <{xml.tag}>")

        decompiler = decompilers[0]

        if decompiler._element:
            args = [ctx, gir, xml]
            kwargs: T.Dict[str, T.Optional[str]] = {}
        else:
            args = [ctx, gir]
            kwargs = {canon(name): value for name, value in xml.attrs.items()}
            if decompiler._cdata:
                if len(xml.children):
                    kwargs["cdata"] = None
                else:
                    kwargs["cdata"] = xml.cdata

        ctx._node_stack.append(xml)
        ctx.start_block()

        try:
            gir = decompiler(*args, **kwargs)
        except TypeError as e:
            raise UnsupportedError(tag=xml.tag)

        if not decompiler._skip_children:
            for child in xml.children:
                decompile_element(ctx, gir, child)

        ctx.end_block()
        ctx._node_stack.pop()

    except UnsupportedError as e:
        raise e


def decompile_xml(xml: Element):
    from . import parser

    ctx = DecompileCtx()
    decompile_element(ctx, None, xml)

    tokens = tokenize(ctx.result)
    ast, errors, warnings = parser.parse(tokens)

    result = ctx.result
    if ast is not None:
        fixes = [*ast.autofix()]
        result = TextEdit.apply_edits(ctx.result, fixes)

    return ast, errors, warnings, result


def decompile(
    data: str,
) -> T.Tuple[T.Any, T.Optional[MultipleErrors], T.List[CompileError], str]:
    return decompile_xml(parse(data))


def decompile_string(data: str) -> str:
    ast, errors, warnings, result = decompile_xml(parse_string(data))
    return result


def canon(string: str) -> str:
    if string == "class":
        return "klass"
    else:
        return string.replace("-", "_").lower()


def truthy(string: str) -> bool:
    return string is not None and string.lower() in ["yes", "true", "t", "y", "1"]


def full_name(gir: GirType) -> str:
    return gir.name if gir.full_name.startswith("Gtk.") else gir.full_name


def lookup_by_cname(gir, cname: str) -> T.Optional[GirType]:
    if isinstance(gir, GirContext):
        return gir.get_type_by_cname(cname)
    else:
        return gir.get_containing(Repository).get_type_by_cname(cname)


def decompiler(
    tag,
    cdata=False,
    parent_type: T.Optional[str] = None,
    parent_tag: T.Optional[str] = None,
    skip_children=False,
    element=False,
):
    def decorator(func):
        func._cdata = cdata
        func._skip_children = skip_children
        func._element = element

        def filter(ctx):
            if parent_type is not None:
                if (
                    ctx.current_obj_type is None
                    or ctx.current_obj_type.full_name != parent_type
                ):
                    return False

            if parent_tag is not None:
                if not any(x.tag == parent_tag for x in ctx._node_stack):
                    return False

            return True

        func._filter = filter

        _DECOMPILERS[tag].append(func)
        return func

    return decorator


@decompiler("interface")
def decompile_interface(ctx, gir, domain=None):
    if domain is not None:
        ctx.print(f"translation-domain {escape_quote(domain)};")
    return gir


@decompiler("requires")
def decompile_requires(ctx, gir, lib=None, version=None):
    return gir


@decompiler("placeholder")
def decompile_placeholder(ctx, gir):
    pass


def decompile_translatable(
    string: str,
    translatable: T.Optional[str],
    context: T.Optional[str],
    comments: T.Optional[str],
) -> T.Tuple[str, str]:
    if translatable is not None and truthy(translatable):
        if comments is None:
            comments = ""
        else:
            comments = comments.replace("/*", " ").replace("*/", " ")
            comments = f"\n/* Translators: {comments} */"

        if context is not None:
            return comments, f"C_({escape_quote(context)}, {escape_quote(string)})"
        else:
            return comments, f"_({escape_quote(string)})"
    else:
        return "", f"{escape_quote(string)}"


@decompiler("property", cdata=True)
def decompile_property(
    ctx: DecompileCtx,
    gir,
    name,
    cdata,
    bind_source=None,
    bind_property=None,
    bind_flags=None,
    translatable="false",
    comments=None,
    context=None,
):
    name = name.replace("_", "-")
    if cdata is None:
        ctx.print(f"{name}: ")
        ctx.end_block_with(";")
    elif bind_source:
        flags = ""
        bind_flags = bind_flags or []
        if "sync-create" not in bind_flags:
            flags += " no-sync-create"
        if "invert-boolean" in bind_flags:
            flags += " inverted"
        if "bidirectional" in bind_flags:
            flags += " bidirectional"

        if bind_source == ctx.template_class:
            bind_source = "template"

        ctx.print(f"{name}: bind {bind_source}.{bind_property}{flags};")
    elif truthy(translatable):
        comments, translatable = decompile_translatable(
            cdata, translatable, context, comments
        )
        if comments is not None:
            ctx.print(comments)
        ctx.print(f"{name}: {translatable};")
    elif gir is None or gir.properties.get(name) is None:
        ctx.print(f"{name}: {escape_quote(cdata)};")
    elif (
        gir.assignable_to(ctx.gir.get_class("BuilderListItemFactory", "Gtk"))
        and name == "bytes"
    ):
        sub_ctx = DecompileCtx(ctx.gir)

        xml = parse_string(cdata)
        decompile_element(sub_ctx, None, xml)

        ctx.print(sub_ctx.result)
    else:
        _, string = ctx.decompile_value(cdata, gir.properties.get(name).type)
        ctx.print(f"{name}: {string};")
    return gir


@decompiler("attribute", cdata=True)
def decompile_attribute(
    ctx, gir, name, cdata, translatable="false", comments=None, context=None
):
    decompile_property(
        ctx,
        gir,
        name,
        cdata,
        translatable=translatable,
        comments=comments,
        context=context,
    )


@decompiler("attributes")
def decompile_attributes(ctx, gir):
    ctx.print("attributes {")


@dataclass
class UnsupportedError(Exception):
    message: str = "unsupported feature"
    tag: T.Optional[str] = None

    def print(self, filename: str):
        print(f"\n{Colors.RED}{Colors.BOLD}error: {self.message}{Colors.CLEAR}")
        print(f"in {Colors.UNDERLINE}{filename}{Colors.NO_UNDERLINE}")
        if self.tag:
            print(f"in tag {Colors.BLUE}{self.tag}{Colors.CLEAR}")
        print(
            f"""{Colors.FAINT}The compiler might support this feature, but the porting tool does not. You
probably need to port this file manually.{Colors.CLEAR}\n"""
        )
