/*
 *  $Id: gl-material.c 29576 2026-03-05 18:45:19Z yeti-dn $
 *  Copyright (C) 2005-2025 David Necas (Yeti), Petr Klapetek.
 *  E-mail: yeti@gwyddion.net, klapetek@gwyddion.net.
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  This program 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 General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with this program; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include <string.h>
#include <glib/gi18n-lib.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"
#include "libgwyddion/utils.h"
#include "libgwyddion/gl-material.h"
#include "libgwyddion/serializable-boxed.h"
#include "libgwyddion/serializable-utils.h"

#include "libgwyddion/internal.h"

G_STATIC_ASSERT(sizeof(GwyRGBA) == 4*sizeof(gdouble));

#define TYPE_NAME "GwyGLMaterial"

enum {
    ITEM_NAME,
    ITEM_AMBIENT,
    ITEM_DIFFUSE,
    ITEM_SPECULAR,
    ITEM_EMISSION,
    ITEM_SHININESS,
    NUM_ITEMS
};

struct _GwyGLMaterialPrivate {
    GwyRGBA ambient;
    GwyRGBA diffuse;
    GwyRGBA specular;
    GwyRGBA emission;
    gdouble shininess;
};

static void             serializable_init      (GwySerializableInterface *iface);
static void             serializable_itemize   (GwySerializable *serializable,
                                                GwySerializableGroup *group);
static gboolean         serializable_construct (GwySerializable *serializable,
                                                GwySerializableGroup *group,
                                                GwyErrorList **error_list);
static GwySerializable* serializable_copy      (GwySerializable *serializable);
static void             serializable_assign    (GwySerializable *destination,
                                                GwySerializable *source);
static gpointer         resource_copy          (gpointer);
static void             reset_data             (GwyGLMaterial *glmaterial);
static void             data_changed           (GwyGLMaterial *glmaterial);
static GwyGLMaterial*   create_gl_material     (const gchar *name,
                                                const GwyRGBA *ambient,
                                                const GwyRGBA *diffuse,
                                                const GwyRGBA *specular,
                                                const GwyRGBA *emission,
                                                gdouble shininess,
                                                gboolean is_const);
static void             resource_dump          (GwyResource *resource,
                                                GString *str);
static GwyResource*     resource_parse         (const gchar *text);
static void             resource_setup_builtins(GwyInventory *inventory);

static GwyResourceClass *parent_class = NULL;
static GwyInventory *inventory = NULL;

static const GwyRGBA null_color = { 0, 0, 0, 0 };

static GwySerializableItem serializable_items[NUM_ITEMS] = {
    { .name = "name",      .ctype = GWY_SERIALIZABLE_STRING, },
    { .name = "ambient",   .ctype = GWY_SERIALIZABLE_BOXED,  },
    { .name = "diffuse",   .ctype = GWY_SERIALIZABLE_BOXED,  },
    { .name = "specular",  .ctype = GWY_SERIALIZABLE_BOXED,  },
    { .name = "emission",  .ctype = GWY_SERIALIZABLE_BOXED,  },
    { .name = "shininess", .ctype = GWY_SERIALIZABLE_DOUBLE, },
};

G_DEFINE_TYPE_WITH_CODE(GwyGLMaterial, gwy_gl_material, GWY_TYPE_RESOURCE,
                        G_ADD_PRIVATE(GwyGLMaterial)
                        G_IMPLEMENT_INTERFACE(GWY_TYPE_SERIALIZABLE, serializable_init))

static void
serializable_init(GwySerializableInterface *iface)
{
    iface->itemize   = serializable_itemize;
    iface->construct = serializable_construct;
    iface->copy      = serializable_copy;
    iface->assign    = serializable_assign;

    GType rgbatype = GWY_TYPE_RGBA;
    serializable_items[ITEM_AMBIENT].aux.boxed_type = rgbatype;
    serializable_items[ITEM_DIFFUSE].aux.boxed_type = rgbatype;
    serializable_items[ITEM_SPECULAR].aux.boxed_type = rgbatype;
    serializable_items[ITEM_EMISSION].aux.boxed_type = rgbatype;

    serializable_items[ITEM_SHININESS].aux.pspec = g_param_spec_double(serializable_items[ITEM_SHININESS].name,
                                                                       NULL, NULL,
                                                                       0.0, 1.0, 0.0,
                                                                       G_PARAM_STATIC_STRINGS);
}

static void
gwy_gl_material_class_init(GwyGLMaterialClass *klass)
{
    GwyResourceClass *res_class = GWY_RESOURCE_CLASS(klass);

    parent_class = gwy_gl_material_parent_class;

    res_class->item_type = *gwy_resource_class_get_item_type(parent_class);
    res_class->item_type.type = G_TYPE_FROM_CLASS(klass);
    res_class->item_type.copy = resource_copy;

    res_class->name = "glmaterials";
    res_class->inventory = inventory = gwy_inventory_new(&res_class->item_type);
    gwy_inventory_set_default_item_name(inventory, GWY_GL_MATERIAL_DEFAULT);
    res_class->setup_builtins = resource_setup_builtins;
    res_class->dump = resource_dump;
    res_class->parse = resource_parse;
}

static void
gwy_gl_material_init(GwyGLMaterial *glmaterial)
{
    glmaterial->priv = gwy_gl_material_get_instance_private(glmaterial);
    reset_data(glmaterial);
}

/**
 * fix_rgba:
 * @color: A color.
 *
 * Fixes color components to range 0..1.
 *
 * Returns: The fixed color.
 **/
static inline gboolean
fix_rgba(GwyRGBA *color, gboolean noisy)
{
    GwyRGBA rgba;

    rgba.r = gwy_clamp(color->r, 0.0, 1.0);
    rgba.g = gwy_clamp(color->g, 0.0, 1.0);
    rgba.b = gwy_clamp(color->b, 0.0, 1.0);
    rgba.a = gwy_clamp(color->a, 0.0, 1.0);
    if (!gwy_rgba_equal(color, &rgba)) {
        if (noisy)
            g_warning("Color component outside [0,1] range.");
        *color = rgba;
        return FALSE;
    }

    return TRUE;
}

static inline gboolean
set_rgba_validated(const GwyRGBA *src, GwyRGBA *dest)
{
    GwyRGBA color = *src;
    fix_rgba(&color, TRUE);
    if (gwy_rgba_equal(&color, dest))
        return FALSE;
    *dest = color;
    return TRUE;
}

/**
 * gwy_gl_material_get_ambient:
 * @glmaterial: A GL material.
 *
 * Gets the ambient reflectance of a GL material.
 *
 * Returns: Ambient reflectance (owned by GL material, must not be modified nor freed).
 **/
const GwyRGBA*
gwy_gl_material_get_ambient(GwyGLMaterial *glmaterial)
{
    g_return_val_if_fail(GWY_IS_GL_MATERIAL(glmaterial), &null_color);
    return &glmaterial->priv->ambient;
}

/**
 * gwy_gl_material_set_ambient:
 * @glmaterial: A GL material.
 * @ambient: Ambient reflectance.
 *
 * Sets the ambient reflectance of a GL material.
 **/
void
gwy_gl_material_set_ambient(GwyGLMaterial *glmaterial,
                            const GwyRGBA *ambient)
{
    g_return_if_fail(GWY_IS_GL_MATERIAL(glmaterial));
    g_return_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(glmaterial)));
    if (set_rgba_validated(ambient, &glmaterial->priv->ambient))
        data_changed(glmaterial);
}

/**
 * gwy_gl_material_get_diffuse:
 * @glmaterial: A GL material.
 *
 * Gets the diffuse reflectance of a GL material.
 *
 * Returns: Diffuse reflectance (owned by GL material, must not be modified nor freed).
 **/
const GwyRGBA*
gwy_gl_material_get_diffuse(GwyGLMaterial *glmaterial)
{
    g_return_val_if_fail(GWY_IS_GL_MATERIAL(glmaterial), &null_color);
    return &glmaterial->priv->diffuse;
}

/**
 * gwy_gl_material_set_diffuse:
 * @glmaterial: A GL material.
 * @diffuse: Diffuse reflectance.
 *
 * Sets the diffuse reflectance of a GL material.
 **/
void
gwy_gl_material_set_diffuse(GwyGLMaterial *glmaterial,
                            const GwyRGBA *diffuse)
{
    g_return_if_fail(GWY_IS_GL_MATERIAL(glmaterial));
    g_return_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(glmaterial)));
    if (set_rgba_validated(diffuse, &glmaterial->priv->diffuse))
        data_changed(glmaterial);
}

/**
 * gwy_gl_material_get_specular:
 * @glmaterial: A GL material.
 *
 * Gets the specular reflectance of a GL material.
 *
 * Returns: Specular reflectance (owned by GL material, must not be modified nor freed).
 **/
const GwyRGBA*
gwy_gl_material_get_specular(GwyGLMaterial *glmaterial)
{
    g_return_val_if_fail(GWY_IS_GL_MATERIAL(glmaterial), &null_color);
    return &glmaterial->priv->specular;
}

/**
 * gwy_gl_material_set_specular:
 * @glmaterial: A GL material.
 * @specular: Specular reflectance.
 *
 * Sets the specular reflectance of a GL material.
 **/
void
gwy_gl_material_set_specular(GwyGLMaterial *glmaterial,
                             const GwyRGBA *specular)
{
    g_return_if_fail(GWY_IS_GL_MATERIAL(glmaterial));
    g_return_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(glmaterial)));
    if (set_rgba_validated(specular, &glmaterial->priv->specular))
        data_changed(glmaterial);
}

/**
 * gwy_gl_material_get_emission:
 * @glmaterial: A GL material.
 *
 * Gets the emission component of a GL material.
 *
 * Returns: Emission component (owned by GL material, must not be modified nor freed).
 **/
const GwyRGBA*
gwy_gl_material_get_emission(GwyGLMaterial *glmaterial)
{
    g_return_val_if_fail(GWY_IS_GL_MATERIAL(glmaterial), &null_color);
    return &glmaterial->priv->emission;
}

/**
 * gwy_gl_material_set_emission:
 * @glmaterial: A GL material.
 * @emission: Emission component.
 *
 * Sets the emission component of a GL material.
 **/
void
gwy_gl_material_set_emission(GwyGLMaterial *glmaterial,
                             const GwyRGBA *emission)
{
    g_return_if_fail(GWY_IS_GL_MATERIAL(glmaterial));
    g_return_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(glmaterial)));
    if (set_rgba_validated(emission, &glmaterial->priv->emission))
        data_changed(glmaterial);
}

/**
 * gwy_gl_material_get_shininess:
 * @glmaterial: A GL material.
 *
 * Gets the shininess value of a GL material.
 *
 * Returns: The shininess value as a value in interval [0,1].
 **/
gdouble
gwy_gl_material_get_shininess(GwyGLMaterial *glmaterial)
{
    g_return_val_if_fail(GWY_IS_GL_MATERIAL(glmaterial), 0.0);
    return glmaterial->priv->shininess;
}

/**
 * gwy_gl_material_set_shininess:
 * @glmaterial: A GL material.
 * @shininess: Shinniness value in interval [0,1].
 *
 * Sets the shininess value of a GL material.
 **/
void
gwy_gl_material_set_shininess(GwyGLMaterial *glmaterial,
                              gdouble shininess)
{
    g_return_if_fail(GWY_IS_GL_MATERIAL(glmaterial));
    g_return_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(glmaterial)));
    GwyGLMaterialPrivate *priv = glmaterial->priv;
    shininess = gwy_clamp(shininess, 0.0, 1.0);
    if (shininess != priv->shininess) {
        priv->shininess = shininess;
        data_changed(glmaterial);
    }
}

/**
 * gwy_gl_material_reset:
 * @glmaterial: A GL material.
 *
 * Resets a GL material to default values.
 **/
void
gwy_gl_material_reset(GwyGLMaterial *glmaterial)
{
    g_return_if_fail(GWY_IS_GL_MATERIAL(glmaterial));
    g_return_if_fail(gwy_resource_is_modifiable(GWY_RESOURCE(glmaterial)));
    reset_data(glmaterial);
    data_changed(glmaterial);
}

static void
reset_data(GwyGLMaterial *glmaterial)
{
    GwyGLMaterialPrivate *priv = glmaterial->priv;
    /* OpenGL reference defines the following default. */
    priv->ambient = (GwyRGBA){ 0.2, 0.2, 0.2, 1.0 };
    priv->diffuse = (GwyRGBA){ 0.8, 0.8, 0.8, 1.0 };
    priv->specular = (GwyRGBA){ 0.0, 0.0, 0.0, 1.0 };
    priv->emission = (GwyRGBA){ 0.0, 0.0, 0.0, 1.0 };
    priv->shininess = 0.0;
}

static void
data_changed(GwyGLMaterial *glmaterial)
{
    gwy_resource_data_changed(GWY_RESOURCE(glmaterial));
}

static GwyGLMaterial*
create_gl_material(const gchar *name,
                   const GwyRGBA *ambient, const GwyRGBA *diffuse, const GwyRGBA *specular, const GwyRGBA *emission,
                   gdouble shininess,
                   gboolean is_const)
{
    GwyGLMaterial *glmaterial = g_object_new(GWY_TYPE_GL_MATERIAL,
                                             "name", name,
                                             "const", is_const,
                                             NULL);
    GwyGLMaterialPrivate *priv = glmaterial->priv;
    if (ambient)
        priv->ambient = *ambient;
    if (diffuse)
        priv->diffuse = *diffuse;
    if (specular)
        priv->specular = *specular;
    if (emission)
        priv->emission = *emission;
    if (shininess >= 0)
        priv->shininess = shininess;

    return glmaterial;
}

static void
resource_setup_builtins(GwyInventory *res_inventory)
{
    GwyGLMaterial *default_material = create_gl_material(GWY_GL_MATERIAL_DEFAULT,
                                                         NULL, NULL, NULL, NULL, 0.0,
                                                         TRUE);
    gwy_inventory_insert_item(res_inventory, default_material);
    g_object_unref(default_material);

    GwyGLMaterial *none_material = create_gl_material(GWY_GL_MATERIAL_NONE,
                                                      &null_color, &null_color, &null_color, &null_color, 0.0,
                                                      TRUE);
    gwy_inventory_insert_item(res_inventory, none_material);
    g_object_unref(none_material);
}

static gpointer
resource_copy(gpointer item)
{
    g_return_val_if_fail(GWY_IS_GL_MATERIAL(item), NULL);

    GwyGLMaterial *glmaterial = GWY_GL_MATERIAL(item);
    GwyGLMaterialPrivate *priv = glmaterial->priv;
    GwyGLMaterial *copy = create_gl_material(gwy_resource_get_name(GWY_RESOURCE(item)),
                                             &priv->ambient, &priv->diffuse, &priv->specular, &priv->emission,
                                             priv->shininess,
                                             FALSE);

    return copy;
}

static void
resource_dump(GwyResource *resource, GString *str)
{
    g_return_if_fail(GWY_IS_GL_MATERIAL(resource));
    GwyGLMaterial *glmaterial = GWY_GL_MATERIAL(resource);
    GwyGLMaterialPrivate *priv = glmaterial->priv;

    gwy_append_doubles_to_gstring(str, (gdouble*)&priv->ambient, 4, 6, " ", TRUE);
    g_string_append_c(str, '\n');
    gwy_append_doubles_to_gstring(str, (gdouble*)&priv->diffuse, 4, 6, " ", TRUE);
    g_string_append_c(str, '\n');
    gwy_append_doubles_to_gstring(str, (gdouble*)&priv->specular, 4, 6, " ", TRUE);
    g_string_append_c(str, '\n');
    gwy_append_doubles_to_gstring(str, (gdouble*)&priv->emission, 4, 6, " ", TRUE);
    g_string_append_c(str, '\n');
    gwy_append_doubles_to_gstring(str, &priv->shininess, 1, 6, " ", TRUE);
    g_string_append_c(str, '\n');
}

static GwyResource*
resource_parse(const gchar *text)
{
    GwyGLMaterial *glmaterial = NULL;
    gint nrows = 4, ncols = 4;
    gdouble data[4*4], shininess;

    g_return_val_if_fail(text, NULL);
    GwyGLMaterialClass *klass = g_type_class_peek(GWY_TYPE_GL_MATERIAL);
    g_return_val_if_fail(klass, NULL);

    gchar *end;
    if (!gwy_parse_doubles(text, data, 0, &nrows, &ncols, &end, NULL)) {
        g_warning("Cannot parse GL material.");
        return NULL;
    }
    nrows = ncols = 1;
    if (!gwy_parse_doubles(end, &shininess, GWY_PARSE_DOUBLES_COMPLETELY, &nrows, &ncols, NULL, NULL)) {
        g_warning("Cannot parse GL material.");
        return NULL;
    }
    shininess = CLAMP(shininess, 0.0, 1.0);

    glmaterial = create_gl_material("", NULL, NULL, NULL, NULL, shininess, FALSE);
    GwyGLMaterialPrivate *priv = glmaterial->priv;

    set_rgba_validated(&priv->ambient, (GwyRGBA*)(data + 0));
    set_rgba_validated(&priv->diffuse, (GwyRGBA*)(data + 4));
    set_rgba_validated(&priv->specular, (GwyRGBA*)(data + 8));
    set_rgba_validated(&priv->emission, (GwyRGBA*)(data + 12));

    return GWY_RESOURCE(glmaterial);
}

/**
 * gwy_gl_materials:
 *
 * Gets inventory with all the GL materials.
 *
 * Returns: (transfer none): GL material inventory.
 **/
GwyInventory*
gwy_gl_materials(void)
{
    return inventory;
}

/**
 * gwy_gl_materials_get_gl_material:
 * @name: (nullable): GL material name.  May be %NULL to get the default GL material.
 *
 * Convenience function to get a GL material from gwy_gl_materials() by name.
 *
 * Returns: (transfer none): GL material identified by @name or the default GL material if @name does not exist.
 **/
GwyGLMaterial*
gwy_gl_materials_get_gl_material(const gchar *name)
{
    g_return_val_if_fail(inventory, NULL);
    return (GwyGLMaterial*)gwy_inventory_get_item_or_default(inventory, name);
}

static void
serializable_itemize(GwySerializable *serializable, GwySerializableGroup *group)
{
    GwyGLMaterial *glmaterial = GWY_GL_MATERIAL(serializable);
    GwyGLMaterialPrivate *priv = glmaterial->priv;

    gwy_serializable_group_alloc_size(group, NUM_ITEMS);
    gwy_serializable_group_append_string(group, serializable_items + ITEM_NAME,
                                         gwy_resource_get_name(GWY_RESOURCE(glmaterial)));
    gwy_serializable_group_append_rgba(group, serializable_items + ITEM_AMBIENT, &priv->ambient);
    gwy_serializable_group_append_rgba(group, serializable_items + ITEM_DIFFUSE, &priv->diffuse);
    gwy_serializable_group_append_rgba(group, serializable_items + ITEM_SPECULAR, &priv->specular);
    gwy_serializable_group_append_rgba(group, serializable_items + ITEM_EMISSION, &priv->emission);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_SHININESS, priv->shininess);
    gwy_serializable_group_itemize(group);
}

static gboolean
serializable_construct(GwySerializable *serializable, GwySerializableGroup *group, GwyErrorList **error_list)
{
    GwySerializableItem its[NUM_ITEMS], *it;
    gwy_assign(its, serializable_items, NUM_ITEMS);
    gwy_deserialize_filter_items(its, NUM_ITEMS, group, TYPE_NAME, error_list);

    GwyGLMaterial *glmaterial = GWY_GL_MATERIAL(serializable);
    GwyGLMaterialPrivate *priv = glmaterial->priv;
    gboolean bad_colour = FALSE;

    it = its + ITEM_AMBIENT;
    if (it->value.v_boxed) {
        priv->ambient = *(GwyRGBA*)it->value.v_boxed;
        if (!fix_rgba(&priv->ambient, FALSE))
            bad_colour = TRUE;
    }

    it = its + ITEM_DIFFUSE;
    if (it->value.v_boxed) {
        priv->diffuse = *(GwyRGBA*)it->value.v_boxed;
        if (!fix_rgba(&priv->diffuse, FALSE))
            bad_colour = TRUE;
    }

    it = its + ITEM_SPECULAR;
    if (it->value.v_boxed) {
        priv->specular = *(GwyRGBA*)it->value.v_boxed;
        if (!fix_rgba(&priv->specular, FALSE))
            bad_colour = TRUE;
    }

    it = its + ITEM_EMISSION;
    if (it->value.v_boxed) {
        priv->emission = *(GwyRGBA*)it->value.v_boxed;
        if (!fix_rgba(&priv->emission, FALSE))
            bad_colour = TRUE;
    }

    if (bad_colour) {
        gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_REPLACED,
                           _("Invalid color data."));
    }

    priv->shininess = its[ITEM_SHININESS].value.v_double;

    const gchar *name;
    if ((name = its[ITEM_NAME].value.v_string)) {
        if (strchr(name, '/') || strchr(name, '\\')) {
            gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_REPLACED,
                               _("Invalid resource name."));
        }
        else
            gwy_resource_rename(GWY_RESOURCE(glmaterial), name);
    }

    g_free(its[ITEM_NAME].value.v_string);

    return TRUE;
}

static GwySerializable*
serializable_copy(GwySerializable *serializable)
{
    GwyGLMaterialPrivate *priv = GWY_GL_MATERIAL(serializable)->priv;
    GwyGLMaterial *copy = create_gl_material(gwy_resource_get_name(GWY_RESOURCE(serializable)),
                                             &priv->ambient, &priv->diffuse, &priv->specular, &priv->emission,
                                             priv->shininess,
                                             FALSE);
    return GWY_SERIALIZABLE(copy);
}

static void
serializable_assign(GwySerializable *destination, GwySerializable *source)
{
    GwyGLMaterialPrivate *spriv = GWY_GL_MATERIAL(source)->priv;
    GwyGLMaterialPrivate *dpriv = GWY_GL_MATERIAL(destination)->priv;
    dpriv->ambient = spriv->ambient;
    dpriv->diffuse = spriv->diffuse;
    dpriv->specular = spriv->specular;
    dpriv->emission = spriv->emission;
    dpriv->shininess = spriv->shininess;
    /* We do not rename the destination resource. It would be wrong for managed resources and it would be confusing to
     * do it only for unmanaged resources. */
    gwy_resource_data_changed(GWY_RESOURCE(destination));
}

/**
 * SECTION: gl-material
 * @title: GwyGLMaterial
 * @short_description: OpenGL material representation
 * @see_also: #Gwy3DView -- 3D data display widget,
 *            #GwyRGBA -- representation of color components,
 *            #GwyInventory -- the container holding all GL materials
 *
 * #GwyGLMaterial represents an OpenGL material.  Its properties directly map to corresponding OpenGL material
 * characteristics, all are in the range [0,1].
 *
 * Gradient objects can be obtained from gwy_gl_materials_get_gl_material(). New GL materials can be created with
 * gwy_inventory_new_item() on the #GwyInventory returned by gwy_gl_materials().
 **/

/**
 * GWY_GL_MATERIAL_DEFAULT:
 *
 * The name of the default OpenGL material.
 *
 * It is guaranteed to always exist.
 *
 * Note this is not the same as user's default material which corresponds to the default item in gwy_gl_materials()
 * inventory and it change over time.
 **/

/**
 * GWY_GL_MATERIAL_NONE:
 *
 * The name of special void material with all characteristics zero.
 *
 * It is guaranteed to exist, but you should rarely actually need it.
 **/

/* vim: set cin columns=120 tw=118 et ts=4 sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
