/*
 * @Copyright 2018-2026 HardBackNutter
 * @License GNU General Public License
 *
 * This file is part of NeverTooManyBooks.
 *
 * NeverTooManyBooks 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.
 *
 * NeverTooManyBooks 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 NeverTooManyBooks. If not, see <http://www.gnu.org/licenses/>.
 */
package com.hardbacknutter.nevertoomanybooks.bookdetails;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.MenuCompat;
import androidx.core.view.MenuProvider;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentContainerView;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.ViewModelProvider;

import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;

import com.hardbacknutter.nevertoomanybooks.ActivityRestarter;
import com.hardbacknutter.nevertoomanybooks.BaseFragment;
import com.hardbacknutter.nevertoomanybooks.BooksOnBookshelf;
import com.hardbacknutter.nevertoomanybooks.BuildConfig;
import com.hardbacknutter.nevertoomanybooks.R;
import com.hardbacknutter.nevertoomanybooks.ServiceLocator;
import com.hardbacknutter.nevertoomanybooks.authorworks.AuthorWorksContract;
import com.hardbacknutter.nevertoomanybooks.authorworks.AuthorWorksInput;
import com.hardbacknutter.nevertoomanybooks.bookedit.EditBookContract;
import com.hardbacknutter.nevertoomanybooks.bookedit.EditBookInput;
import com.hardbacknutter.nevertoomanybooks.bookedit.EditBookOutput;
import com.hardbacknutter.nevertoomanybooks.citations.CitationFactory;
import com.hardbacknutter.nevertoomanybooks.settings.bookshelves.EditBookshelvesContract;
import com.hardbacknutter.nevertoomanybooks.settings.SettingsContract;
import com.hardbacknutter.nevertoomanybooks.search.SearchBookUpdatesSingleBookContract;
import com.hardbacknutter.nevertoomanybooks.booklist.BookChangedListener;
import com.hardbacknutter.nevertoomanybooks.booklist.style.CoverScale;
import com.hardbacknutter.nevertoomanybooks.booklist.style.FieldVisibility;
import com.hardbacknutter.nevertoomanybooks.booklist.style.Style;
import com.hardbacknutter.nevertoomanybooks.bookreadstatus.ReadStatusFragmentFactory;
import com.hardbacknutter.nevertoomanybooks.core.parsers.RealNumberParser;
import com.hardbacknutter.nevertoomanybooks.core.widgets.ViewFocusOrder;
import com.hardbacknutter.nevertoomanybooks.covers.ImageHandler;
import com.hardbacknutter.nevertoomanybooks.database.DBKey;
import com.hardbacknutter.nevertoomanybooks.dialogs.StandardDialogs;
import com.hardbacknutter.nevertoomanybooks.dialogs.entities.lender.EditLenderLauncher;
import com.hardbacknutter.nevertoomanybooks.entities.Author;
import com.hardbacknutter.nevertoomanybooks.entities.Book;
import com.hardbacknutter.nevertoomanybooks.entities.Bookshelf;
import com.hardbacknutter.nevertoomanybooks.entities.TocEntry;
import com.hardbacknutter.nevertoomanybooks.fields.Field;
import com.hardbacknutter.nevertoomanybooks.fields.formatters.ClickableListFormatter;
import com.hardbacknutter.nevertoomanybooks.settings.SettingsInput;
import com.hardbacknutter.nevertoomanybooks.sync.SyncServer;
import com.hardbacknutter.nevertoomanybooks.sync.calibre.CalibreHandler;
import com.hardbacknutter.nevertoomanybooks.sync.calibre.CalibrePreferencesFragment;
import com.hardbacknutter.util.insets.InsetsListenerBuilder;

/**
 * This Fragment can run either in a ViewPager2, or embedded in the BoB activity.
 * Hence, there is NO OnBackPressedCallback in this Fragment.
 *
 * <ul>
 *     <li>{@link ShowBookDetailsViewModel#isEmbedded()} == {@code false}
 *         <ul>
 *             <li>{@link ShowBookPagerFragment}</li>
 *             <li>The vm is the local scope</li>
 *         </ul>
 *     </li>
 *     <li>{@link ShowBookDetailsViewModel#isEmbedded()} == {@code true}
 *          <ul>
 *              <li>{@link BooksOnBookshelf}</li>
 *              <li>The vm is the Activity scope</li>
 *              <li>{@link #bookChangedListener} is valid</li>
 *          </ul>
 *     </li>
 * </ul>
 */
// FIXME: 27/09/2024 we need to unify the update-handling for the TOC
//  and the ReadStatus/Progress fragments

public class ShowBookDetailsFragment
        extends BaseFragment {

    /** Fragment/Log tag. */
    public static final String TAG = "ShowBookDetailsFragment";

    /**
     * Delegate to handle cover replacement, rotation, etc.
     * Individual instances are only created when the corresponding image field is globally enabled.
     * i.o.w. check for {@code null} !
     */
    private final ImageHandler[] imageHandler = new ImageHandler[DBKey.NR_OF_BOOK_COVERS];
    private final ImageView[] coverViews = new ImageView[DBKey.NR_OF_BOOK_COVERS];
    private ToolbarMenuProvider toolbarMenuProvider;
    /** Delegate to handle all interaction with a Calibre server. */
    @Nullable
    private CalibreHandler calibreHandler;
    private ShowBookDetailsActivityViewModel aVm;
    private ShowBookDetailsViewModel vm;
    /**
     * Callback - used when we're running inside another component;
     * e.g. when running on a tablet (or other bigger screen) the BoB is showing
     * the details as an embedded fragment.
     */
    @Nullable
    private BookChangedListener bookChangedListener;

    private ActivityResultLauncher<SettingsInput> editSettingsLauncher;
    private ActivityResultLauncher<Long> manageBookshelvesLauncher;
    /** View all works of an Author. */
    private ActivityResultLauncher<AuthorWorksInput> authorWorksLauncher;
    /** User edits a book. */
    private ActivityResultLauncher<EditBookInput> editBookLauncher;
    /** User updates a book with internet data. */
    private ActivityResultLauncher<Book> updateBookLauncher;
    /** Handle the edit-lender dialog. */
    private EditLenderLauncher editLenderLauncher;

    /**
     * Constructor.
     *
     * @param bookId    to open
     * @param bookshelf current Bookshelf displayed by the BoB
     * @param embedded  flag, whether we're running in tablet-landscape (embedded).
     *
     * @return new instance
     */
    @NonNull
    public static Fragment create(@IntRange(from = 1) final long bookId,
                                  @NonNull final Bookshelf bookshelf,
                                  final boolean embedded) {
        final Fragment fragment = new ShowBookDetailsFragment();
        final ShowBookDetailsInput input = new ShowBookDetailsInput(bookId, bookshelf, embedded);
        fragment.setArguments(input.toBundle());
        return fragment;
    }

    @Override
    public void onAttach(@NonNull final Context context) {
        super.onAttach(context);

        if (context instanceof BookChangedListener) {
            bookChangedListener = (BookChangedListener) context;
        }
    }

    @Override
    public void onCreate(@Nullable final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        final ShowBookDetailsInput args = ShowBookDetailsInput.fromBundle(requireArguments());

        //noinspection DataFlowIssue
        aVm = new ViewModelProvider(getActivity()).get(ShowBookDetailsActivityViewModel.class);
        aVm.init(args.getBookshelf());

        final boolean embedded = args.isEmbedded();
        if (embedded) {
            // Create the vm in the Activity scope allowing it to be accessed by
            // the BooksOnBookshelf activity.
            vm = new ViewModelProvider(getActivity()).get(ShowBookDetailsViewModel.class);
        } else {
            // {@link ShowBookPagerFragment} mode:
            // We have multiple copies of this Fragment each showing a different book.
            // Create the vm in the local scope and NOT in the Activity.
            vm = new ViewModelProvider(this).get(ShowBookDetailsViewModel.class);
        }

        //noinspection DataFlowIssue
        vm.init(getContext(), args, aVm.getStyle());

        createActivityLaunchers();
        createFragmentLaunchers();
    }

    private void createActivityLaunchers() {

        editBookLauncher = registerForActivityResult(
                new EditBookContract(), o -> o.ifPresent(data -> {
                    if (data.isModified()) {
                        onBookEditFinished((String) null);
                    }
                }));

        updateBookLauncher = registerForActivityResult(
                new SearchBookUpdatesSingleBookContract(), o -> o.ifPresent(data -> {
                    if (data.isModified()) {
                        onBookEditFinished((String) null);
                    }
                }));

        authorWorksLauncher = registerForActivityResult(
                new AuthorWorksContract(), o -> o.ifPresent(data -> {
                    if (data.isModified()) {
                        onBookEditFinished((String) null);
                    }
                }));

        manageBookshelvesLauncher = registerForActivityResult(
                new EditBookshelvesContract(), o -> o.ifPresent(data -> {
                    if (data.isModified()) {
                        onBookEditFinished((String) null);
                    }
                }));

        editSettingsLauncher = registerForActivityResult(
                new SettingsContract(), o -> o.ifPresent(data -> {
                    if (data.isRecreateActivity()) {
                        ActivityRestarter.recreate();
                    }
                }));
    }

    private void createFragmentLaunchers() {
        final FragmentManager fm = getChildFragmentManager();

        //noinspection DataFlowIssue
        editLenderLauncher = new EditLenderLauncher(
                getActivity(), this,
                (bookId, loanee) -> onBookEditFinished(DBKey.LOANEE_NAME));
        editLenderLauncher.registerForFragmentResult(fm, this);
    }

    @Override
    @Nullable
    public View onCreateView(@NonNull final LayoutInflater inflater,
                             @Nullable final ViewGroup container,
                             @Nullable final Bundle savedInstanceState) {

        final View root = inflater.inflate(R.layout.fragment_book_details, container, false);
        final TypedArray coverResIds = getResources().obtainTypedArray(R.array.cover_images);
        try {
            for (int cIdx = 0; cIdx < DBKey.NR_OF_BOOK_COVERS; cIdx++) {
                coverViews[cIdx] = root.findViewById(coverResIds.getResourceId(cIdx, 0));
            }
        } finally {
            coverResIds.recycle();
        }
        return root;
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public void onViewCreated(@NonNull final View view,
                              @Nullable final Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        InsetsListenerBuilder.fragmentRootView(view);

        // update all Fields with their current View instances
        vm.getFields().forEach(field -> field.setParentView(view));

        createCoverDelegates();

        // We must create them here, even if the Book has no matching sync id,
        // because the menu setup uses them before the Book is loaded.
        createSyncDelegates();

        vm.onBookLoaded().observe(getViewLifecycleOwner(), this::onBindBook);
        // Always active as fields other than the read/readProgress fragment depend on it
        vm.onUpdateReadStatus().observe(getViewLifecycleOwner(), this::onUpdateReadStatus);

        final TextView authorsView = view.findViewById(R.id.author);

        authorsView.setOnTouchListener((v, event) -> {
            if (event.getAction() == MotionEvent.ACTION_UP) {
                onAuthorClicked((TextView) v, event);
            }
            return true;
        });

        // load the initial book data and trigger a UI update
        vm.displayBook();
    }

    private void onAuthorClicked(@NonNull final TextView v,
                                 @NonNull final MotionEvent event) {

        ClickableListFormatter.getIndex(v, event).ifPresent(index -> {
            final List<Author> authors = vm.getBook().getAuthors();
            if (index < authors.size()) {
                authorWorksLauncher.launch(new AuthorWorksInput(
                        authors.get(index).getId(),
                        aVm.getBookshelf()));
            }
        });
    }

    /**
     * Create the optional sync delegates.
     */
    private void createSyncDelegates() {
        if (SyncServer.CalibreCS.isEnabled()) {
            try {
                //noinspection DataFlowIssue
                calibreHandler = new CalibreHandler(getView(), this)
                        .setProgressFrame(getProgressFrame())
                        .registerForActivityResult(this, getViewLifecycleOwner());
            } catch (@NonNull final CertificateException ignore) {
                //ignore; the user would already have been warned on the BoB screen
            }
        }

        //  if (SyncServer.StripInfo.isEnabled()) {
        //
        //  }
    }

    private void createCoverDelegates() {
        final Context context = getContext();
        //noinspection DataFlowIssue
        final Resources res = context.getResources();
        final TypedArray width = res.obtainTypedArray(R.array.cover_details_max_width);
        try {
            final Style style = aVm.getStyle();
            for (int cIdx = 0; cIdx < DBKey.NR_OF_BOOK_COVERS; cIdx++) {
                if (style.isShowField(FieldVisibility.Screen.Detail, DBKey.COVER[cIdx])) {
                    final int maxWidth = width.getDimensionPixelSize(cIdx, 0);
                    final int maxHeight = (int) (maxWidth / CoverScale.HW_RATIO);

                    imageHandler[cIdx] = new ImageHandler
                            .Builder(this, cIdx, maxWidth, maxHeight)
                            .setImageView(coverViews[cIdx])
                            .setImageOwner(() -> vm.getBook())
                            .setOnReloadImage(this::reloadImage)
                            .setCoverBrowserTitleSupplier(() -> vm.getBook().getTitle())
                            .setCoverBrowserCodeSupplier(() -> vm.getBook().getRawProductCode())
                            .build();
                }
            }
        } finally {
            width.recycle();
        }
    }

    /**
     * Entry point after the current Book was edited; either manually by the user,
     * or automatically with an internet update action.
     *
     * @param keys optional list of keys which were updated.
     *             {@code null} to indicate the entire Book was potentially updated.
     */
    private void onBookEditFinished(@Nullable final String... keys) {
        // Needed when running inside the ViewPager to update the activity result data
        // Ignored if running im embedded mode, but keeping this future-proof
        aVm.setDataModified();

        vm.displayBook();

        // when running in embedded mode, update the BoB list
        if (bookChangedListener != null) {
            bookChangedListener.onBookUpdated(vm.getBook(), keys);
        }
    }

    /**
     * Callback passed to the {@link ImageHandler}; will be called after changing a cover image.
     *
     * @param cIdx 0..n image index
     */
    private void reloadImage(@IntRange(from = 0, to = 3) final int cIdx) {
        // Needed when running inside the ViewPager to update the activity result data
        // Ignored if running im embedded mode, but keeping this future-proof
        aVm.setDataModified();

        // don't reload the whole book, just rebind the images
        bindCoverImages();

        // when running in embedded mode, update the BoB list
        if (bookChangedListener != null && cIdx == 0) {
            bookChangedListener.onBookUpdated(vm.getBook(), DBKey.COVER[0]);
        }
    }

    private void onUpdateReadStatus(@NonNull final Boolean modified) {
        if (!modified) {
            return;
        }

        // Needed when running inside the ViewPager to update the activity result data
        // Ignored if running im embedded mode, but keeping this future-proof
        aVm.setDataModified();

        final Book book = vm.getBook();

        // Refresh the read_end value displayed
        vm.getField(DBKey.READ_END__DATE).ifPresent(field -> {
            field.setValue(book.getString(DBKey.READ_END__DATE));
            //noinspection DataFlowIssue
            field.setVisibility(getView(), true, false);
        });

        vm.getField(DBKey.DATE_LAST_UPDATED__UTC).ifPresent(
                field -> field.setValue(book.getString(DBKey.DATE_LAST_UPDATED__UTC)));

        // when running in embedded mode, update the BoB list
        if (bookChangedListener != null) {
            bookChangedListener.onBookUpdated(book,
                                              // just send all possible fields,
                                              // regardless of mode
                                              DBKey.READ__BOOL, DBKey.READ_PROGRESS,
                                              DBKey.READ_END__DATE);
        }
    }

    /**
     * Called when the ViewModel triggers an update.
     * Dev. note: this will get called FOR EACH fragment currently existing
     * in the ViewPager ... so ALSO for the fragments off-screen.
     * Hence, DO NOT update the toolbar title here!
     *
     * @param book to display
     */
    private void onBindBook(@NonNull final Book book) {
        updateToolbar(book);

        final Collection<Field<?, ? extends View>> fields = vm.getFields();

        final Context context = getContext();
        final RealNumberParser realNumberParser = vm.getRealNumberParser();

        // do NOT call notifyIfChanged, as this is the initial load
        //noinspection DataFlowIssue
        fields.stream()
              .filter(Field::isAutoPopulated)
              .forEach(field -> field.doLoad(context, book, realNumberParser));

        // We're only creating this here instead of in onViewCreated
        // so we are sure the book is loaded when the read-progress fragment displays
        if (ServiceLocator.getInstance().isFieldEnabled(DBKey.READ__BOOL)
            || ServiceLocator.getInstance().isFieldEnabled(DBKey.READ_PROGRESS)) {
            ReadStatusFragmentFactory.createShow(getChildFragmentManager(), R.id.fragment_read,
                                                 aVm.getStyle(), vm.isEmbedded());
        }
        bindCoverImages();
        bindLoanee(book);
        bindToc(book);

        final View parentView = getView();
        //noinspection DataFlowIssue
        fields.forEach(field -> field.setVisibility(parentView, true, false));

        // Hide the 'Edition' label if none of the edition fields are shown
        setSectionVisibility(R.id.lbl_edition,
                             R.id.edition_flags,
                             R.id.edition_info,
                             R.id.print_run);

        // Hide the 'Publication' label if none of the publishing fields are shown.
        setSectionVisibility(R.id.lbl_publication,
                             R.id.publisher,
                             R.id.date_published,
                             R.id.price_listed,
                             R.id.format,
                             R.id.color,
                             R.id.language,
                             R.id.pages);

        // All views should now have proper visibility set, so fix their focus order.
        //noinspection DataFlowIssue
        ViewFocusOrder.fix(parentView);
    }

    private void updateToolbar(@NonNull final Book book) {
        // The menu is entirely dependent on the book we're displaying,
        // so we need to remove the old menu if present,
        // and construct a new menu for each book we're binding.
        final Toolbar toolbar = getToolbar();
        if (toolbarMenuProvider != null) {
            toolbar.removeMenuProvider(toolbarMenuProvider);
        }
        toolbarMenuProvider = new ToolbarMenuProvider();
        // add it, but ONLY display it when THIS fragment is in RESUMED state.
        toolbar.addMenuProvider(toolbarMenuProvider, getViewLifecycleOwner(),
                                Lifecycle.State.RESUMED);

        if (!vm.isEmbedded()) {
            //noinspection DataFlowIssue
            toolbar.setTitle(Author.getLabel(getContext(), book.getAuthors()));

            String bookTitle = book.getTitle();
            if (BuildConfig.DEBUG /* always */) {
                bookTitle = "[" + book.getId() + "] " + bookTitle;
            }
            toolbar.setSubtitle(bookTitle);
        }
    }

    private void bindCoverImages() {
        for (int cIdx = 0; cIdx < DBKey.NR_OF_BOOK_COVERS; cIdx++) {
            if (imageHandler[cIdx] != null) {
                imageHandler[cIdx].onBindView();
            } else if (coverViews[cIdx] != null) {
                coverViews[cIdx].setVisibility(View.GONE);
            }
        }
    }

    /**
     * Inflates 'lend-to' field showing a person the book was lent to.
     *
     * @param book to load
     */
    private void bindLoanee(@NonNull final Book book) {
        //noinspection DataFlowIssue
        final TextView lendTo = getView().findViewById(R.id.lend_to);
        // Use the global setting! The user may have disabled the field on the list screen,
        // but if lending is enabled globally, we MUST display the status on the details screen.
        if (ServiceLocator.getInstance().isFieldEnabled(DBKey.LOANEE_NAME)) {
            final Optional<String> loanee =
                    book.getLoanee().map(s -> getString(R.string.lbl_lend_out_to_name, s));

            if (loanee.isPresent()) {
                lendTo.setText(loanee.get());
                lendTo.setVisibility(View.VISIBLE);
                return;
            }
        }
        lendTo.setVisibility(View.GONE);
    }

    private void bindToc(@NonNull final Book book) {
        final View parentView = getView();

        //noinspection DataFlowIssue
        final TextView lblAnthologyOrCollection = parentView.findViewById(R.id.lbl_anthology);
        switch (book.getContentType()) {
            case Collection:
                lblAnthologyOrCollection.setVisibility(View.VISIBLE);
                lblAnthologyOrCollection.setText(R.string.lbl_book_type_collection);
                break;

            case Anthology:
                lblAnthologyOrCollection.setVisibility(View.VISIBLE);
                lblAnthologyOrCollection.setText(R.string.lbl_book_type_anthology);
                break;

            case Book:
            default:
                lblAnthologyOrCollection.setVisibility(View.GONE);
                break;
        }

        final Button btnShowToc = parentView.findViewById(R.id.btn_show_toc);
        final FragmentContainerView tocFrame = parentView.findViewById(R.id.toc_frame);
        if (aVm.getStyle().isShowField(FieldVisibility.Screen.List, DBKey.FK_TOC_ENTRY)) {
            if (btnShowToc != null) {
                bindTocButton(btnShowToc, book);
            } else if (tocFrame != null) {
                bindTocFrame(tocFrame, book);
            }
        } else {
            if (btnShowToc != null) {
                btnShowToc.setVisibility(View.GONE);
            } else if (tocFrame != null) {
                tocFrame.setVisibility(View.GONE);
            }
        }
    }

    /**
     * Smaller displays have a button to show the TOC in the current Activity.
     *
     * @param showTocBtn the "Show TOC" button view
     * @param book       to load
     */
    private void bindTocButton(@NonNull final Button showTocBtn,
                               @NonNull final Book book) {

        if (book.getToc().isEmpty()) {
            showTocBtn.setVisibility(View.GONE);
        } else {
            showTocBtn.setVisibility(View.VISIBLE);
            showTocBtn.setOnClickListener(v -> {
                final Fragment fragment = ShowTocFragment.create(book, false, aVm.getBookshelf());
                // yes, it must be the Activity FragmentManager,
                // as that is where the R.id.main_fragment View is located.
                // This also means the ShowTocFragment is replacing the ViewPager!
                //noinspection DataFlowIssue
                final FragmentManager fm = getActivity().getSupportFragmentManager();
                fm.beginTransaction()
                  .setReorderingAllowed(true)
                  .addToBackStack(ShowTocFragment.TAG)
                  .replace(R.id.content_frame, fragment, ShowTocFragment.TAG)
                  .commit();
            });
        }
    }

    /**
     * Larger displays show the TOC in a child Fragment of this one.
     *
     * @param tocFrame the TOC frame view
     * @param book     to load
     */
    private void bindTocFrame(@NonNull final FragmentContainerView tocFrame,
                              @NonNull final Book book) {
        final List<TocEntry> tocList = book.getToc();
        if (tocList.isEmpty()) {
            tocFrame.setVisibility(View.GONE);
        } else {
            tocFrame.setVisibility(View.VISIBLE);
            final FragmentManager fm = getChildFragmentManager();

            Fragment fragment = fm.findFragmentByTag(ShowTocFragment.TAG);
            if (fragment == null) {
                fragment = ShowTocFragment.create(book, true, aVm.getBookshelf());
                fm.beginTransaction()
                  .setReorderingAllowed(true)
                  .replace(R.id.toc_frame, fragment, ShowTocFragment.TAG)
                  .commit();
            } else {
                // The above ShowTocFragment will have created a vm in its
                // parent-fragment (i.e. THIS fragment)  scope
                final ShowTocViewModel childVm = new ViewModelProvider(this)
                        .get(ShowTocViewModel.class);
                childVm.reloadBook(book);
            }
        }
    }

    /**
     * If all field Views are {@link View#GONE}, set the section View to {@link View#GONE} as well.
     * Otherwise, set the section View to View.VISIBLE.
     *
     * @param sectionLabel field to set
     * @param fieldViews   to check
     */
    private void setSectionVisibility(final int sectionLabel,
                                      @NonNull final Integer... fieldViews) {

        final View parentView = getView();
        //noinspection DataFlowIssue
        final boolean visible = Arrays.stream(fieldViews)
                                      .map(parentView::findViewById)
                                      .map(v -> ((View) v).getVisibility())
                                      .anyMatch(vis -> vis != View.GONE);
        parentView.findViewById(sectionLabel).setVisibility(visible ? View.VISIBLE : View.GONE);
    }

    private final class ToolbarMenuProvider
            implements MenuProvider {

        @Override
        public void onCreateMenu(@NonNull final Menu menu,
                                 @NonNull final MenuInflater inflater) {
            inflater.inflate(R.menu.book, menu);
            inflater.inflate(R.menu.settings, menu);
            MenuCompat.setGroupDividerEnabled(menu, true);

            // duplicating is not supported from inside this fragment
            menu.findItem(R.id.MENU_BOOK_DUPLICATE).setVisible(false);

            menu.findItem(R.id.MENU_SYNC_LIST_WITH_DETAILS).setVisible(vm.isEmbedded());

            if (calibreHandler != null) {
                calibreHandler.onCreateMenu(menu, inflater);
            }

            final Context context = getContext();
            final Book book = vm.getBook();

            //noinspection DataFlowIssue
            vm.getMenuHandlers().forEach(h -> h.onCreateMenu(context, inflater, menu, book));
        }

        @Override
        public void onPrepareMenu(@NonNull final Menu menu) {
            final Book book = vm.getBook();

            final boolean isRead = book.isRead();
            menu.findItem(R.id.MENU_BOOK_SET_READ).setVisible(!isRead);
            menu.findItem(R.id.MENU_BOOK_SET_UNREAD).setVisible(isRead);

            // Always check LOANEE_NAME usage independent of the style in use.
            if (ServiceLocator.getInstance().isFieldEnabled(DBKey.LOANEE_NAME)) {
                final boolean isLendOut = book.getLoanee().isPresent();
                menu.findItem(R.id.MENU_BOOK_LOAN_ADD).setVisible(!isLendOut);
                menu.findItem(R.id.MENU_BOOK_LOAN_DELETE).setVisible(isLendOut);
            } else {
                menu.findItem(R.id.MENU_BOOK_LOAN_ADD).setVisible(false);
                menu.findItem(R.id.MENU_BOOK_LOAN_DELETE).setVisible(false);
            }

            final Context context = getContext();

            if (calibreHandler != null) {
                //noinspection DataFlowIssue
                calibreHandler.onPrepareMenu(context, menu, book);
            }

            //noinspection DataFlowIssue
            vm.getMenuHandlers().forEach(h -> h.onPrepareMenu(context, menu, book));
        }

        @Override
        public boolean onMenuItemSelected(@NonNull final MenuItem menuItem) {
            final Context context = getContext();
            final Book book = vm.getBook();

            final int menuItemId = menuItem.getItemId();

            if (menuItemId == R.id.MENU_BOOK_EDIT) {
                editBookLauncher.launch(new EditBookInput(book.getId(),
                                                          aVm.getStyle()));
                return true;

            } else if (menuItemId == R.id.MENU_BOOK_DELETE) {
                deleteBook(book);
                return true;

            } else if (menuItemId == R.id.MENU_BOOK_SET_READ
                       || menuItemId == R.id.MENU_BOOK_SET_UNREAD) {
                // toggle the status
                vm.setReadNow(!book.isRead());
                return true;

            } else if (menuItemId == R.id.MENU_BOOK_LOAN_ADD) {
                //noinspection DataFlowIssue
                editLenderLauncher.launch(getActivity(), book);
                return true;

            } else if (menuItemId == R.id.MENU_BOOK_LOAN_DELETE) {
                deleteLoanee(book);
                return true;

            } else if (menuItemId == R.id.MENU_SHARE) {
                //noinspection DataFlowIssue
                startActivity(CitationFactory.getShareIntent(context, book, aVm.getStyle()));
                return true;

            } else if (menuItemId == R.id.MENU_MANAGE_BOOKSHELVES) {
                // ENHANCE: if we ever have a primary-bookshelf, we should pass it here
                manageBookshelvesLauncher.launch(0L);
                return true;

            } else if (menuItemId == R.id.MENU_SETTINGS) {
                editSettingsLauncher.launch(null);
                return true;

            } else if (menuItemId == R.id.MENU_CALIBRE_SETTINGS) {
                // Must use the Activity fm as the current fragment could be hosted by
                // ShowBookPagerFragment or embedded inside the BoB
                //noinspection DataFlowIssue
                final FragmentManager fm = getActivity().getSupportFragmentManager();
                fm.beginTransaction()
                  .setReorderingAllowed(true)
                  .addToBackStack(CalibrePreferencesFragment.TAG)
                  .replace(R.id.content_frame,
                           new CalibrePreferencesFragment(),
                           CalibrePreferencesFragment.TAG)
                  .commit();
                return true;

            } else if (menuItemId == R.id.MENU_UPDATE_ITEM_BY_SEARCH) {
                updateBookLauncher.launch(book);
                return true;

            } else if (menuItemId == R.id.MENU_SYNC_LIST_WITH_DETAILS) {
                // when running in embedded mode, update the BoB list
                if (bookChangedListener != null) {
                    bookChangedListener.onSyncBook(book.getId());
                }
                return true;
            }

            //noinspection DataFlowIssue
            if (calibreHandler != null
                && calibreHandler.onMenuItemSelected(context, menuItemId, book)) {
                return true;
            }

            //noinspection DataFlowIssue
            return vm.getMenuHandlers().stream().anyMatch(
                    h -> h.onMenuItemSelected(context, menuItemId, book));
        }

        private void deleteLoanee(@NonNull final Book book) {
            vm.deleteLoan();
            // Needed when running inside the ViewPager to update the activity result data
            // Ignored if running im embedded mode, but keeping this future-proof
            aVm.setDataModified();

            bindLoanee(book);

            // when running in embedded mode, update the BoB list
            if (bookChangedListener != null) {
                bookChangedListener.onBookUpdated(book, DBKey.LOANEE_NAME);
            }
        }

        private void deleteBook(@NonNull final Book book) {
            final String title = book.getTitle();
            final List<Author> authors = book.getAuthors();
            //noinspection DataFlowIssue
            StandardDialogs.deleteBook(getContext(), title, authors, () -> {
                final long bookIdDeleted = book.getId();
                vm.deleteBook();
                // Needed when running inside the ViewPager to update the activity result data
                // Ignored if running im embedded mode, but keeping this future-proof
                aVm.setDataModified();

                // when running in embedded mode, update the BoB list and stay here
                if (bookChangedListener != null) {
                    bookChangedListener.onBookDeleted(bookIdDeleted);
                } else {
                    // Not embedded, set 0 as the repositionToBookId and return to the BoB
                    // (could hardcode modified=true, but consistency prevails)
                    //noinspection DataFlowIssue
                    new EditBookOutput(aVm.isModified(), 0, 0)
                            .finishActivityAndSend(getActivity());
                }
            });
        }

    }
}
