/* Manage toolkits and their display.
 */

/*

	Copyright (C) 1991-2003 The National Gallery

	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

 */

/*

	These files are distributed with VIPS - http://www.vips.ecs.soton.ac.uk

 */

/*
#define DEBUG_VERBOSE
#define DEBUG_MENUS
#define DEBUG
#define DEBUG_TOOLITEM
 */

#include "nip4.h"

G_DEFINE_TYPE(Tool, tool, FILEMODEL_TYPE)

/* Largest string we let the user set for name/tip/etc.
 */
#define MAX_NAME (256)

void
tool_error(Tool *tool, VipsBuf *buf)
{
	if (tool->lineno != -1) {
		vips_buf_appends(buf, " (");
		if (FILEMODEL(tool->kit)->filename)
			vips_buf_appends(buf,
				FILEMODEL(tool->kit)->filename);
		else
			vips_buf_appends(buf, IOBJECT(tool->kit)->name);
		vips_buf_appendf(buf, ":%d)", tool->lineno);
	}
}

static void *
tool_linkreport_sym_sym(Symbol *child, Symbol *parent, VipsBuf *buf)
{
	/* Don't report generated syms eg. from lcomps or pattern
	 * matches.
	 */
	if (child->type == SYM_ZOMBIE &&
		!child->generated && !parent->generated &&
		!compile_resolve_top(child)) {
		symbol_name_error(parent, buf);
		vips_buf_appendf(buf, " ");

		/* used as in "fred refers to undefined symbol jim"
		 */
		vips_buf_appendf(buf, _("refers to undefined symbol"));
		vips_buf_appendf(buf, " ");
		symbol_qualified_name(child, buf);
		vips_buf_appendf(buf, "\n");
	}

	return NULL;
}

static void *
tool_linkreport_sym(Symbol *sym, VipsBuf *buf)
{
	if (sym->expr)
		return slist_map3(sym->expr->compile->children,
			(SListMap3Fn) tool_linkreport_sym_sym, sym, buf, NULL);

	return NULL;
}

void *
tool_linkreport_tool(Tool *tool, VipsBuf *buf)
{
	if (tool->type != TOOL_SYM)
		return NULL;

	return symbol_map_all(tool->sym,
		(symbol_map_fn) tool_linkreport_sym, buf, NULL);
}

static void
tool_finalize(GObject *gobject)
{
	Tool *tool;

#ifdef DEBUG
	printf("tool_finalize: %p %s\n",
		gobject, IOBJECT(gobject)->name);
#endif /*DEBUG*/

	g_return_if_fail(gobject != NULL);
	g_return_if_fail(IS_TOOL(gobject));

	tool = TOOL(gobject);

	VIPS_FREE(tool->help);

	G_OBJECT_CLASS(tool_parent_class)->finalize(gobject);
}

static void *toolitem_free(Toolitem *toolitem);

/* Remove a tool. Also strip the sym, if any.
 */
static void
tool_dispose(GObject *gobject)
{
	Tool *tool = TOOL(gobject);

#ifdef DEBUG
	printf("tool_dispose: destroying tool for ");
	if (tool->sym)
		symbol_name_print(tool->sym);
	else
		printf("anonymous-tool");
	printf("at addr %p\n", tool);
#endif /*DEBUG*/

	FREESID(tool->new_value_sid, tool->link_sym);

	/* Unlink from symbol and toolkit. This changes the kit - mark it as
	 * dirty.
	 */
	if (tool->sym) {
		Symbol *sym = tool->sym;

		sym->tool = NULL;
		tool->sym = NULL;

		symbol_strip(sym);

		/* Anything that referred to this symbol is going to need a
		 * recalc.
		 */
	}

	if (tool->kit)
		// don't set kit modified, we could be in parse ... set kit modified in
		// program window
		tool->kit = NULL;

	VIPS_FREEF(toolitem_free, tool->toolitem);

	G_OBJECT_CLASS(tool_parent_class)->dispose(gobject);
}

static View *
tool_view_new(Model *model, View *parent)
{
	return toolview_new();
}

/* Save a tool's definition to a file.
 */
static gboolean
tool_save_text(Model *model, iOpenFile *of)
{
	Tool *tool = TOOL(model);
	Symbol *sym = tool->sym;

	switch (tool->type) {
	case TOOL_SYM:
		if (sym->expr)
			if (!ifile_write(of,
					"%s;\n\n", sym->expr->compile->text))
				return FALSE;
		break;

	case TOOL_SEP:
		if (!ifile_write(of, "#separator\n\n"))
			return FALSE;
		break;

	default:
		g_assert(FALSE);
	}

	return TRUE;
}

static char *
tool_type_to_char(Tooltype type)
{
	switch (type) {
	case TOOL_SYM:
		return "symbol";

	case TOOL_SEP:
		return "separator";

	default:
		g_assert(FALSE);

		/* Keep gcc happy.
		 */
		return FALSE;
	}
}

static void
tool_info(iObject *iobject, VipsBuf *buf, int indent)
{
	Tool *tool = TOOL(iobject);

	IOBJECT_CLASS(tool_parent_class)->info(iobject, buf, indent);

	vips_buf_space(buf, indent);
	vips_buf_appendf(buf, "type = \"%s\"\n", tool_type_to_char(tool->type));

	if (tool->type == TOOL_SYM) {
		vips_buf_space(buf, indent);
		vips_buf_appendf(buf, "symbol = \"%s\"\n", IOBJECT(tool->sym)->name);
	}

	if (tool->lineno != -1) {
		vips_buf_space(buf, indent);
		vips_buf_appendf(buf, "lineno = %d\n", tool->lineno);
	}
	if (tool->kit) {
		vips_buf_space(buf, indent);
		vips_buf_appendf(buf, "toolkit = \"%s\"\n", IOBJECT(tool->kit)->name);
	}
}

static void
tool_parent_add(iContainer *child)
{
	Tool *tool = TOOL(child);
	Toolkit *kit = TOOLKIT(child->parent);

	tool->kit = kit;

	ICONTAINER_CLASS(tool_parent_class)->parent_add(child);
}

static void
tool_class_init(ToolClass *class)
{
	GObjectClass *gobject_class = (GObjectClass *) class;
	iObjectClass *iobject_class = (iObjectClass *) class;
	iContainerClass *icontainer_class = (iContainerClass *) class;
	ModelClass *model_class = (ModelClass *) class;

	/* Create signals.
	 */

	/* Init methods.
	 */
	gobject_class->finalize = tool_finalize;
	gobject_class->dispose = tool_dispose;

	iobject_class->info = tool_info;

	icontainer_class->parent_add = tool_parent_add;

	model_class->view_new = tool_view_new;
	model_class->save_text = tool_save_text;
}

static void
tool_init(Tool *tool)
{
	tool->type = TOOL_SEP;
	tool->sym = NULL;
	tool->kit = NULL;
	tool->lineno = -1;
}

/* Add a tool to a toolkit.
 */
static void
tool_link(Tool *tool, Toolkit *kit, int pos, const char *name)
{
#ifdef DEBUG
	printf("tool_link: %s\n", name);
#endif /*DEBUG*/

	// don't set kit modifed, we could be in load ... kit modified should be
	// set during tool creation
	iobject_set(IOBJECT(tool), name, NULL);
	icontainer_child_add(ICONTAINER(kit), ICONTAINER(tool), pos);
}

static void *
toolitem_free(Toolitem *toolitem)
{
	Toolitem *parent = toolitem->parent;

#ifdef DEBUG_TOOLITEM
	printf("toolitem_free: %s\n", toolitem->name);
#endif /*DEBUG_TOOLITEM*/

	slist_map(toolitem->children, (SListMapFn) toolitem_free, NULL);

	g_assert(!toolitem->children);

	if (parent) {
		parent->children = g_slist_remove(parent->children, toolitem);
		toolitem->parent = NULL;
	}

	VIPS_FREE(toolitem->label);
	VIPS_FREE(toolitem->name);
	VIPS_FREE(toolitem->icon);
	VIPS_FREE(toolitem->tooltip);
	VIPS_FREE(toolitem->help);
	VIPS_FREE(toolitem->action);
	VIPS_FREE(toolitem->path);
	VIPS_FREE(toolitem->user_path);
	VIPS_FREE(toolitem);

	return NULL;
}

static Toolitem *
toolitem_new(Toolitem *parent, Compile *compile, Tool *tool)
{
	Toolitem *toolitem;

	if (!(toolitem = INEW(NULL, Toolitem)))
		return NULL;
	toolitem->compile = compile;
	toolitem->tool = tool;
	toolitem->action_sym = NULL;

	toolitem->is_separator = FALSE;
	toolitem->is_pullright = FALSE;
	toolitem->children = NULL;
	toolitem->parent = parent;
	toolitem->is_action = FALSE;

	toolitem->label = NULL;
	toolitem->name = NULL;
	toolitem->icon = NULL;
	toolitem->tooltip = NULL;
	toolitem->help = NULL;
	toolitem->action = NULL;
	toolitem->path = NULL;
	toolitem->user_path = NULL;

	if (parent)
		parent->children =
			g_slist_append(parent->children, toolitem);

	return toolitem;
}

/* Set label & name & icon.

	FIXME ... we will do repeated heap_is_instanceof() during item build,
	do it once and set a flag instead

 */
static void
toolitem_set_name(Toolitem *toolitem, PElement *root)
{
	gboolean result;
	char value[MAX_NAME];
	int i;

	if (root &&
		heap_is_instanceof(CLASS_MENUITEM, root, &result) &&
		result) {
		if (class_get_member_string(root,
				MEMBER_LABEL, value, MAX_NAME)) {
			char *p, *q;

			/* Save the i18n-ed version.
			 */
			VIPS_SETSTR(toolitem->label, _(value));

			/* Strip underscores (they mark mnemonics). Can't use
			 * strrcpy() or memccpy(), we have overlapping blocks.
			 */
			g_strlcpy(value, toolitem->label, MAX_NAME);
			for (p = q = value; *p; p++)
				if (*p != '_')
					*q++ = *p;
			*q = '\0';
			VIPS_SETSTR(toolitem->name, value);
		}

		if (class_get_member_string(root,
				MEMBER_ICON, value, MAX_NAME))
			VIPS_SETSTR(toolitem->icon, value);
	}
	else {
		/* Remove underscores from the object name ... we don't want
		 * them to be mnemonics.
		 */
		g_strlcpy(value,
			IOBJECT(toolitem->compile->sym)->name, MAX_NAME);
		for (i = 0; value[i]; i++)
			if (value[i] == '_')
				value[i] = ' ';

		VIPS_SETSTR(toolitem->label, value);
		VIPS_SETSTR(toolitem->name, toolitem->label);
	}

	if (root &&
		heap_is_instanceof(CLASS_MENUSEPARATOR, root, &result) &&
		result)
		toolitem->is_separator = TRUE;
}

static void
toolitem_set_tooltip(Toolitem *toolitem, PElement *root)
{
	gboolean result;
	char value[MAX_NAME];

	if (root &&
		heap_is_instanceof(CLASS_MENUITEM, root, &result) &&
		result &&
		class_get_member_string(root, MEMBER_TOOLTIP, value, MAX_NAME)) {
		VIPS_SETSTR(toolitem->tooltip, _(value));
	}
	else if (toolitem->tool &&
		toolitem->tool->help)
		VIPS_SETSTR(toolitem->tooltip, toolitem->tool->help);
}

static void
toolitem_set_pullright(Toolitem *toolitem, PElement *root)
{
	gboolean result;

	/* New-style pullright?
	 */
	if (root &&
		heap_is_instanceof(CLASS_MENUPULLRIGHT, root, &result) &&
		result)
		toolitem->is_pullright = TRUE;
	/* Old-style pullright?
	 */
	else if (is_value(toolitem->compile->sym) &&
		is_class(toolitem->compile) &&
		!toolitem->compile->has_super &&
		toolitem->compile->nparam == 0)
		toolitem->is_pullright = TRUE;
}

static void
toolitem_set_action(Toolitem *toolitem, PElement *root)
{
	gboolean result;
	char txt[256];
	VipsBuf buf = VIPS_BUF_STATIC(txt);

	if (toolitem->parent)
		vips_buf_appendf(&buf, "%s.", toolitem->parent->action);
	vips_buf_appendf(&buf, "%s", IOBJECT(toolitem->compile->sym)->name);

	/* If this is a Menuaction, we need the action member.
	 */
	if (root &&
		heap_is_instanceof(CLASS_MENUACTION, root, &result) &&
		result) {
		PElement out;

		toolitem->is_action = TRUE;
		(void) class_get_member(root,
			MEMBER_ACTION, &toolitem->action_sym, &out);
	}

	/* If there's an action member, use that.
	 */
	if (toolitem->is_action)
		vips_buf_appends(&buf, "." MEMBER_ACTION);

	VIPS_SETSTR(toolitem->action, vips_buf_all(&buf));

	/* No action member found and this is an item (ie. not a pullright)?
	 * Default to the sym itself.
	 */
	if (!toolitem->action_sym && !toolitem->is_pullright)
		toolitem->action_sym = toolitem->compile->sym;
}

static void
toolitem_set_path(Toolitem *toolitem)
{
	char txt[256];
	VipsBuf buf = VIPS_BUF_STATIC(txt);

	if (toolitem->parent)
		vips_buf_appendf(&buf, "%s", toolitem->parent->path);
	else
		vips_buf_appendf(&buf, "<mainw>/Toolkits/%s",
			IOBJECT(toolitem->tool->kit)->name);
	vips_buf_appendf(&buf, "/%s", toolitem->name);
	VIPS_SETSTR(toolitem->path, vips_buf_all(&buf));
}

static void
toolitem_set_user_path(Toolitem *toolitem)
{
	char txt[256];
	VipsBuf buf = VIPS_BUF_STATIC(txt);

	if (toolitem->parent)
		vips_buf_appends(&buf, toolitem->parent->user_path);
	else
		vips_buf_appends(&buf, IOBJECT(toolitem->tool->kit)->name);
	vips_buf_appendf(&buf, " > %s", toolitem->name);
	VIPS_SETSTR(toolitem->user_path, vips_buf_all(&buf));
}

static void *
toolitem_set_help_sub(Symbol *param, VipsBuf *buf)
{
	vips_buf_appends(buf, " ");
	vips_buf_appends(buf, IOBJECT(param)->name);

	return NULL;
}

static void
toolitem_set_help(Toolitem *toolitem)
{
	char txt[256];
	VipsBuf buf = VIPS_BUF_STATIC(txt);

	vips_buf_appends(&buf, toolitem->name);

	/* Get the params from the action member if we can.
	 */
	if (toolitem->action_sym &&
		toolitem->action_sym->expr &&
		toolitem->action_sym->expr->compile->param)
		slist_map(toolitem->action_sym->expr->compile->param,
			(SListMapFn) toolitem_set_help_sub, &buf);

	vips_buf_appends(&buf, ": ");
	if (toolitem->tooltip)
		vips_buf_appends(&buf, toolitem->tooltip);
	VIPS_SETSTR(toolitem->help, vips_buf_firstline(&buf));
}

static Toolitem *
toolitem_build(Tool *tool,
	Compile *compile, PElement *root, Toolitem *parent)
{
	Toolitem *toolitem;

	if (!(toolitem = toolitem_new(parent, compile, tool)))
		return NULL;

	toolitem_set_name(toolitem, root);
	toolitem_set_tooltip(toolitem, root);
	toolitem_set_pullright(toolitem, root);
	toolitem_set_action(toolitem, root);
	toolitem_set_path(toolitem);
	toolitem_set_user_path(toolitem);
	toolitem_set_help(toolitem);

#ifdef DEBUG_TOOLITEM
	printf("toolitem_build: %s\n", toolitem->name);
#endif /*DEBUG_TOOLITEM*/

#ifdef DEBUG_VERBOSE
	printf("toolitem_build:\n");
	printf("\tpullright = %d\n", toolitem->is_pullright);
	printf("\tlabel = \"%s\"\n", toolitem->label);
	printf("\tname = \"%s\"\n", toolitem->name);
	printf("\ticon = \"%s\"\n", toolitem->icon);
	printf("\ttooltip = \"%s\"\n", toolitem->tooltip);
	printf("\thelp = \"%s\"\n", toolitem->help);
	printf("\taction = \"%s\"\n", toolitem->action);
	printf("\tpath = \"%s\"\n", toolitem->path);
	printf("\tuser_path = \"%s\"\n", toolitem->user_path);
#endif /*DEBUG_VERBOSE*/

	return toolitem;
}

static Toolitem *
toolitem_build_all(Tool *tool, Compile *compile, PElement *root,
	Toolitem *parent);

static void *
toolitem_build_all_sub(Symbol *sym, Toolitem *parent)
{
	if (is_menuable(sym))
		(void) toolitem_build_all(parent->tool, sym->expr->compile,
			NULL, parent);

	return NULL;
}

static Toolitem *
toolitem_build_all(Tool *tool, Compile *compile, PElement *root,
	Toolitem *parent)
{
	Toolitem *toolitem;
	gboolean result;

	if (!(toolitem = toolitem_build(tool, compile, root, parent)))
		return NULL;

	/* If this is a dynamic pullright, walk the heap to find the members.
	 */
	if (toolitem->is_pullright && root &&
		heap_is_instanceof(CLASS_MENUPULLRIGHT, root, &result) &&
		result) {
		PElement member;
		HeapNode *p;

		PEGETCLASSMEMBER(&member, root);

		if (PEISNODE(&member))
			for (p = PEGETVAL(&member); p; p = GETRIGHT(p)) {
				PElement s, v;
				HeapNode *hn;
				Symbol *sym;

				/* Get the sym/value pair.
				 */
				hn = GETLEFT(p);
				PEPOINTLEFT(hn, &s);
				PEPOINTRIGHT(hn, &v);
				sym = SYMBOL(PEGETSYMREF(&s));

				/* Ignore this/super/check etc.
				 */
				if (!is_menuable(sym))
					continue;

				/* For dynamic menus, only make items for
				 * things which are subclasses of menu.
				 */
				if (!heap_is_instanceof(CLASS_MENU,
						&v, &result) ||
					!result)
					continue;

				(void) toolitem_build_all(tool,
					sym->expr->compile, &v, toolitem);
			}
	}
	else if (toolitem->is_pullright) {
		/* A static pullright... just walk the container.
		 */
		(void) icontainer_map(ICONTAINER(compile),
			(icontainer_map_fn) toolitem_build_all_sub,
			toolitem, NULL);
	}

	return toolitem;
}

#ifdef DEBUG_MENUS
static void
toolitem_print(Toolitem *toolitem)
{
	if (toolitem->is_separator)
		printf("-----------\n");
	else
		printf("%s --- %s\n", toolitem->user_path, toolitem->help);
}

static void *
toolitem_print_all(Toolitem *toolitem)
{
	if (toolitem->is_pullright)
		slist_map(toolitem->children,
			(SListMapFn) toolitem_print_all, NULL);
	else
		toolitem_print(toolitem);

	return NULL;
}
#endif /*DEBUG_MENUS*/

static void
tool_set_help(Tool *tool)
{
	char *p;
	char value[MAX_NAME];

	if (tool->sym &&
		tool->sym->expr &&
		tool->sym->expr->compile &&
		(p = tool->sym->expr->compile->text)) {
		/* Skip leading whitespace.
		 */
		while (isspace((int) (*p)))
			p++;

		/* Skip leading comment, if any.
		 */
		if (p[0] == '/' && p[1] == '*')
			p += 2;
		else if (p[0] == '/' && p[1] == '/')
			p += 2;

		/* Skip more whitespace.
		 */
		while (isspace((int) (*p)))
			p++;

		/* Limit to MAX_NAME chars or 1st line. Strip trailing
		 * whitespace.
		 */
		g_strlcpy(value, p, MAX_NAME);
		if ((p = strchr(value, '\n')))
			*p = '\0';
		*((char *) my_strrspn(value, WHITESPACE)) = '\0';

		VIPS_SETSTR(tool->help, value);
	}
	else if (tool->sym &&
		tool->sym->type == SYM_BUILTIN)
		VIPS_SETSTR(tool->help, g_strdup_printf("%s: %s",
				IOBJECT(tool->sym)->name,
				tool->sym->builtin->desc));
	else
		VIPS_SETSTR(tool->help, NULL);
}

/* Rebuild the toolitem tree.
 */
static void
tool_toolitem_rebuild(Tool *tool)
{
	VIPS_FREEF(toolitem_free, tool->toolitem);

	switch (tool->type) {
	case TOOL_SYM:
		if (is_menuable(tool->sym))
			tool->toolitem = toolitem_build_all(tool,
				tool->sym->expr->compile,
				&tool->sym->expr->root,
				NULL);
		break;

	case TOOL_SEP:
		if ((tool->toolitem = toolitem_new(NULL, NULL, tool)))
			tool->toolitem->is_separator = TRUE;
		break;

	default:
		g_assert(0);
	}

	iobject_changed(IOBJECT(tool));

#ifdef DEBUG_MENUS
	if (tool->toolitem)
		toolitem_print_all(tool->toolitem);
#endif /*DEBUG_MENUS*/
}

/* The expr has a new value.
 */
static void
tool_new_value_cb(Symbol *sym, Tool *tool)
{
#ifdef DEBUG
	printf("tool_new_value_cb: new value for ");
	symbol_name_print(sym);
	printf("\n");
#endif /*DEBUG*/

	tool_toolitem_rebuild(tool);
}

/* Add a symbol to a toolkit.
 */
Tool *
tool_new_sym(Toolkit *kit, int pos, Symbol *sym)
{
	Tool *tool;

	g_assert(kit && sym);

	/* Is there a tool we can reuse? Don't update pos .. assume we want to
	 * keep the old one.
	 */
	if ((tool = sym->tool) &&
		tool->kit == kit) {
		tool->lineno = -1;
		tool_set_help(tool);
		return tool;
	}

	/* Junk any existing tool for this sym.
	 */
	if ((tool = sym->tool)) {
		sym->tool = NULL;
		tool->sym = NULL;

		IDESTROY(tool);
	}

	tool = TOOL(g_object_new(TOOL_TYPE, NULL));
	tool->type = TOOL_SYM;
	tool->sym = sym;
	sym->tool = tool;
	tool->new_value_sid = g_signal_connect(sym, "new_value",
		G_CALLBACK(tool_new_value_cb), tool);
	tool->link_sym = sym;
	tool_link(tool, kit, pos, IOBJECT(sym)->name);

	tool_set_help(tool);

#ifdef DEBUG
	printf("tool_new_sym: new tool for ");
	symbol_name_print(sym);
	printf("at %p\n", tool);
#endif /*DEBUG*/

	return tool;
}

/* Add a separator to a toolkit.
 */
Tool *
tool_new_sep(Toolkit *kit, int pos)
{
	Tool *tool;

	g_assert(kit);

	tool = TOOL(g_object_new(TOOL_TYPE, NULL));
	tool->type = TOOL_SEP;
	iobject_set(IOBJECT(tool), "separator", NULL);
	tool_link(tool, kit, pos, NULL);
	tool_toolitem_rebuild(tool);

	return tool;
}

static Toolitem *
toolitem_lookup_toolitem(Toolitem *toolitem, Symbol *action)
{
	if (toolitem->action_sym == action)
		return toolitem;
	else
		return (Toolitem *) slist_map(toolitem->children,
			(SListMapFn) toolitem_lookup_toolitem, action);
}

static Toolitem *
toolitem_lookup_tool(Tool *tool, Symbol *action)
{
	if (tool->toolitem)
		return toolitem_lookup_toolitem(tool->toolitem, action);
	else
		return NULL;
}

static Toolitem *
toolitem_lookup_toolkit(Toolkit *kit, Symbol *action)
{
	return (Toolitem *) toolkit_map(kit,
		(tool_map_fn) toolitem_lookup_tool, action, NULL);
}

/* Just walk the whole kit. Could use a hash in kitg, but we don't call this
 * so often.
 */
Toolitem *
toolitem_lookup(Toolkitgroup *kitg, Symbol *action)
{
	return (Toolitem *) toolkitgroup_map(kitg,
		(toolkit_map_fn) toolitem_lookup_toolkit, action, NULL);
}
