/*
 *                            COPYRIGHT
 *
 *  cschem - modular/flexible schematics editor - libcschem (core library)
 *  Copyright (C) 2018,2019,2022 Tibor 'Igor2' Palinkas
 *
 *  (Supported by NLnet NGI0 PET Fund in 2022)
 *
 *  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., 31 Milk Street, # 960789 Boston, MA 02196 USA
 *
 *  Contact:
 *    Project page: http://repo.hu/projects/sch-rnd
 *    contact lead developer: http://www.repo.hu/projects/sch-rnd/contact.html
 *    mailing list: http://www.repo.hu/projects/sch-rnd/contact.html
 */

#include "config.h"
#include "libcschem/common_types.h"
#include <stdio.h>
#include <genht/hash.h>
#include <genht/htsp.h>
#include <genvector/gds_char.h>
#include <genvector/vts0.h>
#include <genvector/vtp0.h>
#include "attrib.h"
#include "cnc_obj.h"
#include "cnc_grp.h"
#include "undo.h"
#include "concrete.h"
#include "abstract.h"
#include "project.h"
#include "extobj.h"
#include <librnd/core/compat_misc.h>
#include <librnd/core/rnd_printf.h>
#include <librnd/core/error.h>
#include <librnd/core/misc_util.h>

/*** low level attribute manipulation */

static csch_attrib_t *attr_alloc(const char *key, int prio)
{
	csch_attrib_t *a = calloc(sizeof(csch_attrib_t), 1);
	a->key = rnd_strdup(key);
	a->prio = prio;
	return a;
}

static void attr_free_vts(vts0_t *vt)
{
	size_t n;
	for(n = 0; n < vts0_len(vt); n++)
		free(vt->array[n]);
	vts0_uninit(vt);
}


static void attr_free_val(csch_attrib_t *a)
{
	if (a->val != NULL) {
		free(a->val);
		a->val = NULL;
	}
	else
		attr_free_vts(&a->arr);
}


static void attr_free(csch_attrib_t *a)
{
	free(a->key);
	attr_free_val(a);
	attr_free_vts(&a->source);
	free(a->export_name);
	free(a);
}

void csch_attr_free(csch_attrib_t *a)
{
	attr_free(a);
}


#include "attrib_src.c"

/*** API functions ***/

void csch_attrib_init(csch_attribs_t *attribs)
{
	htsp_init(attribs, strhash, strkeyeq);
}

void csch_attrib_uninit(csch_attribs_t *attribs)
{
	htsp_entry_t *e;
	for(e = htsp_first(attribs); e; e = htsp_next(attribs, e))
		attr_free(e->value);
	htsp_uninit(attribs);
}



static int attrib_sort_cmp(const void *a1_, const void *a2_)
{
	csch_attrib_t * const *a1 = a1_;
	csch_attrib_t * const *a2 = a2_;
	return strcmp((*a1)->key, (*a2)->key);
}

void csch_attrib_sort(vtp0_t *dst, const csch_attribs_t *attribs)
{
	htsp_entry_t *e;

	/* optimization: make room for all the elements, avoiding realloc()s down the road */
	vtp0_enlarge(dst, vtp0_len(dst) + attribs->used - 1);
	dst->used = 0;

	for(e = htsp_first(attribs); e; e = htsp_next(attribs, e))
		vtp0_append(dst, e->value);

	qsort(dst->array, vtp0_len(dst), sizeof(csch_attrib_t *), attrib_sort_cmp);
}

/* Does not free src. Strdups val only if requested */
static int csch_attrib_set_nofree(csch_attribs_t *attribs, int prio, const char *key, const char *val, csch_source_arg_t *source, csch_attrib_t **attr_out, int strdup_val)
{
	csch_attrib_t *a = htsp_get(attribs, key);

	if (a == NULL) {
		a = attr_alloc(key, prio);
		if (attr_out != NULL) *attr_out = a;
		htsp_set(attribs, a->key, a);
	}
	else {
		if (prio > a->prio) {
			append_src(a, prio, source, 1);
			if (attr_out != NULL) *attr_out = a;
			return -1;
		}
		attr_free_val(a);
	}
	if (strdup_val)
		a->val = ((val == NULL) ? NULL : rnd_strdup(val));
	else
		a->val = (char *)val; /* caller explicitly asked for this cast */
	append_src(a, prio, source, 0);
	if (attr_out != NULL) *attr_out = a;
	return 0;
}

int csch_attrib_set(csch_attribs_t *attribs, int prio, const char *key, const char *val, csch_source_arg_t *source, csch_attrib_t **attr_out)
{
	int res = csch_attrib_set_nofree(attribs, prio, key, val, source, attr_out, 1);
	csch_attr_src_free(source);
	return res;
}


int csch_attrib_setptr(csch_attribs_t *attribs, int prio, char *key, const char *val, csch_source_arg_t *source, csch_attrib_t **attr_out)
{
	int res = csch_attrib_set_nofree(attribs, prio, key, val, source, attr_out, 0);
	csch_attr_src_free(source);
	return res;
}


int csch_attrib_seti(csch_attribs_t *attribs, int prio, const char *key, long idx, const char *val, csch_source_arg_t *source, csch_attrib_t **attr_out)
{
	csch_attrib_t *a = htsp_get(attribs, key);
	char *nval;

	if (idx < 0)
		goto error;

	if (a == NULL) {
		a = attr_alloc(key, prio);
		if (attr_out != NULL) *attr_out = a;
		htsp_set(attribs, a->key, a);
	}
	else {
		if (a->val != NULL)
			goto error;
		if (prio > a->prio) {
			append_src(a, prio, source, 1);
			if (attr_out != NULL) *attr_out = a;
			goto error;
		}
	}
	append_src(a, prio, source, 0);
	nval = rnd_strdup(val);
	if (idx < a->arr.used) {
		free(a->arr.array[idx]);
		a->arr.array[idx] = nval;
	}
	vts0_set(&a->arr, idx, nval);
	if (attr_out != NULL) *attr_out = a;
	csch_attr_src_free(source);
	return 0;

	error:;
	csch_attr_src_free(source);
	return -1;
}

/* how is: 0=overwrite whole arr; -1=prepend; +1=append; does not free src */
static int csch_attrib_set_arr_nofree(csch_attribs_t *attribs, int prio, const char *key, const vts0_t *val, csch_source_arg_t *source, csch_attrib_t **attr_out, int how)
{
	long n;
	csch_attrib_t *a = htsp_get(attribs, key);

	if (a == NULL) {
		a = attr_alloc(key, prio);
		if (attr_out != NULL) *attr_out = a;
		htsp_set(attribs, a->key, a);
	}
	else {
		if (a->val != NULL)
			return -1;
		if (prio > a->prio) {
			append_src(a, prio, source, 1);
			if (attr_out != NULL) *attr_out = a;
			return -1;
		}
		if (how == 0)
			attr_free_val(a);
	}
	append_src(a, prio, source, 0);

	if (how == 0) {
		/* overwrite */
		for(n = 0; n < a->arr.used; n++)
			free(a->arr.array[n]);
		a->arr.used = 0;
		for(n = 0; n < val->used; n++)
			vts0_set(&a->arr, n, rnd_strdup(val->array[n]));
	}
	else if (how > 1) {
		/* append */
		for(n = 0; n < val->used; n++)
			vts0_append(&a->arr, rnd_strdup(val->array[n]));
	}
	else {
		/* prepend */
		for(n = 0; n < val->used; n++)
			vts0_append(&a->arr, rnd_strdup(val->array[n]));
	}

	if (attr_out != NULL) *attr_out = a;
	return 0;
}

int csch_attrib_set_arr(csch_attribs_t *attribs, int prio, const char *key, const vts0_t *val, csch_source_arg_t *source, csch_attrib_t **attr_out)
{
	int res = csch_attrib_set_arr_nofree(attribs, prio, key, val, source, attr_out, 0);
	csch_attr_src_free(source);
	return res;
}

int csch_attrib_append(csch_attribs_t *attribs, int prio, const char *key, const char *val, csch_source_arg_t *source)
{
	int res;
	vts0_t tmp = {0};

	tmp.used = tmp.alloced = 1;
	tmp.array = (char **)&val;

	res = csch_attrib_set_arr_nofree(attribs, prio, key, &tmp, source, NULL, 1);
	csch_attr_src_free(source);
	return res;
}

int csch_attrib_prepend(csch_attribs_t *attribs, int prio, const char *key, const char *val, csch_source_arg_t *source)
{
	int res;
	vts0_t tmp = {0};

	tmp.used = tmp.alloced = 1;
	tmp.array = (char **)&val;

	res = csch_attrib_set_arr_nofree(attribs, prio, key, &tmp, source, NULL, -1);
	csch_attr_src_free(source);
	return res;
}

int csch_attrib_append_val(csch_attribs_t *attribs, int prio, const char *key, const csch_attrib_t *srca, csch_source_arg_t *source)
{
	if (srca->val != NULL)
		return csch_attrib_append(attribs, prio, key, srca->val, source);

	return csch_attrib_set_arr_nofree(attribs, prio, key, &srca->arr, source, NULL, 1);
}

int csch_attrib_set_arr_c(csch_attribs_t *attribs, int prio, const char *key, const char **val, csch_source_arg_t *source, csch_attrib_t **attr_out)
{
	int res;
	const char **s;
	vts0_t tmp = {0};

	for(s = val; *s != NULL; s++)
		tmp.used++;
	tmp.alloced = tmp.used;
	tmp.array = (char **)val;

	res = csch_attrib_set_arr_nofree(attribs, prio, key, &tmp, source, attr_out, 0);
	csch_attr_src_free(source);
	return res;
}

void csch_attrib_set_export_name(csch_attrib_t *dst, const char *expname)
{
	free(dst->export_name);
	dst->export_name = rnd_strdup(expname);
}

csch_attrib_t *csch_attrib_dup_rename(csch_attrib_t *src, const char *new_key)
{
	size_t n;
	csch_attrib_t *dst = calloc(sizeof(csch_attrib_t), 1);

	dst->key = rnd_strdup(new_key);
	if (src->val != NULL)
		dst->val = rnd_strdup(src->val);
	else
		dst->val = NULL;
	dst->prio = src->prio;

	if (src->arr.used > 0) {
		vts0_enlarge(&dst->arr, src->arr.used);
		dst->arr.used = 0;
		for(n = 0; n < src->arr.used; n++)
			vts0_append(&dst->arr, rnd_strdup(src->arr.array[n]));
	}
	if (src->source.used > 0) {
		vts0_enlarge(&dst->source, src->source.used);
		dst->source.used = 0;
		for(n = 0; n < src->source.used; n++)
			vts0_append(&dst->source, rnd_strdup_null(src->source.array[n]));
	}

	return dst;
}

csch_attrib_t *csch_attrib_dup(csch_attrib_t *src)
{
	return csch_attrib_dup_rename(src, src->key);
}


int csch_attrib_del(csch_attribs_t *attribs, int prio, const char *key, csch_source_arg_t *source)
{
	csch_attrib_t *a = htsp_get((csch_attribs_t *)attribs, key);

	if (a == NULL) {
		csch_attr_src_free(source);
		return -1;
	}

	a->deleted = 1;
	append_src(a, prio, source, 0);
	csch_attr_src_free(source);
	return 0;
}

void csch_attrib_copy_all(csch_attribs_t *dst, const csch_attribs_t *src)
{
	htsp_entry_t *e;

	for(e = htsp_first(src); e; e = htsp_next(src, e)) {
		csch_attrib_t *a = csch_attrib_dup(e->value);
		htsp_set(dst, a->key, a);
	}
}

static csch_source_arg_t *attr_copy_gen_source(csch_attrib_t *s)
{
	char *sep, *ssrc = s->source.array[0];
	long len;
	csch_source_arg_t *src;

	sep = strchr(ssrc, ':');
	sep++;
	len = strlen(sep);

	src = malloc(sizeof(csch_source_arg_t) + len + 1);
	src->type[0] = 'c';
	src->type[1] = '\0';
	memcpy(src->src_desc, sep, len+1);

	return src;
}

void csch_attrib_copy_all_undoable(csch_sheet_t *sheet, csch_cgrp_t *dst_obj, csch_attribs_t *dst, const csch_attribs_t *src, int undoable)
{
	htsp_entry_t *e;

	assert((dst != NULL) || (dst_obj != NULL));

	if (undoable) {
		assert(dst_obj != NULL);
		assert(sheet != NULL);
	}

	if (dst == NULL)
		dst = &dst_obj->attr;

	for(e = htsp_first(src); e; e = htsp_next(src, e)) {
		csch_attrib_t *s = csch_attrib_dup(e->value);
		csch_attrib_t *d = csch_attrib_get(dst, e->key);

		if ((s != NULL) && (d != NULL) && (csch_attrib_val_eq_(s, d))) {
			/* do not merge if values are the same */
			attr_free(s);
			continue;
		}

		if (s->val == NULL) { /* source is an array */
			int done = 0;

			if (s->arr.used > 0) {
				if ((d == NULL) || ((d->arr.used == 0) && (d->val == NULL))) {
					/* source has an array, destination has nothing: simply copy the array */
					long n;

					if (undoable) {
						if (d == NULL) {
							d = attr_alloc(e->key, s->prio);
							htsp_set(dst, e->key, d);
						}
						vts0_append(&d->arr, NULL); /* dummy cursor for insert before */
						for(n = 0; n < s->arr.used; n++)
							csch_attr_arr_modify_ins_before(sheet, dst_obj, e->key, n, s->arr.array[n], 1);
						d->arr.used--; /* remove dummy cursor */
						done = 1;
					}
					else {
						csch_source_arg_t *source = attr_copy_gen_source(s);
						csch_attrib_set_arr(dst, s->prio, e->key, &s->arr, source, NULL);
						done = 1;
					}
				}
			}

TODO("figure how to merge arrays with differing content");
			if (!done)
				rnd_message(RND_MSG_ERROR, "csch_attrib_copy_all_undoable(): internal error: can't copy/merge array type attribte '%s'\n", e->key);
		}
		else { /* source is scalar */
TODO("handle if dst is an array");
			if ((d == NULL) || ((d->val != NULL))) {
				csch_source_arg_t *source = attr_copy_gen_source(s);

				if (undoable)
					csch_attr_modify_str(sheet, dst_obj, s->prio, e->key, s->val, source, 1);
				else
					csch_attrib_set(dst, s->prio, e->key, s->val, source, NULL);
				attr_free(s);
				s = NULL;
			}
		}
	}
}

int csch_attrib_apply(csch_attribs_t *dst, const csch_attribs_t *src, csch_source_arg_t *source, int *force_prio)
{
	htsp_entry_t *e;
	int err = 0;

	for(e = htsp_first(src); e; e = htsp_next(src, e)) {
		csch_attrib_t *a = e->value;
		int prio = (force_prio != NULL) ? *force_prio : a->prio;
		if (a->val != NULL)
			err |= csch_attrib_set_nofree(dst, prio, a->key, a->val, source, NULL, 1);
		else
			err |= csch_attrib_set_arr_nofree(dst, prio, a->key, &a->arr, source, NULL, 0);
	}

	csch_attr_src_free(source);
	return err;
}

/* invalidate all dyntext on attr change to make sure the new value is visible */
static void attr_side_dyntext(csch_cgrp_t *grp)
{
	htip_entry_t *e;
	for(e = htip_first(&grp->id2obj); e != NULL; e = htip_next(&grp->id2obj, e)) {
		csch_chdr_t *o = e->value;
		if (csch_obj_is_grp(o))
			attr_side_dyntext((csch_cgrp_t *)o);
		else if (o->type == CSCH_CTYPE_TEXT) {
			csch_text_t *t = (csch_text_t *)o;
			if (t->dyntext) {
				csch_text_dyntext_inval(t);
				csch_text_invalidate_font(t);
			}
		}
	}
}

void csch_attr_side_effects(csch_cgrp_t *grp, const char *key)
{
	if (!csch_obj_is_grp(&grp->hdr))
		return;

	attr_side_dyntext(grp);

	if (key != NULL) {
		const char *val = csch_attrib_get_str(&grp->attr, key);
		csch_cgrp_attrib_update(grp->hdr.sheet, grp, 0, key, val);
	}
}

RND_INLINE void notify(csch_chdr_t *obj)
{
	rnd_event(&obj->sheet->hidlib, CSCH_EVENT_OBJ_ATTR_EDITED, "p", obj);
	csch_cobj_redraw(obj);
}

unsigned csch_attrib_hash(const csch_attribs_t *attr)
{
	unsigned res = 0;
	htsp_entry_t *e;

	for(e = htsp_first(attr); e != NULL; e = htsp_next(attr, e)) {
		long n;
		csch_attrib_t *a = e->value;

		res ^= strhash(a->key);
		res ^= ((unsigned)a->prio) << 3;
		res ^= ((unsigned)a->deleted) << 15;
		if (a->val != NULL)
			res ^= strhash(a->val);
		res ^= ((unsigned)a->arr.used) << 12;

		for(n = 0; n < a->arr.used; n++)
			if (a->arr.array[n] != NULL)
				res ^= strhash(a->arr.array[n]);
	}

	return res;
}

int csch_attrib_val_eq_(const csch_attrib_t *a1, const csch_attrib_t *a2)
{
	long n;

	if (a1->val != NULL) {
		if (a2->val == NULL) return 0;
		if (strcmp(a1->val, a2->val) != 0) return 0;
	}
	else
		if (a2->val != NULL) return 0;
	if (a1->arr.used != a2->arr.used) return 0;

	for(n = 0; n < a1->arr.used; n++) {
		if (a1->arr.array[n] != NULL) {
			if (a2->arr.array[n] == NULL) return 0;
			if (strcmp(a1->arr.array[n], a2->arr.array[n]) != 0) return 0;
		}
		else
			if (a2->arr.array[n] != NULL) return 0;
	}

	return 1;
}

int csch_attrib_eq_(const csch_attrib_t *a1, const csch_attrib_t *a2)
{
	/* assume keys match */
	if (a1->prio != a2->prio) return 0;
	if (a1->deleted != a2->deleted) return 0;

	return csch_attrib_val_eq_(a1, a2);
}

int csch_attrib_eq(const csch_attribs_t *attr1, const csch_attribs_t *attr2)
{
	htsp_entry_t *e;

	if (htsp_length(attr1) != htsp_length(attr2)) return 0;

	for(e = htsp_first((htsp_t *)attr1); e != NULL; e = htsp_next((htsp_t *)attr1, e)) {
		csch_attrib_t *a1 = e->value, *a2 = htsp_get((htsp_t *)attr2, e->key);

		if (a2 == NULL) return 0;
		if (!csch_attrib_eq_(a1, a2)) return 0;
	}

	return 1;
}

/*** Modify ***/
typedef struct {
	csch_cgrp_t *obj; /* it is safe to save the object pointer because it is persistent (through the removed object list) */
	csch_attrib_t *a;
	char *key; /* need to store a copy of key separately because on deletion key is free'd */
} undo_attr_modify_t;


static int undo_attr_modify_swap(void *udata)
{
	undo_attr_modify_t *u = udata;
	csch_attrib_t *a;

	csch_cobj_redraw(u->obj);
	csch_extrobj_attr_edit_pre(u->obj, u->key);

	a = htsp_get(&u->obj->attr, u->key);
	if (u->a != NULL)
		htsp_set(&u->obj->attr, u->a->key, u->a);
	else
		htsp_popentry(&u->obj->attr, u->key);
	u->a = a;

	csch_attr_side_effects(u->obj, u->key);

	csch_sheet_set_changed(u->obj->hdr.sheet, 1);
	csch_extrobj_attr_edit_post(u->obj, u->key);
	notify(&u->obj->hdr);
	return 0;
}

static void undo_attr_modify_print(void *udata, char *dst, size_t dst_len)
{
	undo_attr_modify_t *u = udata;

	rnd_snprintf(dst, dst_len, "attr change, key=%s", u->key);
}

static void undo_attr_modify_free(void *udata)
{
	undo_attr_modify_t *u = udata;
	if (u->a != NULL) {
		attr_free(u->a);
		u->a = NULL;
	}
	free(u->key);
	u->key = NULL;
}

static const char core_attr_cookie[] = "libcschem/core/attrib.c";

static const uundo_oper_t undo_attr_modify = {
	core_attr_cookie,
	undo_attr_modify_free,
	undo_attr_modify_swap,
	undo_attr_modify_swap,
	undo_attr_modify_print
};


void csch_attr_modify_str(csch_sheet_t *sheet, csch_cgrp_t *obj, int prio, const char *key, const char *val, csch_source_arg_t *source, int undoable)
{
	undo_attr_modify_t utmp, *u = &utmp;

	if (undoable) u = uundo_append(&sheet->undo, &undo_attr_modify, sizeof(undo_attr_modify_t));

	if (prio < 0) {
		csch_attrib_t *a = htsp_get(&obj->attr, key);
		if (a != NULL)
			prio = a->prio;
		else
			prio = -prio;
	}

	u->obj = obj;
	u->a = attr_alloc(key, prio);
	u->a->val = val == NULL ? NULL : rnd_strdup(val);
	u->key = rnd_strdup(key);
	append_src(u->a, prio, source, 0);

	undo_attr_modify_swap(u);
	if (undoable) csch_undo_inc_serial(sheet);

	csch_attr_src_free(source);
}

void csch_attr_modify_del(csch_sheet_t *sheet, csch_cgrp_t *obj, const char *key, int undoable)
{
	undo_attr_modify_t utmp, *u = &utmp;
	csch_attrib_t *a;

	/* skip creating an undo slot for a non-existing attribute's del (no-op) */
	a = htsp_get(&obj->attr, key);
	if (a == NULL)
		return;

	if (undoable) u = uundo_append(&sheet->undo, &undo_attr_modify, sizeof(undo_attr_modify_t));

	u->obj = obj;
	u->a = NULL;
	u->key = rnd_strdup(key);

	undo_attr_modify_swap(u);
	if (undoable) csch_undo_inc_serial(sheet);
}

void csch_attr_modify_rename(csch_sheet_t *sheet, csch_cgrp_t *grp, csch_attrib_t *a, const char *new_name, csch_source_arg_t *src, int undoable)
{
	int prio = a->prio;

	assert(strcmp(new_name, a->key) != 0);

	if (undoable)
		uundo_freeze_serial(&sheet->undo);

	csch_attr_modify_str(sheet, grp, prio, new_name, a->val, src, undoable);
	csch_attr_modify_del(sheet, grp, a->key, undoable);

	if (undoable) {
		uundo_unfreeze_serial(&sheet->undo);
		uundo_inc_serial(&sheet->undo);
	}
}

typedef struct {
	csch_chdr_t *obj;
	csch_attrib_t *a;
	short int prio;
} undo_attr_modify_prio_t;


static int undo_attr_modify_prio_swap(void *udata)
{
	undo_attr_modify_prio_t *u = udata;
	short int tmp;

	tmp = u->a->prio;
	u->a->prio = u->prio;
	u->prio = tmp;

	csch_sheet_set_changed(u->obj->sheet, 1);
	notify(u->obj);
	return 0;
}

static void undo_attr_modify_prio_print(void *udata, char *dst, size_t dst_len)
{
	undo_attr_modify_prio_t *u = udata;

	rnd_snprintf(dst, dst_len, "attr prio change, key=%s %d %d", u->a->key, u->a->prio, u->prio);
}

static const uundo_oper_t undo_attr_modify_prio = {
	core_attr_cookie,
	/*undo_attr_modify_free*/ NULL,
	undo_attr_modify_prio_swap,
	undo_attr_modify_prio_swap,
	undo_attr_modify_prio_print
};


void csch_attr_modify_prio(csch_sheet_t *sheet, csch_cgrp_t *grp, csch_attrib_t *a, int prio, csch_source_arg_t *source, int undoable)
{
	undo_attr_modify_prio_t utmp, *u = &utmp;

	if (undoable) u = uundo_append(&sheet->undo, &undo_attr_modify_prio, sizeof(undo_attr_modify_prio_t));

	u->obj = &grp->hdr;
	u->a = a;
	u->prio = prio;
	append_src(u->a, prio, source, 0);

	undo_attr_modify_prio_swap(u);
	if (undoable) csch_undo_inc_serial(sheet);

	csch_attr_src_free(source);
}



static const char *NEWVAL_DEL = "NEWVAL_DEL";
typedef struct {
	csch_cgrp_t *obj; /* it is safe to save the object pointer because it is persistent (through the removed object list) */
	char *key;
	long idx;         /* current index */
	long delta;       /* if non-zero, move item within the list */
	char *newval;     /* if non-NULL, replace item content; if NEWVAL_DEL, remove item */

	char *saved;      /* saved string value if newval is NEWVAL_DEL */
} undo_attr_arr_modify_t;

/* returns delta achieved */
RND_INLINE long attr_arr_move(csch_attrib_t *a, long idx, long delta)
{
	long n, end, res = 0;
	char *save;

	if (delta == 0)
		return 0;

	if (delta > 0) {
		if (idx >= a->arr.used-1)
			return 0; /* moving last item in positive dir -> noop */
		end = idx + delta;
		if (end > a->arr.used-1)
			end = a->arr.used-1;
		save = a->arr.array[idx];
		for(n = idx; n < end; n++,res++)
			a->arr.array[n] = a->arr.array[n+1];
		a->arr.array[end] = save;
	}
	else { /* delta < 0 */
		if (idx <= 0)
			return 0; /* moving first item in negative dir -> noop */
		end = idx + delta;
		if (end < 0)
			end = 0;
		save = a->arr.array[idx];
		for(n = idx; n > end; n--,res++)
			a->arr.array[n] = a->arr.array[n-1];
		a->arr.array[end] = save;
	}

	return res;
}

RND_INLINE void attr_arr_del(csch_attrib_t *a, long idx)
{
	long n, end = a->arr.used-1;

	for(n = idx; n < end; n++)
		a->arr.array[n] = a->arr.array[n+1];

	a->arr.used--;
}

/* create a new, empty slot at [idx], moving all items starting from
   idx to +1. Effectively inserts an empty slot at idx, before the
   item that was at idx. */
RND_INLINE void attr_arr_ins_before(csch_attrib_t *a, long idx)
{
	long n, end = a->arr.used;


	vts0_enlarge(&a->arr, a->arr.used);
	for(n = end; n > idx; n--)
		a->arr.array[n] = a->arr.array[n-1];

	a->arr.array[idx] = NULL;
}

static int undo_attr_arr_modify_swap(void *udata)
{
	undo_attr_arr_modify_t *u = udata;
	csch_attrib_t *a;
	long end;

	a = htsp_get(&u->obj->attr, u->key);
	if (a == NULL) {
		rnd_message(RND_MSG_ERROR, "undo_attr_arr_modify_swap(): no such attribute '%s' in object\n", u->key);
		return -1;
	}

	end = a->arr.used;
	if ((u->newval == NEWVAL_DEL) && (u->saved != NULL))
		end++; /* special case: allow (re-)insert after the last item */

	if ((u->idx < 0) || (u->idx >= end)) {
		rnd_message(RND_MSG_ERROR, "undo_attr_arr_modify_swap(): arr index %ld out of range (0..%ld)\n", u->idx, a->arr.used);
		return -1;
	}

	csch_cobj_redraw(u->obj);
	if (u->delta != 0)
		u->delta = -attr_arr_move(a, u->idx, u->delta);
	if (u->newval == NEWVAL_DEL) {
		if (u->saved == NULL) {
			u->saved = a->arr.array[u->idx];
			attr_arr_del(a, u->idx);
		}
		else {
			attr_arr_ins_before(a, u->idx);
			a->arr.array[u->idx] = u->saved;
			u->saved = NULL;
		}

	}
	else if (u->newval != NULL)
		rnd_swap(char *, a->arr.array[u->idx], u->newval);

	csch_attr_side_effects(u->obj, u->key);

	csch_sheet_set_changed(u->obj->hdr.sheet, 1);
	notify(&u->obj->hdr);
	return 0;
}

static void undo_attr_arr_modify_print(void *udata, char *dst, size_t dst_len)
{
	undo_attr_arr_modify_t *u = udata;

	if (u->newval == NEWVAL_DEL)
		rnd_snprintf(dst, dst_len, "attr arr %s, key=%s:%ld", (u->saved == NULL ? "del" : "ins"), u->key, u->idx);
	else if (u->newval != NULL)
		rnd_snprintf(dst, dst_len, "attr arr change, key=%s:%ld '%s'", u->key, u->idx, u->newval);
	else if (u->delta != 0)
		rnd_snprintf(dst, dst_len, "attr arr move, key=%s:%ld by %ld", u->key, u->idx, u->delta);
	else
		rnd_snprintf(dst, dst_len, "attr arr noop, key=%s:%ld", u->key, u->idx);
}

static void undo_attr_arr_modify_free(void *udata)
{
	undo_attr_arr_modify_t *u = udata;
	free(u->key);
	u->key = NULL;
	if (u->newval != NEWVAL_DEL)
		free(u->newval);
	u->newval = NULL;
	free(u->saved);
	u->saved = NULL;
}

static const uundo_oper_t undo_attr_arr_modify = {
	core_attr_cookie,
	undo_attr_arr_modify_free,
	undo_attr_arr_modify_swap,
	undo_attr_arr_modify_swap,
	undo_attr_arr_modify_print
};


void csch_attr_arr_modify_str(csch_sheet_t *sheet, csch_cgrp_t *obj, const char *key, long idx, const char *newval, int undoable)
{
	undo_attr_arr_modify_t utmp, *u = &utmp;

	if (undoable) u = uundo_append(&sheet->undo, &undo_attr_arr_modify, sizeof(undo_attr_arr_modify_t));

	u->obj = obj;
	u->key = rnd_strdup(key);
	u->idx = idx;
	u->delta = 0;
	u->newval = (newval == NEWVAL_DEL ? (char *)NEWVAL_DEL : rnd_strdup(newval));
	u->saved = NULL;

	undo_attr_arr_modify_swap(u);
	if (undoable) csch_undo_inc_serial(sheet);
}

void csch_attr_arr_modify_del(csch_sheet_t *sheet, csch_cgrp_t *obj, const char *key, long idx, int undoable)
{
	csch_attr_arr_modify_str(sheet, obj, key, idx, NEWVAL_DEL, undoable);
}


void csch_attr_arr_modify_move(csch_sheet_t *sheet, csch_cgrp_t *obj, const char *key, long idx, long delta, int undoable)
{
	undo_attr_arr_modify_t utmp, *u = &utmp;
	csch_attrib_t *a = htsp_get(&obj->attr, key);

	/* don't do anything on edges to save undo slots */
	if (a == NULL)
		return;
	if ((idx < 0) || (idx >= a->arr.used))
		return;
	if (delta == 0)
		return;
	if ((idx == 0) && (delta < 0))
		return;
	if ((idx == a->arr.used-1) && (delta > 0))
		return;

	if (undoable) u = uundo_append(&sheet->undo, &undo_attr_arr_modify, sizeof(undo_attr_arr_modify_t));

	u->obj = obj;
	u->key = rnd_strdup(key);
	u->idx = idx;
	u->delta = delta;
	u->newval = NULL;
	u->saved = NULL;

	undo_attr_arr_modify_swap(u);
	if (undoable) csch_undo_inc_serial(sheet);
}

void csch_attr_arr_modify_ins_before(csch_sheet_t *sheet, csch_cgrp_t *obj, const char *key, long idx, const char *newval, int undoable)
{
	undo_attr_arr_modify_t utmp, *u = &utmp;

	if (undoable) u = uundo_append(&sheet->undo, &undo_attr_arr_modify, sizeof(undo_attr_arr_modify_t));

	u->obj = obj;
	u->key = rnd_strdup(key);
	u->idx = idx;
	u->delta = 0;
	u->newval = (char *)NEWVAL_DEL;
	u->saved = rnd_strdup(newval);

	undo_attr_arr_modify_swap(u);
	if (undoable) csch_undo_inc_serial(sheet);
}



typedef struct {
	csch_cgrp_t *obj; /* it is safe to save the object pointer because it is persistent (through the removed object list) */
	char *key;

	/* swap values */
	char *val;  /* scalar; if NULL, use arr */
	vts0_t arr;
} undo_attr_modify_conv_t;


static int undo_attr_modify_conv_swap(void *udata)
{
	undo_attr_modify_conv_t *u = udata;
	csch_attrib_t *a;

	a = htsp_get(&u->obj->attr, u->key);
	if (a == NULL) {
		rnd_message(RND_MSG_ERROR, "Internal error: undo_attr_modify_conv_swap(): attrib not found\n");
		return -1;
	}

	if (u->val == NULL) {
		if (a->val == NULL) {
			rnd_message(RND_MSG_ERROR, "Internal error: undo_attr_modify_conv_swap(): converting array to array?\n");
			return -1;
		}
		u->val = a->val;
		a->val = NULL;
		a->arr = u->arr;
		memset(&u->arr, 0, sizeof(u->arr));
	}
	else { /* u->val != NULL */
		if (a->val != NULL) {
			rnd_message(RND_MSG_ERROR, "Internal error: undo_attr_modify_conv_swap(): converting string to string?\n");
			return -1;
		}
		a->val = u->val;
		u->val = NULL;
		u->arr = a->arr;
		memset(&a->arr, 0, sizeof(a->arr));
	}

	csch_cobj_redraw(u->obj);
	csch_attr_side_effects(u->obj, u->key);
	csch_sheet_set_changed(u->obj->hdr.sheet, 1);
	notify(&u->obj->hdr);
	return 0;
}

static void undo_attr_modify_conv_print(void *udata, char *dst, size_t dst_len)
{
	undo_attr_modify_conv_t *u = udata;

	rnd_snprintf(dst, dst_len, "attr conversion between string and array key=%s", u->key);
}

static void undo_attr_modify_conv_free(void *udata)
{
	undo_attr_modify_conv_t *u = udata;

	free(u->val);
	u->val = NULL;
	attr_free_vts(&u->arr);

	free(u->key);
	u->key = NULL;
}

static const uundo_oper_t undo_attr_modify_conv = {
	core_attr_cookie,
	undo_attr_modify_conv_free,
	undo_attr_modify_conv_swap,
	undo_attr_modify_conv_swap,
	undo_attr_modify_conv_print
};


void csch_attr_modify_conv_to_arr(csch_sheet_t *sheet, csch_cgrp_t *obj, const char *key, int undoable)
{
	undo_attr_modify_conv_t utmp, *u = &utmp;
	csch_attrib_t *a = htsp_get(&obj->attr, key);

	if ((a == NULL) || (a->val == NULL))
		return; /* doesn't exist or already a string */

	if (undoable) u = uundo_append(&sheet->undo, &undo_attr_modify_conv, sizeof(undo_attr_modify_conv_t));

	memset(u, 0, sizeof(undo_attr_modify_conv_t));
	u->obj = obj;
	u->key = rnd_strdup(key);
	if (*a->val != '\0')
		vts0_append(&u->arr, rnd_strdup(a->val));

	undo_attr_modify_conv_swap(u);
	if (undoable) csch_undo_inc_serial(sheet);
}

void csch_attr_modify_conv_to_str(csch_sheet_t *sheet, csch_cgrp_t *obj, const char *key, int undoable)
{
	undo_attr_modify_conv_t utmp, *u = &utmp;
	csch_attrib_t *a = htsp_get(&obj->attr, key);
	long n;
	gds_t tmp = {0};

	if ((a == NULL) || (a->val != NULL))
		return; /* doesn't exist or already a string */

	if (undoable) u = uundo_append(&sheet->undo, &undo_attr_modify_conv, sizeof(undo_attr_modify_conv_t));

	memset(u, 0, sizeof(undo_attr_modify_conv_t));
	u->obj = obj;
	u->key = rnd_strdup(key);

	/* concat array members with ';' as separator */
	for(n = 0; n < a->arr.used; n++) {
		if (n > 0)
			gds_append(&tmp, ';');
		gds_append_str(&tmp, a->arr.array[n]);
	}

	if (tmp.array == NULL)
		u->val = rnd_strdup("");
	else
		u->val = tmp.array;

	undo_attr_modify_conv_swap(u);
	if (undoable) csch_undo_inc_serial(sheet);
}
