// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
// SPDX-FileCopyrightText: 2020 Philip Chimento <philip.chimento@gmail.com>

/* exported _checkAccessors, _createBuilderConnectFunc, _createClosure,
_registerType, _createWrappersForPlatformSpecificNamespace,
_defineDeprecatedWrapper */

// This is a helper module in which to put code that is common between the
// legacy GObject.Class system and the new GObject.registerClass system.

const {warnDeprecatedOncePerCallsite, RENAMED, PLATFORM_SPECIFIC_TYPELIB} = imports._print;

var _registerType = Symbol('GObject register type hook');

function _generateAccessors(pspec, propdesc, GObject) {
    const {name, flags} = pspec;
    const readable = flags & GObject.ParamFlags.READABLE;
    const writable = flags & GObject.ParamFlags.WRITABLE;

    if (!propdesc) {
        propdesc = {
            configurable: true,
            enumerable: true,
        };
    }

    if (readable && writable) {
        if (!propdesc.get && !propdesc.set) {
            const privateName = Symbol(`__autogeneratedAccessor__${name}`);
            const defaultValue = pspec.get_default_value();
            propdesc.get = function () {
                if (!(privateName in this))
                    this[privateName] = defaultValue;
                return this[privateName];
            };
            propdesc.set = function (value) {
                if (value !== this[privateName]) {
                    this[privateName] = value;
                    this.notify(name);
                }
            };
        } else if (!propdesc.get) {
            propdesc.get = function () {
                throw new Error(`setter defined without getter for property ${name}`);
            };
        } else if (!propdesc.set) {
            propdesc.set = function () {
                throw new Error(`getter defined without setter for property ${name}`);
            };
        }
    } else if (readable && !propdesc.get) {
        propdesc.get = function () {
            throw new Error(`missing getter for read-only property ${name}`);
        };
    } else if (writable && !propdesc.set) {
        propdesc.set = function () {
            throw new Error(`missing setter for write-only property ${name}`);
        };
    }

    return propdesc;
}

function _checkAccessors(proto, pspec, GObject) {
    const {name, flags} = pspec;
    if (flags & GObject.ParamFlags.CONSTRUCT_ONLY)
        return;

    const underscoreName = name.replace(/-/g, '_');
    const camelName = name.replace(/-([a-z])/g, match => match[1].toUpperCase());
    let propdesc = Object.getOwnPropertyDescriptor(proto, name);
    let dashPropdesc = propdesc, underscorePropdesc, camelPropdesc;
    const nameIsCompound = name.includes('-');
    if (nameIsCompound) {
        underscorePropdesc = Object.getOwnPropertyDescriptor(proto, underscoreName);
        camelPropdesc = Object.getOwnPropertyDescriptor(proto, camelName);
        if (!propdesc)
            propdesc = underscorePropdesc;
        if (!propdesc)
            propdesc = camelPropdesc;
    }

    const readable = flags & GObject.ParamFlags.READABLE;
    const writable = flags & GObject.ParamFlags.WRITABLE;
    if (!propdesc || (readable && !propdesc.get) || (writable && !propdesc.set))
        propdesc = _generateAccessors(pspec, propdesc, GObject);

    if (!dashPropdesc)
        Object.defineProperty(proto, name, propdesc);
    if (nameIsCompound) {
        if (!underscorePropdesc)
            Object.defineProperty(proto, underscoreName, propdesc);
        if (!camelPropdesc)
            Object.defineProperty(proto, camelName, propdesc);
    }
}

function _createClosure(thisArg, handlerName, swapped, connectObject) {
    connectObject ??= thisArg;

    if (swapped) {
        throw new Error('Unsupported template signal flag "swapped"');
    } else if (typeof thisArg[handlerName] === 'undefined') {
        throw new Error(`A handler called ${handlerName} was not ` +
            `defined on ${thisArg}`);
    }

    return thisArg[handlerName].bind(connectObject);
}

function _createBuilderConnectFunc(klass) {
    const {GObject} = imports.gi;
    return function (builder, obj, signalName, handlerName, connectObj, flags) {
        const objects = builder.get_objects();
        const thisObj = objects.find(o => o instanceof klass);
        const swapped = flags & GObject.ConnectFlags.SWAPPED;
        const closure = _createClosure(thisObj, handlerName, swapped, connectObj);

        if (flags & GObject.ConnectFlags.AFTER)
            obj.connect_after(signalName, closure);
        else
            obj.connect(signalName, closure);
    };
}

function _createWrappersForPlatformSpecificNamespace(namespace) {
    // Redefine namespace properties with platform-specific implementations to
    // be backward compatible with gi-repository 1.0, however when possible we
    // notify a deprecation warning, to ensure that the surrounding code is
    // updated.

    let platformNamespace;
    let platformName;
    const namespaceName = namespace.__name__;
    try {
        platformName = 'Unix';
        platformNamespace = imports.gi[`${namespaceName}${platformName}`];
    } catch {
        try {
            platformName = 'Win32';
            platformNamespace = imports.gi[`${namespaceName}${platformName}`];
        } catch {
            return;
        }
    }

    const platformNameLower = platformName.toLowerCase();
    Object.entries(Object.getOwnPropertyDescriptors(platformNamespace)).forEach(([prop, desc]) => {
        let genericProp = prop;

        const originalValue = platformNamespace[prop];
        const gtypeName = originalValue.$gtype?.name;
        if (gtypeName?.startsWith(`G${platformName}`))
            genericProp = `${platformName}${prop}`;
        else if (originalValue instanceof Function &&
            originalValue.name.startsWith(`g_${platformNameLower}_`))
            genericProp = `${platformNameLower}_${prop}`;
        else if (originalValue instanceof Object &&
                 (!gtypeName || gtypeName === 'void') &&
                 (!originalValue.name || originalValue.name.startsWith(
                     `${namespaceName}${platformName}_`)))
            genericProp = `${platformName}${prop}`;

        if (Object.hasOwn(namespace, genericProp)) {
            console.log(`${namespaceName} already contains property ${genericProp}`);
            namespace[genericProp] = originalValue;
            return;
        }

        _defineDeprecatedWrapperForDescriptor(
            namespace, platformNamespace, genericProp, prop, desc,
            PLATFORM_SPECIFIC_TYPELIB);
    });
}

function _defineDeprecatedWrapperForDescriptor(
    namespace, newNamespace, oldName, newName, desc, deprecationReason = RENAMED) {
    const namespaceName = namespace.__name__;
    if (Object.hasOwn(namespace, oldName)) {
        console.log(`${namespaceName} already contains property ${oldName}`);
        return;
    }

    Object.defineProperty(namespace, oldName, {
        enumerable: true,
        configurable: false,
        get() {
            warnDeprecatedOncePerCallsite(deprecationReason,
                `${namespaceName}.${oldName}`, `${newNamespace.__name__}.${newName}`);
            return desc.get?.() ?? desc.value;
        },
    });
}

function _defineDeprecatedWrapper(
    namespace, newNamespace, oldName, newName, deprecationReason = RENAMED) {
    _defineDeprecatedWrapperForDescriptor(
        namespace, newNamespace, oldName, newName,
        Object.getOwnPropertyDescriptor(namespace, newName), deprecationReason);
}
