#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2003-2007  Donald N. Allingham
# Copyright (C) 2007-2012  Brian G. Matherly
# Copyright (C) 2010       Jakim Friant
# Copyright (C) 2012       Nick Hall
# Copyright (C) 2011-2016  Paul Franklin
#
# 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 2 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

""" GUI dialog for creating and managing books """

# Written by Alex Roitman,
# largely based on the BaseDoc classes by Don Allingham

# -------------------------------------------------------------------------
#
# Standard Python modules
#
# -------------------------------------------------------------------------

# ------------------------------------------------------------------------
#
# Set up logging
#
# ------------------------------------------------------------------------
import logging

LOG = logging.getLogger(".Book")

# -------------------------------------------------------------------------
#
# GTK/Gnome modules
#
# -------------------------------------------------------------------------
from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import GObject

# -------------------------------------------------------------------------
#
# Gramps modules
#
# -------------------------------------------------------------------------
from gramps.gen.const import GRAMPS_LOCALE as glocale

_ = glocale.translation.gettext
from ...listmodel import ListModel
from gramps.gen.errors import FilterError, ReportError
from gramps.gen.const import URL_MANUAL_PAGE
from ...display import display_help
from ...pluginmanager import GuiPluginManager
from ...dialog import WarningDialog, ErrorDialog, QuestionDialog2
from gramps.gen.plug.menu import PersonOption, FamilyOption
from gramps.gen.plug.docgen import StyleSheet
from ...managedwindow import ManagedWindow, set_titles
from ...glade import Glade
from ...utils import is_right_click, open_file_with_default_application
from ...user import User
from .. import make_gui_option

# Import from specific modules in ReportBase
from gramps.gen.plug.report import BookList, Book, BookItem, append_styles
from gramps.gen.plug.report import CATEGORY_BOOK, book_categories
from gramps.gen.plug.report._options import ReportOptions
from ._reportdialog import ReportDialog
from ._docreportdialog import DocReportDialog

# ------------------------------------------------------------------------
#
# Private Constants
#
# ------------------------------------------------------------------------
_UNSUPPORTED = _("Unsupported")

_RETURN = Gdk.keyval_from_name("Return")
_KP_ENTER = Gdk.keyval_from_name("KP_Enter")
WIKI_HELP_PAGE = URL_MANUAL_PAGE + "_-_Reports_-_part_3"
WIKI_HELP_SEC = _("Books")
GENERATE_WIKI_HELP_SEC = _("Generate_Book_dialog")


# ------------------------------------------------------------------------
#
# Private Functions
#
# ------------------------------------------------------------------------
def _initialize_options(options, dbstate, uistate):
    """
    Validates all options by making sure that their values are consistent with
    the database.

    menu: The Menu class
    dbase: the database the options will be applied to
    """
    if not hasattr(options, "menu"):
        return
    dbase = dbstate.get_database()
    if dbase.get_total() == 0:
        return
    menu = options.menu

    for name in menu.get_all_option_names():
        option = menu.get_option_by_name(name)
        value = option.get_value()

        if isinstance(option, PersonOption):
            if not dbase.get_person_from_gramps_id(value):
                person_handle = uistate.get_active("Person")
                person = dbase.get_person_from_handle(person_handle)
                option.set_value(person.get_gramps_id())

        elif isinstance(option, FamilyOption):
            if not dbase.get_family_from_gramps_id(value):
                person_handle = uistate.get_active("Person")
                person = dbase.get_person_from_handle(person_handle)
                if person is None:
                    continue
                family_list = person.get_family_handle_list()
                if family_list:
                    family_handle = family_list[0]
                else:
                    try:
                        family_handle = next(dbase.iter_family_handles())
                    except StopIteration:
                        family_handle = None
                if family_handle:
                    family = dbase.get_family_from_handle(family_handle)
                    option.set_value(family.get_gramps_id())
                else:
                    print("No family specified for ", name)


# ------------------------------------------------------------------------
#
# BookListDisplay class
#
# ------------------------------------------------------------------------
class BookListDisplay:
    """
    Interface into a dialog with the list of available books.

    Allows the user to select and/or delete a book from the list.
    """

    def __init__(self, booklist, nodelete=False, dosave=False, parent=None):
        """
        Create a BookListDisplay object that displays the books in BookList.

        booklist:   books that are displayed -- a :class:`.BookList` instance
        nodelete:   if True then the Delete button is hidden
        dosave:     if True then the book list is flagged to be saved if needed
        """

        self.booklist = booklist
        self.dosave = dosave
        self.xml = Glade("book.glade", toplevel="book")
        self.top = self.xml.toplevel
        self.unsaved_changes = False

        set_titles(self.top, self.xml.get_object("title2"), _("Available Books"))

        if nodelete:
            delete_button = self.xml.get_object("delete_button")
            delete_button.hide()
        self.xml.connect_signals(
            {
                "on_booklist_cancel_clicked": self.on_booklist_cancel_clicked,
                "on_booklist_ok_clicked": self.on_booklist_ok_clicked,
                "on_booklist_delete_clicked": self.on_booklist_delete_clicked,
                "on_book_ok_clicked": self.do_nothing,
                "on_book_help_clicked": self.do_nothing,
                "destroy_passed_object": self.do_nothing,
                "on_setup_clicked": self.do_nothing,
                "on_down_clicked": self.do_nothing,
                "on_up_clicked": self.do_nothing,
                "on_remove_clicked": self.do_nothing,
                "on_add_clicked": self.do_nothing,
                "on_edit_clicked": self.do_nothing,
                "on_open_clicked": self.do_nothing,
                "on_save_clicked": self.do_nothing,
                "on_clear_clicked": self.do_nothing,
            }
        )
        self.guilistbooks = self.xml.get_object("list")
        self.guilistbooks.connect("button-press-event", self.on_button_press)
        self.guilistbooks.connect("key-press-event", self.on_key_pressed)
        self.blist = ListModel(
            self.guilistbooks,
            [("Name", -1, 10)],
        )

        self.redraw()
        self.selection = None
        self.top.set_transient_for(parent)
        self.top.run()

    def redraw(self):
        """Redraws the list of currently available books"""

        self.blist.model.clear()
        names = self.booklist.get_book_names()
        if not len(names):
            return
        for name in names:
            the_iter = self.blist.add([name])
        if the_iter:
            self.blist.selection.select_iter(the_iter)

    def on_booklist_ok_clicked(self, obj):
        """
        Return selected book.
        Also marks the current list to be saved into the xml file, if needed.
        """
        store, the_iter = self.blist.get_selected()
        if the_iter:
            data = self.blist.get_data(the_iter, [0])
            self.selection = self.booklist.get_book(str(data[0]))
        if self.dosave and self.unsaved_changes:
            self.booklist.set_needs_saving(True)

    def on_booklist_delete_clicked(self, obj):
        """
        Deletes selected book from the list.

        This change is not final. OK button has to be clicked to save the list.
        """
        store, the_iter = self.blist.get_selected()
        if not the_iter:
            return
        data = self.blist.get_data(the_iter, [0])
        self.booklist.delete_book(str(data[0]))
        self.blist.remove(the_iter)
        self.unsaved_changes = True
        self.top.run()

    def on_booklist_cancel_clicked(self, *obj):
        """cancel the booklist dialog"""
        if self.unsaved_changes:
            qqq = QuestionDialog2(
                _("Discard Unsaved Changes"),
                _("You have made changes which have not been saved."),
                _("Proceed"),
                _("Cancel"),
                parent=self.top,
            )
            if not qqq.run():
                self.top.run()

    def on_button_press(self, obj, event):
        """
        Checks for a double click event. In the list, we want to
        treat a double click as if it was OK button press.
        """
        if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS and event.button == 1:
            store, the_iter = self.blist.get_selected()
            if not the_iter:
                return False
            self.on_booklist_ok_clicked(obj)
            # emit OK response on dialog to close it automatically
            self.top.response(-5)
            return True
        return False

    def on_key_pressed(self, obj, event):
        """
        Handles the return key being pressed on list. If the key is pressed,
        the Edit button handler is called
        """
        if event.type == Gdk.EventType.KEY_PRESS:
            if event.keyval in (_RETURN, _KP_ENTER):
                self.on_booklist_ok_clicked(obj)
                # emit OK response on dialog to close it automatically
                self.top.response(-5)
                return True
        return False

    def do_nothing(self, obj):
        """do nothing"""
        pass


# ------------------------------------------------------------------------
#
# Book Options
#
# ------------------------------------------------------------------------
class BookOptions(ReportOptions):
    """
    Defines options and provides handling interface.
    """

    def __init__(self, name, dbase):
        ReportOptions.__init__(self, name, dbase)

        # Options specific for this report
        self.options_dict = {
            "bookname": "",
        }
        # TODO since the CLI code for the "book" generates its own "help" now,
        # the GUI code would be faster if it didn't list all the possible books
        self.options_help = {
            "bookname": (
                "=name",
                _("Name of the book. MANDATORY"),
                BookList("books.xml", dbase).get_book_names(),
                False,
            ),
        }


# -------------------------------------------------------------------------
#
# Book creation dialog
#
# -------------------------------------------------------------------------
class BookSelector(ManagedWindow):
    """
    Interface into a dialog setting up the book.

    Allows the user to add/remove/reorder/setup items for the current book
    and to clear/load/save/edit whole books.
    """

    def __init__(self, dbstate, uistate):
        self._db = dbstate.db
        self.dbstate = dbstate
        self.uistate = uistate
        self.title = _("Manage Books")
        self.file = "books.xml"

        ManagedWindow.__init__(self, uistate, [], self.__class__)

        self.xml = Glade("book.glade", toplevel="top")
        window = self.xml.toplevel

        title_label = self.xml.get_object("title")
        self.set_window(window, title_label, self.title)
        self.setup_configs("interface.bookselector", 700, 600)
        self.show()
        self.xml.connect_signals(
            {
                "on_add_clicked": self.on_add_clicked,
                "on_remove_clicked": self.on_remove_clicked,
                "on_up_clicked": self.on_up_clicked,
                "on_down_clicked": self.on_down_clicked,
                "on_setup_clicked": self.on_setup_clicked,
                "on_clear_clicked": self.on_clear_clicked,
                "on_save_clicked": self.on_save_clicked,
                "on_open_clicked": self.on_open_clicked,
                "on_edit_clicked": self.on_edit_clicked,
                "on_book_help_clicked": lambda x: display_help(
                    WIKI_HELP_PAGE, WIKI_HELP_SEC
                ),
                "on_book_ok_clicked": self.on_book_ok_clicked,
                "destroy_passed_object": self.on_close_clicked,
                # Insert dummy handlers for second top level in the glade file
                "on_booklist_ok_clicked": lambda _: None,
                "on_booklist_delete_clicked": lambda _: None,
                "on_booklist_cancel_clicked": lambda _: None,
                "on_booklist_ok_clicked": lambda _: None,
                "on_booklist_ok_clicked": lambda _: None,
            }
        )

        self.avail_tree = self.xml.get_object("avail_tree")
        self.book_tree = self.xml.get_object("book_tree")
        self.avail_tree.connect("button-press-event", self.avail_button_press)
        self.book_tree.connect("button-press-event", self.book_button_press)

        self.name_entry = self.xml.get_object("name_entry")
        self.name_entry.set_text(_("New Book"))

        avail_label = self.xml.get_object("avail_label")
        avail_label.set_text("<b>%s</b>" % _("_Available items"))
        avail_label.set_use_markup(True)
        avail_label.set_use_underline(True)
        book_label = self.xml.get_object("book_label")
        book_label.set_text("<b>%s</b>" % _("Current _book"))
        book_label.set_use_underline(True)
        book_label.set_use_markup(True)

        avail_titles = [(_("Name"), 0, 230), (_("Type"), 1, 80), ("", -1, 0)]

        book_titles = [
            (_("Item name"), -1, 230),
            (_("Type"), -1, 80),
            ("", -1, 0),
            (_("Subject"), -1, 50),
        ]

        self.avail_nr_cols = len(avail_titles)
        self.book_nr_cols = len(book_titles)

        self.avail_model = ListModel(self.avail_tree, avail_titles)
        self.book_model = ListModel(self.book_tree, book_titles)
        self.draw_avail_list()

        self.book = Book()
        self.book_list = BookList(self.file, self._db)
        self.book_list.set_needs_saving(False)  # just read in: no need to save

    def build_menu_names(self, obj):
        return (_("Book selection list"), self.title)

    def draw_avail_list(self):
        """
        Draw the list with the selections available for the book.

        The selections are read from the book item registry.
        """
        pmgr = GuiPluginManager.get_instance()
        regbi = pmgr.get_reg_bookitems()
        if not regbi:
            return

        available_reports = []
        for pdata in regbi:
            category = _UNSUPPORTED
            if pdata.supported and pdata.category in book_categories:
                category = book_categories[pdata.category]
            available_reports.append([pdata.name, category, pdata.id])
        for data in sorted(available_reports):
            new_iter = self.avail_model.add(data)

        self.avail_model.connect_model()

        if new_iter:
            self.avail_model.selection.select_iter(new_iter)
            path = self.avail_model.model.get_path(new_iter)
            col = self.avail_tree.get_column(0)
            self.avail_tree.scroll_to_cell(path, col, 1, 1, 0.0)

    def open_book(self, book):
        """
        Open the book: set the current set of selections to this book's items.

        book:   the book object to load.
        """
        if book.get_paper_name():
            self.book.set_paper_name(book.get_paper_name())
        if book.get_orientation() is not None:  # 0 is legal
            self.book.set_orientation(book.get_orientation())
        if book.get_paper_metric() is not None:  # 0 is legal
            self.book.set_paper_metric(book.get_paper_metric())
        if book.get_custom_paper_size():
            self.book.set_custom_paper_size(book.get_custom_paper_size())
        if book.get_margins():
            self.book.set_margins(book.get_margins())
        if book.get_format_name():
            self.book.set_format_name(book.get_format_name())
        if book.get_output():
            self.book.set_output(book.get_output())
        if book.get_dbname() != self._db.get_save_path():
            WarningDialog(
                _("Different database"),
                _(
                    "This book was created with the references to database "
                    "%s.\n\n This makes references to the central person "
                    "saved in the book invalid.\n\n"
                    "Therefore, the central person for each item is being set "
                    "to the active person of the currently opened database."
                )
                % book.get_dbname(),
                parent=self.window,
            )

        self.book.clear()
        self.book_model.clear()
        for saved_item in book.get_item_list():
            name = saved_item.get_name()
            item = BookItem(self._db, name)

            # The option values were loaded magically by the book parser.
            # But they still need to be applied to the menu options.
            opt_dict = item.option_class.handler.options_dict
            orig_opt_dict = saved_item.option_class.handler.options_dict
            menu = item.option_class.menu
            for optname in opt_dict:
                opt_dict[optname] = orig_opt_dict[optname]
                menu_option = menu.get_option_by_name(optname)
                if menu_option:
                    menu_option.set_value(opt_dict[optname])

            _initialize_options(item.option_class, self.dbstate, self.uistate)
            item.set_style_name(saved_item.get_style_name())
            self.book.append_item(item)

            data = [item.get_translated_name(), item.get_category(), item.get_name()]

            data[2] = item.option_class.get_subject()
            self.book_model.add(data)

    def on_add_clicked(self, obj):
        """
        Add an item to the current selections.

        Use the selected available item to get the item's name in the registry.
        """
        store, the_iter = self.avail_model.get_selected()
        if not the_iter:
            return
        data = self.avail_model.get_data(the_iter, list(range(self.avail_nr_cols)))
        item = BookItem(self._db, data[2])
        _initialize_options(item.option_class, self.dbstate, self.uistate)
        data[2] = item.option_class.get_subject()
        self.book_model.add(data)
        self.book.append_item(item)

    def on_remove_clicked(self, obj):
        """
        Remove the item from the current list of selections.
        """
        store, the_iter = self.book_model.get_selected()
        if not the_iter:
            return
        row = self.book_model.get_selected_row()
        self.book.pop_item(row)
        self.book_model.remove(the_iter)

    def on_clear_clicked(self, obj):
        """
        Clear the whole current book.
        """
        self.book_model.clear()
        self.book.clear()

    def on_up_clicked(self, obj):
        """
        Move the currently selected item one row up in the selection list.
        """
        row = self.book_model.get_selected_row()
        if not row or row == -1:
            return
        store, the_iter = self.book_model.get_selected()
        data = self.book_model.get_data(the_iter, list(range(self.book_nr_cols)))
        self.book_model.remove(the_iter)
        self.book_model.insert(row - 1, data, None, 1)
        item = self.book.pop_item(row)
        self.book.insert_item(row - 1, item)

    def on_down_clicked(self, obj):
        """
        Move the currently selected item one row down in the selection list.
        """
        row = self.book_model.get_selected_row()
        if row + 1 >= self.book_model.count or row == -1:
            return
        store, the_iter = self.book_model.get_selected()
        data = self.book_model.get_data(the_iter, list(range(self.book_nr_cols)))
        self.book_model.remove(the_iter)
        self.book_model.insert(row + 1, data, None, 1)
        item = self.book.pop_item(row)
        self.book.insert_item(row + 1, item)

    def on_setup_clicked(self, obj):
        """
        Configure currently selected item.
        """
        store, the_iter = self.book_model.get_selected()
        if not the_iter:
            WarningDialog(
                _("No selected book item"),
                _("Please select a book item to configure."),
                parent=self.window,
            )
            return
        row = self.book_model.get_selected_row()
        item = self.book.get_item(row)
        option_class = item.option_class
        option_class.handler.set_default_stylesheet_name(item.get_style_name())
        item.is_from_saved_book = bool(self.book.get_name())
        item_dialog = BookItemDialog(self.dbstate, self.uistate, item, self.track)

        while True:
            response = item_dialog.window.run()
            if response == Gtk.ResponseType.OK:
                # dialog will be closed by connect, now continue work while
                # rest of dialog is unresponsive, release when finished
                style = option_class.handler.get_default_stylesheet_name()
                item.set_style_name(style)
                subject = option_class.get_subject()
                self.book_model.model.set_value(the_iter, 2, subject)
                self.book.set_item(row, item)
                item_dialog.close()
                break
            elif response == Gtk.ResponseType.CANCEL:
                item_dialog.close()
                break
            elif response == Gtk.ResponseType.DELETE_EVENT:
                # just stop, in ManagedWindow, delete-event is already coupled to
                # correct action.
                break
        opt_dict = option_class.handler.options_dict
        for optname in opt_dict:
            menu_option = option_class.menu.get_option_by_name(optname)
            if menu_option:
                menu_option.set_value(opt_dict[optname])

    def book_button_press(self, obj, event):
        """
        Double-click on the current book selection is the same as setup.
        Right click evokes the context menu.
        """
        if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS and event.button == 1:
            self.on_setup_clicked(obj)
        elif is_right_click(event):
            self.build_book_context_menu(event)

    def avail_button_press(self, obj, event):
        """
        Double-click on the available selection is the same as add.
        Right click evokes the context menu.
        """
        if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS and event.button == 1:
            self.on_add_clicked(obj)
        elif is_right_click(event):
            self.build_avail_context_menu(event)

    def build_book_context_menu(self, event):
        """Builds the menu with item-centered and book-centered options."""

        store, the_iter = self.book_model.get_selected()
        if the_iter:
            sensitivity = 1
        else:
            sensitivity = 0
        entries = [
            (_("_Up"), self.on_up_clicked, sensitivity),
            (_("_Down"), self.on_down_clicked, sensitivity),
            (_("Setup"), self.on_setup_clicked, sensitivity),
            (_("_Remove"), self.on_remove_clicked, sensitivity),
            ("", None, 0),
            (_("Clear the book"), self.on_clear_clicked, 1),
            (_("_Save"), self.on_save_clicked, 1),
            (_("_Open"), self.on_open_clicked, 1),
            (_("_Edit"), self.on_edit_clicked, 1),
        ]

        self.menu1 = Gtk.Menu()  # TODO could this be just a local "menu ="?
        self.menu1.set_reserve_toggle_size(False)
        for title, callback, sensitivity in entries:
            item = Gtk.MenuItem.new_with_mnemonic(title)
            Gtk.Label.new_with_mnemonic
            if callback:
                item.connect("activate", callback)
            else:
                item = Gtk.SeparatorMenuItem()
            item.set_sensitive(sensitivity)
            item.show()
            self.menu1.append(item)
        self.menu1.popup_at_pointer(event)

    def build_avail_context_menu(self, event):
        """Builds the menu with the single Add option."""

        store, the_iter = self.avail_model.get_selected()
        if the_iter:
            sensitivity = 1
        else:
            sensitivity = 0
        entries = [
            (_("_Add"), self.on_add_clicked, sensitivity),
        ]

        self.menu2 = Gtk.Menu()  # TODO could this be just a local "menu ="?
        self.menu2.set_reserve_toggle_size(False)
        for title, callback, sensitivity in entries:
            item = Gtk.MenuItem.new_with_mnemonic(title)
            if callback:
                item.connect("activate", callback)
            item.set_sensitive(sensitivity)
            item.show()
            self.menu2.append(item)
        self.menu2.popup_at_pointer(event)

    def on_close_clicked(self, obj):
        """
        close the BookSelector dialog, saving any changes if needed
        """
        if self.book_list.get_needs_saving():
            self.book_list.save()
        ManagedWindow.close(self, *obj)

    def on_book_ok_clicked(self, obj):
        """
        Run final BookDialog with the current book.
        """
        if self.book.get_item_list():
            old_paper_name = self.book.get_paper_name()  # from books.xml
            old_orientation = self.book.get_orientation()
            old_paper_metric = self.book.get_paper_metric()
            old_custom_paper_size = self.book.get_custom_paper_size()
            old_margins = self.book.get_margins()
            old_format_name = self.book.get_format_name()
            old_output = self.book.get_output()
            BookDialog(
                self.dbstate, self.uistate, self.book, BookOptions, track=self.track
            )
            new_paper_name = self.book.get_paper_name()
            new_orientation = self.book.get_orientation()
            new_paper_metric = self.book.get_paper_metric()
            new_custom_paper_size = self.book.get_custom_paper_size()
            new_margins = self.book.get_margins()
            new_format_name = self.book.get_format_name()
            new_output = self.book.get_output()
            # only books in the booklist have a name (not "ad hoc" ones)
            if self.book.get_name() and (
                old_paper_name != new_paper_name
                or old_orientation != new_orientation
                or old_paper_metric != new_paper_metric
                or old_custom_paper_size != new_custom_paper_size
                or old_margins != new_margins
                or old_format_name != new_format_name
                or old_output != new_output
            ):
                self.book.set_dbname(self._db.get_save_path())
                self.book_list.set_book(self.book.get_name(), self.book)
                self.book_list.set_needs_saving(True)
            if self.book_list.get_needs_saving():
                self.book_list.save()
        else:
            WarningDialog(
                _("No items"), _("This book has no items."), parent=self.window
            )
            return
        self.close()

    def on_save_clicked(self, obj):
        """
        Save the current book in the xml booklist file.
        """
        if not self.book.get_item_list():
            WarningDialog(
                _("No items"), _("This book has no items."), parent=self.window
            )
            return
        name = str(self.name_entry.get_text())
        if not name:
            WarningDialog(
                _("No book name"),
                _(
                    "You are about to save away a book with no name.\n\n"
                    "Please give it a name before saving it away."
                ),
                parent=self.window,
            )
            return
        if name in self.book_list.get_book_names():
            qqq = QuestionDialog2(
                _("Book name already exists"),
                _(
                    "You are about to save away a "
                    "book with a name which already exists."
                ),
                _("Proceed"),
                _("Cancel"),
                parent=self.window,
            )
            if not qqq.run():
                return

        # previously, the same book could be added to the booklist
        # under multiple names, which became different books once the
        # booklist was saved into a file so everything was fine, but
        # this created a problem once the paper settings were added
        # to the Book object in the BookDialog, since those settings
        # were retrieved from the Book object in BookList.save, so mutiple
        # books (differentiated by their names) were assigned the
        # same paper values, so the solution is to make each Book be
        # unique in the booklist, so if multiple copies are saved away
        # only the last one will get the paper values assigned to it
        # (although when the earlier books are then eventually run,
        # they'll be assigned paper values also)
        self.book.set_name(name)
        self.book.set_dbname(self._db.get_save_path())
        self.book_list.set_book(name, self.book)
        self.book_list.set_needs_saving(True)  # user clicked on save
        self.book = Book(self.book, exact_copy=False)  # regenerate old items
        self.book.set_name(name)
        self.book.set_dbname(self._db.get_save_path())

    def on_open_clicked(self, obj):
        """
        Run the BookListDisplay dialog to present the choice of books to open.
        """
        booklistdisplay = BookListDisplay(
            self.book_list, nodelete=True, dosave=False, parent=self.window
        )
        booklistdisplay.top.destroy()
        book = booklistdisplay.selection
        if book:
            self.open_book(book)
            self.name_entry.set_text(book.get_name())
            self.book.set_name(book.get_name())

    def on_edit_clicked(self, obj):
        """
        Run the BookListDisplay dialog to present the choice of books to delete.
        """
        booklistdisplay = BookListDisplay(
            self.book_list, nodelete=False, dosave=True, parent=self.window
        )
        booklistdisplay.top.destroy()
        book = booklistdisplay.selection
        if book:
            self.open_book(book)
            self.name_entry.set_text(book.get_name())
            self.book.set_name(book.get_name())


# ------------------------------------------------------------------------
#
# Book Item Options dialog
#
# ------------------------------------------------------------------------
class BookItemDialog(ReportDialog):
    """
    This class overrides the interface methods common for different reports
    in a way specific for this report. This is a book item dialog.
    """

    def __init__(self, dbstate, uistate, item, track=[]):
        option_class = item.option_class
        name = item.get_name()
        translated_name = item.get_translated_name()
        self.category = CATEGORY_BOOK
        self.database = dbstate.db
        self.option_class = option_class
        self.is_from_saved_book = item.is_from_saved_book
        ReportDialog.__init__(
            self, dbstate, uistate, option_class, name, translated_name, track
        )

    def on_ok_clicked(self, obj):
        """The user is satisfied with the dialog choices. Parse all options
        and close the window."""

        # Preparation
        self.parse_style_frame()
        self.parse_user_options()

        self.options.handler.save_options()

    def setup_target_frame(self):
        """Target frame is not used."""
        pass

    def parse_target_frame(self):
        """Target frame is not used."""
        return 1

    def init_options(self, option_class):
        try:
            if issubclass(option_class, object):
                self.options = option_class(self.raw_name, self.database)
        except TypeError:
            self.options = option_class
        if not self.is_from_saved_book:
            self.options.load_previous_values()

    def add_user_options(self):
        """
        Generic method to add user options to the gui.
        """
        if not hasattr(self.options, "menu"):
            return
        menu = self.options.menu
        options_dict = self.options.options_dict
        for category in menu.get_categories():
            for name in menu.get_option_names(category):
                option = menu.get_option(category, name)

                # override option default with xml-saved value:
                if name in options_dict:
                    option.set_value(options_dict[name])

                widget, label = make_gui_option(
                    option,
                    self.dbstate,
                    self.uistate,
                    self.track,
                    self.is_from_saved_book,
                )
                if widget is not None:
                    if label:
                        self.add_frame_option(category, option.get_label(), widget)
                    else:
                        self.add_frame_option(category, "", widget)


# -------------------------------------------------------------------------
#
# _BookFormatComboBox
#
# -------------------------------------------------------------------------
class _BookFormatComboBox(Gtk.ComboBox):
    """
    Build a menu of report types that are appropriate for a book
    """

    def __init__(self, active):
        Gtk.ComboBox.__init__(self)

        pmgr = GuiPluginManager.get_instance()
        self.__bookdoc_plugins = []
        for plugin in pmgr.get_docgen_plugins():
            if plugin.get_text_support() and plugin.get_draw_support():
                self.__bookdoc_plugins.append(plugin)

        self.store = Gtk.ListStore(GObject.TYPE_STRING)
        self.set_model(self.store)
        cell = Gtk.CellRendererText()
        self.pack_start(cell, True)
        self.add_attribute(cell, "text", 0)

        index = 0
        active_index = 0
        for plugin in self.__bookdoc_plugins:
            name = plugin.get_name()
            self.store.append(row=[name])
            if plugin.get_extension() == active:
                active_index = index
            index += 1
        self.set_active(active_index)

    def get_active_plugin(self):
        """
        Get the plugin represented by the currently active selection.
        """
        return self.__bookdoc_plugins[self.get_active()]


# ------------------------------------------------------------------------
#
# The final dialog - paper, format, target, etc.
#
# ------------------------------------------------------------------------
class BookDialog(DocReportDialog):
    """
    A usual Report.Dialog subclass.

    Create a dialog selecting target, format, and paper/HTML options.
    """

    def __init__(self, dbstate, uistate, book, options, track=[]):
        self.format_menu = None
        self.options = options
        self.page_html_added = False
        self.book = book
        self.title = _("Generate Book")
        self.database = dbstate.db
        DocReportDialog.__init__(
            self, dbstate, uistate, options, "book", self.title, track=track
        )
        self.options.options_dict["bookname"] = self.book.get_name()

        while True:
            response = self.window.run()
            if response != Gtk.ResponseType.HELP:
                break
        if response == Gtk.ResponseType.OK:
            handler = self.options.handler
            if self.book.get_paper_name() != handler.get_paper_name():
                self.book.set_paper_name(handler.get_paper_name())
            if self.book.get_orientation() != handler.get_orientation():
                self.book.set_orientation(handler.get_orientation())
            if self.book.get_paper_metric() != handler.get_paper_metric():
                self.book.set_paper_metric(handler.get_paper_metric())
            if self.book.get_custom_paper_size() != handler.get_custom_paper_size():
                self.book.set_custom_paper_size(handler.get_custom_paper_size())
            if self.book.get_margins() != handler.get_margins():
                self.book.set_margins(handler.get_margins())
            if self.book.get_format_name() != handler.get_format_name():
                self.book.set_format_name(handler.get_format_name())
            if self.book.get_output() != self.options.get_output():
                self.book.set_output(self.options.get_output())
            try:
                self.make_book()
            except (IOError, OSError) as msg:
                ErrorDialog(str(msg), parent=self.window)
        if response != Gtk.ResponseType.DELETE_EVENT:  # already closed
            self.close()

    def setup_style_frame(self):
        pass

    def setup_other_frames(self):
        pass

    def parse_style_frame(self):
        pass

    def get_title(self):
        """get the title"""
        return self.title

    def get_header(self, name):
        """get the header"""
        return _("Gramps Book")

    def make_doc_menu(self, active=None):
        """Build a menu of document types that are appropriate for
        this text report.  This menu will be generated based upon
        whether the document requires table support, etc."""
        self.format_menu = _BookFormatComboBox(active)

    def on_help_clicked(self, *obj):
        display_help(WIKI_HELP_PAGE, GENERATE_WIKI_HELP_SEC)

    def make_document(self):
        """Create a document of the type requested by the user."""
        user = User(uistate=self.uistate)
        self.rptlist = []
        selected_style = StyleSheet()

        pstyle = self.paper_frame.get_paper_style()
        self.doc = self.format(None, pstyle)

        for item in self.book.get_item_list():
            item.option_class.set_document(self.doc)
            report_class = item.get_write_item()
            obj = (
                write_book_item(self.database, report_class, item.option_class, user),
                item.get_translated_name(),
            )
            self.rptlist.append(obj)
            append_styles(selected_style, item)

        self.doc.set_style_sheet(selected_style)
        self.doc.open(self.target_path)

    def make_book(self):
        """
        The actual book. Start it out, then go through the item list
        and call each item's write_book_item method (which were loaded
        by the previous make_document method).
        """

        try:
            self.doc.init()
            newpage = 0
            for rpt, name in self.rptlist:
                if newpage:
                    self.doc.page_break()
                newpage = 1
                if rpt:
                    rpt.begin_report()
                    rpt.write_report()
            self.doc.close()
        except ReportError as msg:
            (msg1, msg2) = msg.messages()
            msg2 += " (%s)" % name  # which report has the error?
            ErrorDialog(msg1, msg2, parent=self.uistate.window)
            return
        except FilterError as msg:
            (msg1, msg2) = msg.messages()
            ErrorDialog(msg1, msg2, parent=self.uistate.window)
            return

        if self.open_with_app.get_active():
            open_file_with_default_application(self.target_path, self.uistate)

    def init_options(self, option_class):
        try:
            if issubclass(option_class, object):
                self.options = option_class(self.raw_name, self.database)
        except TypeError:
            self.options = option_class
        self.options.load_previous_values()
        handler = self.options.handler
        if self.book.get_paper_name():
            handler.set_paper_name(self.book.get_paper_name())
        if self.book.get_orientation() is not None:  # 0 is legal
            handler.set_orientation(self.book.get_orientation())
        if self.book.get_paper_metric() is not None:  # 0 is legal
            handler.set_paper_metric(self.book.get_paper_metric())
        if self.book.get_custom_paper_size():
            handler.set_custom_paper_size(self.book.get_custom_paper_size())
        if self.book.get_margins():
            handler.set_margins(self.book.get_margins())
        if self.book.get_format_name():
            handler.set_format_name(self.book.get_format_name())
        if self.book.get_output():
            self.options.set_output(self.book.get_output())


# ------------------------------------------------------------------------
#
# Generic task function for book
#
# ------------------------------------------------------------------------
def write_book_item(database, report_class, options, user):
    """
    Write the report using options set.
    All user dialog has already been handled and the output file opened.
    """
    try:
        return report_class(database, options, user)
    except ReportError as msg:
        (msg1, msg2) = msg.messages()
        ErrorDialog(msg1, msg2, parent=user.uistate.window)
    except FilterError as msg:
        (msg1, msg2) = msg.messages()
        ErrorDialog(msg1, msg2, parent=user.uistate.window)
    except:
        LOG.error("Failed to write book item.", exc_info=True)
    return None
