/*
 *                            COPYRIGHT
 *
 *  cschem - modular/flexible schematics editor - libcschem (core library)
 *  Copyright (C) 2018,2022,2023 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 <stdlib.h>
#include <librnd/core/compat_misc.h>
#include <librnd/core/misc_util.h>
#include <librnd/core/rnd_printf.h>

#include "event.h"
#include "concrete.h"
#include "cnc_obj.h"
#include "cnc_text.h"
#include "cnc_any_obj.h"
#include "cnc_pen.h"
#include "undo.h"
#include "op_common.h"

csch_cpen_t csch_pen_default_unknown = {
	{{0}, 0, CSCH_CTYPE_PEN, 0},
	{"<NULL>"},
	/* tip */
	CSCH_PSHP_ROUND, 1,
	{0xF0, 0xC0, 0x00, 0x00,   0xF0C00000,   0.95, 0.8, 0, 0,   "#F0C000"},
	0, 0,
	/* font */
	3500,
	NULL, NULL
};

static const char *pen_shape_names[] = {"round", "square"};

const char *csch_pen_shape_name(csch_pen_shape_t shape)
{
	if ((shape < 0) || (shape >= sizeof(pen_shape_names) / sizeof(pen_shape_names[0])))
		return "invalid";
	return pen_shape_names[shape];
}

csch_cpen_t *csch_pen_alloc(csch_sheet_t *sheet, csch_cgrp_t *parent, const char *name)
{
	csch_cpen_t *pen;
	csch_oid_t oid;
	
	sheet->auto_oid--;
	oid = sheet->auto_oid;

	pen = htip_get(&parent->id2obj, oid);
	if (pen != NULL)
		return NULL;
	pen = calloc(sizeof(csch_cpen_t), 1);

	csch_cobj_init(&pen->hdr, sheet, parent, oid, CSCH_CTYPE_PEN);
	pen->name = csch_comm_str(sheet, name, 1);
	if (pen->name.str != NULL)
		htsp_set(&parent->name2pen, (char *)pen->name.str, pen);
	return pen;
}

csch_cpen_t *csch_pen_dup2(csch_sheet_t *sheet, csch_cgrp_t *parent, const csch_cpen_t *src, const char *new_name)
{
	csch_cpen_t *dst = csch_pen_alloc(sheet, parent, new_name);
	dst->shape = src->shape;
	dst->size = src->size;
	dst->color = src->color;
	dst->dash = src->dash;

	dst->font_height = src->font_height;
	dst->font_family = src->font_family == NULL ? NULL : rnd_strdup(src->font_family);
	dst->font_style  = src->font_style == NULL ? NULL : rnd_strdup(src->font_style);

	return dst;
}

csch_cpen_t *csch_pen_dup(csch_sheet_t *sheet, csch_cgrp_t *parent, const csch_cpen_t *src)
{
	return csch_pen_dup2(sheet, parent, src, src->name.str);
}

void (*csch_cb_pen_invalidate_font)(csch_sheet_t *sheet, csch_cpen_t *pen);

RND_INLINE int csch_pen_same(csch_cpen_t *pen, csch_chdr_t *hdr, int is_fill)
{
	csch_comm_str_t *target;

	if (is_fill)
		target = &hdr->stroke_name;
	else
		target = &hdr->fill_name;

	if (target->str == NULL)
		return 0;

	/* it is safe to compare str pointers because they are from the same hash */
	return target->str == pen->name.str;
}

/* Remove cached ->pen pointers that depend on the given pen from all
   object headers that have it, recursively down from grp */
static void csch_pen_invalidate_(csch_cpen_t *pen, csch_cgrp_t *grp, int tip, int font)
{
	htip_entry_t *e;

	for(e = htip_first(&grp->id2obj); e; e = htip_next(&grp->id2obj, e)) {
		csch_chdr_t *hdr = e->value;
		if ((hdr->stroke == pen) || csch_pen_same(pen, hdr, 0)) {
			if (tip)
				hdr->stroke = NULL;
			if (hdr->type == CSCH_CTYPE_TEXT)
				csch_text_invalidate_font((csch_text_t *)hdr);
		}

		if (tip && ((hdr->fill == pen) || csch_pen_same(pen, hdr, 1)))
			hdr->fill = NULL;
		if (csch_obj_is_grp(hdr))
			csch_pen_invalidate_(pen, (csch_cgrp_t *)hdr, tip, font);
	}
}

void csch_pen_invalidate(csch_cpen_t *pen, int tip, int font, int remov)
{
	csch_sheet_t *sheet = pen->hdr.sheet;
	htip_entry_t *e;

	if ((tip == 0) && (font == 0))
		return;

	if (sheet != NULL)
		csch_pen_invalidate_(pen, pen->hdr.parent, tip, font);

	if (font && (csch_cb_pen_invalidate_font != NULL))
		csch_cb_pen_invalidate_font(pen->hdr.sheet, pen);

	if (remov && (pen->name.str != NULL)) {
		htsp_popentry(&pen->hdr.parent->name2pen, pen->name.str);

		/* it's unlikely, but if another pen has the same name in the same group, we should fall back to that */
		for(e = htip_first(&pen->hdr.parent->id2obj); e; e = htip_next(&pen->hdr.parent->id2obj, e)) {
			csch_cpen_t *p = e->value;
			if ((p->hdr.type == CSCH_CTYPE_PEN) && (p != pen) && (p->name.str != NULL) && (p->name.str == pen->name.str)) {
				htsp_set(&pen->hdr.parent->name2pen, (char *)p->name.str, p);
				break;
			}
		}
	}
}

void csch_pen_free(csch_cpen_t *pen)
{
	/* remove cached pens from any object and pen from group */
	csch_pen_invalidate(pen, 1, 1, 1);

	free(pen->font_family);
	free(pen->font_style);
	csch_cobj_uninit(&pen->hdr);
	free(pen);
}


csch_cpen_t *csch_pen_get(csch_sheet_t *sheet, csch_cgrp_t *grp, const char *name)
{
	csch_cpen_t *pen;

	if (name == NULL)
		return NULL;

	pen = htsp_get(&grp->name2pen, name);
	if ((pen != NULL) && (pen->hdr.type != CSCH_CTYPE_PEN))
		return NULL;
	return pen;
}

csch_cpen_t *csch_pen_resolve(csch_cgrp_t *grp, const char *pen_name)
{
	while((grp != NULL) && csch_obj_is_grp(&grp->hdr)) {
		csch_cpen_t *pen = csch_pen_get(grp->hdr.sheet, grp, pen_name);
		if (pen != NULL)
			return pen;
		grp = grp->hdr.parent;
	}
	return NULL;
}


static const char *pen_new_name(csch_cgrp_t *parent, char *tmp, int len)
{
	int n = 0;
#define PREFIX "new_pen"

	if (htsp_get(&parent->name2pen, PREFIX) == NULL)
		return PREFIX;

	for(n = 2; n < 32766; n++) {
		rnd_snprintf(tmp, len, PREFIX "_%d", n);
		if (htsp_get(&parent->name2pen, tmp) == NULL)
			return tmp;
	}
#undef PREFIX
	fprintf(stderr, "Failed to allocate new pen name - it's unlikely you have 2^15 new pens!\n");
	abort();
}

static csch_chdr_t *pen_create(csch_sheet_t *sheet, csch_cgrp_t *parent)
{
	char tmp[128];
	csch_cpen_t *pen = csch_pen_alloc(sheet, parent, pen_new_name(parent, tmp, sizeof(tmp)));
	if (pen == NULL) return NULL;
	return &pen->hdr;
}

static void pen_remove_alloc(csch_undo_remove_t *slot)
{
}

static void pen_remove_redo(csch_undo_remove_t *slot)
{
	csch_pen_invalidate((csch_cpen_t *)slot->obj, 1, 1, 1);
	csch_cnc_common_remove_redo(slot, CSCH_REM_FROM_RTREE | CSCH_REM_FROM_PARENT | CSCH_REM_DEL_EMPTY_PARENT);
}

static void pen_remove_undo(csch_undo_remove_t *slot)
{
	csch_cpen_t *pen = (csch_cpen_t *)slot->obj;

	csch_cnc_common_remove_undo(slot, CSCH_REM_FROM_RTREE | CSCH_REM_FROM_PARENT | CSCH_REM_DEL_EMPTY_PARENT);

	if (pen->name.str != NULL)
		htsp_set(&pen->hdr.parent->name2pen, (char *)pen->name.str, pen);
}

static int pen_isc_with_box(csch_chdr_t *obj, csch_rtree_box_t *box)
{
	/* pens do not have bounding box */
	return 0;
}

const csch_ops_t csch_ops_pen = {
	pen_create, pen_remove_alloc, pen_remove_redo, pen_remove_undo,
	pen_isc_with_box
};


/*** hash ***/

unsigned csch_pen_hash(const csch_cpen_t *pen, csch_hash_ignore_t ignore)
{
	unsigned res = csch_chdr_hash(&pen->hdr);

	res ^= strhash(pen->name.str);
	res ^= ((unsigned)pen->shape);
	res ^= csch_coord_hash(pen->size);
	res ^= (unsigned)pen->color.r << 16 | (unsigned)pen->color.g << 8 | (unsigned)pen->color.b;
	res ^= ((unsigned)pen->dash) << 3;
	res ^= csch_coord_hash(pen->dash_period);
	return res;
}

int csch_pen_keyeq(const csch_cpen_t *p1, const csch_cpen_t *p2, csch_hash_ignore_t ignore)
{
	if (!csch_chdr_eq(&p1->hdr, &p2->hdr)) return 0;

	if (p1->shape != p2->shape) return 0;
	if (p1->size != p2->size) return 0;
	if (p1->color.packed != p2->color.packed) return 0;
	if (p1->dash != p2->dash) return 0;
	if (p1->dash_period != p2->dash_period) return 0;

	if (p1->hdr.sheet == p2->hdr.sheet) {
		if (p1->name.str != p2->name.str) return 0;
	}
	else {
		if (strcmp(p1->name.str, p2->name.str) != 0) return 0;
	}

	return 1;
}



/*** Modify ***/
typedef struct {
	csch_cpen_t *pen; /* it is safe to save the object pointer because it is persistent (through the removed object list) */
	csch_pen_shape_t shape;
	csch_coord_t size;
	rnd_color_t color;
	csch_comm_str_t name;
	unsigned short dash;
	csch_coord_t dash_period;

	unsigned copied_from_default:1;
} undo_pen_modify_tip_t;


static int undo_pen_modify_tip_swap(void *udata)
{
	undo_pen_modify_tip_t *u = udata;
	csch_sheet_t *sheet = u->pen->hdr.sheet;

	if (u->name.str != u->pen->name.str)
		csch_pen_invalidate(u->pen, 1, 1, 1);
	else
		csch_pen_invalidate(u->pen, 0, 1, 0); /* font color may depend on re-render */

	rnd_swap(csch_pen_shape_t, u->shape, u->pen->shape);
	rnd_swap(csch_coord_t, u->size, u->pen->size);
	rnd_swap(rnd_color_t, u->color, u->pen->color);
	rnd_swap(csch_comm_str_t, u->name, u->pen->name);
	rnd_swap(unsigned short, u->dash, u->pen->dash);
	rnd_swap(csch_coord_t, u->dash_period, u->pen->dash_period);
	rnd_swap(int, u->copied_from_default, u->pen->copied_from_default);

	if (u->name.str != u->pen->name.str)
		htsp_set(&u->pen->hdr.parent->name2pen, (char *)u->pen->name.str, u->pen);

	csch_pen_update_objs_for(sheet, &sheet->direct, u->pen);

	csch_sheet_set_changed(u->pen->hdr.sheet, 1);

	return 0;
}

static void undo_pen_modify_tip_print(void *udata, char *dst, size_t dst_len)
{
	undo_pen_modify_tip_t *u = udata;
	if (u->pen->name.str != u->name.str)
		rnd_snprintf(dst, dst_len, "pen name change: %s <--> %s", u->pen->name.str, u->name.str);
	else
		rnd_snprintf(dst, dst_len, "pen tip modification (in %s)", u->pen->name.str);
}

static const char core_cpen_cookie[] = "libcschem/core/cnc_pen.c";

static const uundo_oper_t undo_pen_modify_tip = {
	core_cpen_cookie,
	NULL,
	undo_pen_modify_tip_swap,
	undo_pen_modify_tip_swap,
	undo_pen_modify_tip_print
};


void csch_pen_modify_tip(csch_sheet_t *sheet, csch_cpen_t *pen, csch_pen_shape_t *shape, csch_coord_t *size, rnd_color_t *color, char **name, unsigned short *dash, csch_coord_t *dash_period, int undoable)
{
	undo_pen_modify_tip_t utmp, *u = &utmp;

	if (undoable) u = uundo_append(&sheet->undo, &undo_pen_modify_tip, sizeof(undo_pen_modify_tip_t));

	u->pen = pen;
	u->shape = CSCH_UNDO_MODIFY(pen, shape);
	u->size = CSCH_UNDO_MODIFY(pen, size);
	u->color = CSCH_UNDO_MODIFY(pen, color);
	u->dash = CSCH_UNDO_MODIFY(pen, dash);
	u->dash_period = CSCH_UNDO_MODIFY(pen, dash_period);
	u->copied_from_default = 0; /* always reset */
	if (name == NULL)
		u->name = pen->name;
	else
		u->name = csch_comm_str(sheet, *name, 1);

	csch_pen_update_objs_for(sheet, &sheet->direct, u->pen);

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


typedef struct {
	csch_cpen_t *pen; /* it is safe to save the object pointer because it is persistent (through the removed object list) */

	csch_coord_t font_height;
	char *font_family;
	char *font_style;

	unsigned copied_from_default:1;
} undo_pen_modify_font_t;


static int undo_pen_modify_font_swap(void *udata)
{
	undo_pen_modify_font_t *u = udata;
	csch_sheet_t *sheet = u->pen->hdr.sheet;

	csch_pen_invalidate(u->pen, 0, 1, 0);

	rnd_swap(csch_coord_t, u->font_height, u->pen->font_height);
	rnd_swap(char *, u->font_family, u->pen->font_family);
	rnd_swap(char *, u->font_style, u->pen->font_style);
	rnd_swap(int, u->copied_from_default, u->pen->copied_from_default);

	csch_pen_update_objs_for(sheet, &sheet->direct, u->pen);

	csch_sheet_set_changed(sheet, 1);

	return 0;
}

static void undo_pen_modify_font_print(void *udata, char *dst, size_t dst_len)
{
	undo_pen_modify_font_t *u = udata;
	rnd_snprintf(dst, dst_len, "pen font modification (in %s)", u->pen->name.str);
}

static void undo_pen_modify_font_free(void *udata)
{
	undo_pen_modify_font_t *u = udata;
	if (u->font_family != u->pen->font_family)
		free(u->font_family);
	if (u->font_style != u->pen->font_style)
		free(u->font_style);
}


static const uundo_oper_t undo_pen_modify_font = {
	core_cpen_cookie,
	undo_pen_modify_font_free,
	undo_pen_modify_font_swap,
	undo_pen_modify_font_swap,
	undo_pen_modify_font_print
};


void csch_pen_modify_font(csch_sheet_t *sheet, csch_cpen_t *pen, csch_coord_t *font_height, char **font_family, char **font_style, int undoable)
{
	undo_pen_modify_font_t utmp, *u = &utmp;

	if (undoable) u = uundo_append(&sheet->undo, &undo_pen_modify_font, sizeof(undo_pen_modify_font_t));

	u->pen = pen;
	u->font_height = CSCH_UNDO_MODIFY(pen, font_height);
	u->font_family = CSCH_UNDO_MODIFY(pen, font_family);
	u->font_style = CSCH_UNDO_MODIFY(pen, font_style);
	u->copied_from_default = 0; /* always reset */

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


void csch_pen_update_objs_for(csch_sheet_t *sheet, csch_cgrp_t *parent, csch_cpen_t *pen)
{
	htip_entry_t *e;
	for(e = htip_first(&parent->id2obj); e != NULL; e = htip_next(&parent->id2obj, e)) {
		csch_chdr_t *obj = e->value;
		if (!csch_obj_is_grp(obj)) {
			if (obj->stroke == pen) {
				if (obj->type == CSCH_CTYPE_TEXT) {
					((csch_text_t *)obj)->bbox_calced = 0;
					csch_cobj_update(sheet, obj, 0);
				}
				else
					csch_cobj_update(sheet, obj, 1); /* have to redo xform for the new pen */
			}
		}
		else
			csch_pen_update_objs_for(sheet, (csch_cgrp_t *)obj, pen);
	}
}
