/*
 * ***** 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) 2004 by Blender Foundation
 * All rights reserved.
 *
 * The Original Code is: all of this file.
 *
 * Contributor(s): Morten Mikkelsen,
 *                 Sergey Sharybin
 *
 * ***** END GPL LICENSE BLOCK *****
 */

/** \file blender/editors/object/object_bake.c
 *  \ingroup edobj
 */

#include <string.h>

#include "MEM_guardedalloc.h"

#include "DNA_scene_types.h"
#include "DNA_screen_types.h"
#include "DNA_space_types.h"
#include "DNA_world_types.h"
#include "DNA_object_types.h"
#include "DNA_mesh_types.h"
#include "DNA_meshdata_types.h"

#include "BLI_blenlib.h"
#include "BLI_threads.h"
#include "BLI_utildefines.h"
#include "BLI_math.h"
#include "BLI_math_geom.h"

#include "BKE_blender.h"
#include "BKE_ccg.h"
#include "BKE_screen.h"
#include "BKE_context.h"
#include "BKE_global.h"
#include "BKE_image.h"
#include "BKE_main.h"
#include "BKE_multires.h"
#include "BKE_report.h"
#include "BKE_cdderivedmesh.h"
#include "BKE_modifier.h"
#include "BKE_DerivedMesh.h"
#include "BKE_subsurf.h"
#include "BKE_depsgraph.h"
#include "BKE_mesh.h"
#include "BKE_scene.h"

#include "RE_pipeline.h"
#include "RE_shader_ext.h"
#include "RE_multires_bake.h"

#include "PIL_time.h"

#include "IMB_imbuf_types.h"
#include "IMB_imbuf.h"
#include "IMB_colormanagement.h"

#include "GPU_draw.h" /* GPU_free_image */

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

#include "ED_object.h"

#include "object_intern.h"

/* ****************** multires BAKING ********************** */

/* holder of per-object data needed for bake job
 * needed to make job totally thread-safe */
typedef struct MultiresBakerJobData {
	struct MultiresBakerJobData *next, *prev;
	DerivedMesh *lores_dm, *hires_dm;
	int simple, lvl, tot_lvl;
	ListBase images;
} MultiresBakerJobData;

/* data passing to multires-baker job */
typedef struct {
	ListBase data;
	int bake_clear, bake_filter;
	short mode, use_lores_mesh;
	int number_of_rays;
	float bias;
	int raytrace_structure;
	int octree_resolution;
	int threads;
} MultiresBakeJob;

static int multiresbake_check(bContext *C, wmOperator *op)
{
	Scene *scene = CTX_data_scene(C);
	Object *ob;
	Mesh *me;
	MultiresModifierData *mmd;
	int ok = 1, a;

	CTX_DATA_BEGIN (C, Base *, base, selected_editable_bases)
	{
		ob = base->object;

		if (ob->type != OB_MESH) {
			BKE_report(op->reports, RPT_ERROR, "Baking of multires data only works with an active mesh object");

			ok = 0;
			break;
		}

		me = (Mesh *)ob->data;
		mmd = get_multires_modifier(scene, ob, 0);

		/* Multi-resolution should be and be last in the stack */
		if (ok && mmd) {
			ModifierData *md;

			ok = mmd->totlvl > 0;

			for (md = (ModifierData *)mmd->modifier.next; md && ok; md = md->next) {
				if (modifier_isEnabled(scene, md, eModifierMode_Realtime)) {
					ok = 0;
				}
			}
		}
		else {
			ok = 0;
		}

		if (!ok) {
			BKE_report(op->reports, RPT_ERROR, "Multires data baking requires multi-resolution object");

			break;
		}

		if (mmd->lvl == 0) {
			BKE_report(op->reports, RPT_ERROR, "Multires data baking is not supported for preview subdivision level 0");

			break;
		}

		if (!me->mtpoly) {
			BKE_report(op->reports, RPT_ERROR, "Mesh should be unwrapped before multires data baking");

			ok = 0;
		}
		else {
			a = me->totpoly;
			while (ok && a--) {
				Image *ima = me->mtpoly[a].tpage;

				if (!ima) {
					BKE_report(op->reports, RPT_ERROR, "You should have active texture to use multires baker");

					ok = 0;
				}
				else {
					ImBuf *ibuf = BKE_image_acquire_ibuf(ima, NULL, NULL);

					if (!ibuf) {
						BKE_report(op->reports, RPT_ERROR, "Baking should happen to image with image buffer");

						ok = 0;
					}
					else {
						if (ibuf->rect == NULL && ibuf->rect_float == NULL)
							ok = 0;

						if (ibuf->rect_float && !(ibuf->channels == 0 || ibuf->channels == 4))
							ok = 0;

						if (!ok)
							BKE_report(op->reports, RPT_ERROR, "Baking to unsupported image type");
					}

					BKE_image_release_ibuf(ima, ibuf, NULL);
				}
			}
		}

		if (!ok)
			break;
	}
	CTX_DATA_END;

	return ok;
}

static DerivedMesh *multiresbake_create_loresdm(Scene *scene, Object *ob, int *lvl)
{
	DerivedMesh *dm;
	MultiresModifierData *mmd = get_multires_modifier(scene, ob, 0);
	Mesh *me = (Mesh *)ob->data;

	*lvl = mmd->lvl;

	if (*lvl == 0) {
		DerivedMesh *tmp_dm = CDDM_from_mesh(me, ob);

		DM_set_only_copy(tmp_dm, CD_MASK_BAREMESH | CD_MASK_MTFACE);

		dm = CDDM_copy(tmp_dm);
		tmp_dm->release(tmp_dm);
	}
	else {
		MultiresModifierData tmp_mmd = *mmd;
		DerivedMesh *cddm = CDDM_from_mesh(me, ob);

		DM_set_only_copy(cddm, CD_MASK_BAREMESH | CD_MASK_MTFACE);

		tmp_mmd.lvl = *lvl;
		tmp_mmd.sculptlvl = *lvl;
		dm = multires_make_derived_from_derived(cddm, &tmp_mmd, ob, 0);
		cddm->release(cddm);
	}

	return dm;
}

static DerivedMesh *multiresbake_create_hiresdm(Scene *scene, Object *ob, int *lvl, int *simple)
{
	Mesh *me = (Mesh *)ob->data;
	MultiresModifierData *mmd = get_multires_modifier(scene, ob, 0);
	MultiresModifierData tmp_mmd = *mmd;
	DerivedMesh *cddm = CDDM_from_mesh(me, ob);
	DerivedMesh *dm;

	DM_set_only_copy(cddm, CD_MASK_BAREMESH);

	/* TODO: DM_set_only_copy wouldn't set mask for loop and poly data,
	 *       but we really need BAREMESH only to save lots of memory
	 */
	CustomData_set_only_copy(&cddm->loopData, CD_MASK_BAREMESH);
	CustomData_set_only_copy(&cddm->polyData, CD_MASK_BAREMESH);

	*lvl = mmd->totlvl;
	*simple = mmd->simple;

	tmp_mmd.lvl = mmd->totlvl;
	tmp_mmd.sculptlvl = mmd->totlvl;
	dm = multires_make_derived_from_derived(cddm, &tmp_mmd, ob, 0);
	cddm->release(cddm);

	return dm;
}

typedef enum ClearFlag {
	CLEAR_NORMAL = 1
} ClearFlag;


static void clear_images(MTFace *mtface, int totface, ClearFlag flag)
{
	int a;
	const float vec_alpha[4] = {0.0f, 0.0f, 0.0f, 0.0f};
	const float vec_solid[4] = {0.0f, 0.0f, 0.0f, 1.0f};
	const float nor_alpha[4] = {0.5f, 0.5f, 1.0f, 0.0f};
	const float nor_solid[4] = {0.5f, 0.5f, 1.0f, 1.0f};

	for (a = 0; a < totface; a++)
		mtface[a].tpage->id.flag &= ~LIB_DOIT;

	for (a = 0; a < totface; a++) {
		Image *ima = mtface[a].tpage;

		if ((ima->id.flag & LIB_DOIT) == 0) {
			ImBuf *ibuf = BKE_image_acquire_ibuf(ima, NULL, NULL);

			if (flag == CLEAR_NORMAL)
				IMB_rectfill(ibuf, (ibuf->planes == R_IMF_PLANES_RGBA) ? nor_alpha : nor_solid);
			else
				IMB_rectfill(ibuf, (ibuf->planes == R_IMF_PLANES_RGBA) ? vec_alpha : vec_solid);

			ima->id.flag |= LIB_DOIT;

			BKE_image_release_ibuf(ima, ibuf, NULL);
		}
	}

	for (a = 0; a < totface; a++)
		mtface[a].tpage->id.flag &= ~LIB_DOIT;
}

static int multiresbake_image_exec_locked(bContext *C, wmOperator *op)
{
	Object *ob;
	Scene *scene = CTX_data_scene(C);
	int objects_baked = 0;

	if (!multiresbake_check(C, op))
		return OPERATOR_CANCELLED;

	if (scene->r.bake_flag & R_BAKE_CLEAR) {  /* clear images */
		CTX_DATA_BEGIN (C, Base *, base, selected_editable_bases)
		{
			Mesh *me;

			ob = base->object;
			me = (Mesh *)ob->data;

			if (scene->r.bake_mode == RE_BAKE_NORMALS && scene->r.bake_normal_space == R_BAKE_SPACE_TANGENT)
				clear_images(me->mtface, me->totface, CLEAR_NORMAL);
			else
				clear_images(me->mtface, me->totface, 0);
		}
		CTX_DATA_END;
	}

	CTX_DATA_BEGIN (C, Base *, base, selected_editable_bases)
	{
		MultiresBakeRender bkr = {NULL};

		ob = base->object;

		multires_force_update(ob);

		/* copy data stored in job descriptor */
		bkr.bake_filter = scene->r.bake_filter;
		bkr.mode = scene->r.bake_mode;
		bkr.use_lores_mesh = scene->r.bake_flag & R_BAKE_LORES_MESH;
		bkr.bias = scene->r.bake_biasdist;
		bkr.number_of_rays = scene->r.bake_samples;
		bkr.raytrace_structure = scene->r.raytrace_structure;
		bkr.octree_resolution = scene->r.ocres;
		bkr.threads = BKE_scene_num_threads(scene);

		/* create low-resolution DM (to bake to) and hi-resolution DM (to bake from) */
		bkr.hires_dm = multiresbake_create_hiresdm(scene, ob, &bkr.tot_lvl, &bkr.simple);
		bkr.lores_dm = multiresbake_create_loresdm(scene, ob, &bkr.lvl);

		RE_multires_bake_images(&bkr);

		BLI_freelistN(&bkr.image);

		bkr.lores_dm->release(bkr.lores_dm);
		bkr.hires_dm->release(bkr.hires_dm);

		objects_baked++;
	}
	CTX_DATA_END;

	if (!objects_baked)
		BKE_report(op->reports, RPT_ERROR, "No objects found to bake from");

	return OPERATOR_FINISHED;
}

/* Multiresbake adopted for job-system executing */
static void init_multiresbake_job(bContext *C, MultiresBakeJob *bkj)
{
	Scene *scene = CTX_data_scene(C);
	Object *ob;

	/* backup scene settings, so their changing in UI would take no effect on baker */
	bkj->bake_filter = scene->r.bake_filter;
	bkj->mode = scene->r.bake_mode;
	bkj->use_lores_mesh = scene->r.bake_flag & R_BAKE_LORES_MESH;
	bkj->bake_clear = scene->r.bake_flag & R_BAKE_CLEAR;
	bkj->bias = scene->r.bake_biasdist;
	bkj->number_of_rays = scene->r.bake_samples;
	bkj->raytrace_structure = scene->r.raytrace_structure;
	bkj->octree_resolution = scene->r.ocres;
	bkj->threads = BKE_scene_num_threads(scene);

	CTX_DATA_BEGIN (C, Base *, base, selected_editable_bases)
	{
		MultiresBakerJobData *data;
		int lvl;

		ob = base->object;

		multires_force_update(ob);

		data = MEM_callocN(sizeof(MultiresBakerJobData), "multiresBaker derivedMesh_data");

		/* create low-resolution DM (to bake to) and hi-resolution DM (to bake from) */
		data->hires_dm = multiresbake_create_hiresdm(scene, ob, &data->tot_lvl, &data->simple);
		data->lores_dm = multiresbake_create_loresdm(scene, ob, &lvl);
		data->lvl = lvl;

		BLI_addtail(&bkj->data, data);
	}
	CTX_DATA_END;
}

static void multiresbake_startjob(void *bkv, short *stop, short *do_update, float *progress)
{
	MultiresBakerJobData *data;
	MultiresBakeJob *bkj = bkv;
	int baked_objects = 0, tot_obj;

	tot_obj = BLI_countlist(&bkj->data);

	if (bkj->bake_clear) {  /* clear images */
		for (data = bkj->data.first; data; data = data->next) {
			DerivedMesh *dm = data->lores_dm;
			MTFace *mtface = CustomData_get_layer(&dm->faceData, CD_MTFACE);

			if (bkj->mode == RE_BAKE_NORMALS)
				clear_images(mtface, dm->getNumTessFaces(dm), CLEAR_NORMAL);
			else
				clear_images(mtface, dm->getNumTessFaces(dm), 0);
		}
	}

	for (data = bkj->data.first; data; data = data->next) {
		MultiresBakeRender bkr = {NULL};

		/* copy data stored in job descriptor */
		bkr.bake_filter = bkj->bake_filter;
		bkr.mode = bkj->mode;
		bkr.use_lores_mesh = bkj->use_lores_mesh;

		/* create low-resolution DM (to bake to) and hi-resolution DM (to bake from) */
		bkr.lores_dm = data->lores_dm;
		bkr.hires_dm = data->hires_dm;
		bkr.tot_lvl = data->tot_lvl;
		bkr.lvl = data->lvl;
		bkr.simple = data->simple;

		/* needed for proper progress bar */
		bkr.tot_obj = tot_obj;
		bkr.baked_objects = baked_objects;

		bkr.stop = stop;
		bkr.do_update = do_update;
		bkr.progress = progress;

		bkr.bias = bkj->bias;
		bkr.number_of_rays = bkj->number_of_rays;
		bkr.raytrace_structure = bkj->raytrace_structure;
		bkr.octree_resolution = bkj->octree_resolution;
		bkr.threads = bkj->threads;

		RE_multires_bake_images(&bkr);

		data->images = bkr.image;

		baked_objects++;
	}
}

static void multiresbake_freejob(void *bkv)
{
	MultiresBakeJob *bkj = bkv;
	MultiresBakerJobData *data, *next;
	LinkData *link;

	data = bkj->data.first;
	while (data) {
		next = data->next;
		data->lores_dm->release(data->lores_dm);
		data->hires_dm->release(data->hires_dm);

		/* delete here, since this delete will be called from main thread */
		for (link = data->images.first; link; link = link->next) {
			Image *ima = (Image *)link->data;
			GPU_free_image(ima);
		}

		BLI_freelistN(&data->images);

		MEM_freeN(data);
		data = next;
	}

	MEM_freeN(bkj);
}

static int multiresbake_image_exec(bContext *C, wmOperator *op)
{
	Scene *scene = CTX_data_scene(C);
	MultiresBakeJob *bkr;
	wmJob *wm_job;

	if (!multiresbake_check(C, op))
		return OPERATOR_CANCELLED;

	bkr = MEM_callocN(sizeof(MultiresBakeJob), "MultiresBakeJob data");
	init_multiresbake_job(C, bkr);

	if (!bkr->data.first) {
		BKE_report(op->reports, RPT_ERROR, "No objects found to bake from");
		return OPERATOR_CANCELLED;
	}

	/* setup job */
	wm_job = WM_jobs_get(CTX_wm_manager(C), CTX_wm_window(C), scene, "Multires Bake",
	                     WM_JOB_EXCL_RENDER | WM_JOB_PRIORITY | WM_JOB_PROGRESS, WM_JOB_TYPE_OBJECT_BAKE_TEXTURE);
	WM_jobs_customdata_set(wm_job, bkr, multiresbake_freejob);
	WM_jobs_timer(wm_job, 0.2, NC_IMAGE, 0); /* TODO - only draw bake image, can we enforce this */
	WM_jobs_callbacks(wm_job, multiresbake_startjob, NULL, NULL, NULL);

	G.is_break = FALSE;

	WM_jobs_start(CTX_wm_manager(C), wm_job);
	WM_cursor_wait(0);

	/* add modal handler for ESC */
	WM_event_add_modal_handler(C, op);

	return OPERATOR_RUNNING_MODAL;
}

/* ****************** render BAKING ********************** */

/* threaded break test */
static int thread_break(void *UNUSED(arg))
{
	return G.is_break;
}

typedef struct BakeRender {
	Render *re;
	Main *main;
	Scene *scene;
	struct Object *actob;
	int result, ready;

	ReportList *reports;

	short *stop;
	short *do_update;
	float *progress;
	
	ListBase threads;

	/* backup */
	short prev_wo_amb_occ;
	short prev_r_raytrace;

	/* for redrawing */
	ScrArea *sa;
} BakeRender;

/* use by exec and invoke */
static int test_bake_internal(bContext *C, ReportList *reports)
{
	Scene *scene = CTX_data_scene(C);

	if ((scene->r.bake_flag & R_BAKE_TO_ACTIVE) && CTX_data_active_object(C) == NULL) {
		BKE_report(reports, RPT_ERROR, "No active object");
	}
	else if (scene->r.bake_mode == RE_BAKE_AO && scene->world == NULL) {
		BKE_report(reports, RPT_ERROR, "No world set up");
	}
	else {
		return 1;
	}

	return 0;
}

static void init_bake_internal(BakeRender *bkr, bContext *C)
{
	Scene *scene = CTX_data_scene(C);
	bScreen *sc = CTX_wm_screen(C);

	/* get editmode results */
	ED_object_editmode_load(CTX_data_edit_object(C));

	bkr->sa = sc ? BKE_screen_find_big_area(sc, SPACE_IMAGE, 10) : NULL; /* can be NULL */
	bkr->main = CTX_data_main(C);
	bkr->scene = scene;
	bkr->actob = (scene->r.bake_flag & R_BAKE_TO_ACTIVE) ? OBACT : NULL;
	bkr->re = RE_NewRender("_Bake View_");

	if (scene->r.bake_mode == RE_BAKE_AO) {
		/* If raytracing or AO is disabled, switch it on temporarily for baking. */
		bkr->prev_wo_amb_occ = (scene->world->mode & WO_AMB_OCC) != 0;
		scene->world->mode |= WO_AMB_OCC;
	}
	if (scene->r.bake_mode == RE_BAKE_AO || bkr->actob) {
		bkr->prev_r_raytrace = (scene->r.mode & R_RAYTRACE) != 0;
		scene->r.mode |= R_RAYTRACE;
	}
}

static void finish_bake_internal(BakeRender *bkr)
{
	Image *ima;

	RE_Database_Free(bkr->re);

	/* restore raytrace and AO */
	if (bkr->scene->r.bake_mode == RE_BAKE_AO)
		if (bkr->prev_wo_amb_occ == 0)
			bkr->scene->world->mode &= ~WO_AMB_OCC;

	if (bkr->scene->r.bake_mode == RE_BAKE_AO || bkr->actob)
		if (bkr->prev_r_raytrace == 0)
			bkr->scene->r.mode &= ~R_RAYTRACE;

	/* force OpenGL reload and mipmap recalc */
	if ((bkr->scene->r.bake_flag & R_BAKE_VCOL) == 0) {
		for (ima = G.main->image.first; ima; ima = ima->id.next) {
			ImBuf *ibuf = BKE_image_acquire_ibuf(ima, NULL, NULL);

			/* some of the images could have been changed during bake,
			 * so recreate mipmaps regardless bake result status
			 */
			if (ima->ok == IMA_OK_LOADED) {
				if (ibuf) {
					if (ibuf->userflags & IB_BITMAPDIRTY) {
						GPU_free_image(ima);
						imb_freemipmapImBuf(ibuf);
					}

					/* invalidate display buffers for changed images */
					if (ibuf->userflags & IB_BITMAPDIRTY)
						ibuf->userflags |= IB_DISPLAY_BUFFER_INVALID;
				}
			}

			/* freed when baking is done, but if its canceled we need to free here */
			if (ibuf) {
				if (ibuf->userdata) {
					BakeImBufuserData *userdata = (BakeImBufuserData *) ibuf->userdata;
					if (userdata->mask_buffer)
						MEM_freeN(userdata->mask_buffer);
					if (userdata->displacement_buffer)
						MEM_freeN(userdata->displacement_buffer);
					MEM_freeN(userdata);
					ibuf->userdata = NULL;
				}
			}

			BKE_image_release_ibuf(ima, ibuf, NULL);
		}
	}

	if (bkr->scene->r.bake_flag & R_BAKE_VCOL) {
		/* update all tagged meshes */
		Mesh *me;
		BLI_assert(BLI_thread_is_main());
		for (me = G.main->mesh.first; me; me = me->id.next) {
			if (me->id.flag & LIB_DOIT) {
				DAG_id_tag_update(&me->id, OB_RECALC_DATA);
				BKE_mesh_tessface_clear(me);
			}
		}
	}

}

static void *do_bake_render(void *bake_v)
{
	BakeRender *bkr = bake_v;

	bkr->result = RE_bake_shade_all_selected(bkr->re, bkr->scene->r.bake_mode, bkr->actob, NULL, bkr->progress);
	bkr->ready = 1;

	return NULL;
}

static void bake_startjob(void *bkv, short *stop, short *do_update, float *progress)
{
	BakeRender *bkr = bkv;
	Scene *scene = bkr->scene;
	Main *bmain = bkr->main;

	bkr->stop = stop;
	bkr->do_update = do_update;
	bkr->progress = progress;

	RE_test_break_cb(bkr->re, NULL, thread_break);
	G.is_break = FALSE;   /* blender_test_break uses this global */

	RE_Database_Baking(bkr->re, bmain, scene, scene->lay, scene->r.bake_mode, bkr->actob);

	/* baking itself is threaded, cannot use test_break in threads. we also update optional imagewindow */
	bkr->result = RE_bake_shade_all_selected(bkr->re, scene->r.bake_mode, bkr->actob, bkr->do_update, bkr->progress);
}

static void bake_update(void *bkv)
{
	BakeRender *bkr = bkv;

	if (bkr->sa && bkr->sa->spacetype == SPACE_IMAGE) { /* in case the user changed while baking */
		SpaceImage *sima = bkr->sa->spacedata.first;
		if (sima)
			sima->image = RE_bake_shade_get_image();
	}
}

static void bake_freejob(void *bkv)
{
	BakeRender *bkr = bkv;
	finish_bake_internal(bkr);

	if (bkr->result == BAKE_RESULT_NO_OBJECTS)
		BKE_report(bkr->reports, RPT_ERROR, "No objects or images found to bake to");
	else if (bkr->result == BAKE_RESULT_FEEDBACK_LOOP)
		BKE_report(bkr->reports, RPT_WARNING, "Circular reference in texture stack");

	MEM_freeN(bkr);
	G.is_rendering = FALSE;
}

/* catch esc */
static int objects_bake_render_modal(bContext *C, wmOperator *UNUSED(op), const wmEvent *event)
{
	/* no running blender, remove handler and pass through */
	if (0 == WM_jobs_test(CTX_wm_manager(C), CTX_data_scene(C), WM_JOB_TYPE_OBJECT_BAKE_TEXTURE))
		return OPERATOR_FINISHED | OPERATOR_PASS_THROUGH;

	/* running render */
	switch (event->type) {
		case ESCKEY:
			return OPERATOR_RUNNING_MODAL;
			break;
	}
	return OPERATOR_PASS_THROUGH;
}

static int is_multires_bake(Scene *scene)
{
	if (ELEM3(scene->r.bake_mode, RE_BAKE_NORMALS, RE_BAKE_DISPLACEMENT, RE_BAKE_AO))
		return scene->r.bake_flag & R_BAKE_MULTIRES;

	return 0;
}

static int objects_bake_render_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(_event))
{
	Scene *scene = CTX_data_scene(C);
	int result = OPERATOR_CANCELLED;

	if (is_multires_bake(scene)) {
		result = multiresbake_image_exec(C, op);
	}
	else {
		/* only one render job at a time */
		if (WM_jobs_test(CTX_wm_manager(C), scene, WM_JOB_TYPE_OBJECT_BAKE_TEXTURE))
			return OPERATOR_CANCELLED;

		if (test_bake_internal(C, op->reports) == 0) {
			return OPERATOR_CANCELLED;
		}
		else {
			BakeRender *bkr = MEM_callocN(sizeof(BakeRender), "render bake");
			wmJob *wm_job;

			init_bake_internal(bkr, C);
			bkr->reports = op->reports;

			/* setup job */
			wm_job = WM_jobs_get(CTX_wm_manager(C), CTX_wm_window(C), scene, "Texture Bake",
			                     WM_JOB_EXCL_RENDER | WM_JOB_PRIORITY | WM_JOB_PROGRESS, WM_JOB_TYPE_OBJECT_BAKE_TEXTURE);
			WM_jobs_customdata_set(wm_job, bkr, bake_freejob);
			WM_jobs_timer(wm_job, 0.2, NC_IMAGE, 0); /* TODO - only draw bake image, can we enforce this */
			WM_jobs_callbacks(wm_job, bake_startjob, NULL, bake_update, NULL);

			G.is_break = FALSE;
			G.is_rendering = TRUE;

			WM_jobs_start(CTX_wm_manager(C), wm_job);

			WM_cursor_wait(0);

			/* add modal handler for ESC */
			WM_event_add_modal_handler(C, op);
		}

		result = OPERATOR_RUNNING_MODAL;
	}

	WM_event_add_notifier(C, NC_SCENE | ND_RENDER_RESULT, scene);

	return result;
}


static int bake_image_exec(bContext *C, wmOperator *op)
{
	Main *bmain = CTX_data_main(C);
	Scene *scene = CTX_data_scene(C);
	int result = OPERATOR_CANCELLED;

	if (is_multires_bake(scene)) {
		result = multiresbake_image_exec_locked(C, op);
	}
	else {
		if (test_bake_internal(C, op->reports) == 0) {
			return OPERATOR_CANCELLED;
		}
		else {
			ListBase threads;
			BakeRender bkr = {NULL};

			init_bake_internal(&bkr, C);
			bkr.reports = op->reports;

			RE_test_break_cb(bkr.re, NULL, thread_break);
			G.is_break = FALSE;   /* blender_test_break uses this global */

			RE_Database_Baking(bkr.re, bmain, scene, scene->lay, scene->r.bake_mode, (scene->r.bake_flag & R_BAKE_TO_ACTIVE) ? OBACT : NULL);

			/* baking itself is threaded, cannot use test_break in threads  */
			BLI_init_threads(&threads, do_bake_render, 1);
			bkr.ready = 0;
			BLI_insert_thread(&threads, &bkr);

			while (bkr.ready == 0) {
				PIL_sleep_ms(50);
				if (bkr.ready)
					break;

				/* used to redraw in 2.4x but this is just for exec in 2.5 */
				if (!G.background)
					blender_test_break();
			}
			BLI_end_threads(&threads);

			if (bkr.result == BAKE_RESULT_NO_OBJECTS)
				BKE_report(op->reports, RPT_ERROR, "No valid images found to bake to");
			else if (bkr.result == BAKE_RESULT_FEEDBACK_LOOP)
				BKE_report(op->reports, RPT_ERROR, "Circular reference in texture stack");

			finish_bake_internal(&bkr);

			result = OPERATOR_FINISHED;
		}
	}

	WM_event_add_notifier(C, NC_SCENE | ND_RENDER_RESULT, scene);

	return result;
}

void OBJECT_OT_bake_image(wmOperatorType *ot)
{
	/* identifiers */
	ot->name = "Bake";
	ot->description = "Bake image textures of selected objects";
	ot->idname = "OBJECT_OT_bake_image";

	/* api callbacks */
	ot->exec = bake_image_exec;
	ot->invoke = objects_bake_render_invoke;
	ot->modal = objects_bake_render_modal;
}
