/* Gobby - GTK-based collaborative text editor
 * Copyright (C) 2008-2015 Armin Burgmeier <armin@arbur.net>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include "commands/autosave-commands.hpp"

#include "operations/operation-save.hpp"

#include "core/sessionuserview.hpp"

#include <glibmm/main.h>

#include <ctime>

class Gobby::AutosaveCommands::Info
{
public:
	Info(AutosaveCommands& commands, TextSessionView& view):
		m_commands(commands), m_view(view), m_save_op(NULL)
	{
		GtkSourceBuffer* buffer = m_view.get_text_buffer();

		m_modified_changed_handler = g_signal_connect_after(
			G_OBJECT(buffer), "modified-changed",
			G_CALLBACK(on_modified_changed_static), this);

		// We can't get this correct, so we assume the document was
		// synchronized to disk at the current time. If it has been
		// modified, we schedule a first autosave.
		m_sync_time = std::time(NULL);

		Operations& operations = m_commands.m_operations;
		OperationSave* save_op =
			operations.get_save_operation_for_document(view);
		if(save_op != NULL)
			begin_save_operation(save_op);
		else if(gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(buffer)))
			schedule();
	}

	~Info()
	{
		GtkSourceBuffer* buffer = m_view.get_text_buffer();

		g_signal_handler_disconnect(G_OBJECT(buffer),
		                            m_modified_changed_handler);

		m_timeout_handler.disconnect();
	}

	// Called by AutosaveCommands when the timeout interval has changed.
	// Just reschedule the timeout.
	void reschedule()
	{
		if(m_timeout_handler.connected())
		{
			m_timeout_handler.disconnect();
			schedule();
		}
	}

	// Called by AutosaveCommands
	void begin_save_operation(OperationSave* save_op)
	{
		g_assert(m_save_op == NULL);

		// The document is already being saved, so we don't
		// need to autosave anymore. Reschedule autosave when save
		// operation finished.
		if(m_timeout_handler.connected())
			m_timeout_handler.disconnect();

		m_save_op = save_op;

		m_save_op->signal_finished().connect(
			sigc::mem_fun(
				*this,
				&Info::on_save_operation_finished));
	}

protected:
	void on_modified_changed()
	{
		if(m_save_op == NULL)
		{
			GtkTextBuffer* buffer =
				GTK_TEXT_BUFFER(m_view.get_text_buffer());

			if(!gtk_text_buffer_get_modified(buffer))
				m_timeout_handler.disconnect();
			else
			{
				// Until now the document on disk is in sync
				// with the in-memory buffer. Now the document
				// has changed, so schedule an autosave.
				m_sync_time = std::time(NULL);
				schedule();
			}
		}
	}

	void schedule()
	{
		g_assert(!m_timeout_handler.connected());
		g_assert(m_save_op == NULL);

		// Don't schedule a timeout in case the document has no entry
		// in the document info storage. This means we don't have an
		// uri yet where to save the document. However, we
		// automatically retry when the document is assigned an URI,
		// since the modification flag will change with this anyway.
		const std::string& key = m_view.get_info_storage_key();
		const DocumentInfoStorage::Info* info =
			m_commands.m_info_storage.get_info(key);
		if(!info || info->uri.empty())
			return;

		guint elapsed_seconds = std::time(NULL) - m_sync_time;
		guint autosave_interval = 60 *
			m_commands.m_preferences.editor.autosave_interval;

		if(elapsed_seconds > autosave_interval)
		{
			on_timeout();
		}
		else
		{
			m_timeout_handler =
				Glib::signal_timeout().connect_seconds(
					sigc::mem_fun(
						*this, &Info::on_timeout),
					autosave_interval - elapsed_seconds);
		}
	}

	void on_save_operation_finished(bool success)
	{
		GtkTextBuffer* buffer =
			GTK_TEXT_BUFFER(m_view.get_text_buffer());

		if(success)
			m_sync_time = m_save_op->get_start_time();

		m_save_op = NULL;

		// Schedule the next save operation in case the buffer has
		// been modified since the save operation was started.
		if(gtk_text_buffer_get_modified(buffer))
			schedule();
	}

	bool on_timeout()
	{
		const std::string& key = m_view.get_info_storage_key();
		const DocumentInfoStorage::Info* info =
			m_commands.m_info_storage.get_info(key);

		// Might have been removed from info in the meanwhile, so
		// don't assert here.
		if(info != NULL)
		{
			Glib::RefPtr<Gio::File> file =
				Gio::File::create_for_uri(info->uri);
			m_commands.m_operations.save_document(
				m_view, file,
				info->encoding, info->eol_style);

			g_assert(m_save_op != NULL);

			// Set sync time to operation's start time even though
			// we don't know yet whether the operation will fail,
			// because otherwise we would try to save the
			// document the whole time. This way, we simply retry
			// when the timeout triggers again.
			m_sync_time = m_save_op->get_start_time();
		}

		return false;
	}

private:
	static void on_modified_changed_static(GtkTextBuffer* buffer,
	                                       gpointer user_data)
	{
		static_cast<Info*>(user_data)->on_modified_changed();
	}

	AutosaveCommands& m_commands;
	TextSessionView& m_view;

	gulong m_modified_changed_handler;

	sigc::connection m_timeout_handler;
	OperationSave* m_save_op;

	std::time_t m_sync_time;
};

Gobby::AutosaveCommands::AutosaveCommands(const Folder& folder,
                                          Operations& operations,
                                          const DocumentInfoStorage& storage,
					  const Preferences& preferences):
	m_folder(folder), m_operations(operations),
	m_info_storage(storage), m_preferences(preferences)
{
	m_folder.signal_document_added().connect(
		sigc::mem_fun(*this, &AutosaveCommands::on_document_added));
	m_folder.signal_document_removed().connect(
		sigc::mem_fun(*this, &AutosaveCommands::on_document_removed));

	m_operations.signal_begin_save_operation().connect(
		sigc::mem_fun(
			*this,
			&AutosaveCommands::on_begin_save_operation));

	m_preferences.editor.autosave_enabled.signal_changed().connect(
		sigc::mem_fun(
			*this,
			&AutosaveCommands::on_autosave_enabled_changed));

	m_preferences.editor.autosave_interval.signal_changed().connect(
		sigc::mem_fun(
			*this,
			&AutosaveCommands::on_autosave_interval_changed));

	// Create autosave infos for initial documents
	on_autosave_enabled_changed();
}

Gobby::AutosaveCommands::~AutosaveCommands()
{
	for(InfoMap::iterator iter = m_info_map.begin();
	    iter != m_info_map.end(); ++ iter)
	{
		delete iter->second;
	}
}

void Gobby::AutosaveCommands::on_document_added(SessionView& view)
{
	if(m_preferences.editor.autosave_enabled)
	{
		// We can only save text views:
		TextSessionView* text_view =
			dynamic_cast<TextSessionView*>(&view);
		if(text_view)
		{
			g_assert(m_info_map.find(text_view) ==
			         m_info_map.end());
			m_info_map[text_view] = new Info(*this, *text_view);
		}
	}
}

void Gobby::AutosaveCommands::on_document_removed(SessionView& view)
{
	if(m_preferences.editor.autosave_enabled)
	{
		TextSessionView* text_view =
			dynamic_cast<TextSessionView*>(&view);

		if(text_view)
		{
			InfoMap::iterator iter = m_info_map.find(text_view);
			g_assert(iter != m_info_map.end());
			delete iter->second;
			m_info_map.erase(iter);
		}
	}
}

void Gobby::AutosaveCommands::on_begin_save_operation(
	OperationSave* operation)
{
	TextSessionView* view = operation->get_view();
	// Save operation just started, document must be present
	g_assert(view != NULL);

	if(m_preferences.editor.autosave_enabled)
	{
		InfoMap::iterator iter = m_info_map.find(view);
		g_assert(iter != m_info_map.end());

		iter->second->begin_save_operation(operation);
	}
}

void Gobby::AutosaveCommands::on_autosave_enabled_changed()
{
	if(m_preferences.editor.autosave_enabled)
	{
		for(unsigned int i = 0;
		    i < static_cast<unsigned int>(m_folder.get_n_pages());
		    ++i)
		{
			// TODO: Add convenience API to folder, so that we
			// don't need to know here that it actually contains
			// SessionUserViews.
			const SessionUserView* userview =
				static_cast<const SessionUserView*>(
					m_folder.get_nth_page(i));

			SessionView& view = userview->get_session_view();
			TextSessionView* text_view =
				dynamic_cast<TextSessionView*>(&view);

			if(text_view)
			{
				m_info_map[text_view] =
					new Info(*this, *text_view);
			}
		}
	}
	else
	{
		for(InfoMap::iterator iter = m_info_map.begin();
		    iter != m_info_map.end(); ++ iter)
		{
			delete iter->second;
		}

		m_info_map.clear();
	}
}

void Gobby::AutosaveCommands::on_autosave_interval_changed()
{
	// Propagate to infos
	for(InfoMap::iterator iter = m_info_map.begin();
	    iter != m_info_map.end(); ++ iter)
	{
		return iter->second->reschedule();
	}
}
