/*
 * ***** 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.
 *
 * Contributor(s): Nicholas Bishop
 *
 * ***** END GPL LICENSE BLOCK *****
 */

/** \file blender/bmesh/operators/bmo_symmetrize.c
 *  \ingroup bmesh
 *
 * Makes the mesh symmetrical by splitting along an axis and duplicating the geometry.
 */


#include "MEM_guardedalloc.h"

#include "BLI_array.h"
#include "BLI_math.h"
#include "BLI_utildefines.h"

#include "bmesh.h"
#include "intern/bmesh_operators_private.h"

enum {
	SYMM_OUTPUT_GEOM = (1 << 0)
};

/* Note: don't think there's much need to make these user-adjustable? */
#define SYMM_AXIS_THRESHOLD 0.00002f
#define SYMM_VERT_THRESHOLD 0.00002f

typedef enum {
	/* Coordinate lies on the side being copied from */
	SYMM_SIDE_KEEP,
	/* Coordinate lies on the side being copied from and within the
	 * axis threshold */
	SYMM_SIDE_AXIS,
	/* Coordinate lies on the side being copied to */
	SYMM_SIDE_KILL
} SymmSide;

typedef struct {
	BMesh *bm;
	BMOperator *op;

	int axis;
	BMO_SymmDirection direction;

	/* Maps from input vertices to their mirrors. If the vertex
	 * doesn't have a mirror, it's not in this map. If the vertex is
	 * within the axis threshold, it's mapped to itself. */
	GHash *vert_symm_map;

	/* Edges that cross the symmetry plane and are asymmetric get
	 * split. This map goes from input edges to output vertices. If an
	 * edge is not split, it's not in this map. */
	GHash *edge_split_map;
} Symm;

/* Return which side the coordinate lies on */
static SymmSide symm_co_side(const Symm *symm,
                             const float *co)
{
	float comp = co[symm->axis];
	if (ELEM3(symm->direction,
	          BMO_SYMMETRIZE_NEGATIVE_X,
	          BMO_SYMMETRIZE_NEGATIVE_Y,
	          BMO_SYMMETRIZE_NEGATIVE_Z))
	{
		comp = -comp;
	}

	if (comp >= 0) {
		if (comp < SYMM_AXIS_THRESHOLD)
			return SYMM_SIDE_AXIS;
		else
			return SYMM_SIDE_KEEP;
	}
	else
		return SYMM_SIDE_KILL;
}

/* Output vertices and the vert_map array */
static void symm_verts_mirror(Symm *symm)
{
	BMOIter oiter;
	BMVert *src_v, *dst_v;

	symm->vert_symm_map = BLI_ghash_ptr_new(AT);

	BMO_ITER (src_v, &oiter, symm->op->slots_in, "input", BM_VERT) {
		SymmSide side = symm_co_side(symm, src_v->co);
		float co[3];

		switch (side) {
			case SYMM_SIDE_KEEP:
				/* The vertex is outside the axis area; output its mirror */
				copy_v3_v3(co, src_v->co);
				co[symm->axis] = -co[symm->axis];

				dst_v = BM_vert_create(symm->bm, co, src_v, 0);
				BMO_elem_flag_enable(symm->bm, dst_v, SYMM_OUTPUT_GEOM);
				BLI_ghash_insert(symm->vert_symm_map, src_v, dst_v);
				break;

			case SYMM_SIDE_AXIS:
				/* The vertex is within the axis area, snap to center */
				src_v->co[symm->axis] = 0;
				/* Vertex isn't copied, map to itself */
				BLI_ghash_insert(symm->vert_symm_map, src_v, src_v);
				break;

			case SYMM_SIDE_KILL:
				/* The vertex does not lie in the half-space being
				 * copied from, nothing to do */
				break;
		}
	}
}

static int symm_edge_crosses_axis(const Symm *symm, const BMEdge *e)
{
	const int sides[2] = {symm_co_side(symm, e->v1->co),
		                  symm_co_side(symm, e->v2->co)};

	return ((sides[0] != SYMM_SIDE_AXIS) &&
	        (sides[1] != SYMM_SIDE_AXIS) &&
	        (sides[0] != sides[1]));
}

/* Output edge split vertices for asymmetric edges and the edge_splits
 * mapping array */
static void symm_split_asymmetric_edges(Symm *symm)
{
	BMOIter oiter;
	BMEdge *e;

	symm->edge_split_map = BLI_ghash_ptr_new(AT);

	BMO_ITER (e, &oiter, symm->op->slots_in, "input", BM_EDGE) {
		float flipped[3];

		copy_v3_v3(flipped, e->v1->co);
		flipped[symm->axis] = -flipped[symm->axis];

		if (symm_edge_crosses_axis(symm, e) &&
		    (!compare_v3v3(e->v2->co, flipped, SYMM_VERT_THRESHOLD)))
		{
			/* Endpoints lie on opposite sides and are asymmetric */

			BMVert *v;
			float lambda = 0, edge_dir[3], co[3];
			float plane_co[3][3][3] = {
				/* axis == 0 */
				{{0, 0, 0}, {0, 1, 0}, {0, 0, 1}},
				/* axis == 1 */
				{{0, 0, 0}, {1, 0, 0}, {0, 0, 1}},
				/* axis == 2 */
				{{0, 0, 0}, {1, 0, 0}, {0, 1, 0}},
			};
			int r;

			/* Find intersection of edge with symmetry plane */
			sub_v3_v3v3(edge_dir, e->v2->co, e->v1->co);
			normalize_v3(edge_dir);
			r = isect_ray_plane_v3(e->v1->co,
			                       edge_dir,
			                       plane_co[symm->axis][0],
			                       plane_co[symm->axis][1],
			                       plane_co[symm->axis][2],
			                       &lambda, true);
			BLI_assert(r);

			madd_v3_v3v3fl(co, e->v1->co, edge_dir, lambda);
			co[symm->axis] = 0;

			/* Edge is asymmetric, split it with a new vertex */
			v = BM_vert_create(symm->bm, co, e->v1, 0);
			BMO_elem_flag_enable(symm->bm, v, SYMM_OUTPUT_GEOM);
			BLI_ghash_insert(symm->edge_split_map, e, v);
		}
	}
}

static void symm_mirror_edges(Symm *symm)
{
	BMOIter oiter;
	BMEdge *e;

	BMO_ITER (e, &oiter, symm->op->slots_in, "input", BM_EDGE) {
		BMVert *v1 = NULL, *v2 = NULL;
		BMEdge *e_new;

		v1 = BLI_ghash_lookup(symm->vert_symm_map, e->v1);
		v2 = BLI_ghash_lookup(symm->vert_symm_map, e->v2);

		if (v1 && v2) {
			e_new = BM_edge_create(symm->bm, v1, v2, e, BM_CREATE_NO_DOUBLE);
			BMO_elem_flag_enable(symm->bm, e_new, SYMM_OUTPUT_GEOM);
		}
		else if (v1 || v2) {
			if (BLI_ghash_haskey(symm->edge_split_map, e)) {
				BMVert *v_split = BLI_ghash_lookup(symm->edge_split_map, e);

				/* Output the keep side of the split edge */
				if (!v1) {
					e_new = BM_edge_create(symm->bm, v_split, e->v2, e, BM_CREATE_NO_DOUBLE);
					BMO_elem_flag_enable(symm->bm, e_new, SYMM_OUTPUT_GEOM);
					v1 = v_split;
				}
				else {
					e_new = BM_edge_create(symm->bm, e->v1, v_split, e, BM_CREATE_NO_DOUBLE);
					BMO_elem_flag_enable(symm->bm, e_new, SYMM_OUTPUT_GEOM);
					v2 = v_split;
				}

				/* Output the kill side of the split edge */
				e_new = BM_edge_create(symm->bm, v1, v2, e, BM_CREATE_NO_DOUBLE);
				BMO_elem_flag_enable(symm->bm, e_new, SYMM_OUTPUT_GEOM);
			}
		}
	}
}

/****************************** SymmPoly ******************************/

typedef struct {
	/* Indices into the source mvert array (or -1 if not in that array) */
	BMVert **src_verts;
	/* Indices into the destination mvert array, these are vertices
	 * created by an edge split (-1 for vertices not created by edge
	 * split) */
	BMVert **edge_verts;

	/* Number of vertices in the polygon */
	int len;

	/* True only if none of the polygon's edges were split */
	bool already_symmetric;

	BMFace *src_face;
} SymmPoly;

static void symm_poly_with_splits(const Symm *symm,
                                  BMFace *f,
                                  SymmPoly *out)
{
	BMIter iter;
	BMLoop *l;
	int i;

	out->src_face = f;

	/* Count vertices and check for edge splits */
	out->len = f->len;
	out->already_symmetric = true;
	BM_ITER_ELEM (l, &iter, f, BM_LOOPS_OF_FACE) {
		if (BLI_ghash_haskey(symm->edge_split_map, l->e)) {
			out->len++;
			out->already_symmetric = false;
		}
	}

	i = 0;
	BM_ITER_ELEM (l, &iter, f, BM_LOOPS_OF_FACE) {
		BMVert *split = BLI_ghash_lookup(symm->edge_split_map, l->e);

		out->src_verts[i] = l->v;
		out->edge_verts[i] = NULL;
		i++;

		if (split) {
			out->src_verts[i] = NULL;
			out->edge_verts[i] = split;
			i++;
		}
	}
}

static const float *symm_poly_co(const SymmPoly *sp, int v)
{
	if (sp->src_verts[v])
		return sp->src_verts[v]->co;
	else if (sp->edge_verts[v])
		return sp->edge_verts[v]->co;
	else
		return NULL;
}

static SymmSide symm_poly_co_side(const Symm *symm,
                                  const SymmPoly *sp,
                                  int v)
{
	return symm_co_side(symm, symm_poly_co(sp, v));
}

/* Return the index of the vertex in the destination array at corner
 * 'v' of the polygon, or -1 if not in that array */
static BMVert *symm_poly_dst(const SymmPoly *sp, int v)
{
	if (sp->edge_verts[v])
		return sp->edge_verts[v];
	else if (sp->src_verts[v])
		return sp->src_verts[v];
	else
		return NULL;
}

/* Same as above, but returns the index of the mirror if available, or
 * the same index if on the axis, or -1 otherwise */
static BMVert *symm_poly_mirror_dst(const Symm *symm,
                                    const SymmPoly *sp,
                                    int v)
{
	if (sp->edge_verts[v])
		return sp->edge_verts[v];
	else if (sp->src_verts[v]) {
		if (BLI_ghash_haskey(symm->vert_symm_map, sp->src_verts[v]))
			return BLI_ghash_lookup(symm->vert_symm_map, sp->src_verts[v]);
		else
			return sp->src_verts[v];
	}
	else
		return NULL;
}

static bool symm_poly_next_crossing(const Symm *symm,
                                    const SymmPoly *sp,
                                    int start,
                                    int *l1,
                                    int *l2)
{
	int i;

	for (i = 0; i < sp->len; i++) {
		(*l1) = (start + i) % sp->len;
		(*l2) = ((*l1) + 1) % sp->len;

		if ((symm_poly_co_side(symm, sp, *l1) == SYMM_SIDE_KILL) ^
		    (symm_poly_co_side(symm, sp, *l2) == SYMM_SIDE_KILL))
		{
			return true;
		}
	}

	BLI_assert(!"symm_poly_next_crossing failed");
	return false;
}

static BMFace *symm_face_create_v(BMesh *bm, BMFace *example,
                                  BMVert **fv, BMEdge **fe, int len)
{
	BMFace *f_new;
	int i;

	/* TODO: calling symmetrize in dynamic-topology sculpt mode
	 * frequently tries to create faces of length less than two,
	 * should investigate further */
	if (len < 3)
		return NULL;

	for (i = 0; i < len; i++) {
		int j = (i + 1) % len;
		fe[i] = BM_edge_exists(fv[i], fv[j]);
		if (!fe[i]) {
			fe[i] = BM_edge_create(bm, fv[i], fv[j], NULL, 0);
			BMO_elem_flag_enable(bm, fe[i], SYMM_OUTPUT_GEOM);
		}
	}
	f_new = BM_face_create(bm, fv, fe, len, BM_CREATE_NO_DOUBLE);
	if (example)
		BM_elem_attrs_copy(bm, bm, example, f_new);
	BM_face_select_set(bm, f_new, true);
	BMO_elem_flag_enable(bm, f_new, SYMM_OUTPUT_GEOM);

	return f_new;
}

static void symm_mesh_output_poly_zero_splits(Symm *symm,
                                              SymmPoly *sp,
                                              BMVert **fv,
                                              BMEdge **fe,
                                              int segment_len,
                                              int start)
{
	int i, j;

	j = 0;

	/* Output the keep side of the input polygon */
	for (i = 0; i < segment_len; i++) {
		const int offset = (start + i) % sp->len;
		BLI_assert(sp->src_verts[offset]);
		fv[j++] = sp->src_verts[offset];
	}

	/* Output the kill side of the polygon */
	for (i = segment_len - 1; i >= 0; i--) {
		const int offset = (start + i) % sp->len;

		if (symm_poly_co_side(symm, sp, offset) == SYMM_SIDE_KEEP) {
			BLI_assert(sp->src_verts[offset]);
			fv[j++] = BLI_ghash_lookup(symm->vert_symm_map,
			                           sp->src_verts[offset]);
		}
	}

	symm_face_create_v(symm->bm, sp->src_face, fv, fe, j);
}

static void symm_mesh_output_poly_with_splits(Symm *symm,
                                              SymmPoly *sp,
                                              BMVert **fv,
                                              BMEdge **fe,
                                              int segment_len,
                                              int start)
{
	int i;

	/* Output the keep side of the input polygon */

	for (i = 0; i < segment_len; i++) {
		const int offset = (start + i) % sp->len;
		BMVert *v = symm_poly_dst(sp, offset);

		BLI_assert(v);

		fv[i] = v;
	}

	symm_face_create_v(symm->bm, sp->src_face, fv, fe, segment_len);

	/* Output the kill side of the input polygon */

	for (i = 0; i < segment_len; i++) {
		const int offset = (start + i) % sp->len;
		BMVert *v = symm_poly_mirror_dst(symm, sp, offset);

		fv[segment_len - i - 1] = v;

	}

	symm_face_create_v(symm->bm, sp->src_face, fv, fe, segment_len);
}

static void symm_mirror_polygons(Symm *symm)
{
	BMOIter oiter;
	BMFace *f;
	BMVert **pv = NULL;
	BMVert **fv = NULL;
	BMEdge **fe = NULL;
	BLI_array_declare(pv);
	BLI_array_declare(fv);
	BLI_array_declare(fe);

	BMO_ITER (f, &oiter, symm->op->slots_in, "input", BM_FACE) {
		BMIter iter;
		BMLoop *l;
		bool mirror_all = true, ignore_all = true;

		/* Check if entire polygon can be mirrored or ignored */
		BM_ITER_ELEM (l, &iter, f, BM_LOOPS_OF_FACE) {
			const SymmSide side = symm_co_side(symm, l->v->co);
			if (side == SYMM_SIDE_KILL)
				mirror_all = false;
			else if (side == SYMM_SIDE_KEEP)
				ignore_all = false;
		}

		if (mirror_all) {
			int i;

			/* Make a mirrored copy of the polygon */

			BLI_array_empty(fv);
			BLI_array_empty(fe);
			BLI_array_grow_items(fv, f->len);
			BLI_array_grow_items(fe, f->len);

			i = f->len;
			BM_ITER_ELEM (l, &iter, f, BM_LOOPS_OF_FACE) {
				i--;

				if (symm_co_side(symm, l->v->co) == SYMM_SIDE_KEEP)
					fv[i] = BLI_ghash_lookup(symm->vert_symm_map, l->v);
				else
					fv[i] = l->v;
			}

			symm_face_create_v(symm->bm, f, fv, fe, f->len);
		}
		else if (ignore_all) {
			BM_face_kill(symm->bm, f);
		}
		else {
			SymmPoly sp;
			int l1, l2, l3, l4;
			int double_l2, double_l3;
			int segment_len;

			BLI_array_empty(pv);
			BLI_array_grow_items(pv, f->len * 4);
			sp.src_verts = pv;
			sp.edge_verts = pv + f->len * 2;
			symm_poly_with_splits(symm, f, &sp);

			/* Find first loop edge crossing the axis */
			symm_poly_next_crossing(symm, &sp, 0, &l1, &l2);

			/* If crossing isn't kill to keep, find the next one */
			if (symm_poly_co_side(symm, &sp, l1) != SYMM_SIDE_KILL) {
				symm_poly_next_crossing(symm, &sp, l2, &l1, &l2);
			}

			/* Find next crossing (keep to kill) */
			symm_poly_next_crossing(symm, &sp, l2, &l3, &l4);

			if (l2 == l3)
				segment_len = 0;
			else if (l2 < l3)
				segment_len = l3 - l2 + 1;
			else
				segment_len = (sp.len - l2 + 1) + l3;

			double_l2 = symm_poly_co_side(symm, &sp, l2) == SYMM_SIDE_KEEP;
			double_l3 = symm_poly_co_side(symm, &sp, l3) == SYMM_SIDE_KEEP;

			/* Calculate number of new polygons/loops */
			if (segment_len == 0) {
			}
			else if (sp.already_symmetric) {
				int new_loops;

				if (double_l2 && double_l3)
					new_loops = segment_len * 2;
				else if (!double_l2 && !double_l3)
					new_loops = segment_len * 2 - 2;
				else
					new_loops = segment_len * 2 - 1;

				BLI_array_empty(fv);
				BLI_array_empty(fe);
				BLI_array_grow_items(fv, new_loops);
				BLI_array_grow_items(fe, new_loops);

				symm_mesh_output_poly_zero_splits(symm, &sp,
				                                  fv, fe,
				                                  segment_len, l2);
				BM_face_kill(symm->bm, f);
			}
			else if (!double_l2 && !double_l3) {
				BLI_array_empty(fv);
				BLI_array_empty(fe);
				BLI_array_grow_items(fv, segment_len);
				BLI_array_grow_items(fe, segment_len);

				symm_mesh_output_poly_with_splits(symm, &sp,
				                                  fv, fe,
				                                  segment_len,
				                                  l2);

				BM_face_kill(symm->bm, f);
			}
			else {
				BLI_array_empty(fv);
				BLI_array_empty(fe);
				BLI_array_grow_items(fv, segment_len);
				BLI_array_grow_items(fe, segment_len);

				symm_mesh_output_poly_with_splits(symm, &sp,
				                                  fv, fe,
				                                  segment_len,
				                                  l2);

				BM_face_kill(symm->bm, f);

				/* Output bridge triangle */

				BLI_array_empty(fv);
				BLI_array_empty(fe);
				BLI_array_grow_items(fv, 3);
				BLI_array_grow_items(fe, 3);

				if (double_l2) {
					fv[0] = symm_poly_dst(&sp, l2);
					fv[1] = symm_poly_mirror_dst(symm, &sp, l2);
					fv[2] = symm_poly_dst(&sp, l3);
				}
				else if (double_l3) {
					fv[0] = symm_poly_dst(&sp, l3);
					fv[1] = symm_poly_mirror_dst(symm, &sp, l3);
					fv[2] = symm_poly_dst(&sp, l2);
				}

				BLI_assert(fv[0] && fv[1] && fv[2]);

				symm_face_create_v(symm->bm, NULL, fv, fe, 3);
			}
		}
	}

	BLI_array_free(pv);
	BLI_array_free(fv);
	BLI_array_free(fe);
}

/* Remove unused edges and vertices from the side being copied to */
static void symm_kill_unused(Symm *symm)
{
	BMOIter oiter;
	BMEdge *e;
	BMVert *v;

	/* Kill unused edges */
	BMO_ITER (e, &oiter, symm->op->slots_in, "input", BM_EDGE) {
		const int crosses = symm_edge_crosses_axis(symm, e);
		const int symmetric = (crosses &&
		                       (!BLI_ghash_haskey(symm->edge_split_map, e)));

		if (((symm_co_side(symm, e->v1->co) == SYMM_SIDE_KILL) ||
		     (symm_co_side(symm, e->v2->co) == SYMM_SIDE_KILL)) &&
		    !symmetric)
		{
			/* The edge might be used by a face outside the input set */
			if (BM_edge_is_wire(e))
				BM_edge_kill(symm->bm, e);
		}
	}

	/* Kill unused vertices */
	BMO_ITER (v, &oiter, symm->op->slots_in, "input", BM_VERT) {
		if (symm_co_side(symm, v->co) == SYMM_SIDE_KILL) {
			if (BM_vert_edge_count(v) == 0)
				BM_vert_kill(symm->bm, v);
		}
	}
}

void bmo_symmetrize_exec(BMesh *bm, BMOperator *op)
{
	Symm symm;
	BMO_SymmDirection direction = BMO_slot_int_get(op->slots_in, "direction");

	symm.bm = bm;
	symm.op = op;
	symm.axis = (ELEM(direction,
	                  BMO_SYMMETRIZE_NEGATIVE_X,
	                  BMO_SYMMETRIZE_POSITIVE_X) ? 0 :
	             ELEM(direction,
	                  BMO_SYMMETRIZE_NEGATIVE_Y,
	                  BMO_SYMMETRIZE_POSITIVE_Y) ? 1 :
	             ELEM(direction,
	                  BMO_SYMMETRIZE_NEGATIVE_Z,
	                  BMO_SYMMETRIZE_POSITIVE_Z) ? 2 : 0);
	symm.direction = direction;

	symm_verts_mirror(&symm);
	symm_split_asymmetric_edges(&symm);
	symm_mirror_edges(&symm);
	symm_mirror_polygons(&symm);
	symm_kill_unused(&symm);

	BLI_ghash_free(symm.vert_symm_map, NULL, NULL);
	BLI_ghash_free(symm.edge_split_map, NULL, NULL);

	BMO_slot_buffer_from_enabled_flag(bm, op, op->slots_out, "geom.out",
	                                  BM_ALL_NOLOOP, SYMM_OUTPUT_GEOM);
}
