/* -*- c-file-style: "ruby"; indent-tabs-mode: nil -*- */
/*
 *  Copyright (C) 2015-2023  Ruby-GNOME Project Team
 *
 *  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, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *  MA  02110-1301  USA
 */

#include "rbgprivate.h"

#define RG_TARGET_NAMESPACE cVariant

#define _SELF(s) (RTYPEDDATA_DATA(s))

static VALUE RG_TARGET_NAMESPACE;

VALUE
rbg_variant_to_ruby(GVariant *variant)
{
    const GVariantType *type;

    if (!variant) {
        return Qnil;
    }

    type = g_variant_get_type(variant);

    if (g_variant_type_equal(type, G_VARIANT_TYPE_BOOLEAN)) {
        return CBOOL2RVAL(g_variant_get_boolean(variant));
    } else if (g_variant_type_equal(type, G_VARIANT_TYPE_BYTE)) {
        return UINT2NUM(g_variant_get_byte(variant));
    } else if (g_variant_type_equal(type, G_VARIANT_TYPE_INT16)) {
        return INT2NUM(g_variant_get_int16(variant));
    } else if (g_variant_type_equal(type, G_VARIANT_TYPE_UINT16)) {
        return UINT2NUM(g_variant_get_uint16(variant));
    } else if (g_variant_type_equal(type, G_VARIANT_TYPE_INT32)) {
        return INT2NUM(g_variant_get_int32(variant));
    } else if (g_variant_type_equal(type, G_VARIANT_TYPE_UINT32)) {
        return UINT2NUM(g_variant_get_uint32(variant));
    } else if (g_variant_type_equal(type, G_VARIANT_TYPE_INT64)) {
        return rbglib_int64_to_num(g_variant_get_int64(variant));
    } else if (g_variant_type_equal(type, G_VARIANT_TYPE_UINT64)) {
        return rbglib_uint64_to_num(g_variant_get_uint64(variant));
    } else if (g_variant_type_equal(type, G_VARIANT_TYPE_DOUBLE)) {
        return rb_float_new(g_variant_get_double(variant));
    } else if (g_variant_type_equal(type, G_VARIANT_TYPE_STRING) ||
               g_variant_type_equal(type, G_VARIANT_TYPE_OBJECT_PATH) ||
               g_variant_type_equal(type, G_VARIANT_TYPE_SIGNATURE)) {
        const gchar *string;
        gsize string_length;
        string = g_variant_get_string(variant, &string_length);
        return CSTR2RVAL_LEN(string, string_length);
    } else if (g_variant_type_equal(type, G_VARIANT_TYPE_HANDLE)) {
        return INT2NUM(g_variant_get_handle(variant));
    } else if (g_variant_type_equal(type, G_VARIANT_TYPE_VARIANT)) {
        GVariant *val = g_variant_get_variant(variant);
        VALUE value = rbg_variant_to_ruby(val);
        g_variant_unref(val);
        return value;
    } else if (g_variant_type_is_array(type) ||
               g_variant_type_is_tuple(type)) {
        gsize i, len = g_variant_n_children(variant);
        VALUE ary = rb_ary_new_capa(len);
        for (i = 0; i < len; i++) {
            GVariant *val = g_variant_get_child_value(variant, i);
            rb_ary_store(ary, i, rbg_variant_to_ruby(val));
            g_variant_unref(val);
        }
        return ary;
    } else {
        rb_raise(rb_eNotImpError,
                 "TODO: GVariant(%.*s) -> Ruby",
                 (int)g_variant_type_get_string_length(type),
                 g_variant_type_peek_string(type));
    }

    return Qnil;
}

GVariant *
rbg_variant_from_ruby(VALUE rb_variant)
{
    if (NIL_P(rb_variant)) {
        return NULL;
    } else {
        return _SELF(rb_variant);
    }
}

static void
rg_variant_free(gpointer object)
{
    GVariant *variant = object;

    g_variant_unref(variant);
}

static const rb_data_type_t rbg_variant_type = {
    "GLib::Variant",
    {
        NULL,
        rg_variant_free,
    },
    NULL,
    NULL,
    RUBY_TYPED_FREE_IMMEDIATELY |
    RUBY_TYPED_WB_PROTECTED |
    RUBY_TYPED_FROZEN_SHAREABLE,
};

static VALUE
rg_variant_allocate(VALUE klass)
{
    return TypedData_Wrap_Struct(klass, &rbg_variant_type, NULL);
}

static VALUE
rg_s_parse(int argc, VALUE *argv, VALUE klass)
{
    VALUE rb_text;
    VALUE rb_variant_type;
    rb_scan_args(argc, argv, "11", &rb_text, &rb_variant_type);
    const gchar *text = RVAL2CSTR(rb_text);
    const gchar *limit = text + RSTRING_LEN(rb_text);
    const GVariantType *variant_type = NULL;
    if (!NIL_P(rb_variant_type)) {
        variant_type = RVAL2GVARIANTTYPE(rb_variant_type);
    }
    GError *error = NULL;
    GVariant *variant = g_variant_parse(variant_type,
                                        text,
                                        limit,
                                        NULL,
                                        &error);
    if (error) {
        GError *detailed_error = NULL;
        gchar *context = g_variant_parse_error_print_context(error, text);
        g_set_error_literal(&detailed_error,
                            error->domain,
                            error->code,
                            context);
        g_free(context);
        g_error_free(error);
        RG_RAISE_ERROR(detailed_error);
    }
    return TypedData_Wrap_Struct(klass, &rbg_variant_type, variant);
}

static GVariant *
rg_ruby_to_variant(VALUE rb_value, VALUE rb_variant_type)
{
    const GVariantType *variant_type;

    if (NIL_P(rb_variant_type)) {
        switch (rb_type(rb_value)) {
          case RUBY_T_TRUE:
          case RUBY_T_FALSE:
            variant_type = G_VARIANT_TYPE_BOOLEAN;
            break;
          case RUBY_T_FIXNUM:
            variant_type = G_VARIANT_TYPE_INT64;
            break;
          case RUBY_T_FLOAT:
            variant_type = G_VARIANT_TYPE_DOUBLE;
            break;
          case RUBY_T_STRING:
            variant_type = G_VARIANT_TYPE_STRING;
            break;
          case RUBY_T_ARRAY:
            variant_type = G_VARIANT_TYPE_ARRAY;
            break;
          default:
            rb_raise(rb_eNotImpError,
                     "TODO: Ruby -> GVariantType: %s",
                     RBG_INSPECT(rb_value));
            break;
        }
    } else {
        variant_type = RVAL2GVARIANTTYPE(rb_variant_type);
    }

    if (g_variant_type_equal(variant_type, G_VARIANT_TYPE_BOOLEAN)) {
        return g_variant_new_boolean(RVAL2CBOOL(rb_value));
    } else if (g_variant_type_equal(variant_type, G_VARIANT_TYPE_BYTE)) {
        return g_variant_new_byte(NUM2UINT(rb_value));
    } else if (g_variant_type_equal(variant_type, G_VARIANT_TYPE_INT16)) {
        return g_variant_new_int16(NUM2INT(rb_value));
    } else if (g_variant_type_equal(variant_type, G_VARIANT_TYPE_UINT16)) {
        return g_variant_new_uint16(NUM2UINT(rb_value));
    } else if (g_variant_type_equal(variant_type, G_VARIANT_TYPE_INT32)) {
        return g_variant_new_int32(NUM2INT(rb_value));
    } else if (g_variant_type_equal(variant_type, G_VARIANT_TYPE_UINT32)) {
        return g_variant_new_uint32(NUM2UINT(rb_value));
    } else if (g_variant_type_equal(variant_type, G_VARIANT_TYPE_INT64)) {
        return g_variant_new_int64(NUM2LONG(rb_value));
    } else if (g_variant_type_equal(variant_type, G_VARIANT_TYPE_UINT64)) {
        return g_variant_new_uint64(NUM2ULONG(rb_value));
    } else if (g_variant_type_equal(variant_type, G_VARIANT_TYPE_HANDLE)) {
        return g_variant_new_handle(NUM2INT(rb_value));
    } else if (g_variant_type_equal(variant_type, G_VARIANT_TYPE_DOUBLE)) {
        return g_variant_new_double(NUM2DBL(rb_value));
    } else if (g_variant_type_equal(variant_type, G_VARIANT_TYPE_STRING)) {
        return g_variant_new_string(RVAL2CSTR_ACCEPT_NIL(rb_value));
    } else if (g_variant_type_equal(variant_type, G_VARIANT_TYPE_OBJECT_PATH)) {
        return g_variant_new_object_path(RVAL2CSTR_ACCEPT_NIL(rb_value));
    } else if (g_variant_type_equal(variant_type, G_VARIANT_TYPE_SIGNATURE)) {
        return g_variant_new_signature(RVAL2CSTR_ACCEPT_NIL(rb_value));
    } else if (g_variant_type_equal(variant_type, G_VARIANT_TYPE_VARIANT)) {
        return g_variant_new_variant(rbg_variant_from_ruby(rb_value));
    } else if (g_variant_type_equal(variant_type,
                                    G_VARIANT_TYPE_STRING_ARRAY)) {
        const gchar **strings;
        gssize length;
        if (NIL_P(rb_value)) {
            strings = NULL;
            length = 0;
        } else {
            gssize i;

            length = RARRAY_LEN(rb_value);
            strings = ALLOCA_N(const gchar *, length);
            for (i = 0; i < length; i++) {
                VALUE rb_string = RARRAY_PTR(rb_value)[i];
                strings[i] = RVAL2CSTR_ACCEPT_NIL(rb_string);
            }
        }
        return g_variant_new_strv(strings, length);
    } else if (g_variant_type_equal(variant_type,
                                    G_VARIANT_TYPE_OBJECT_PATH_ARRAY)) {
        const gchar **paths;
        gssize length;
        if (NIL_P(rb_value)) {
            paths = NULL;
            length = 0;
        } else {
            gssize i;

            length = RARRAY_LEN(rb_value);
            paths = ALLOCA_N(const gchar *, length);
            for (i = 0; i < length; i++) {
                VALUE rb_path = RARRAY_PTR(rb_value)[i];
                paths[i] = RVAL2CSTR_ACCEPT_NIL(rb_path);
            }
        }
        return g_variant_new_objv(paths, length);
    } else if (g_variant_type_equal(variant_type, G_VARIANT_TYPE_ARRAY)) {
        int i;
        GVariantBuilder builder;
        g_variant_builder_init(&builder, G_VARIANT_TYPE_ARRAY);
        for (i = 0; i < RARRAY_LEN(rb_value); i++) {
            GVariant *val = rg_ruby_to_variant(rb_ary_entry(rb_value, i), Qnil);
            g_variant_builder_add_value(&builder, val);
        }
        return g_variant_builder_end(&builder);
    } else if (g_variant_type_equal(variant_type, G_VARIANT_TYPE_BYTESTRING)) {
        return g_variant_new_bytestring(RVAL2CSTR_RAW_ACCEPT_NIL(rb_value));
    } else if (g_variant_type_equal(variant_type,
                                    G_VARIANT_TYPE_BYTESTRING_ARRAY)) {
        const gchar **strings;
        gssize length;
        if (NIL_P(rb_value)) {
            strings = NULL;
            length = 0;
        } else {
            gssize i;

            length = RARRAY_LEN(rb_value);
            strings = ALLOCA_N(const gchar *, length);
            for (i = 0; i < length; i++) {
                VALUE rb_string = RARRAY_PTR(rb_value)[i];
                strings[i] = RVAL2CSTR_RAW_ACCEPT_NIL(rb_string);
            }
        }
        return g_variant_new_bytestring_array(strings, length);
    } else {
        rb_raise(rb_eNotImpError,
                 "TODO: Ruby -> GVariant(%.*s): %s",
                 (int)g_variant_type_get_string_length(variant_type),
                 g_variant_type_peek_string(variant_type),
                 RBG_INSPECT(rb_value));
    }
}

static VALUE
rg_initialize(int argc, VALUE *argv, VALUE self)
{
    GVariant *variant;
    VALUE rb_value;
    VALUE rb_variant_type;

    rb_scan_args(argc, argv, "11", &rb_value, &rb_variant_type);
    variant = rg_ruby_to_variant(rb_value, rb_variant_type);
    g_variant_ref_sink(variant);
    RTYPEDDATA_DATA(self) = variant;

    return Qnil;
}

static VALUE
rg_value(VALUE self)
{
    GVariant *variant;

    variant = _SELF(self);

    return rbg_variant_to_ruby(variant);
}

static VALUE
rg_type(VALUE self)
{
    GVariant *variant;

    variant = _SELF(self);

    return GVARIANTTYPE2RVAL((GVariantType *)g_variant_get_type(variant));
}

static VALUE
rg_to_s(int argc, VALUE *argv, VALUE self)
{
    VALUE options = Qnil;
    gboolean type_annotate = FALSE;
    rb_scan_args(argc, argv, ":", &options);
    if (!NIL_P(options)) {
        ID keywords[1];
        VALUE values[1];
        keywords[0] = rb_intern("type_annotate");
        rb_get_kwargs(options, keywords, 0, 1, values);
        if (values[0] != Qundef) {
            type_annotate = CBOOL2RVAL(values[0]);
        }
    }

    GVariant *variant = _SELF(self);
    gchar *string = g_variant_print(variant, type_annotate);
    return CSTR2RVAL_FREE(string);
}

void
Init_glib_variant(void)
{
    RG_TARGET_NAMESPACE = G_DEF_CLASS(G_TYPE_VARIANT, "Variant", rbg_mGLib());

    rb_define_alloc_func(RG_TARGET_NAMESPACE, rg_variant_allocate);

    RG_DEF_SMETHOD(parse, -1);

    RG_DEF_METHOD(initialize, -1);
    RG_DEF_METHOD(value, 0);
    RG_DEF_METHOD(type, 0);
    RG_DEF_METHOD(to_s, -1);

    G_DEF_ERROR(G_VARIANT_PARSE_ERROR,
                "VariantParseError",
                rbg_mGLib(),
                rb_eRuntimeError,
                G_TYPE_VARIANT_PARSE_ERROR);
}
