/*
 *	subtitleeditor -- a tool to create or edit subtitle
 *
 *	http://home.gna.org/subtitleeditor/
 *	https://gna.org/projects/subtitleeditor/
 *
 *	Copyright @ 2005-2009, kitone
 *
 *	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 3 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, see <http://www.gnu.org/licenses/>.
 *
 *	This class is inspired from gtkspell (gtkspell.sf.net), thanks!
 */

#include "automaticspellchecker.h"
#include "spellchecker.h"
#include <i18n.h>
#include <memory>
#include <algorithm>
#include <gtkmm.h>
#include <isocodes.h>

/*
 *
 */
AutomaticSpellChecker* AutomaticSpellChecker::create_from_textview(Gtk::TextView *view)
{
	g_return_val_if_fail(view, NULL);

	AutomaticSpellChecker *asc = new AutomaticSpellChecker(view);
	return asc;
}

/*
 * Delete the instance of AutomaticSpellChecker attached to the TextView.
 */
void AutomaticSpellChecker::automatic_spell_checker_destroy(gpointer data)
{
	AutomaticSpellChecker* i = reinterpret_cast<AutomaticSpellChecker*>(data);
	delete i;
}

/*
 * The instance is attached to the textview, 
 * it will be destroy himself with the textview.
 */
AutomaticSpellChecker::AutomaticSpellChecker(Gtk::TextView *view)
: Glib::ObjectBase(typeid(AutomaticSpellChecker))
{
	init(view);
}

/*
 *
 */
AutomaticSpellChecker::~AutomaticSpellChecker()
{
}

/*
 * Connect signals with the textview and the textbuffer,
 * create the tag 'asc-misspelled' ...
 */
void AutomaticSpellChecker::init(Gtk::TextView *view)
{
	m_textview = view;

	m_textview->set_data("AutomaticSpellChecker", this, 
			AutomaticSpellChecker::automatic_spell_checker_destroy);

	Glib::RefPtr<Gtk::TextBuffer> m_buffer = view->get_buffer();

	// signals
	m_buffer->signal_insert().connect(
			sigc::mem_fun(*this, &AutomaticSpellChecker::on_insert_text_before), false);
	
	m_buffer->signal_insert().connect(
			sigc::mem_fun(*this, &AutomaticSpellChecker::on_insert_text_after), true);
	
	m_buffer->signal_erase().connect(
			sigc::mem_fun(*this, &AutomaticSpellChecker::on_erase), true);
	
	m_buffer->signal_mark_set().connect(
			sigc::mem_fun(*this, &AutomaticSpellChecker::on_mark_set));

	// Create highlight tag
	m_tag_highlight = m_buffer->create_tag("asc-misspelled");
	m_tag_highlight->property_underline() = Pango::UNDERLINE_ERROR;

	// Tag table signals
	Glib::RefPtr<Gtk::TextBuffer::TagTable> tag_table = m_buffer->get_tag_table();
	
	tag_table->signal_tag_added().connect(
			sigc::mem_fun(*this, &AutomaticSpellChecker::on_tag_added_or_removed));
	
	tag_table->signal_tag_removed().connect(
			sigc::mem_fun(*this, &AutomaticSpellChecker::on_tag_added_or_removed));

	tag_table->signal_tag_changed().connect(
			sigc::mem_fun(*this, &AutomaticSpellChecker::on_tag_changed));

	// Create marks
	Gtk::TextIter start, end; 
	m_buffer->get_bounds(start, end);

	m_mark_insert_start = m_buffer->create_mark("asc-insert-start", start, true);
	m_mark_insert_end = m_buffer->create_mark("asc-insert-end", start, true);
	m_mark_click = m_buffer->create_mark("asc-click", start, true);
	
	m_deferred_check = false;

	// Attach to the view
	view->signal_button_press_event().connect(
			sigc::mem_fun(*this, &AutomaticSpellChecker::on_button_press_event), false);
	view->signal_populate_popup().connect(
			sigc::mem_fun(*this, &AutomaticSpellChecker::on_populate_popup));
	view->signal_popup_menu().connect(
			sigc::mem_fun(*this, &AutomaticSpellChecker::on_popup_menu));
}

/*
 * 
 */
Glib::RefPtr<Gtk::TextBuffer> AutomaticSpellChecker::get_buffer()
{
	return m_textview->get_buffer();
}

/*
 * Insertion works like this:
 *	- before the text is inserted, we mark the position in the buffer.
 *	- after the text is inserted, we see where our mark is and use that and
 *		the current position to check the entire range of inserted text.
 */
void AutomaticSpellChecker::on_insert_text_before(const Gtk::TextBuffer::iterator &pos, const Glib::ustring &text, int bytes)
{
	Glib::RefPtr<Gtk::TextBuffer> m_buffer = get_buffer();
	m_buffer->move_mark(m_mark_insert_start, pos);
}

/*
 * Insertion works like this:
 *	- before the text is inserted, we mark the position in the buffer.
 *	- after the text is inserted, we see where our mark is and use that and
 *		the current position to check the entire range of inserted text.
 */
void AutomaticSpellChecker::on_insert_text_after(const Gtk::TextBuffer::iterator &pos, const Glib::ustring &text, int bytes)
{
	Glib::RefPtr<Gtk::TextBuffer> m_buffer = get_buffer();

	Gtk::TextIter start;
	// we need to check a range of text
	start = m_buffer->get_iter_at_mark(m_mark_insert_start);

	check_range(start, pos, false);

	m_buffer->move_mark(m_mark_insert_end, pos);
}

/*
 * Deleting is more simple:  we're given the range of deleted text.
 * After deletion, the start and end iters should be at the same position
 * (because all of the text between them was deleted!).
 * This means we only really check the words immediately bounding the deletion.
 */
void AutomaticSpellChecker::on_erase(const Gtk::TextBuffer::iterator &start, const Gtk::TextBuffer::iterator &end)
{
	check_range(start, end, false);
}

/*
 *
 */
void AutomaticSpellChecker::on_mark_set(const Gtk::TextBuffer::iterator &location, const Glib::RefPtr<Gtk::TextBuffer::Mark> &mark)
{
	Glib::RefPtr<Gtk::TextBuffer> m_buffer = get_buffer();

	// if the cursor has moved and there is a deferred check so handle it now
	if((mark == m_buffer->get_insert()) && m_deferred_check)
		check_deferred_range(false);
}

/*
 * Set the tag 'highlight' as priority.
 */
void AutomaticSpellChecker::tag_table_changed()
{
	Glib::RefPtr<Gtk::TextBuffer> m_buffer = get_buffer();

	Glib::RefPtr<Gtk::TextBuffer::TagTable> tag_table = m_buffer->get_tag_table();
	m_tag_highlight->set_priority(tag_table->get_size() -1);
}

/*
 * Update the tag 'highlight' priority.
 */
void AutomaticSpellChecker::on_tag_changed(const Glib::RefPtr<Gtk::TextTag> &tag, bool size_changed)
{
	tag_table_changed();
}

/*
 * Update the tag 'highlight' priority.
 */
void AutomaticSpellChecker::on_tag_added_or_removed(const Glib::RefPtr<Gtk::TextTag> &tag)
{
	tag_table_changed();
}

/*
 * Set iterators with the word from mark.
 */
void AutomaticSpellChecker::get_word_extents_from_mark(const Glib::RefPtr<Gtk::TextBuffer::Mark> &mark, Gtk::TextIter &start, Gtk::TextIter &end)
{
	Glib::RefPtr<Gtk::TextBuffer> m_buffer = get_buffer();

	start = m_buffer->get_iter_at_mark(mark);

	if(!start.starts_word())
		iter_backward_word_start(start);

	end = start;

	if(end.inside_word())
		iter_forward_word_end(end);
}

/*
 * Check the word delimited by the iterators and if it's misspell tag it.
 */
void AutomaticSpellChecker::check_word(Gtk::TextIter start, Gtk::TextIter end)
{
	Glib::RefPtr<Gtk::TextBuffer> m_buffer = get_buffer();

	Glib::ustring word = m_buffer->get_text(start, end, false);

	if(!SpellChecker::instance()->check(word))
	{
		m_buffer->apply_tag(m_tag_highlight, start, end);
	}
}

/*
 *
 */
void AutomaticSpellChecker::check_deferred_range(bool force_all)
{
	Glib::RefPtr<Gtk::TextBuffer> m_buffer = get_buffer();

	Gtk::TextIter start, end;
	start = m_buffer->get_iter_at_mark(m_mark_insert_start);
	end = m_buffer->get_iter_at_mark(m_mark_insert_end);

	check_range(start, end, force_all);
}

/*
 * Check words delimited by the iterators.
 */
void AutomaticSpellChecker::check_range(Gtk::TextIter start, Gtk::TextIter end, bool force_all)
{
	// we need to "split" on word boundaries.
	// luckily, Pango knows what "words" are 
	// so we don't have to figure it out.
	Gtk::TextIter wstart, wend, cursor, precursor;
	bool highlight;

	if(end.inside_word())
		iter_forward_word_end(end);

	if(!start.starts_word())
	{
		if(start.inside_word() || start.ends_word())
		{
			iter_backward_word_start(start);
		}
		else
		{
			// if we're neither at the beginning nor inside a word,
			// me must be in some spaces.
			// skip forward to the beginning of the next word.
			if(iter_forward_word_end(start))
				iter_backward_word_start(start);
		}
	}

	Glib::RefPtr<Gtk::TextBuffer> m_buffer = get_buffer();

	cursor = m_buffer->get_iter_at_mark(m_buffer->get_insert());
	
	precursor = cursor;
	precursor.backward_char();

	highlight = cursor.has_tag(m_tag_highlight) || precursor.has_tag(m_tag_highlight);

	m_buffer->remove_tag(m_tag_highlight, start, end);

	// Fix a corner case when replacement occurs at beginning of buffer:
	// An iter at offset 0 seems to always be inside a word,
	// even if it's not.  Possibly a pango bug.
	if(start.get_offset() == 0)
	{
		iter_forward_word_end(start);
		iter_backward_word_start(start);
	}

	wstart = start;

	while(wstart.compare(end) < 0)
	{
		// move wend to the end of the current word.
		wend = wstart;
		iter_forward_word_end(wend);

		bool inword = (wstart.compare(cursor) < 0) && (cursor.compare(wend) < 0);

		if(inword && !force_all)
		{
			// this word is being actively edited,
			// only check if it's already highligted,
			// otherwise defer this check until later.
			if(highlight)
				check_word(wstart, wend);
			else
				m_deferred_check = true;
		}
		else
		{
			check_word(wstart, wend);
			m_deferred_check = false;
		}

		// now move wend to the beginning of the next word,
		iter_forward_word_end(wend);
		iter_backward_word_start(wend);

		// make sure we've actually advanced
		// (we don't advance in some corner cases),
		if(wstart.compare(wend) == 0)
			break; // we're done in these cases..

		// and then pick this as the new next word beginning.
		wstart = wend;
	}
}

/*
 * Recheck all the textbuffer.
 */
void AutomaticSpellChecker::recheck_all()
{
	Gtk::TextIter start, end;

	get_buffer()->get_bounds(start, end);

	check_range(start, end, true);
}

/*
 * Update the mark click.
 */
bool AutomaticSpellChecker::on_popup_menu()
{
	Gtk::TextIter iter;
	Glib::RefPtr<Gtk::TextBuffer> m_buffer = get_buffer();

	// handle deferred check if it exists
	if(m_deferred_check)
		check_deferred_range(true);

	iter = m_buffer->get_iter_at_mark(m_buffer->get_insert());
	m_buffer->move_mark(m_mark_click, iter);
	return false;
}

/*
 * Build spell check menu.
 *
 * The menu 'languages' is always added to allow the user to change the dictionary 
 * without misspell word unlike the menu 'suggestions' added only if the word is misspell.
 */
void AutomaticSpellChecker::on_populate_popup(Gtk::Menu *menu)
{
	Gtk::Image *img;
	Gtk::MenuItem *mi;
	Gtk::TextIter start, end;
	Glib::ustring word;

	// menu separator comes first.
	mi = manage(new Gtk::MenuItem);
	mi->show();
	menu->prepend(*mi);

	// the languages menu.
	img = manage(new Gtk::Image(Gtk::Stock::SPELL_CHECK, Gtk::ICON_SIZE_MENU));
	mi = manage(new Gtk::ImageMenuItem(*img, _("_Languages"), true));
	mi->set_submenu(*build_languages_menu());
	mi->show_all();
	menu->prepend(*mi);

	// we need to figure out if they picked a misspelled word.
	get_word_extents_from_mark(m_mark_click, start, end);

	// if our highlight algorithm ever messes up,
	// this isn't correct, either.
	if(!start.has_tag(m_tag_highlight))
		return; // word wasn't misspelled

	// build suggestions from the misspelled word
	word = get_buffer()->get_text(start, end, false);
	
	//mi->set_submenu(*build_suggestion_menu(word));
	build_suggestion_menu(word, menu);
}

/*
 * When the user right-clicks on a word, they want to check that word.
 * Here, we do NOT  move the cursor to the location of the clicked-upon word
 * since that prevents the use of edit functions on the context menu.
 */
bool AutomaticSpellChecker::on_button_press_event(GdkEventButton *ev)
{
	if(ev->button == 3)
	{
		gint x, y;
		Gtk::TextIter iter;

		Glib::RefPtr<Gtk::TextBuffer> m_buffer = get_buffer();

		// handle deferred check if it exists
		if(m_deferred_check)
			check_deferred_range(true);

		m_textview->window_to_buffer_coords(Gtk::TEXT_WINDOW_TEXT, ev->x, ev->y, x, y);
		m_textview->get_iter_at_location(iter, x, y);

		m_buffer->move_mark(m_mark_click, iter);
	}
	// return false: let gtk process this event, too.
	// we don't want to eat any events.
	return false; 
}

/*
 * Build a suggestions menu from misspelled word, 'menu' is directly used to add menu items.
 *
 * 'Ignore All' and 'Add "%" to Dictionary" are always created.
 * Create suggestions list, if they are more than ten, create submenu 'More...' for they.
 */
void AutomaticSpellChecker::build_suggestion_menu(const Glib::ustring &word, Gtk::Menu *menu)
{
	Gtk::MenuItem *mi;

	// separator
	mi = manage(new Gtk::MenuItem);
	mi->show();
	menu->prepend(*mi);
	// ignore all
	mi = manage(new Gtk::ImageMenuItem(
				*manage(new Gtk::Image(Gtk::Stock::REMOVE, Gtk::ICON_SIZE_MENU)) , 
				_("_Ignore all"), true));
	mi->signal_activate().connect(
			sigc::mem_fun(*this, &AutomaticSpellChecker::on_ignore_all));
	mi->show();
	menu->prepend(*mi);
	// add to dictionary
	mi = manage(new Gtk::ImageMenuItem(
				*manage(new Gtk::Image(Gtk::Stock::ADD, Gtk::ICON_SIZE_MENU)) , 
				Glib::ustring::compose(
					_("_Add \"%1\" to Dictionary"), word), true));
	mi->signal_activate().connect(
			sigc::mem_fun(*this, &AutomaticSpellChecker::on_add_to_dictionary));
	mi->show();
	menu->prepend(*mi);


	// suggestions
	std::vector<Glib::ustring> suggestions = SpellChecker::instance()->get_suggest(word);

	if(suggestions.empty())
	{
		Gtk::Label* label = manage(new Gtk::Label);
		label->set_text(_("(no suggested words)"));
		label->set_alignment(Gtk::ALIGN_LEFT, Gtk::ALIGN_CENTER);

		mi = manage(new Gtk::MenuItem);
		mi->set_sensitive(false);
		mi->add(*label);
		mi->show_all();
		menu->prepend(*mi);
	}
	else
	{
		// Need to reverse the list as we add items using preprend to
		// respects the order of the suggestions
		std::reverse(suggestions.begin(), suggestions.end());

		Gtk::Menu *me = menu;

		for(unsigned int i=0; i< suggestions.size(); ++i)
		{
			if( i != 0 && i % 10 == 0)
			{
				// separator
				mi = manage(new Gtk::MenuItem);
				me->prepend(*mi);
				// menu more
				mi = manage(new Gtk::ImageMenuItem(
							*manage(new Gtk::Image(Gtk::Stock::SPELL_CHECK, Gtk::ICON_SIZE_MENU)), 
							_("_More..."), true));
				mi->show_all();
	
				me->prepend(*mi);
				// create and add submenu to 'More...'
				me = manage(new Gtk::Menu);
				me->show();
				mi->set_submenu(*me);
			}

			Gtk::Label* label = manage(new Gtk::Label);
			label->set_text(Glib::ustring::compose("<b>%1</b>", suggestions[i]));
			label->set_alignment(Gtk::ALIGN_LEFT, Gtk::ALIGN_CENTER);
			label->set_use_markup(true);

			mi = manage(new Gtk::MenuItem);
			mi->signal_activate().connect(
					sigc::bind(sigc::mem_fun(*this, &AutomaticSpellChecker::on_replace_word), suggestions[i]));
			mi->add(*label);
			mi->show_all();
			me->prepend(*mi);
		}
	}
}

/*
 * We get the word from the 'mark click' and we replace it by the 'newword'.
 * Store the replacement in the SpellChecker, that future occurrences of the word
 * will be replaced with the 'newword'.
 *
 * It's call by the item spell suggestions.
 */
void AutomaticSpellChecker::on_replace_word(const Glib::ustring &newword)
{
	if(newword.empty())
		return;

	Gtk::TextIter start, end;

	Glib::RefPtr<Gtk::TextBuffer> m_buffer = get_buffer();

	get_word_extents_from_mark(m_mark_click, start, end);

	Glib::ustring oldword = m_buffer->get_text(start, end, false);

	m_buffer->begin_user_action();
	start = m_buffer->erase(start, end);
	m_buffer->insert(start, newword);
	m_buffer->end_user_action();

	SpellChecker::instance()->store_replacement(oldword, newword);
}

/*
 * We get the word from the 'mark click' and we add the word to 
 * the session of the spell checker. 
 * It will be ignored in the future the time of the session.
 *
 * It's call by the suggestions item "Ignore all".
 */
void AutomaticSpellChecker::on_ignore_all()
{
	Gtk::TextIter start, end;

	Glib::RefPtr<Gtk::TextBuffer> m_buffer = get_buffer();

	get_word_extents_from_mark(m_mark_click, start, end);

	Glib::ustring word = m_buffer->get_text(start, end, false);

	SpellChecker::instance()->add_word_to_session(word);

	recheck_all();
}

/*
 * We get the word from the 'mark click' and we add the word to 
 * the personal dictionary. 
 * It will be ignored in the future.
 *
 * It's call by the suggestions item "Add %s to Dictionary".
 */
void AutomaticSpellChecker::on_add_to_dictionary()
{
	Gtk::TextIter start, end;

	Glib::RefPtr<Gtk::TextBuffer> m_buffer = get_buffer();

	get_word_extents_from_mark(m_mark_click, start, end);

	Glib::ustring word = m_buffer->get_text(start, end, false);

	SpellChecker::instance()->add_word_to_personal(word);

	recheck_all();
}

/*
 * Create and return a languages menu.
 */
Gtk::Menu* AutomaticSpellChecker::build_languages_menu()
{
	Gtk::Menu *menu;
	Gtk::MenuItem* mi;

	menu = manage(new Gtk::Menu);

	// Get dictionaries avaible
	std::vector<Glib::ustring> dictionaries = SpellChecker::instance()->get_dictionaries();

	// Transform isocode to human label ex: 'fr_fr' to 'French (France)'
	std::map<Glib::ustring, Glib::ustring> languages;

	for(unsigned int i=0; i< dictionaries.size(); ++i)
	{
		Glib::ustring isocode = dictionaries[i];
		Glib::ustring name = isocodes::to_name(isocode);

		languages[name] = isocode;
	}

	Glib::ustring current = SpellChecker::instance()->get_dictionary();

	// Create menu items
	for(std::map<Glib::ustring, Glib::ustring>::iterator it = languages.begin(); it != languages.end(); ++it)
	{
		if(it->second == current)
		{
			mi = manage(new Gtk::ImageMenuItem(
						*manage(new Gtk::Image(Gtk::Stock::APPLY, Gtk::ICON_SIZE_MENU)) , 
						it->first, true));
		}
		else
		{
			mi = manage(new Gtk::MenuItem(it->first));
		}
		mi->signal_activate().connect(
				sigc::bind(sigc::mem_fun(*this, &AutomaticSpellChecker::on_set_current_language), it->second));
		menu->append(*mi);
	}

	menu->show_all();
	return menu;
}

/*
 * Update the spellchecker with a new dictionary.
 */
void AutomaticSpellChecker::on_set_current_language(const Glib::ustring &isocode)
{
	SpellChecker::instance()->set_dictionary(isocode);
	// FIXME: connect to SpellChecker ?
	recheck_all();
}

/*
 * heuristic:
 * if we're on an singlequote/apostrophe and
 * if the next letter is alphanumeric, this is an apostrophe.
 */
bool AutomaticSpellChecker::iter_forward_word_end(Gtk::TextIter &i)
{
	if(!i.forward_word_end())
		return false;
	if(i.get_char() != '\'')
		return true;

	Gtk::TextIter iter = i;
	if(iter.forward_char())
		if(g_unichar_isalpha(iter.get_char()))
			return i.forward_word_end();
	return true;
}

/*
 * heuristic:
 * if we're on an singlequote/apostrophe and
 * if the next letter is alphanumeric, this is an apostrophe.
 */
bool AutomaticSpellChecker::iter_backward_word_start(Gtk::TextIter &i)
{
	if(!i.backward_word_start())
		return false;

	Gtk::TextIter iter = i;
	if(iter.backward_char())
		if(iter.get_char() == '\'')
			if(iter.backward_char())
				if(g_unichar_isalpha(iter.get_char()))
					return i.backward_word_start();
	return true;
}

