# -*- coding: utf-8 -*-

# Copyright (C) 2005 Osmo Salomaa
#
# 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 <https://www.gnu.org/licenses/>.

"""Dialog for checking and correcting spelling."""

import aeidon
import gaupol

from aeidon.i18n   import _
from gi.repository import Gtk

class SpellCheckDialog(gaupol.BuilderDialog):

    """
    Dialog for checking and correcting spelling.

    :ivar _doc: :attr:`aeidon.documents` item currectly being checked
    :ivar _entry_handler: Handler for replacement entry's "changed" signal
    :ivar _language: Language code for :attr:`_navigator`
    :ivar _navigator: :class:`aeidon.SpellCheckNavigator` instance used
    :ivar _new_rows: List of rows in the current page with changed texts
    :ivar _new_texts: List of changed texts in the current page
    :ivar _page: :class:`gaupol.Page` instance currectly being checked
    :ivar _pager: Iterator to iterate over all target pages
    :ivar _row: Row currently being checked
    :ivar _rower: Iterator to iterate over all rows
    """

    _widgets = [
        "add_button",
        "entry",
        "grid",
        "ignore_all_button",
        "ignore_button",
        "join_back_button",
        "join_forward_button",
        "replace_all_button",
        "replace_button",
        "save_button",
        "suggestions_list",
        "suggestions_scroller",
        "text_view",
    ]

    def __init__(self, parent, application):
        """Initialize a :class:`SpellCheckDialog` instance."""
        gaupol.BuilderDialog.__init__(self, "spell-check-dialog.ui")
        self.application = application
        self._doc = None
        self._entry_handler = None
        self._language = gaupol.conf.spell_check.language
        self._language_name = aeidon.locales.code_to_name(self._language)
        self._navigator = aeidon.SpellCheckNavigator(self._language)
        self._new_rows = []
        self._new_texts = []
        self._page = None
        self._pager = None
        self._row = None
        self._rower = None
        self._init_dialog(parent)
        self._init_widgets()
        self._init_sensitivities()
        self.set_default_size(*gaupol.conf.spell_check.size)
        self._pager = self._yield_pages()
        next(self._pager)
        self._proceed()

    def _append_changes(self):
        """Append current changes to the list of changes."""
        subtitle = self._page.project.subtitles[self._row]
        if self._navigator.text == subtitle.get_text(self._doc): return
        self._new_rows.append(self._row)
        self._new_texts.append(self._navigator.text)

    def _init_dialog(self, parent):
        """Initialize the dialog."""
        self.set_default_response(Gtk.ResponseType.CLOSE)
        self.set_transient_for(parent)
        self.set_modal(True)

    def _init_sensitivities(self):
        """Initialize widget sensitivities."""
        self._join_back_button.set_sensitive(False)
        self._join_forward_button.set_sensitive(False)
        self._replace_all_button.set_sensitive(False)
        self._replace_button.set_sensitive(False)
        self._save_button.set_sensitive(False)

    def _init_widgets(self):
        """Initialize widget properties."""
        gaupol.style.use_font(self._entry, "custom")
        gaupol.style.use_font(self._text_view, "custom")
        gaupol.style.use_font(self._suggestions_list, "custom")
        self._text_view.set_top_margin(6)
        self._text_view.set_bottom_margin(6)
        text_buffer = self._text_view.get_buffer()
        text_buffer.connect("changed", self._on_text_buffer_changed)
        scale = gaupol.util.scale_to_size
        scale(self._text_view, nchar=55, nlines=4, font="custom")
        scale(self._suggestions_scroller, nchar=20, nlines=6, font="custom")
        self._entry_handler = self._entry.connect("changed", self._on_entry_changed)

    def _on_add_button_clicked(self, *args):
        """Add the current word to personal word list."""
        self._navigator.add()
        self._proceed()

    def _on_entry_changed(self, entry):
        """Populate suggestions based on text in `entry`."""
        word = entry.get_text()
        suggestions = self._navigator.checker.suggest(word) if word else []
        self._populate_suggestions(suggestions, select=False)
        self._replace_button.set_sensitive(bool(word))
        self._replace_all_button.set_sensitive(bool(word))

    def _on_ignore_all_button_clicked(self, *args):
        """Ignore the current word and any subsequent instances."""
        self._navigator.ignore_all()
        self._proceed()

    def _on_ignore_button_clicked(self, *args):
        """Ignore the current word."""
        self._navigator.ignore()
        self._proceed()

    def _on_join_back_button_clicked(self, *args):
        """Join the current word with the previous."""
        self._navigator.join_with_previous()
        self._proceed()

    def _on_join_forward_button_clicked(self, *args):
        """Join the current word with the next."""
        self._navigator.join_with_next()
        self._proceed()

    def _on_replace_all_button_clicked(self, *args):
        """Replace the current word and subsequent instances with `replacement`."""
        self._navigator.replace_all(self._entry.get_text())
        self._proceed()

    def _on_replace_button_clicked(self, *args):
        """Replace the current word with `replacement`."""
        self._navigator.replace(self._entry.get_text())
        self._proceed()

    def _on_response(self, dialog, response):
        """Apply changes to the current page."""
        self._register_changes()
        self._save_geometry()
        self._set_done()

    def _on_suggestions_list_row_selected(self, list_box, row):
        """Copy the selected suggestion into the entry."""
        if row is None: return
        self._set_entry_text(row.get_child().get_text())

    def _on_save_button_clicked(self, *args):
        """Resume checking with text as edited in the text view."""
        text_buffer = self._text_view.get_buffer()
        start, end = text_buffer.get_bounds()
        text = text_buffer.get_text(start, end, False)
        self._navigator.reset(text)
        self._proceed()

    def _on_text_buffer_changed(self, *args):
        """Allow saving and disable actions on the current word."""
        self._save_button.set_sensitive(True)
        self._save_button.add_css_class("suggested-action")
        self._add_button.set_sensitive(False)
        self._ignore_all_button.set_sensitive(False)
        self._ignore_button.set_sensitive(False)
        self._join_back_button.set_sensitive(False)
        self._join_forward_button.set_sensitive(False)
        self._replace_all_button.set_sensitive(False)
        self._replace_button.set_sensitive(False)

    def _populate_suggestions(self, suggestions, select=True):
        """Populate the suggestions list with `suggestions`."""
        self._suggestions_list.remove_all()
        for suggestion in suggestions:
            label = Gtk.Label(label=suggestion,
                              xalign=0,
                              margin_top=6,
                              margin_bottom=6,
                              margin_start=6,
                              margin_end=6)

            self._suggestions_list.append(label)
        if select and suggestions:
            row = self._suggestions_list.get_row_at_index(0)
            self._suggestions_list.select_row(row)
        self._suggestions_scroller.get_vadjustment().set_value(0)

    def _proceed(self):
        """Move on to the next spelling error."""
        try:
            # Move on to the next spelling error in the current text.
            return self._proceed_current()
        except StopIteration:
            # Commit changes to the current text.
            self._append_changes()
        try:
            # Move on to the next row.
            next(self._rower)
            return self._proceed()
        except StopIteration:
            # Commit changes to the current page.
            self._register_changes()
        try:
            # Move on to the next page.
            next(self._pager)
            # Use idle_add here to break out of recursion to
            # reduce the likelihood of hitting the recursion limit.
            return gaupol.util.idle_add(self._proceed)
        except StopIteration:
            self._set_done()

    def _proceed_current(self):
        """Move on to the next spelling error in the current text."""
        next(self._navigator)
        col = self._page.document_to_text_column(self._doc)
        self._page.view.set_focus(self._row, col)
        self._page.view.scroll_to_row(self._row)
        text_buffer = self._text_view.get_buffer()
        text_buffer.set_text(self._navigator.text)
        end = text_buffer.get_iter_at_offset(self._navigator.endpos)
        mark = text_buffer.create_mark(None, end, True)
        self._text_view.scroll_to_mark(mark=mark,
                                       within_margin=0,
                                       use_align=False,
                                       xalign=0.5,
                                       yalign=0.5)

        self._text_view.grab_focus()
        def select_word(text_buffer, pos, endpos):
            ins = text_buffer.get_iter_at_offset(pos)
            bound = text_buffer.get_iter_at_offset(endpos)
            text_buffer.select_range(ins, bound)
        # Some calls fail to select unless idle_add used (GTK-3.24).
        gaupol.util.idle_add(select_word, text_buffer,
                             self._navigator.pos, self._navigator.endpos)
        self._save_button.set_sensitive(False)
        self._save_button.remove_css_class("suggested-action")
        self._add_button.set_sensitive(True)
        self._ignore_all_button.set_sensitive(True)
        self._ignore_button.set_sensitive(True)
        leading = self._navigator.leading_context(1)
        trailing = self._navigator.trailing_context(1)
        self._join_back_button.set_sensitive(leading.isspace())
        self._join_forward_button.set_sensitive(trailing.isspace())
        self._set_entry_text("")
        self._populate_suggestions(self._navigator.suggest())

    def _register_changes(self):
        """Register changes to the current page."""
        if not self._new_rows: return
        self._page.project.replace_texts(
            self._new_rows, self._doc, self._new_texts)
        self._page.project.set_action_description(
            aeidon.registers.DO, _("Spell-checking"))
        self._new_rows = []
        self._new_texts = []

    def _save_geometry(self):
        """Save dialog size."""
        if self.is_maximized(): return
        gaupol.conf.spell_check.size = list(self.get_default_size())

    def _set_done(self):
        """Set state of widgets for finished spell-check."""
        self._text_view.get_buffer().set_text("")
        self._set_entry_text("")
        self._populate_suggestions([])
        self._save_button.set_sensitive(False)
        self._save_button.remove_css_class("suggested-action")
        self._grid.set_sensitive(False)
        self._navigator.checker.write_replacements()

    def _set_entry_text(self, word):
        """Set `word` to the entry with its "changed" handler blocked."""
        self._entry.handler_block(self._entry_handler)
        self._entry.set_text(word)
        self._entry.handler_unblock(self._entry_handler)
        self._replace_button.set_sensitive(bool(word))
        self._replace_all_button.set_sensitive(bool(word))

    def _yield_pages(self):
        """Yield pages to check spelling in."""
        field = gaupol.conf.spell_check.field
        doc = gaupol.util.text_field_to_document(field)
        target = gaupol.conf.spell_check.target
        for page in self.application.get_target_pages(target):
            if not page.project.subtitles: continue
            self.application.set_current_page(page)
            self._page = page
            self._doc = doc
            self._rower = self._yield_rows(page)
            next(self._rower)
            yield page

    def _yield_rows(self, page):
        """Yield rows to check spelling in."""
        for row, subtitle in enumerate(page.project.subtitles):
            self._row = row
            text = subtitle.get_text(self._doc)
            self._navigator.reset(text)
            yield row
