// application/javascript;version=1.8
// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
// SPDX-FileCopyrightText: 2013 Giovanni Campagna

const Legacy = imports._legacy;
const {Gio, GjsPrivate, GLib, GObject} = imports.gi;
const {_createBuilderConnectFunc, _createClosure, _registerType} = imports._common;
const Gi = imports._gi;

let Gtk;
let TemplateBuilderScope;

function _init() {
    Gtk = this;

    Gtk.children = GObject.__gtkChildren__;
    Gtk.cssName = GObject.__gtkCssName__;
    Gtk.internalChildren = GObject.__gtkInternalChildren__;
    Gtk.template = GObject.__gtkTemplate__;

    let {GtkWidgetClass} = Legacy.defineGtkLegacyObjects(GObject, Gtk);
    Gtk.Widget.prototype.__metaclass__ = GtkWidgetClass;

    if (Gtk.Container && Gtk.Container.prototype.child_set_property) {
        Gtk.Container.prototype.child_set_property = function (child, property, value) {
            GjsPrivate.gtk_container_child_set_property(this, child, property, value);
        };
    }

    if (Gtk.CustomSorter) {
        Gtk.CustomSorter.new = GjsPrivate.gtk_custom_sorter_new;
        Gtk.CustomSorter.prototype.set_sort_func = function (sortFunc) {
            GjsPrivate.gtk_custom_sorter_set_sort_func(this, sortFunc);
        };
    }

    Gtk.Widget.prototype._init = function (params) {
        const klass = this.constructor;
        const wrapper = GObject.Object.prototype._init.call(this, params) ?? this;

        if (klass[Gtk.template]) {
            let children = klass[Gtk.children] ?? [];
            for (let child of children) {
                wrapper[child.replace(/-/g, '_')] =
                    wrapper.get_template_child(klass, child);
            }

            let internalChildren = klass[Gtk.internalChildren] ?? [];
            for (let child of internalChildren) {
                wrapper[`_${child.replace(/-/g, '_')}`] =
                    wrapper.get_template_child(klass, child);
            }
        }

        return wrapper;
    };

    Gtk.Widget._classInit = function (klass) {
        return GObject.Object._classInit(klass);
    };

    Object.defineProperty(Gtk.Widget, _registerType, {
        value: _registerWidgetType,
        writable: false,
        configurable: false,
        enumerable: false,
    });

    if (Gtk.Widget.prototype.get_first_child) {
        Gtk.Widget.prototype[Symbol.iterator] = function* () {
            for (let c = this.get_first_child(); c; c = c.get_next_sibling())
                yield c;
        };
    }

    // Everything after this is GTK4-only, for the Gtk.Builder JS implementation
    if (!Gtk.BuilderScope)
        return;

    TemplateBuilderScope = GObject.registerClass({
        Implements: [Gtk.BuilderScope],
    }, class extends GObject.Object {
        vfunc_create_closure(builder, handlerName, flags, connectObject) {
            const swapped = flags & Gtk.BuilderClosureFlags.SWAPPED;
            const thisArg = builder.get_current_object();
            return Gi.associateClosure(
                connectObject ?? thisArg,
                _createClosure(thisArg, handlerName, swapped, connectObject)
            );
        }
    });

    const NonTemplateBuilderScope = GObject.registerClass(class extends GObject.Object {
        static [GObject.GTypeName] = 'NonTemplateBuilderScope';
        static [GObject.interfaces] = [Gtk.BuilderScope];
        #callbacks;

        constructor(callbacks = {}) {
            super();
            this.#callbacks = callbacks;
        }

        vfunc_create_closure(builder, handlerName, flags, connectObject) {
            const swapped = flags & Gtk.BuilderClosureFlags.SWAPPED;
            const builderCurrentObject = builder.get_current_object();
            if (connectObject || builderCurrentObject || this.#callbacks instanceof GObject.Object) {
                const lifetimeObject = connectObject ?? builderCurrentObject ?? this.#callbacks;
                return Gi.associateClosure(lifetimeObject,
                    _createClosure(this.#callbacks, handlerName,
                        swapped, connectObject ?? builderCurrentObject));
            }
            return _createClosure(this.#callbacks, handlerName, swapped);
        }
    });

    class GtkJSBuilder extends Gtk.Builder {
        static [GObject.GTypeName] = 'GtkJSBuilder';
        static {
            GObject.registerClass(GtkJSBuilder);
        }

        /**
         * @param {object} [props] Construct properties
         * @param {string | Uint8Array} [props.data] XML interface description
         * @param {string} [props.filename] Local filename for XML interface
         * @param {string} [props.resource] Resource path for XML interface
         * @param {object} [props.callbacks] Object with callbacks to expose
         * @param {object} [props.objects] Object with other objects to expose
         * @param {Gtk.BuilderConstructParameters} [props.props] Other
         *   Gtk.Builder construct properties
         */
        constructor({data, filename, resource, callbacks, objects, ...props} = {}) {
            const countOfDataSources = [data, filename, resource]
            .filter(source => source !== undefined)
            .length;

            if (countOfDataSources === 0) {
                if (!callbacks && !objects) {
                    super(props);
                    return;  // default behaviour if no extra properties passed
                }
            } else if (countOfDataSources !== 1) {
                throw new Error('Pass at most one of data, filename, or resource');
            }
            if (props.scope)
                throw new Error("Don't pass a scope property when using JS Gtk.Builder features");

            const scope = new NonTemplateBuilderScope(callbacks);
            super({...props, scope});

            if (objects)
                this.exposeObjects(objects);

            if (data) {
                const str = typeof data === 'string' ? data : new TextDecoder().decode(data);
                this.add_from_string(str, -1);
            } else if (resource) {
                this.add_from_resource(resource);
            } else if (filename) {
                this.add_from_file(filename);
            }
        }

        // override for original get_objects() method that returns a Proxy which
        // accesses get_object() on property accesses, so that you can do
        // const {name1, name2} = builder.get_objects();
        // Note the builder instance is kept alive by the proxy!
        get_objects() {
            const builder = this;
            const proxyHandler = {
                get(objectsArray, id, receiver) {
                    if (Reflect.has(objectsArray, id))
                        return Reflect.get(objectsArray, id, receiver);
                    if (typeof id !== 'string')
                        return undefined;
                    const obj = builder.get_object(id);
                    objectsArray[id] = obj;
                    return obj;
                },
            };
            const objectsArray = super.get_objects();
            return new Proxy(objectsArray, proxyHandler);
        }

        // convenience method
        exposeObjects(objects) {
            for (const [name, object] of Object.entries(objects))
                this.expose_object(name, object);
        }
    }
    Gtk.Builder = GtkJSBuilder;
}

function _registerWidgetType(klass) {
    const template = klass[Gtk.template];
    const cssName = klass[Gtk.cssName];
    const children = klass[Gtk.children];
    const internalChildren = klass[Gtk.internalChildren];

    if (template) {
        klass.prototype._instance_init = function () {
            this.init_template();
        };
    }

    GObject.Object[_registerType](klass);

    if (cssName)
        Gtk.Widget.set_css_name.call(klass, cssName);

    if (template) {
        if (typeof template === 'string') {
            try {
                const uri = GLib.Uri.parse(template, GLib.UriFlags.NONE);
                const scheme = uri.get_scheme();

                if (scheme === 'resource') {
                    Gtk.Widget.set_template_from_resource.call(klass, uri.get_path());
                } else if (scheme === 'file') {
                    const file = Gio.File.new_for_uri(template);
                    const [, contents] = file.load_contents(null);
                    Gtk.Widget.set_template.call(klass, contents);
                } else {
                    throw new TypeError(`Invalid template URI: ${template}`);
                }
            } catch (err) {
                if (!(err instanceof GLib.UriError))
                    throw err;

                const contents = new TextEncoder().encode(template);
                Gtk.Widget.set_template.call(klass, contents);
            }
        } else {
            Gtk.Widget.set_template.call(klass, template);
        }

        if (TemplateBuilderScope)
            Gtk.Widget.set_template_scope.call(klass, new TemplateBuilderScope());
        else
            Gtk.Widget.set_connect_func.call(klass, _createBuilderConnectFunc(klass));
    }

    if (children) {
        children.forEach(child =>
            Gtk.Widget.bind_template_child_full.call(klass, child, false, 0));
    }

    if (internalChildren) {
        internalChildren.forEach(child =>
            Gtk.Widget.bind_template_child_full.call(klass, child, true, 0));
    }
}
