/*
 * ***** BEGIN GPL LICENSE BLOCK *****
 *
 * 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.
 *
 * The Original Code is Copyright (C) 2005 Blender Foundation.
 * All rights reserved.
 *
 * The Original Code is: all of this file.
 *
 * Contributor(s): David Millan Escriva, Juho Vepsäläinen, Nathan Letwory
 *
 * ***** END GPL LICENSE BLOCK *****
 */

/** \file blender/editors/space_node/node_group.c
 *  \ingroup spnode
 */

#include <stdlib.h>

#include "MEM_guardedalloc.h"

#include "DNA_node_types.h"
#include "DNA_object_types.h"
#include "DNA_anim_types.h"

#include "BLI_listbase.h"
#include "BLI_string.h"
#include "BLI_rect.h"
#include "BLI_math.h"

#include "BKE_action.h"
#include "BKE_animsys.h"
#include "BKE_context.h"
#include "BKE_global.h"
#include "BKE_library.h"
#include "BKE_main.h"
#include "BKE_report.h"

#include "ED_node.h"  /* own include */
#include "ED_screen.h"
#include "ED_render.h"

#include "RNA_access.h"
#include "RNA_define.h"
#include "RNA_enum_types.h"

#include "WM_api.h"
#include "WM_types.h"

#include "UI_resources.h"

#include "node_intern.h"  /* own include */
#include "NOD_socket.h"

static EnumPropertyItem socket_in_out_items[] = {
	{ SOCK_IN, "SOCK_IN", 0, "Input", "" },
	{ SOCK_OUT, "SOCK_OUT", 0, "Output", "" },
	{ 0, NULL, 0, NULL, NULL },
};

/* ***************** Edit Group operator ************* */

void snode_make_group_editable(SpaceNode *snode, bNode *gnode)
{
	bNode *node;

	/* make sure nothing has group editing on */
	for (node = snode->nodetree->nodes.first; node; node = node->next) {
		nodeGroupEditClear(node);

		/* while we're here, clear texture active */
		if (node->typeinfo->nclass == NODE_CLASS_TEXTURE) {
			/* this is not 100% sure to be reliable, see comment on the flag */
			node->flag &= ~NODE_ACTIVE_TEXTURE;
		}
	}

	if (gnode == NULL) {
		/* with NULL argument we do a toggle */
		if (snode->edittree == snode->nodetree)
			gnode = nodeGetActive(snode->nodetree);
	}

	if (gnode) {
		snode->edittree = nodeGroupEditSet(gnode, 1);

		/* deselect all other nodes, so we can also do grabbing of entire subtree */
		for (node = snode->nodetree->nodes.first; node; node = node->next) {
			node_deselect(node);

			if (node->typeinfo->nclass == NODE_CLASS_TEXTURE) {
				/* this is not 100% sure to be reliable, see comment on the flag */
				node->flag &= ~NODE_ACTIVE_TEXTURE;
			}
		}
		node_select(gnode);
	}
	else
		snode->edittree = snode->nodetree;
}

static int node_group_edit_exec(bContext *C, wmOperator *UNUSED(op))
{
	SpaceNode *snode = CTX_wm_space_node(C);

	ED_preview_kill_jobs(C);

	if (snode->nodetree == snode->edittree) {
		bNode *gnode = nodeGetActive(snode->edittree);
		snode_make_group_editable(snode, gnode);
	}
	else
		snode_make_group_editable(snode, NULL);

	WM_event_add_notifier(C, NC_SCENE | ND_NODES, NULL);

	return OPERATOR_FINISHED;
}

static int node_group_edit_invoke(bContext *C, wmOperator *op, wmEvent *UNUSED(event))
{
	SpaceNode *snode = CTX_wm_space_node(C);
	bNode *gnode;

	/* XXX callback? */
	if (snode->nodetree == snode->edittree) {
		gnode = nodeGetActive(snode->edittree);
		if (gnode && gnode->id && GS(gnode->id->name) == ID_NT && gnode->id->lib) {
			uiPupMenuOkee(C, op->type->idname, "Make group local?");
			return OPERATOR_CANCELLED;
		}
	}

	return node_group_edit_exec(C, op);
}

void NODE_OT_group_edit(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Edit Group";
	ot->description = "Edit node group";
	ot->idname = "NODE_OT_group_edit";

	/* api callbacks */
	ot->invoke = node_group_edit_invoke;
	ot->exec = node_group_edit_exec;
	ot->poll = ED_operator_node_active;

	/* flags */
	ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}

/* ***************** Add Group Socket operator ************* */

static int node_group_socket_add_exec(bContext *C, wmOperator *op)
{
	SpaceNode *snode = CTX_wm_space_node(C);
	int in_out = -1;
	char name[MAX_NAME] = "";
	int type = SOCK_FLOAT;
	bNodeTree *ngroup = snode->edittree;
	/* bNodeSocket *sock; */ /* UNUSED */

	ED_preview_kill_jobs(C);

	if (RNA_struct_property_is_set(op->ptr, "name"))
		RNA_string_get(op->ptr, "name", name);

	if (RNA_struct_property_is_set(op->ptr, "type"))
		type = RNA_enum_get(op->ptr, "type");

	if (RNA_struct_property_is_set(op->ptr, "in_out"))
		in_out = RNA_enum_get(op->ptr, "in_out");
	else
		return OPERATOR_CANCELLED;

	/* using placeholder subtype first */
	/* sock = */ /* UNUSED */ node_group_add_socket(ngroup, name, type, in_out);

	ntreeUpdateTree(ngroup);

	snode_notify(C, snode);

	return OPERATOR_FINISHED;
}

void NODE_OT_group_socket_add(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Add Group Socket";
	ot->description = "Add node group socket";
	ot->idname = "NODE_OT_group_socket_add";

	/* api callbacks */
	ot->exec = node_group_socket_add_exec;
	ot->poll = ED_operator_node_active;

	/* flags */
	ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

	RNA_def_enum(ot->srna, "in_out", socket_in_out_items, SOCK_IN, "Socket Type", "Input or Output");
	RNA_def_string(ot->srna, "name", "", MAX_NAME, "Name", "Group socket name");
	RNA_def_enum(ot->srna, "type", node_socket_type_items, SOCK_FLOAT, "Type", "Type of the group socket");
}

/* ***************** Remove Group Socket operator ************* */

static int node_group_socket_remove_exec(bContext *C, wmOperator *op)
{
	SpaceNode *snode = CTX_wm_space_node(C);
	int index = -1;
	int in_out = -1;
	bNodeTree *ngroup = snode->edittree;
	bNodeSocket *sock;

	ED_preview_kill_jobs(C);

	if (RNA_struct_property_is_set(op->ptr, "index"))
		index = RNA_int_get(op->ptr, "index");
	else
		return OPERATOR_CANCELLED;

	if (RNA_struct_property_is_set(op->ptr, "in_out"))
		in_out = RNA_enum_get(op->ptr, "in_out");
	else
		return OPERATOR_CANCELLED;

	sock = (bNodeSocket *)BLI_findlink(in_out == SOCK_IN ? &ngroup->inputs : &ngroup->outputs, index);
	if (sock) {
		node_group_remove_socket(ngroup, sock, in_out);
		ntreeUpdateTree(ngroup);

		snode_notify(C, snode);
	}

	return OPERATOR_FINISHED;
}

void NODE_OT_group_socket_remove(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Remove Group Socket";
	ot->description = "Remove a node group socket";
	ot->idname = "NODE_OT_group_socket_remove";

	/* api callbacks */
	ot->exec = node_group_socket_remove_exec;
	ot->poll = ED_operator_node_active;

	/* flags */
	ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

	RNA_def_int(ot->srna, "index", 0, 0, INT_MAX, "Index", "", 0, INT_MAX);
	RNA_def_enum(ot->srna, "in_out", socket_in_out_items, SOCK_IN, "Socket Type", "Input or Output");
}

/* ***************** Move Group Socket Up operator ************* */

static int node_group_socket_move_up_exec(bContext *C, wmOperator *op)
{
	SpaceNode *snode = CTX_wm_space_node(C);
	int index = -1;
	int in_out = -1;
	bNodeTree *ngroup = snode->edittree;
	bNodeSocket *sock, *prev;

	ED_preview_kill_jobs(C);

	if (RNA_struct_property_is_set(op->ptr, "index"))
		index = RNA_int_get(op->ptr, "index");
	else
		return OPERATOR_CANCELLED;

	if (RNA_struct_property_is_set(op->ptr, "in_out"))
		in_out = RNA_enum_get(op->ptr, "in_out");
	else
		return OPERATOR_CANCELLED;

	/* swap */
	if (in_out == SOCK_IN) {
		sock = (bNodeSocket *)BLI_findlink(&ngroup->inputs, index);
		prev = sock->prev;
		/* can't move up the first socket */
		if (!prev)
			return OPERATOR_CANCELLED;
		BLI_remlink(&ngroup->inputs, sock);
		BLI_insertlinkbefore(&ngroup->inputs, prev, sock);

		ngroup->update |= NTREE_UPDATE_GROUP_IN;
	}
	else if (in_out == SOCK_OUT) {
		sock = (bNodeSocket *)BLI_findlink(&ngroup->outputs, index);
		prev = sock->prev;
		/* can't move up the first socket */
		if (!prev)
			return OPERATOR_CANCELLED;
		BLI_remlink(&ngroup->outputs, sock);
		BLI_insertlinkbefore(&ngroup->outputs, prev, sock);

		ngroup->update |= NTREE_UPDATE_GROUP_OUT;
	}
	ntreeUpdateTree(ngroup);

	snode_notify(C, snode);

	return OPERATOR_FINISHED;
}

void NODE_OT_group_socket_move_up(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Move Group Socket Up";
	ot->description = "Move up node group socket";
	ot->idname = "NODE_OT_group_socket_move_up";

	/* api callbacks */
	ot->exec = node_group_socket_move_up_exec;
	ot->poll = ED_operator_node_active;

	/* flags */
	ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

	RNA_def_int(ot->srna, "index", 0, 0, INT_MAX, "Index", "", 0, INT_MAX);
	RNA_def_enum(ot->srna, "in_out", socket_in_out_items, SOCK_IN, "Socket Type", "Input or Output");
}

/* ***************** Move Group Socket Up operator ************* */

static int node_group_socket_move_down_exec(bContext *C, wmOperator *op)
{
	SpaceNode *snode = CTX_wm_space_node(C);
	int index = -1;
	int in_out = -1;
	bNodeTree *ngroup = snode->edittree;
	bNodeSocket *sock, *next;

	ED_preview_kill_jobs(C);

	if (RNA_struct_property_is_set(op->ptr, "index"))
		index = RNA_int_get(op->ptr, "index");
	else
		return OPERATOR_CANCELLED;

	if (RNA_struct_property_is_set(op->ptr, "in_out"))
		in_out = RNA_enum_get(op->ptr, "in_out");
	else
		return OPERATOR_CANCELLED;

	/* swap */
	if (in_out == SOCK_IN) {
		sock = (bNodeSocket *)BLI_findlink(&ngroup->inputs, index);
		next = sock->next;
		/* can't move down the last socket */
		if (!next)
			return OPERATOR_CANCELLED;
		BLI_remlink(&ngroup->inputs, sock);
		BLI_insertlinkafter(&ngroup->inputs, next, sock);

		ngroup->update |= NTREE_UPDATE_GROUP_IN;
	}
	else if (in_out == SOCK_OUT) {
		sock = (bNodeSocket *)BLI_findlink(&ngroup->outputs, index);
		next = sock->next;
		/* can't move down the last socket */
		if (!next)
			return OPERATOR_CANCELLED;
		BLI_remlink(&ngroup->outputs, sock);
		BLI_insertlinkafter(&ngroup->outputs, next, sock);

		ngroup->update |= NTREE_UPDATE_GROUP_OUT;
	}
	ntreeUpdateTree(ngroup);

	snode_notify(C, snode);

	return OPERATOR_FINISHED;
}

void NODE_OT_group_socket_move_down(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Move Group Socket Down";
	ot->description = "Move down node group socket";
	ot->idname = "NODE_OT_group_socket_move_down";

	/* api callbacks */
	ot->exec = node_group_socket_move_down_exec;
	ot->poll = ED_operator_node_active;

	/* flags */
	ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

	RNA_def_int(ot->srna, "index", 0, 0, INT_MAX, "Index", "", 0, INT_MAX);
	RNA_def_enum(ot->srna, "in_out", socket_in_out_items, SOCK_IN, "Socket Type", "Input or Output");
}

/* ******************** Ungroup operator ********************** */

/* returns 1 if its OK */
static int node_group_ungroup(bNodeTree *ntree, bNode *gnode)
{
	bNodeLink *link, *linkn;
	bNode *node, *nextn;
	bNodeTree *ngroup, *wgroup;
	ListBase anim_basepaths = {NULL, NULL};

	ngroup = (bNodeTree *)gnode->id;
	if (ngroup == NULL) return 0;

	/* clear new pointers, set in copytree */
	for (node = ntree->nodes.first; node; node = node->next)
		node->new_node = NULL;

	/* wgroup is a temporary copy of the NodeTree we're merging in
	 * - all of wgroup's nodes are transferred across to their new home
	 * - ngroup (i.e. the source NodeTree) is left unscathed
	 * - temp copy. don't change ID usercount
	 */
	wgroup = ntreeCopyTree_ex(ngroup, FALSE);

	/* add the nodes into the ntree */
	for (node = wgroup->nodes.first; node; node = nextn) {
		nextn = node->next;

		/* keep track of this node's RNA "base" path (the part of the path identifying the node)
		 * if the old nodetree has animation data which potentially covers this node
		 */
		if (wgroup->adt) {
			PointerRNA ptr;
			char *path;

			RNA_pointer_create(&wgroup->id, &RNA_Node, node, &ptr);
			path = RNA_path_from_ID_to_struct(&ptr);

			if (path)
				BLI_addtail(&anim_basepaths, BLI_genericNodeN(path));
		}

		/* migrate node */
		BLI_remlink(&wgroup->nodes, node);
		BLI_addtail(&ntree->nodes, node);

		/* ensure unique node name in the nodee tree */
		nodeUniqueName(ntree, node);

		if (!node->parent) {
			node->locx += gnode->locx;
			node->locy += gnode->locy;
		}

		node->flag |= NODE_SELECT;
	}

	/* restore external links to and from the gnode */
	for (link = ntree->links.first; link; link = link->next) {
		if (link->fromnode == gnode) {
			if (link->fromsock->groupsock) {
				bNodeSocket *gsock = link->fromsock->groupsock;
				if (gsock->link) {
					if (gsock->link->fromnode) {
						/* NB: using the new internal copies here! the groupsock pointer still maps to the old tree */
						link->fromnode = (gsock->link->fromnode ? gsock->link->fromnode->new_node : NULL);
						link->fromsock = gsock->link->fromsock->new_sock;
					}
					else {
						/* group output directly maps to group input */
						bNodeSocket *insock = node_group_find_input(gnode, gsock->link->fromsock);
						if (insock->link) {
							link->fromnode = insock->link->fromnode;
							link->fromsock = insock->link->fromsock;
						}
					}
				}
				else {
					/* copy the default input value from the group socket default to the external socket */
					node_socket_convert_default_value(link->tosock->type, link->tosock->default_value, gsock->type, gsock->default_value);
				}
			}
		}
	}
	/* remove internal output links, these are not used anymore */
	for (link = wgroup->links.first; link; link = linkn) {
		linkn = link->next;
		if (!link->tonode)
			nodeRemLink(wgroup, link);
	}
	/* restore links from internal nodes */
	for (link = wgroup->links.first; link; link = linkn) {
		linkn = link->next;
		/* indicates link to group input */
		if (!link->fromnode) {
			/* NB: can't use find_group_node_input here,
			 * because gnode sockets still point to the old tree!
			 */
			bNodeSocket *insock;
			for (insock = gnode->inputs.first; insock; insock = insock->next)
				if (insock->groupsock->new_sock == link->fromsock)
					break;
			if (insock->link) {
				link->fromnode = insock->link->fromnode;
				link->fromsock = insock->link->fromsock;
			}
			else {
				/* copy the default input value from the group node socket default to the internal socket */
				node_socket_convert_default_value(link->tosock->type, link->tosock->default_value, insock->type, insock->default_value);
				nodeRemLink(wgroup, link);
			}
		}
	}

	/* add internal links to the ntree */
	for (link = wgroup->links.first; link; link = linkn) {
		linkn = link->next;
		BLI_remlink(&wgroup->links, link);
		BLI_addtail(&ntree->links, link);
	}

	/* and copy across the animation,
	 * note that the animation data's action can be NULL here */
	if (wgroup->adt) {
		LinkData *ld, *ldn = NULL;
		bAction *waction;

		/* firstly, wgroup needs to temporary dummy action that can be destroyed, as it shares copies */
		waction = wgroup->adt->action = BKE_action_copy(wgroup->adt->action);

		/* now perform the moving */
		BKE_animdata_separate_by_basepath(&wgroup->id, &ntree->id, &anim_basepaths);

		/* paths + their wrappers need to be freed */
		for (ld = anim_basepaths.first; ld; ld = ldn) {
			ldn = ld->next;

			MEM_freeN(ld->data);
			BLI_freelinkN(&anim_basepaths, ld);
		}

		/* free temp action too */
		if (waction) {
			BKE_libblock_free(&G.main->action, waction);
		}
	}

	/* delete the group instance. this also removes old input links! */
	nodeFreeNode(ntree, gnode);

	/* free the group tree (takes care of user count) */
	BKE_libblock_free(&G.main->nodetree, wgroup);

	ntree->update |= NTREE_UPDATE_NODES | NTREE_UPDATE_LINKS;

	return 1;
}

static int node_group_ungroup_exec(bContext *C, wmOperator *op)
{
	SpaceNode *snode = CTX_wm_space_node(C);
	bNode *gnode;

	ED_preview_kill_jobs(C);

	/* are we inside of a group? */
	gnode = node_tree_get_editgroup(snode->nodetree);
	if (gnode)
		snode_make_group_editable(snode, NULL);

	gnode = nodeGetActive(snode->edittree);
	if (gnode == NULL)
		return OPERATOR_CANCELLED;

	if (gnode->type != NODE_GROUP) {
		BKE_report(op->reports, RPT_WARNING, "Not a group");
		return OPERATOR_CANCELLED;
	}
	else if (node_group_ungroup(snode->nodetree, gnode)) {
		ntreeUpdateTree(snode->nodetree);
	}
	else {
		BKE_report(op->reports, RPT_WARNING, "Cannot ungroup");
		return OPERATOR_CANCELLED;
	}

	snode_notify(C, snode);
	snode_dag_update(C, snode);

	return OPERATOR_FINISHED;
}

void NODE_OT_group_ungroup(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Ungroup";
	ot->description = "Ungroup selected nodes";
	ot->idname = "NODE_OT_group_ungroup";

	/* api callbacks */
	ot->exec = node_group_ungroup_exec;
	ot->poll = ED_operator_node_active;

	/* flags */
	ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}

/* ******************** Separate operator ********************** */

/* returns 1 if its OK */
static int node_group_separate_selected(bNodeTree *ntree, bNode *gnode, int make_copy)
{
	bNodeLink *link, *link_next;
	bNode *node, *node_next, *newnode;
	bNodeTree *ngroup;
	ListBase anim_basepaths = {NULL, NULL};

	ngroup = (bNodeTree *)gnode->id;
	if (ngroup == NULL) return 0;

	/* deselect all nodes in the target tree */
	for (node = ntree->nodes.first; node; node = node->next)
		node_deselect(node);

	/* clear new pointers, set in nodeCopyNode */
	for (node = ngroup->nodes.first; node; node = node->next)
		node->new_node = NULL;

	/* add selected nodes into the ntree */
	for (node = ngroup->nodes.first; node; node = node_next) {
		node_next = node->next;
		if (node->flag & NODE_SELECT) {
			
			if (make_copy) {
				/* make a copy */
				newnode = nodeCopyNode(ngroup, node);
			}
			else {
				/* use the existing node */
				newnode = node;
			}
			
			/* keep track of this node's RNA "base" path (the part of the path identifying the node)
			 * if the old nodetree has animation data which potentially covers this node
			 */
			if (ngroup->adt) {
				PointerRNA ptr;
				char *path;
				
				RNA_pointer_create(&ngroup->id, &RNA_Node, newnode, &ptr);
				path = RNA_path_from_ID_to_struct(&ptr);
				
				if (path)
					BLI_addtail(&anim_basepaths, BLI_genericNodeN(path));
			}
			
			/* ensure valid parent pointers, detach if parent stays inside the group */
			if (newnode->parent && !(newnode->parent->flag & NODE_SELECT))
				nodeDetachNode(newnode);
			
			/* migrate node */
			BLI_remlink(&ngroup->nodes, newnode);
			BLI_addtail(&ntree->nodes, newnode);
			
			/* ensure unique node name in the node tree */
			nodeUniqueName(ntree, newnode);
			
			if (!newnode->parent) {
				newnode->locx += gnode->locx;
				newnode->locy += gnode->locy;
			}
		}
		else {
			/* ensure valid parent pointers, detach if child stays inside the group */
			if (node->parent && (node->parent->flag & NODE_SELECT))
				nodeDetachNode(node);
		}
	}

	/* add internal links to the ntree */
	for (link = ngroup->links.first; link; link = link_next) {
		int fromselect = (link->fromnode && (link->fromnode->flag & NODE_SELECT));
		int toselect = (link->tonode && (link->tonode->flag & NODE_SELECT));
		link_next = link->next;

		if (make_copy) {
			/* make a copy of internal links */
			if (fromselect && toselect)
				nodeAddLink(ntree, link->fromnode->new_node, link->fromsock->new_sock, link->tonode->new_node, link->tosock->new_sock);
		}
		else {
			/* move valid links over, delete broken links */
			if (fromselect && toselect) {
				BLI_remlink(&ngroup->links, link);
				BLI_addtail(&ntree->links, link);
			}
			else if (fromselect || toselect) {
				nodeRemLink(ngroup, link);
			}
		}
	}

	/* and copy across the animation,
	 * note that the animation data's action can be NULL here */
	if (ngroup->adt) {
		LinkData *ld, *ldn = NULL;

		/* now perform the moving */
		BKE_animdata_separate_by_basepath(&ngroup->id, &ntree->id, &anim_basepaths);

		/* paths + their wrappers need to be freed */
		for (ld = anim_basepaths.first; ld; ld = ldn) {
			ldn = ld->next;

			MEM_freeN(ld->data);
			BLI_freelinkN(&anim_basepaths, ld);
		}
	}

	ntree->update |= NTREE_UPDATE_NODES | NTREE_UPDATE_LINKS;
	if (!make_copy)
		ngroup->update |= NTREE_UPDATE_NODES | NTREE_UPDATE_LINKS;

	return 1;
}

typedef enum eNodeGroupSeparateType {
	NODE_GS_COPY,
	NODE_GS_MOVE
} eNodeGroupSeparateType;

/* Operator Property */
EnumPropertyItem node_group_separate_types[] = {
	{NODE_GS_COPY, "COPY", 0, "Copy", "Copy to parent node tree, keep group intact"},
	{NODE_GS_MOVE, "MOVE", 0, "Move", "Move to parent node tree, remove from group"},
	{0, NULL, 0, NULL, NULL}
};

static int node_group_separate_exec(bContext *C, wmOperator *op)
{
	SpaceNode *snode = CTX_wm_space_node(C);
	bNode *gnode;
	int type = RNA_enum_get(op->ptr, "type");

	ED_preview_kill_jobs(C);

	/* are we inside of a group? */
	gnode = node_tree_get_editgroup(snode->nodetree);
	if (!gnode) {
		BKE_report(op->reports, RPT_WARNING, "Not inside node group");
		return OPERATOR_CANCELLED;
	}

	switch (type) {
		case NODE_GS_COPY:
			if (!node_group_separate_selected(snode->nodetree, gnode, 1)) {
				BKE_report(op->reports, RPT_WARNING, "Cannot separate nodes");
				return OPERATOR_CANCELLED;
			}
			break;
		case NODE_GS_MOVE:
			if (!node_group_separate_selected(snode->nodetree, gnode, 0)) {
				BKE_report(op->reports, RPT_WARNING, "Cannot separate nodes");
				return OPERATOR_CANCELLED;
			}
			break;
	}

	/* switch to parent tree */
	snode_make_group_editable(snode, NULL);

	ntreeUpdateTree(snode->nodetree);

	snode_notify(C, snode);
	snode_dag_update(C, snode);

	return OPERATOR_FINISHED;
}

static int node_group_separate_invoke(bContext *C, wmOperator *UNUSED(op), wmEvent *UNUSED(event))
{
	uiPopupMenu *pup = uiPupMenuBegin(C, "Separate", ICON_NONE);
	uiLayout *layout = uiPupMenuLayout(pup);

	uiLayoutSetOperatorContext(layout, WM_OP_EXEC_DEFAULT);
	uiItemEnumO(layout, "NODE_OT_group_separate", NULL, 0, "type", NODE_GS_COPY);
	uiItemEnumO(layout, "NODE_OT_group_separate", NULL, 0, "type", NODE_GS_MOVE);

	uiPupMenuEnd(C, pup);

	return OPERATOR_CANCELLED;
}

void NODE_OT_group_separate(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Separate";
	ot->description = "Separate selected nodes from the node group";
	ot->idname = "NODE_OT_group_separate";

	/* api callbacks */
	ot->invoke = node_group_separate_invoke;
	ot->exec = node_group_separate_exec;
	ot->poll = ED_operator_node_active;

	/* flags */
	ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

	RNA_def_enum(ot->srna, "type", node_group_separate_types, NODE_GS_COPY, "Type", "");
}

/* ****************** Make Group operator ******************* */

static int node_group_make_test(bNodeTree *ntree, bNode *gnode)
{
	bNode *node;
	bNodeLink *link;
	int totnode = 0;

	/* is there something to group? also do some clearing */
	for (node = ntree->nodes.first; node; node = node->next) {
		if (node == gnode)
			continue;

		if (node->flag & NODE_SELECT) {
			/* no groups in groups */
			if (node->type == NODE_GROUP)
				return 0;
			totnode++;
		}

		node->done = 0;
	}
	if (totnode == 0) return 0;

	/* check if all connections are OK, no unselected node has both
	 * inputs and outputs to a selection */
	for (link = ntree->links.first; link; link = link->next) {
		if (link->fromnode && link->tonode && link->fromnode->flag & NODE_SELECT && link->fromnode != gnode)
			link->tonode->done |= 1;
		if (link->fromnode && link->tonode && link->tonode->flag & NODE_SELECT && link->tonode != gnode)
			link->fromnode->done |= 2;
	}

	for (node = ntree->nodes.first; node; node = node->next) {
		if (node == gnode)
			continue;
		if ((node->flag & NODE_SELECT) == 0)
			if (node->done == 3)
				break;
	}
	if (node)
		return 0;

	return 1;
}


static void node_get_selected_minmax(bNodeTree *ntree, bNode *gnode, float *min, float *max)
{
	bNode *node;
	float loc[2];
	INIT_MINMAX2(min, max);
	for (node = ntree->nodes.first; node; node = node->next) {
		if (node == gnode)
			continue;
		if (node->flag & NODE_SELECT) {
			nodeToView(node, 0.0f, 0.0f, &loc[0], &loc[1]);
			minmax_v2v2_v2(min, max, loc);
		}
	}
}

static int node_group_make_insert_selected(bNodeTree *ntree, bNode *gnode)
{
	bNodeTree *ngroup = (bNodeTree *)gnode->id;
	bNodeLink *link, *linkn;
	bNode *node, *nextn;
	bNodeSocket *gsock, *sock;
	ListBase anim_basepaths = {NULL, NULL};
	float min[2], max[2];

	/* deselect all nodes in the target tree */
	for (node = ngroup->nodes.first; node; node = node->next)
		node_deselect(node);

	node_get_selected_minmax(ntree, gnode, min, max);

	/* move nodes over */
	for (node = ntree->nodes.first; node; node = nextn) {
		nextn = node->next;
		if (node == gnode)
			continue;
		if (node->flag & NODE_SELECT) {
			/* keep track of this node's RNA "base" path (the part of the pat identifying the node)
			 * if the old nodetree has animation data which potentially covers this node
			 */
			if (ntree->adt) {
				PointerRNA ptr;
				char *path;

				RNA_pointer_create(&ntree->id, &RNA_Node, node, &ptr);
				path = RNA_path_from_ID_to_struct(&ptr);

				if (path)
					BLI_addtail(&anim_basepaths, BLI_genericNodeN(path));
			}

			/* ensure valid parent pointers, detach if parent stays outside the group */
			if (node->parent && !(node->parent->flag & NODE_SELECT))
				nodeDetachNode(node);

			/* change node-collection membership */
			BLI_remlink(&ntree->nodes, node);
			BLI_addtail(&ngroup->nodes, node);

			/* ensure unique node name in the ngroup */
			nodeUniqueName(ngroup, node);

			if (!node->parent) {
				node->locx -= 0.5f * (min[0] + max[0]);
				node->locy -= 0.5f * (min[1] + max[1]);
			}
		}
		else {
			/* if the parent is to be inserted but not the child, detach properly */
			if (node->parent && (node->parent->flag & NODE_SELECT))
				nodeDetachNode(node);
		}
	}

	/* move animation data over */
	if (ntree->adt) {
		LinkData *ld, *ldn = NULL;

		BKE_animdata_separate_by_basepath(&ntree->id, &ngroup->id, &anim_basepaths);

		/* paths + their wrappers need to be freed */
		for (ld = anim_basepaths.first; ld; ld = ldn) {
			ldn = ld->next;

			MEM_freeN(ld->data);
			BLI_freelinkN(&anim_basepaths, ld);
		}
	}

	/* node groups don't use internal cached data */
	ntreeFreeCache(ngroup);

	/* relink external sockets */
	for (link = ntree->links.first; link; link = linkn) {
		int fromselect = (link->fromnode && (link->fromnode->flag & NODE_SELECT) && link->fromnode != gnode);
		int toselect = (link->tonode && (link->tonode->flag & NODE_SELECT) && link->tonode != gnode);
		linkn = link->next;

		if ((fromselect && link->tonode == gnode) || (toselect && link->fromnode == gnode)) {
			/* remove all links to/from the gnode.
			 * this can remove link information, but there's no general way to preserve it.
			 */
			nodeRemLink(ntree, link);
		}
		else if (fromselect && toselect) {
			BLI_remlink(&ntree->links, link);
			BLI_addtail(&ngroup->links, link);
		}
		else if (toselect) {
			gsock = node_group_expose_socket(ngroup, link->tosock, SOCK_IN);
			link->tosock->link = nodeAddLink(ngroup, NULL, gsock, link->tonode, link->tosock);
			link->tosock = node_group_add_extern_socket(ntree, &gnode->inputs, SOCK_IN, gsock);
			link->tonode = gnode;
		}
		else if (fromselect) {
			/* search for existing group node socket */
			for (gsock = ngroup->outputs.first; gsock; gsock = gsock->next)
				if (gsock->link && gsock->link->fromsock == link->fromsock)
					break;
			if (!gsock) {
				gsock = node_group_expose_socket(ngroup, link->fromsock, SOCK_OUT);
				gsock->link = nodeAddLink(ngroup, link->fromnode, link->fromsock, NULL, gsock);
				link->fromsock = node_group_add_extern_socket(ntree, &gnode->outputs, SOCK_OUT, gsock);
			}
			else
				link->fromsock = node_group_find_output(gnode, gsock);
			link->fromnode = gnode;
		}
	}

	/* auto-add interface for "solo" nodes */
	node = ((bNodeTree *)gnode->id)->nodes.first;
	if (node && !node->next) {
		for (sock = node->inputs.first; sock; sock = sock->next) {
			int skip = FALSE;
			
			for (link = ((bNodeTree *)gnode->id)->links.first; link; link = link->next)
				if (link->tosock == sock)
					skip = TRUE;

			if (skip == TRUE)
				continue;

			gsock = node_group_expose_socket(ngroup, sock, SOCK_IN);
			node_group_add_extern_socket(ntree, &gnode->inputs, SOCK_IN, gsock);
			nodeAddLink(ngroup, NULL, gsock, node, sock);
		}

		for (sock = node->outputs.first; sock; sock = sock->next) {
			int skip = FALSE;
			
			for (link = ((bNodeTree *)gnode->id)->links.first; link; link = link->next)
				if (link->fromsock == sock)
					skip = TRUE;

			if (skip == TRUE)
				continue;

			gsock = node_group_expose_socket(ngroup, sock, SOCK_OUT);
			node_group_add_extern_socket(ntree, &gnode->outputs, SOCK_OUT, gsock);
			nodeAddLink(ngroup, NULL, gsock, node, sock);
		}
	}

	/* update of the group tree */
	ngroup->update |= NTREE_UPDATE | NTREE_UPDATE_LINKS;
	/* update of the tree containing the group instance node */
	ntree->update |= NTREE_UPDATE_NODES | NTREE_UPDATE_LINKS;

	return 1;
}

static bNode *node_group_make_from_selected(bNodeTree *ntree)
{
	bNode *gnode;
	bNodeTree *ngroup;
	float min[2], max[2];
	bNodeTemplate ntemp;

	node_get_selected_minmax(ntree, NULL, min, max);

	/* new nodetree */
	ngroup = ntreeAddTree(G.main, "NodeGroup", ntree->type, NODE_GROUP);

	/* make group node */
	ntemp.type = NODE_GROUP;
	ntemp.ngroup = ngroup;
	gnode = nodeAddNode(ntree, &ntemp);
	gnode->locx = 0.5f * (min[0] + max[0]);
	gnode->locy = 0.5f * (min[1] + max[1]);

	node_group_make_insert_selected(ntree, gnode);

	/* update of the tree containing the group instance node */
	ntree->update |= NTREE_UPDATE_NODES;

	return gnode;
}

typedef enum eNodeGroupMakeType {
	NODE_GM_NEW,
	NODE_GM_INSERT
} eNodeGroupMakeType;

/* Operator Property */
EnumPropertyItem node_group_make_types[] = {
	{NODE_GM_NEW, "NEW", 0, "New", "Create a new node group from selected nodes"},
	{NODE_GM_INSERT, "INSERT", 0, "Insert", "Insert into active node group"},
	{0, NULL, 0, NULL, NULL}
};

static int node_group_make_exec(bContext *C, wmOperator *op)
{
	SpaceNode *snode = CTX_wm_space_node(C);
	bNode *gnode;
	int type = RNA_enum_get(op->ptr, "type");

	if (snode->edittree != snode->nodetree) {
		BKE_report(op->reports, RPT_WARNING, "Cannot add a new group in a group");
		return OPERATOR_CANCELLED;
	}

	/* for time being... is too complex to handle */
	if (snode->treetype == NTREE_COMPOSIT) {
		for (gnode = snode->nodetree->nodes.first; gnode; gnode = gnode->next) {
			if (gnode->flag & SELECT)
				if (gnode->type == CMP_NODE_R_LAYERS)
					break;
		}

		if (gnode) {
			BKE_report(op->reports, RPT_WARNING, "Cannot add a Render Layers node in a group");
			return OPERATOR_CANCELLED;
		}
	}

	ED_preview_kill_jobs(C);

	switch (type) {
		case NODE_GM_NEW:
			if (node_group_make_test(snode->nodetree, NULL)) {
				gnode = node_group_make_from_selected(snode->nodetree);
			}
			else {
				BKE_report(op->reports, RPT_WARNING, "Cannot make group");
				return OPERATOR_CANCELLED;
			}
			break;
		case NODE_GM_INSERT:
			gnode = nodeGetActive(snode->nodetree);
			if (!gnode || gnode->type != NODE_GROUP) {
				BKE_report(op->reports, RPT_WARNING, "No active group node");
				return OPERATOR_CANCELLED;
			}
			if (node_group_make_test(snode->nodetree, gnode)) {
				node_group_make_insert_selected(snode->nodetree, gnode);
			}
			else {
				BKE_report(op->reports, RPT_WARNING, "Cannot insert into group");
				return OPERATOR_CANCELLED;
			}
			break;
	}

	if (gnode) {
		nodeSetActive(snode->nodetree, gnode);
		snode_make_group_editable(snode, gnode);
	}

	if (gnode)
		ntreeUpdateTree((bNodeTree *)gnode->id);
	ntreeUpdateTree(snode->nodetree);

	snode_notify(C, snode);
	snode_dag_update(C, snode);

	return OPERATOR_FINISHED;
}

static int node_group_make_invoke(bContext *C, wmOperator *UNUSED(op), wmEvent *UNUSED(event))
{
	SpaceNode *snode = CTX_wm_space_node(C);
	bNode *act = nodeGetActive(snode->edittree);
	uiPopupMenu *pup = uiPupMenuBegin(C, "Make Group", ICON_NONE);
	uiLayout *layout = uiPupMenuLayout(pup);

	uiLayoutSetOperatorContext(layout, WM_OP_EXEC_DEFAULT);
	uiItemEnumO(layout, "NODE_OT_group_make", NULL, 0, "type", NODE_GM_NEW);

	/* if active node is a group, add insert option */
	if (act && act->type == NODE_GROUP) {
		uiItemEnumO(layout, "NODE_OT_group_make", NULL, 0, "type", NODE_GM_INSERT);
	}

	uiPupMenuEnd(C, pup);

	return OPERATOR_CANCELLED;
}

void NODE_OT_group_make(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Group";
	ot->description = "Make group from selected nodes";
	ot->idname = "NODE_OT_group_make";

	/* api callbacks */
	ot->invoke = node_group_make_invoke;
	ot->exec = node_group_make_exec;
	ot->poll = ED_operator_node_active;

	/* flags */
	ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

	RNA_def_enum(ot->srna, "type", node_group_make_types, NODE_GM_NEW, "Type", "");
}
