// subtitleeditor -- a tool to create or edit subtitle
//
// https://kitone.github.io/subtitleeditor/
// https://github.com/kitone/subtitleeditor/
//
// Copyright @ 2005-2018, 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/>.

#include <extension/action.h>
#include <gtkmm_utility.h>
#include <isocodes.h>
#include <spellchecker.h>
#include <utility.h>
#include <memory>

class DialogSpellChecking : public Gtk::Dialog {
  class ComboBoxLanguages : public Gtk::ComboBox {
    class Column : public Gtk::TreeModel::ColumnRecord {
     public:
      Column() {
        add(label);
        add(isocode);
      }
      Gtk::TreeModelColumn<Glib::ustring> label;
      Gtk::TreeModelColumn<Glib::ustring> isocode;
    };

   public:
    ComboBoxLanguages(BaseObjectType* cobject,
                      const Glib::RefPtr<Gtk::Builder>&)
        : Gtk::ComboBox(cobject) {
      liststore = Gtk::ListStore::create(column);
      set_model(liststore);

      Gtk::CellRendererText* renderer = manage(new Gtk::CellRendererText);
      pack_start(*renderer);
      add_attribute(*renderer, "text", 0);

      liststore->set_sort_column(0, Gtk::SORT_ASCENDING);
    }

    void append_lang(const Glib::ustring& isocode) {
      Gtk::TreeIter it = liststore->append();
      (*it)[column.isocode] = isocode;
      (*it)[column.label] = isocodes::to_name(isocode);
    }

    Glib::ustring get_active_lang() {
      Gtk::TreeIter it = get_active();
      if (it)
        return (*it)[column.isocode];
      return Glib::ustring();
    }

    bool set_active_lang(const Glib::ustring& isocode) {
      Gtk::TreeIter it = liststore->children().begin();
      while (it) {
        if ((*it)[column.isocode] == isocode) {
          set_active(it);
          return true;
        }
        ++it;
      }
      return false;
    }

   protected:
    Column column;
    Glib::RefPtr<Gtk::ListStore> liststore;
  };

  class SuggestionColumn : public Gtk::TreeModel::ColumnRecord {
   public:
    SuggestionColumn() {
      add(string);
    }
    Gtk::TreeModelColumn<Glib::ustring> string;
  };

 public:
  DialogSpellChecking(BaseObjectType* cobject,
                      const Glib::RefPtr<Gtk::Builder>& xml)
      : Gtk::Dialog(cobject),
        m_current_document(NULL),
        m_current_column("text") {
    se_dbg_msg(SE_DBG_SPELL_CHECKING, "create spellchecking dialog...");

    utility::set_transient_parent(*this);

    xml->get_widget_derived("combobox-languages", m_comboLanguages);

    xml->get_widget("textview", m_textview);
    xml->get_widget("entry-replace-with", m_entryReplaceWith);
    xml->get_widget("button-check-word", m_buttonCheckWord);

    xml->get_widget("treeview-suggestions", m_treeviewSuggestions);

    xml->get_widget("button-replace", m_buttonReplace);
    xml->get_widget("button-ignore", m_buttonIgnore);
    xml->get_widget("button-ignore-all", m_buttonIgnoreAll);
    xml->get_widget("button-add-word", m_buttonAddWord);

    setup_languages();
    setup_signals();
    setup_text_view();
    setup_suggestions_view();
  }

  void execute(Document* doc) {
    g_return_if_fail(doc);

    m_current_document = doc;
    // If the current focus is in the translation column,
    // check the translation. default value is "text".
    if (doc->get_current_column_name() == "translation")
      m_current_column = "translation";

    show_column_warning();

    m_current_sub = doc->subtitles().get_first();

    init_text_view_with_subtitle(m_current_sub);
    update_status_from_replace_word();  // init the state of buttons
    check_next_word();

    doc->start_command(_("Spell Checking"));
    run();
    doc->finish_command();
  }

 protected:
  // Information: Display a message about how-to spellcheck
  // the translation column.
  void show_column_warning() {
    // Don't disable this message again option
    if (cfg::has_key("spell-checking", "disable-column-warning")) {
      if (cfg::get_boolean("spell-checking", "disable-column-warning")) {
        return;
      }
    }

    Glib::ustring msg(
        _("The spell check is applied to the column \"text\" as default. "
          "You can check the column \"translation\" by setting the focus "
          "to this column before starting the spell check."));

    Gtk::MessageDialog dialog(msg);

    Gtk::CheckButton checkDisable(_("_Do not show this message again"), true);
    checkDisable.show();
    dialog.get_vbox()->pack_start(checkDisable, false, false);

    dialog.run();
    // Save the status if it's activated
    if (checkDisable.get_active()) {
      cfg::set_boolean("spell-checking", "disable-column-warning", true);
    }
  }

  void setup_languages() {
    se_dbg_msg(SE_DBG_SPELL_CHECKING, "setup languages dictionaries");

    // Add dictionaries
    auto dicts = SpellChecker::instance()->get_dictionaries();
    for (const auto& dictionary : dicts) {
      m_comboLanguages->append_lang(dictionary);
    }
    // Set the current dictionary
    m_comboLanguages->set_active_lang(
        SpellChecker::instance()->get_dictionary());

    m_comboLanguages->signal_changed().connect(
        sigc::mem_fun(*this, &DialogSpellChecking::on_combo_languages_changed));
  }

  void setup_signals() {
    se_dbg_msg(SE_DBG_SPELL_CHECKING, "setup signals (buttons ...)");

    m_buttonCheckWord->signal_clicked().connect(
        sigc::mem_fun(*this, &DialogSpellChecking::on_check_word));

    m_buttonReplace->signal_clicked().connect(
        sigc::mem_fun(*this, &DialogSpellChecking::on_replace));

    m_buttonIgnore->signal_clicked().connect(
        sigc::mem_fun(*this, &DialogSpellChecking::on_ignore));

    m_buttonIgnoreAll->signal_clicked().connect(
        sigc::mem_fun(*this, &DialogSpellChecking::on_ignore_all));

    m_buttonAddWord->signal_clicked().connect(
        sigc::mem_fun(*this, &DialogSpellChecking::on_add_word_to_dictionary));

    //
    m_entryReplaceWith->signal_changed().connect(sigc::mem_fun(
        *this, &DialogSpellChecking::update_status_from_replace_word));

    m_entryReplaceWith->signal_activate().connect(
        sigc::mem_fun(*this, &DialogSpellChecking::on_replace));
  }

  void setup_text_view() {
    se_dbg_msg(SE_DBG_SPELL_CHECKING,
               "setup textview, create highlight tag and marks");

    m_textview->set_editable(false);
    m_textview->set_sensitive(false);

    m_buffer = m_textview->get_buffer();
    // create misspelled tag
    m_tag_highlight = m_buffer->create_tag("misspelled");
    m_tag_highlight->property_foreground() = "red";
    m_tag_highlight->property_weight() = Pango::WEIGHT_BOLD;
    // create start/end marks
    m_mark_start = m_buffer->create_mark("mark-start", m_buffer->begin(), true);
    m_mark_end = m_buffer->create_mark("mark-end", m_buffer->begin(), true);
  }

  // Create a model and a column "Suggestions" and connect
  // the signal selection-changed to update the text value
  // of the widget m_entryReplaceWith.
  void setup_suggestions_view() {
    se_dbg_msg(SE_DBG_SPELL_CHECKING,
               "create model and view for the suggestions");

    SuggestionColumn column;
    m_listSuggestions = Gtk::ListStore::create(column);
    m_treeviewSuggestions->set_model(m_listSuggestions);

    Gtk::TreeViewColumn* col =
        manage(new Gtk::TreeViewColumn(_("Suggestions")));
    Gtk::CellRendererText* renderer = manage(new Gtk::CellRendererText);

    col->pack_start(*renderer, false);
    col->add_attribute(renderer->property_text(), column.string);
    m_treeviewSuggestions->append_column(*col);

    m_treeviewSuggestions->get_selection()->signal_changed().connect(
        sigc::mem_fun(*this,
                      &DialogSpellChecking::on_suggestions_selection_changed));

    m_treeviewSuggestions->signal_row_activated().connect(sigc::mem_fun(
        *this, &DialogSpellChecking::on_suggestions_row_activated));
  }

  // Initialize the textview with the current subtitle text (text or
  // translation); update the mark to the beginning.
  bool init_text_view_with_subtitle(const Subtitle& sub) {
    if (!sub) {
      se_dbg_msg(SE_DBG_SPELL_CHECKING, "Subtitle is not valid");
      return false;
    }
    // Check the translation or the text column.
    Glib::ustring text = (m_current_column == "translation")
                             ? sub.get_translation()
                             : sub.get_text();

    se_dbg_msg(SE_DBG_SPELL_CHECKING,
               "Update the textview with (%s column): '%s'",
               m_current_column.c_str(), text.c_str());

    m_buffer->set_text(text);
    m_textview->set_sensitive(!text.empty());
    // move the marks to the beginning
    Gtk::TextIter begin = m_buffer->begin();
    m_buffer->move_mark(m_mark_start, begin);
    m_buffer->move_mark(m_mark_end, begin);
    return true;
  }

  // Update the subtitle text (or translation)
  // from the current text in the textview.
  void update_subtitle_from_text_view() {
    if (!m_current_sub)
      return;

    Glib::ustring text = m_buffer->get_text();

    se_dbg_msg(SE_DBG_SPELL_CHECKING,
               "the subtitle (%s) is update with the text '%s'",
               m_current_column.c_str(), text.c_str());

    if (m_current_column == "translation") {
      if (m_current_sub.get_translation() != text)
        m_current_sub.set_translation(text);
    } else {  // "text"
      if (m_current_sub.get_text() != text)
        m_current_sub.set_text(text);
    }
  }

  // Initialize the list with the suggestions array.
  void init_suggestions(const Glib::ustring& word) {
    se_dbg_msg(SE_DBG_SPELL_CHECKING,
               "initialize the suggestion with the word '%s'", word.c_str());

    m_entryReplaceWith->set_text("");
    m_listSuggestions->clear();

    if (word.empty())
      return;

    auto suggs = SpellChecker::instance()->get_suggest(word);

    SuggestionColumn column;

    for (const auto& suggesion : suggs) {
      Gtk::TreeIter it = m_listSuggestions->append();
      (*it)[column.string] = suggesion;

      se_dbg_msg(SE_DBG_SPELL_CHECKING, "suggested word: '%s'",
                 suggesion.c_str());
    }
  }

  bool next_check() {
    if (check_next_word())
      return true;
    return check_next_subtitle();
  }

  bool check_next_subtitle() {
    if (!m_current_sub || !(++m_current_sub)) {
      completed_spell_changed();
      return false;
    }
    init_text_view_with_subtitle(m_current_sub);
    return next_check();
  }

  bool 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;
  }

  bool 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;
  }

  // return True if there is misspelled word.
  bool check_next_word() {
    Gtk::TextIter start = m_buffer->begin();
    Gtk::TextIter end = m_buffer->end();

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

    Gtk::TextIter wstart, wend;

    // Start at the mark_end, go to the next word
    wstart = m_mark_end->get_iter();
    if (!iter_forward_word_end(wstart) || !iter_backward_word_start(wstart))
      return check_next_subtitle();

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

      // Check the word
      if (is_misspelled(wstart, wend))
        return true;  // misspelled word

      // Good word
      // so we move wend to the beginning of the next word
      iter_forward_word_end(wend);
      iter_backward_word_start(wend);

      if (wstart.compare(wend) == 0)
        break;

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

  // Check the word (start, end)
  // Return true if the word is misspelled.
  bool is_misspelled(Gtk::TextIter start, Gtk::TextIter end) {
    Glib::ustring word = m_textview->get_buffer()->get_text(start, end, false);

    se_dbg_msg(SE_DBG_SPELL_CHECKING, "check the word : '%s'", word.c_str());

    if (SpellChecker::instance()->check(word)) {
      se_dbg_msg(SE_DBG_SPELL_CHECKING, "the word '%s' is not misspelled",
                 word.c_str());
      return false;
    }
    se_dbg_msg(SE_DBG_SPELL_CHECKING, "the word '%s' is misspelled",
               word.c_str());

    m_buffer->apply_tag(m_tag_highlight, start, end);
    m_buffer->move_mark(m_mark_start, start);
    m_buffer->move_mark(m_mark_end, end);

    // Update the suggestions list
    init_suggestions(word);
    // Select the subtitle if it's not already select
    if (!m_current_document->subtitles().is_selected(m_current_sub))
      m_current_document->subtitles().select(m_current_sub);
    return true;
  }

  // Return the current word misspelled (the mark tags are used)
  Glib::ustring get_current_word() {
    Gtk::TextIter start = m_mark_start->get_iter();
    Gtk::TextIter end = m_mark_end->get_iter();

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

    se_dbg_msg(SE_DBG_SPELL_CHECKING, "the current word is '%s'", word.c_str());

    return word;
  }

  // Callbacks

  // Used by the button "Check Word" (m_buttonCheckWord)
  // Check the word from the entry "Replace With:" (m_entryReplaceWith)
  // and display show the suggestions.
  void on_check_word() {
    se_dbg(SE_DBG_SPELL_CHECKING);

    Glib::ustring newword = m_entryReplaceWith->get_text();
    init_suggestions(newword);
    // Because init_suggestions clean the widget with empty text,
    // we need to reset it with the neword
    m_entryReplaceWith->set_text(newword);
  }

  // Replace the word by the selected suggestion (m_entryReplaceWith)
  void on_replace() {
    se_dbg(SE_DBG_SPELL_CHECKING);

    Glib::ustring newword = m_entryReplaceWith->get_text();
    if (newword.empty())
      return;

    Gtk::TextIter start = m_mark_start->get_iter();
    Gtk::TextIter end = m_mark_end->get_iter();

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

    se_dbg_msg(SE_DBG_SPELL_CHECKING,
               "replace the word '%s' by the new word '%s'", oldword.c_str(),
               newword.c_str());

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

    // update the mark_end, the mark_start doesn't move
    m_buffer->move_mark(m_mark_end, end);

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

    // we update the subtitle with the text changed
    update_subtitle_from_text_view();

    next_check();
  }

  // Ignore the word and just go to the next word.
  void on_ignore() {
    se_dbg_msg(SE_DBG_SPELL_CHECKING, "ignore the word '%s'",
               get_current_word().c_str());

    next_check();
  }

  // Ignore the word the time of the session.
  void on_ignore_all() {
    Glib::ustring word = get_current_word();

    se_dbg_msg(SE_DBG_SPELL_CHECKING,
               "ignore all the word '%s' by adding the word to the session",
               word.c_str());

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

  // Add the misspelled word to the personal dictionary
  void on_add_word_to_dictionary() {
    Glib::ustring word = get_current_word();

    se_dbg_msg(SE_DBG_SPELL_CHECKING,
               "add the word '%s' to the personal dictionary", word.c_str());

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

    next_check();
  }

  // Update the entry "Replace With:" with the
  // current selected suggestion.
  void on_suggestions_selection_changed() {
    Gtk::TreeIter it = m_treeviewSuggestions->get_selection()->get_selected();
    if (it) {
      SuggestionColumn col;
      Glib::ustring word = (*it)[col.string];

      m_entryReplaceWith->set_text(word);
    }
  }

  // Double click on the suggestion replace with the word activated
  void on_suggestions_row_activated(const Gtk::TreeModel::Path& path,
                                    Gtk::TreeViewColumn*) {
    Gtk::TreeIter it = m_listSuggestions->get_iter(path);
    if (it) {
      SuggestionColumn col;
      Glib::ustring word = (*it)[col.string];

      m_entryReplaceWith->set_text(word);

      on_replace();
    }
  }

  // The user change the current dictionary.
  // We need to recheck the current word after we have setting a new dictionary.
  void on_combo_languages_changed() {
    Glib::ustring lang = m_comboLanguages->get_active_lang();
    if (lang == SpellChecker::instance()->get_dictionary())
      return;

    SpellChecker::instance()->set_dictionary(lang);
    // recheck the current word and if it's not misspelled check the next word
    if (!is_misspelled(m_mark_start->get_iter(), m_mark_end->get_iter()))
      next_check();
  }

  // Update the sensitivity of some buttons
  // from the status of the entry "Replace With:" (empty or not)
  void update_status_from_replace_word() {
    bool state = !m_entryReplaceWith->get_text().empty();

    se_dbg_msg(SE_DBG_SPELL_CHECKING, "set sensitive to %s",
               (state) ? "true" : "false");

    m_buttonCheckWord->set_sensitive(state);
    m_buttonReplace->set_sensitive(state);
  }

  // Disable the interface and display a message:
  // "Completed spell checking."
  void completed_spell_changed() {
    se_dbg_msg(SE_DBG_SPELL_CHECKING,
               "completed spell checking, disable the ui.");

    m_comboLanguages->set_sensitive(false);

    m_textview->set_sensitive(false);
    m_buffer->set_text(_("Completed spell checking."));

    m_entryReplaceWith->set_sensitive(false);
    m_buttonCheckWord->set_sensitive(false);

    m_treeviewSuggestions->set_sensitive(false);
    m_buttonIgnore->set_sensitive(false);
    m_buttonIgnoreAll->set_sensitive(false);
    m_buttonReplace->set_sensitive(false);
    m_buttonAddWord->set_sensitive(false);
  }

 protected:
  ComboBoxLanguages* m_comboLanguages;

  Gtk::TextView* m_textview;
  Glib::RefPtr<Gtk::TextBuffer> m_buffer;
  Glib::RefPtr<Gtk::TextMark> m_mark_start;
  Glib::RefPtr<Gtk::TextMark> m_mark_end;
  Glib::RefPtr<Gtk::TextTag> m_tag_highlight;

  Gtk::Entry* m_entryReplaceWith;
  Gtk::TreeView* m_treeviewSuggestions;
  Glib::RefPtr<Gtk::ListStore> m_listSuggestions;

  Gtk::Button* m_buttonCheckWord;
  Gtk::Button* m_buttonReplace;
  Gtk::Button* m_buttonIgnore;
  Gtk::Button* m_buttonIgnoreAll;
  Gtk::Button* m_buttonAddWord;

  Document* m_current_document;
  Glib::ustring m_current_column;
  Subtitle m_current_sub;
};

class SpellCheckingPlugin : public Action {
 public:
  SpellCheckingPlugin() {
    activate();
    update_ui();
  }

  ~SpellCheckingPlugin() {
    deactivate();
  }

  void activate() {
    se_dbg(SE_DBG_PLUGINS);

    // actions
    action_group = Gtk::ActionGroup::create("SpellCheckingPlugin");

    action_group->add(
        Gtk::Action::create("spell-checking", Gtk::Stock::SPELL_CHECK,
                            _("_Spell Check"), _("Launch the spell checking")),
        Gtk::AccelKey("F7"),
        sigc::mem_fun(*this, &SpellCheckingPlugin::on_execute));

    // ui
    Glib::RefPtr<Gtk::UIManager> ui = get_ui_manager();

    ui_id = ui->new_merge_id();

    ui->insert_action_group(action_group);

    ui->add_ui(ui_id, "/menubar/menu-tools/checking", "spell-checking",
               "spell-checking");
  }

  void deactivate() {
    se_dbg(SE_DBG_PLUGINS);

    Glib::RefPtr<Gtk::UIManager> ui = get_ui_manager();

    ui->remove_ui(ui_id);
    ui->remove_action_group(action_group);
  }

  void update_ui() {
    se_dbg(SE_DBG_PLUGINS);

    bool visible = (get_current_document() != NULL);

    action_group->get_action("spell-checking")->set_sensitive(visible);
  }

 protected:
  void on_execute() {
    se_dbg(SE_DBG_PLUGINS);

    Document* doc = get_current_document();
    g_return_if_fail(doc);

    // create dialog
    std::unique_ptr<DialogSpellChecking> dialog(
        gtkmm_utility::get_widget_derived<DialogSpellChecking>(
            SE_DEV_VALUE(SE_PLUGIN_PATH_UI, SE_PLUGIN_PATH_DEV),
            "dialog-spell-checking.ui", "dialog-spell-checking"));

    dialog->execute(doc);
  }

 protected:
  Gtk::UIManager::ui_merge_id ui_id;
  Glib::RefPtr<Gtk::ActionGroup> action_group;
};

REGISTER_EXTENSION(SpellCheckingPlugin)
