#!/usr/bin/env python
# jsonxpt.py - Generate json XPT typelib files from IDL.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

"""Generate a json XPT typelib for an IDL file"""

import itertools
import json

from xpidl import xpidl

# A map of xpidl.py types to xpt enum variants
TypeMap = {
    # builtins
    "boolean": "TD_BOOL",
    "void": "TD_VOID",
    "int8_t": "TD_INT8",
    "int16_t": "TD_INT16",
    "int32_t": "TD_INT32",
    "int64_t": "TD_INT64",
    "uint8_t": "TD_UINT8",
    "uint16_t": "TD_UINT16",
    "uint32_t": "TD_UINT32",
    "uint64_t": "TD_UINT64",
    "nsresult": "TD_UINT32",
    "float": "TD_FLOAT",
    "double": "TD_DOUBLE",
    "char": "TD_CHAR",
    "string": "TD_PSTRING",
    "wchar": "TD_WCHAR",
    "wstring": "TD_PWSTRING",
    "char16_t": "TD_UINT16",
    # special types
    "nsid": "TD_NSID",
    "astring": "TD_ASTRING",
    "utf8string": "TD_UTF8STRING",
    "cstring": "TD_CSTRING",
    "jsval": "TD_JSVAL",
    "promise": "TD_PROMISE",
}


def flags(*flags):
    return [flag for flag, cond in flags if cond]


def get_type(type, calltype, iid_is=None, size_is=None, needs_scriptable=None):
    while isinstance(type, xpidl.Typedef):
        type = type.realtype

    if isinstance(type, xpidl.Builtin):
        ret = {"tag": TypeMap[type.name]}
        if type.name in ["string", "wstring"] and size_is is not None:
            ret["tag"] += "_SIZE_IS"
            ret["size_is"] = size_is
        return ret

    if isinstance(type, xpidl.Array):
        # NB: For a Array<T> we pass down the iid_is to get the type of T.
        #     This allows Arrays of InterfaceIs types to work.
        return {
            "tag": "TD_ARRAY",
            "element": get_type(type.type, calltype, iid_is, None, needs_scriptable),
        }

    if isinstance(type, xpidl.LegacyArray):
        # NB: For a Legacy [array] T we pass down iid_is to get the type of T.
        #     This allows [array] of InterfaceIs types to work.
        return {
            "tag": "TD_LEGACY_ARRAY",
            "size_is": size_is,
            "element": get_type(type.type, calltype, iid_is, None, needs_scriptable),
        }

    if isinstance(type, xpidl.Interface) or isinstance(type, xpidl.Forward):
        if isinstance(needs_scriptable, set):
            needs_scriptable.add(type.name)
        return {
            "tag": "TD_INTERFACE_TYPE",
            "name": type.name,
        }

    if isinstance(type, xpidl.WebIDL):
        return {
            "tag": "TD_DOMOBJECT",
            "name": type.name,
            "native": type.native,
            "headerFile": type.headerFile,
        }

    if isinstance(type, xpidl.Native):
        if type.specialtype == "nsid" and type.isPtr(calltype):
            return {"tag": "TD_NSIDPTR"}
        elif type.specialtype:
            return {"tag": TypeMap[type.specialtype]}
        elif iid_is is not None:
            return {
                "tag": "TD_INTERFACE_IS_TYPE",
                "iid_is": iid_is,
            }
        else:
            return {"tag": "TD_VOID"}

    if isinstance(type, xpidl.CEnum):
        # As far as XPConnect is concerned, cenums are just unsigned integers.
        return {"tag": f"TD_UINT{type.width}"}

    raise Exception("Unknown type!")


def mk_param(type, in_=0, out=0, optional=0):
    return {
        "type": type,
        "flags": flags(
            ("in", in_),
            ("out", out),
            ("optional", optional),
        ),
    }


def mk_method(method, params, getter=0, setter=0, optargc=0, hasretval=0, symbol=0):
    return {
        "name": method.name,
        # NOTE: We don't include any return value information here, as we'll
        # never call the methods if they're marked notxpcom, and all xpcom
        # methods return the same type (nsresult).
        # XXX: If we ever use these files for other purposes than xptcodegen we
        # may want to write that info.
        "params": params,
        "flags": flags(
            ("getter", getter),
            ("setter", setter),
            ("hidden", not method.isScriptable()),
            ("optargc", optargc),
            ("jscontext", method.implicit_jscontext),
            ("hasretval", hasretval),
            ("symbol", method.symbol),
        ),
    }


def attr_param_idx(p, m, attr):
    attr_val = getattr(p, attr, None)
    if not attr_val:
        return None
    for i, param in enumerate(m.params):
        if param.name == attr_val:
            return i
    raise Exception(f"Need parameter named '{attr_val}' for attribute '{attr}'")


def build_interface(iface):
    if iface.namemap is None:
        raise Exception("Interface was not resolved.")

    assert iface.attributes.scriptable, (
        "Don't generate XPT info for non-scriptable interfaces"
    )

    # State used while building an interface
    consts = []
    methods = []

    # Interfaces referenced from scriptable members need to be [scriptable].
    needs_scriptable = set()

    def build_const(c):
        consts.append({
            "name": c.name,
            "type": get_type(c.basetype, ""),
            "value": c.getValue(),  # All of our consts are numbers
        })

    def build_cenum(b):
        for var in b.variants:
            consts.append({
                "name": var.name,
                "type": get_type(b, "in"),
                "value": var.value,
            })

    def build_method(m, needs_scriptable=None):
        params = []
        for p in m.params:
            params.append(
                mk_param(
                    get_type(
                        p.realtype,
                        p.paramtype,
                        iid_is=attr_param_idx(p, m, "iid_is"),
                        size_is=attr_param_idx(p, m, "size_is"),
                        needs_scriptable=needs_scriptable,
                    ),
                    in_=p.paramtype.count("in"),
                    out=p.paramtype.count("out"),
                    optional=p.optional,
                )
            )

        hasretval = len(m.params) > 0 and m.params[-1].retval
        if not m.notxpcom and m.realtype.name != "void":
            hasretval = True
            type = get_type(m.realtype, "out", needs_scriptable=needs_scriptable)
            params.append(mk_param(type, out=1))

        methods.append(
            mk_method(m, params, optargc=m.optional_argc, hasretval=hasretval)
        )

    def build_attr(a, needs_scriptable=None):
        assert a.realtype.name != "void"

        # Write the getter
        getter_params = []
        if not a.notxpcom:
            type = get_type(a.realtype, "out", needs_scriptable=needs_scriptable)
            getter_params.append(mk_param(type, out=1))

        methods.append(mk_method(a, getter_params, getter=1, hasretval=1))

        # And maybe the setter
        if not a.readonly:
            type = get_type(a.realtype, "in", needs_scriptable=needs_scriptable)
            methods.append(mk_method(a, [mk_param(type, in_=1)], setter=1))

    for member in iface.members:
        if isinstance(member, xpidl.ConstMember):
            build_const(member)
        elif isinstance(member, xpidl.Attribute):
            build_attr(member, member.isScriptable() and needs_scriptable)
        elif isinstance(member, xpidl.Method):
            build_method(member, member.isScriptable() and needs_scriptable)
        elif isinstance(member, xpidl.CEnum):
            build_cenum(member)
        elif isinstance(member, xpidl.CDATA):
            pass
        else:
            raise Exception(f"Unexpected interface member: {member}")

    for ref in set(needs_scriptable):
        p = iface.idl.getName(xpidl.TypeId(ref), None)
        if isinstance(p, xpidl.Interface):
            needs_scriptable.remove(ref)
            if not p.attributes.scriptable:
                raise Exception(
                    f"Scriptable member in {iface.name} references non-scriptable {ref}. "
                    "The interface must be marked as [scriptable], "
                    "or the referencing member with [noscript]."
                )

    return {
        "name": iface.name,
        "uuid": iface.attributes.uuid,
        "methods": methods,
        "consts": consts,
        "parent": iface.base,
        "needs_scriptable": sorted(needs_scriptable),
        "flags": flags(
            ("function", iface.attributes.function),
            ("builtinclass", iface.attributes.builtinclass),
            ("main_process_only", iface.attributes.main_process_scriptable_only),
        ),
    }


# These functions are the public interface of this module. They are very simple
# functions, but are exported so that if we need to do something more
# complex in them in the future we can.


def build_typelib(idl):
    """Given a parsed IDL file, generate and return the typelib"""
    return [
        build_interface(p)
        for p in idl.productions
        if p.kind == "interface" and p.attributes.scriptable
    ]


def link(typelibs):
    """Link a list of typelibs together into a single typelib"""
    linked = list(itertools.chain.from_iterable(typelibs))
    assert len(set(iface["name"] for iface in linked)) == len(linked), (
        "Multiple typelibs containing the same interface were linked together"
    )
    return linked


def write(typelib, fd):
    """Write typelib into fd"""
    json.dump(typelib, fd, indent=2, sort_keys=True)
