/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
/*
 * Libburner-burn
 * Copyright (C) Philippe Rouquier 2005-2009 <bonfire-app@wanadoo.fr>
 *
 * Libburner-burn 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.
 *
 * The Libburner-burn authors hereby grant permission for non-GPL compatible
 * GStreamer plugins to be used and distributed together with GStreamer
 * and Libburner-burn. This permission is above and beyond the permissions granted
 * by the GPL license by which Libburner-burn is covered. If you modify this code
 * you may extend this exception to your version of the code, but you are not
 * obligated to do so. If you do not wish to do so, delete this exception
 * statement from your version.
 * 
 * Libburner-burn 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 Library 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.
 */

#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#include <glib.h>
#include <glib-object.h>
#include <glib/gi18n-lib.h>

#include <gdk/gdk.h>

#include "burn-basics.h"
#include "burn-debug.h"
#include "burner-session.h"
#include "burn-task.h"
#include "burn-task-item.h"
#include "burn-task-ctx.h"

#include "burner-track-image.h"
#include "burner-track-stream.h"

static void burner_task_class_init (BurnerTaskClass *klass);
static void burner_task_init (BurnerTask *sp);
static void burner_task_finalize (GObject *object);

typedef struct _BurnerTaskPrivate BurnerTaskPrivate;

struct _BurnerTaskPrivate {
	/* The loop for the task */
	GMainLoop *loop;

	/* used to poll for progress (every 0.5 sec) */
	gint clock_id;

	BurnerTaskItem *leader;
	BurnerTaskItem *first;

	/* result of the task */
	BurnerBurnResult retval;
	GError *error;
};

#define BURNER_TASK_PRIVATE(o)  (G_TYPE_INSTANCE_GET_PRIVATE ((o), BURNER_TYPE_TASK, BurnerTaskPrivate))
G_DEFINE_TYPE (BurnerTask, burner_task, BURNER_TYPE_TASK_CTX);

static GObjectClass *parent_class = NULL;

#define MAX_JOB_START_ATTEMPTS			5
#define JOB_ATTEMPTS_WAIT_TIME			1

void
burner_task_add_item (BurnerTask *task, BurnerTaskItem *item)
{
	BurnerTaskPrivate *priv;

	g_return_if_fail (BURNER_IS_TASK (task));
	g_return_if_fail (BURNER_IS_TASK_ITEM (item));

	priv = BURNER_TASK_PRIVATE (task);

	if (priv->leader) {
		burner_task_item_link (priv->leader, item);
		g_object_unref (priv->leader);
	}

	if (!priv->first)
		priv->first = item;

	priv->leader = item;
	g_object_ref (priv->leader);
}

static void
burner_task_reset_real (BurnerTask *task)
{
	BurnerTaskPrivate *priv;

	priv = BURNER_TASK_PRIVATE (task);

	if (priv->loop)
		g_main_loop_unref (priv->loop);

	priv->loop = NULL;
	priv->clock_id = 0;
	priv->retval = BURNER_BURN_OK;

	if (priv->error) {
		g_error_free (priv->error);
		priv->error = NULL;
	}

	burner_task_ctx_reset (BURNER_TASK_CTX (task));
}

void
burner_task_reset (BurnerTask *task)
{
	BurnerTaskPrivate *priv;

	priv = BURNER_TASK_PRIVATE (task);

	if (burner_task_is_running (task))
		burner_task_cancel (task, TRUE);

	g_object_unref (priv->leader);
	burner_task_reset_real (task);
}

static gboolean
burner_task_clock_tick (gpointer data)
{
	BurnerTask *task = BURNER_TASK (data);
	BurnerTaskPrivate *priv;
	BurnerTaskItem *item;

	priv = BURNER_TASK_PRIVATE (task);

	/* some jobs need to be called periodically to update their status
	 * because the main process run in a thread. We do it before calling
	 * progress/action changed so they can update the task on time */
	for (item = priv->leader; item; item = burner_task_item_previous (item)) {
		BurnerTaskItemIFace *klass;

		klass = BURNER_TASK_ITEM_GET_CLASS (item);
		if (klass->clock_tick)
			klass->clock_tick (item, BURNER_TASK_CTX (task), NULL);
	}

	/* now call ctx to update progress */
	burner_task_ctx_report_progress (BURNER_TASK_CTX (data));

	return TRUE;
}

/**
 * Used to run/stop task
 */

static BurnerBurnResult
burner_task_deactivate_item (BurnerTask *task,
			      BurnerTaskItem *item,
			      GError **error)
{
	BurnerBurnResult result = BURNER_BURN_OK;
	BurnerTaskItemIFace *klass;

	if (!burner_task_item_is_active (item)) {
		BURNER_BURN_LOG ("%s already stopped", G_OBJECT_TYPE_NAME (item));
		return BURNER_BURN_OK;
	}

	/* stop task for real now */
	BURNER_BURN_LOG ("stopping %s", G_OBJECT_TYPE_NAME (item));

	klass = BURNER_TASK_ITEM_GET_CLASS (item);
	if (klass->stop)
		result = klass->stop (item,
				      BURNER_TASK_CTX (task),
				      error);

	BURNER_BURN_LOG ("stopped %s", G_OBJECT_TYPE_NAME (item));
	return result;
}

static BurnerBurnResult
burner_task_send_stop_signal (BurnerTask *task,
			       BurnerBurnResult retval,
			       GError **error)
{
	BurnerTaskItem *item;
	BurnerTaskPrivate *priv;
	GError *local_error = NULL;
	BurnerBurnResult result = retval;

	priv = BURNER_TASK_PRIVATE (task);

	item = priv->leader;
	while (burner_task_item_previous (item))
		item = burner_task_item_previous (item);

	/* we stop all the slaves first and then go up the list */
	for (; item; item = burner_task_item_next (item)) {
		GError *item_error;

		item_error = NULL;

		/* stop task for real now */
		result = burner_task_deactivate_item (task, item, &item_error);
		if (item_error) {
			g_error_free (local_error);
			local_error = item_error;
		}
	}

	if (local_error) {
		if (error && *error == NULL)
			g_propagate_error (error, local_error);
		else
			g_error_free (local_error);
	}

	/* we don't want to lose the original result if it was not OK */
	return (result == BURNER_BURN_OK? retval:result);
}

static gboolean
burner_task_wakeup (gpointer user_data)
{
	BurnerTaskPrivate *priv;

	priv = BURNER_TASK_PRIVATE (user_data);

	if (priv->loop)
		g_main_loop_quit (priv->loop);

	priv->clock_id = 0;
	priv->retval = BURNER_BURN_OK;
	return FALSE;
}

static BurnerBurnResult
burner_task_sleep (BurnerTask *self, guint sec)
{
	BurnerTaskPrivate *priv;

	priv = BURNER_TASK_PRIVATE (self);

	BURNER_BURN_LOG ("wait loop");

	priv->loop = g_main_loop_new (NULL, FALSE);
	priv->clock_id = g_timeout_add_seconds (sec,
	                                        burner_task_wakeup,
	                                        self);

	GDK_THREADS_LEAVE ();  
	g_main_loop_run (priv->loop);
	GDK_THREADS_ENTER ();

	g_main_loop_unref (priv->loop);
	priv->loop = NULL;

	if (priv->clock_id) {
		g_source_remove (priv->clock_id);
		priv->clock_id = 0;
	}

	return priv->retval;
}

static BurnerBurnResult
burner_task_start_item (BurnerTask *task,
			 BurnerTaskItem *item,
			 GError **error)
{
	guint attempts = 0;
	BurnerBurnResult result;
	GError *ret_error = NULL;
	BurnerTaskItemIFace *klass;

	klass = BURNER_TASK_ITEM_GET_CLASS (item);
	if (!klass->start)
		return BURNER_BURN_ERR;

	BURNER_BURN_LOG ("::start method %s", G_OBJECT_TYPE_NAME (item));

	result = klass->start (item, &ret_error);
	while (result == BURNER_BURN_RETRY) {
		/* FIXME: a GError?? */
		if (attempts >= MAX_JOB_START_ATTEMPTS) {
			if (ret_error)
				g_propagate_error (error, ret_error);

			return BURNER_BURN_ERR;
		}

		if (ret_error) {
			g_error_free (ret_error);
			ret_error = NULL;
		}

		result = burner_task_sleep (task, 1);
		if (result != BURNER_BURN_OK)
			return result;

		attempts ++;
		result = klass->start (item, &ret_error);
	}

	if (ret_error)
		g_propagate_error (error, ret_error);

	return result;
}

static BurnerBurnResult
burner_task_activate_item (BurnerTask *task,
			    BurnerTaskItem *item,
			    GError **error)
{
	BurnerTaskItemIFace *klass;
	BurnerBurnResult result = BURNER_BURN_OK;

	klass = BURNER_TASK_ITEM_GET_CLASS (item);
	if (!klass->activate)
		return BURNER_BURN_ERR;

	BURNER_BURN_LOG ("::activate method %s", G_OBJECT_TYPE_NAME (item));

	result = klass->activate (item, BURNER_TASK_CTX (task), error);
	return result;
}

static void
burner_task_stop (BurnerTask *task,
		   BurnerBurnResult retval,
		   GError *error)
{
	BurnerBurnResult result;
	BurnerTaskPrivate *priv;

	priv = BURNER_TASK_PRIVATE (task);

	/* burner_job_error/burner_job_finished ()
	 * should not be called during ::init and ::start.
	 * Instead a job should return errors directly */
	result = burner_task_send_stop_signal (task, retval, &error);

	priv->retval = retval;
	priv->error = error;

	if (priv->loop && g_main_loop_is_running (priv->loop))
		g_main_loop_quit (priv->loop);
	else
		BURNER_BURN_LOG ("task was asked to stop (%i/%i) during ::init or ::start",
				  result, retval);
}

BurnerBurnResult
burner_task_cancel (BurnerTask *task,
		     gboolean protect)
{
	if (protect && burner_task_ctx_get_dangerous (BURNER_TASK_CTX (task)))
		return BURNER_BURN_DANGEROUS;

	burner_task_stop (task, BURNER_BURN_CANCEL, NULL);
	return BURNER_BURN_OK;
}

gboolean
burner_task_is_running (BurnerTask *task)
{
	BurnerTaskPrivate *priv;

	priv = BURNER_TASK_PRIVATE (task);
	return (priv->loop && g_main_loop_is_running (priv->loop));
}

static void
burner_task_finished (BurnerTaskCtx *ctx,
		       BurnerBurnResult retval,
		       GError *error)
{
	BurnerTask *self;
	BurnerTaskPrivate *priv;

	self = BURNER_TASK (ctx);
	priv = BURNER_TASK_PRIVATE (self);

	/* see if we have really started a loop */
	/* FIXME: shouldn't it be an error if it is called
	 * while the loop is not running ? */
	if (!burner_task_is_running (self))
		return;
		
	if (retval == BURNER_BURN_RETRY) {
		BurnerTaskItem *item;
		GError *error_item = NULL;

		/* There are some tracks left, get the first
		 * task item and restart it. */
		item = priv->leader;
		while (burner_task_item_previous (item))
			item = burner_task_item_previous (item);

		if (burner_task_item_start (item, &error_item) != BURNER_BURN_OK)
			burner_task_stop (self, BURNER_BURN_ERR, error_item);

		return;
	}

	burner_task_stop (self, retval, error);
}

static BurnerBurnResult
burner_task_run_loop (BurnerTask *self,
		       GError **error)
{
	BurnerTaskPrivate *priv;

	priv = BURNER_TASK_PRIVATE (self);

	burner_task_ctx_report_progress (BURNER_TASK_CTX (self));

	priv->clock_id = g_timeout_add (500,
					burner_task_clock_tick,
					self);

	priv->loop = g_main_loop_new (NULL, FALSE);

	BURNER_BURN_LOG ("entering loop");

	GDK_THREADS_LEAVE ();  
	g_main_loop_run (priv->loop);
	GDK_THREADS_ENTER ();

	BURNER_BURN_LOG ("got out of loop");
	g_main_loop_unref (priv->loop);
	priv->loop = NULL;

	if (priv->error) {
		g_propagate_error (error, priv->error);
		priv->error = NULL;
	}

	/* stop all progress reporting thing */
	if (priv->clock_id) {
		g_source_remove (priv->clock_id);
		priv->clock_id = 0;
	}

	if (priv->retval == BURNER_BURN_OK
	&&  burner_task_ctx_get_progress (BURNER_TASK_CTX (self), NULL) == BURNER_BURN_OK) {
		burner_task_ctx_set_progress (BURNER_TASK_CTX (self), 1.0);
		burner_task_ctx_report_progress (BURNER_TASK_CTX (self));
	}

	burner_task_ctx_stop_progress (BURNER_TASK_CTX (self));
	return priv->retval;	
}

static BurnerBurnResult
burner_task_set_track_output_size_default (BurnerTask *self,
					    GError **error)
{
	BurnerTrack *track = NULL;

	BURNER_BURN_LOG ("Trying to set a default output size");

	burner_task_ctx_get_current_track (BURNER_TASK_CTX (self), &track);
//	BURNER_BURN_LOG_TYPE (&input, "Track type");

	if (BURNER_IS_TRACK_IMAGE (track)
	||  BURNER_IS_TRACK_STREAM (track)) {
		BurnerBurnResult result;
		goffset sectors = 0;
		goffset bytes = 0;

		result = burner_track_get_size (track,
						 &sectors,
						 &bytes);
		if (result != BURNER_BURN_OK)
			return result;

		BURNER_BURN_LOG ("Got a default image or stream track length %lli", sectors);
		burner_task_ctx_set_output_size_for_current_track (BURNER_TASK_CTX (self),
								    sectors,
								    bytes);
	}

	return BURNER_BURN_OK;
}

static BurnerBurnResult
burner_task_activate_items (BurnerTask *self,
			     GError **error)
{
	BurnerTaskPrivate *priv;
	BurnerBurnResult retval;
	BurnerTaskItem *item;

	priv = BURNER_TASK_PRIVATE (self);

	retval = BURNER_BURN_NOT_RUNNING;
	for (item = priv->first; item; item = burner_task_item_next (item)) {
		BurnerBurnResult result;

		result = burner_task_activate_item (self, item, error);
		if (result == BURNER_BURN_NOT_RUNNING) {
			BURNER_BURN_LOG ("::start skipped for %s",
					  G_OBJECT_TYPE_NAME (item));
			continue;
		}

		if (result != BURNER_BURN_OK)
			return result;

		retval = BURNER_BURN_OK;
	}

	return retval;
}

static BurnerBurnResult
burner_task_start_items (BurnerTask *self,
			  GError **error)
{
	BurnerBurnResult retval;
	BurnerTaskPrivate *priv;
	BurnerTaskItem *item;

	priv = BURNER_TASK_PRIVATE (self);

	/* start from the master down to the slave */
	retval = BURNER_BURN_NOT_SUPPORTED;
	for (item = priv->leader; item; item = burner_task_item_previous (item)) {
		BurnerBurnResult result;

		if (!burner_task_item_is_active (item))
			continue;

		result = burner_task_start_item (self, item, error);
		if (result == BURNER_BURN_NOT_SUPPORTED) {
			BURNER_BURN_LOG ("%s doesn't support action",
					  G_OBJECT_TYPE_NAME (item));

			/* "fake mode" to get size. Forgive the jobs that cannot
			 * retrieve the size for one track. Just deactivate and
			 * go on with the next.
			 * NOTE: after this result the job is no longer active */
			continue;
		}

		/* if the following is true don't stop everything */
		if (result == BURNER_BURN_NOT_RUNNING)
			return result;

		if (result != BURNER_BURN_OK)
			return result;

		retval = BURNER_BURN_OK;
	}

	if (retval == BURNER_BURN_NOT_SUPPORTED) {
		/* if all jobs did not want/could not run then resort to a
		 * default function and return BURNER_BURN_NOT_RUNNING */
		retval = burner_task_set_track_output_size_default (self, error);
		if (retval != BURNER_BURN_OK)
			return retval;

		return BURNER_BURN_NOT_RUNNING;
	}

	return burner_task_run_loop (self, error);
}

static BurnerBurnResult
burner_task_start (BurnerTask *self,
		    gboolean fake,
		    GError **error)
{
	BurnerBurnResult result = BURNER_BURN_OK;
	BurnerTaskPrivate *priv;

	priv = BURNER_TASK_PRIVATE (self);

	BURNER_BURN_LOG ("Starting %s task (%i)",
			  fake ? "fake":"normal",
			  burner_task_ctx_get_action (BURNER_TASK_CTX (self)));

	/* check the task is not running */
	if (burner_task_is_running (self)) {
		BURNER_BURN_LOG ("task is already running");
		return BURNER_BURN_RUNNING;
	}

	if (!priv->leader) {
		BURNER_BURN_LOG ("no jobs");
		return BURNER_BURN_RUNNING;
	}

	burner_task_ctx_set_fake (BURNER_TASK_CTX (self), fake);
	burner_task_ctx_reset (BURNER_TASK_CTX (self));

	/* Activate all items that can be. If no item can be then skip */
	result = burner_task_activate_items (self, error);
	if (result == BURNER_BURN_NOT_RUNNING) {
		BURNER_BURN_LOG ("Task skipped");
		return BURNER_BURN_OK;
	}

	if (result != BURNER_BURN_OK)
		return result;

	result = burner_task_start_items (self, error);
	while (result == BURNER_BURN_NOT_RUNNING) {
		BURNER_BURN_LOG ("current track skipped");

		/* this track was skipped without actual loop therefore see if
		 * there is another track and, if there is, start again */
		result = burner_task_ctx_next_track (BURNER_TASK_CTX (self));
		if (result != BURNER_BURN_RETRY) {
			burner_task_send_stop_signal (self, result, NULL);
			return result;
		}

		result = burner_task_start_items (self, error);
	}

	if (result != BURNER_BURN_OK)
		burner_task_send_stop_signal (self, result, NULL);

	return result;
}

BurnerBurnResult
burner_task_check (BurnerTask *self,
		    GError **error)
{
	BurnerTaskAction action;

	g_return_val_if_fail (BURNER_IS_TASK (self), BURNER_BURN_ERR);

	/* the task MUST be of a BURNER_TASK_ACTION_NORMAL type */
	action = burner_task_ctx_get_action (BURNER_TASK_CTX (self));
	if (action != BURNER_TASK_ACTION_NORMAL)
		return BURNER_BURN_OK;

	/* The main purpose of this function is to get the final size of the
	 * task output whether it be recorded to disc or stored as a file later.
	 * That size will be stored by task-ctx.
	 * To do this we run all the task in fake mode that means we don't write
	 * anything to disc or hard drive. Only the last running job in the
	 * chain will be aware that we're running in fake mode / get-size mode.
	 * All others will be told to image. To determine what should be the
	 * last job and therefore the one telling the final size of the output,
	 * we start to call ::init for each job starting from the leader. we
	 * don't skip recording jobs in case they modify the contents (and
	 * therefore the output size). When a job returns NOT_RUNNING after
	 * ::init then we skip it; this return value will mean that it won't
	 * change the output size or that it has already determined the output
	 * size. Only the last running job is allowed to set the final size (see
	 * burn-jobs.c), all values from other jobs will be ignored. */

	return burner_task_start (self, TRUE, error);
}

BurnerBurnResult
burner_task_run (BurnerTask *self,
		  GError **error)
{
	g_return_val_if_fail (BURNER_IS_TASK (self), BURNER_BURN_ERR);

	return burner_task_start (self, FALSE, error);
}

static void
burner_task_class_init (BurnerTaskClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	BurnerTaskCtxClass *ctx_class = BURNER_TASK_CTX_CLASS (klass);

	g_type_class_add_private (klass, sizeof (BurnerTaskPrivate));

	parent_class = g_type_class_peek_parent(klass);
	object_class->finalize = burner_task_finalize;

	ctx_class->finished = burner_task_finished;
}

static void
burner_task_init (BurnerTask *obj)
{ }

static void
burner_task_finalize (GObject *object)
{
	BurnerTask *cobj;
	BurnerTaskPrivate *priv;

	cobj = BURNER_TASK (object);
	priv = BURNER_TASK_PRIVATE (cobj);

	if (priv->leader) {
		g_object_unref (priv->leader);
		priv->leader = NULL;
	}

	G_OBJECT_CLASS (parent_class)->finalize (object);
}

BurnerTask *
burner_task_new ()
{
	BurnerTask *obj;

	obj = BURNER_TASK (g_object_new (BURNER_TYPE_TASK, NULL));
	return obj;
}
