/*
 * @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;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.LocaleList;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.io.File;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;

import com.hardbacknutter.nevertoomanybooks.booklist.style.FieldVisibility;
import com.hardbacknutter.nevertoomanybooks.core.database.SynchronizedDb;
import com.hardbacknutter.nevertoomanybooks.core.network.BiscuitStore;
import com.hardbacknutter.nevertoomanybooks.core.network.NetworkChecker;
import com.hardbacknutter.nevertoomanybooks.covers.CoverStorage;
import com.hardbacknutter.nevertoomanybooks.database.CacheDbHelper;
import com.hardbacknutter.nevertoomanybooks.database.DBDefinitions;
import com.hardbacknutter.nevertoomanybooks.database.DBHelper;
import com.hardbacknutter.nevertoomanybooks.database.DBKey;
import com.hardbacknutter.nevertoomanybooks.database.dao.AuthorDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.BedethequeCacheDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.BookDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.BookshelfDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.CalibreCustomFieldDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.CalibreDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.CalibreLibraryDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.ColorDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.CoverCacheDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.DeletedBooksDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.FormatDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.FtsDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.IdentifierDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.IdentifierValueDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.IsoLanguageDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.LanguageDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.LoaneeDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.LocationDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.PublicationFrequencyDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.PublisherDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.SeriesDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.StripInfoDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.StyleDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.StylesHelper;
import com.hardbacknutter.nevertoomanybooks.database.dao.TagDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.TagMappingDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.TocEntryDao;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.AuthorDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.BedethequeCacheDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.BookDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.BookshelfDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.CalibreCustomFieldDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.CalibreDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.CalibreLibraryDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.ColorDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.CoverCacheDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.DeletedBooksDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.FormatDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.FtsDaoHelper;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.FtsDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.IdentifierDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.IdentifierValueDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.IsoLanguageDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.LanguageDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.LoaneeDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.LocationDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.PublicationFrequencyDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.PublisherDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.SeriesDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.StripInfoDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.StyleDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.TagDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.TagMappingDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.dao.impl.TocEntryDaoImpl;
import com.hardbacknutter.nevertoomanybooks.database.updates.PreferenceKeyMigration;
import com.hardbacknutter.nevertoomanybooks.network.NetworkCheckerImpl;
import com.hardbacknutter.nevertoomanybooks.settings.Prefs;
import com.hardbacknutter.nevertoomanybooks.utils.AppLocale;
import com.hardbacknutter.nevertoomanybooks.utils.AppLocaleImpl;
import com.hardbacknutter.nevertoomanybooks.utils.Languages;
import com.hardbacknutter.nevertoomanybooks.utils.LocalizedContext;
import com.hardbacknutter.nevertoomanybooks.utils.notifier.Notifier;
import com.hardbacknutter.nevertoomanybooks.utils.notifier.NotifierImpl;
import com.hardbacknutter.util.logger.FileLogger;
import com.hardbacknutter.util.logger.Logger;
import com.hardbacknutter.util.logger.LoggerFactory;

import okhttp3.JavaNetCookieJar;
import okhttp3.OkHttpClient;

public final class ServiceLocator {

    /**
     * Subdirectory of {@link Context#getFilesDir()}.
     * Database backup taken during app upgrades.
     */
    private static final String DIR_UPGRADES = "upgrades";

    @SuppressWarnings("StaticVariableMayNotBeInitialized")
    private static ServiceLocator sInstance;

    /** Either the real Application Context, or the injected context when running in unit tests. */
    @NonNull
    private final Context appContext;

    @Nullable
    private Prefs prefs;

    @Nullable
    private DBHelper dbHelper;

    @Nullable
    private CacheDbHelper cacheDbHelper;

    @Nullable
    private NetworkChecker networkChecker;

    @Nullable
    private StylesHelper stylesHelper;

    @Nullable
    private FieldVisibility fieldVisibility;

    @Nullable
    private Languages languages;

    @Nullable
    private CoverStorage coverStorage;

    @Nullable
    private CookieManager cookieManager;
    @Nullable
    private OkHttpClient okHttpClient;

    @Nullable
    private AppLocale appLocale;

    @Nullable
    private Notifier notifier;

    @Nullable
    private AuthorDao authorDao;
    @Nullable
    private BedethequeCacheDao bedethequeCacheDao;
    @Nullable
    private BookDao bookDao;
    @Nullable
    private BookshelfDao bookshelfDao;
    @Nullable
    private CalibreDao calibreDao;
    @Nullable
    private CalibreLibraryDao calibreLibraryDao;
    @Nullable
    private CalibreCustomFieldDao calibreCustomFieldDao;
    @Nullable
    private ColorDao colorDao;
    @Nullable
    private CoverCacheDao coverCacheDao;
    @Nullable
    private DeletedBooksDao deletedBooksDao;
    @Nullable
    private FormatDao formatDao;
    @Nullable
    private FtsDao ftsDao;
    @Nullable
    private FtsDaoHelper ftsDaoHelper;
    @Nullable
    private IdentifierDao identifierDao;
    @Nullable
    private IdentifierValueDao authorIdentifierDao;
    @Nullable
    private IdentifierValueDao bookIdentifierDao;
    @Nullable
    private IdentifierValueDao seriesIdentifierDao;
    @Nullable
    private IsoLanguageDao isoLanguageDao;
    @Nullable
    private LanguageDao languageDao;
    @Nullable
    private LoaneeDao loaneeDao;
    @Nullable
    private LocationDao locationDao;
    @Nullable
    private PublisherDao publisherDao;
    @Nullable
    private PublicationFrequencyDao publicationFrequencyDao;
    @Nullable
    private SeriesDao seriesDao;
    @Nullable
    private StripInfoDao stripInfoDao;
    @Nullable
    private StyleDao styleDao;
    @Nullable
    private TagDao tagDao;
    @Nullable
    private TagMappingDao tagMappingDao;
    @Nullable
    private TocEntryDao tocEntryDao;

    /**
     * Private constructor.
     *
     * @param context Current context
     */
    private ServiceLocator(@NonNull final Context context) {
        appContext = context.getApplicationContext();
    }

    /**
     * Constructor.
     *
     * @param context Current context
     */
    public static void create(@NonNull final Context context) {
        synchronized (ServiceLocator.class) {
            if (sInstance == null) {
                sInstance = new ServiceLocator(context);
                // We MUST bootstrap it here to ensure it's active
                // before the first http request is sent.
                sInstance.getCookieManager();
            }
        }
    }

    /**
     * Retrieve the singleton instance.
     *
     * @return singleton
     */
    @NonNull
    public static ServiceLocator getInstance() {
        //noinspection StaticVariableUsedBeforeInitialization
        return sInstance;
    }

    /**
     * Get the Application Context <strong>with the device Locale</strong>.
     *
     * @return raw Application Context
     */
    @NonNull
    public Context getAppContext() {
        return appContext;
    }

    /**
     * Get the Application Context Global Preferences.
     *
     * @return global {@link Prefs}
     */
    @NonNull
    public Prefs getSharedPreferences() {
        if (prefs == null) {
            prefs = new Prefs(getRawSharedPreferences());
        }
        return prefs;
    }

    /**
     * Get the <strong>raw</strong> Global Preferences.
     *
     * @return raw SharedPreferences
     */
    @NonNull
    public SharedPreferences getRawSharedPreferences() {
        // Use same file name as used by the legacy androidx.preference
        return appContext.getSharedPreferences(
                appContext.getPackageName() + "_preferences",
                Context.MODE_PRIVATE);
    }

    /**
     * Return the device {@link LocaleList}.
     * <p>
     * Dev. note: the only reason for this method is so we can mock getting the LocaleList.
     *
     * @return LocaleList
     */
    @NonNull
    public LocaleList getSystemLocaleList() {
        return Resources.getSystem().getConfiguration().getLocales();
    }

    @NonNull
    public NetworkChecker getNetworkChecker() {
        if (networkChecker == null) {
            networkChecker = new NetworkCheckerImpl(this::getAppContext);
        }
        return networkChecker;
    }

    @Nullable
    public File getLogDir() {
        final Logger logger = LoggerFactory.getLogger();
        if (logger instanceof FileLogger) {
            return ((FileLogger) logger).getLogDir();
        }
        return null;
    }

    @NonNull
    public File getUpgradesDir() {
        final File dir = new File(appContext.getFilesDir(), DIR_UPGRADES);
        if (!dir.exists()) {
            //noinspection ResultOfMethodCallIgnored
            dir.mkdirs();
        }
        return dir;
    }

    /**
     * Get a <strong>new</strong> localised Application Context.
     *
     * @return Application Context using the user preferred Locale
     */
    @LocalizedContext
    @NonNull
    public Context getLocalizedAppContext() {
        return getAppLocale().apply(sInstance.appContext);
    }


    /**
     * A callback when the preferences have been changed.
     */
    public void onPreferencesUpdated() {
        // destroy the cached copy
        fieldVisibility = null;

        // Migrate/remove any obsolete keys
        PreferenceKeyMigration.migrate(appContext);
    }

    /**
     * Get the styles cache container.
     *
     * @return singleton
     */
    @NonNull
    public StylesHelper getStyles() {
        synchronized (this) {
            if (stylesHelper == null) {
                stylesHelper = new StylesHelper(this::getStyleDao);
            }
        }
        return stylesHelper;
    }

    /**
     * Get the language cache container.
     *
     * @return singleton
     */
    @NonNull
    public Languages getLanguages() {
        synchronized (this) {
            if (languages == null) {
                languages = new Languages(this::getIsoLanguageDao);
            }
        }
        return languages;
    }

    /**
     * Get the global field visibility settings.
     *
     * @return singleton
     */
    @NonNull
    public FieldVisibility getGlobalFieldVisibility() {
        synchronized (this) {
            if (fieldVisibility == null) {
                fieldVisibility = new FieldVisibility();
                fieldVisibility.load();
            }
        }
        return fieldVisibility;
    }

    /**
     * Convenience method to make it easier to identify where we use the global.
     * i.e. differentiate between 'isVisible' on a List/Detail screen versus the global.
     *
     * @param dbKey to check
     *
     * @return {@code true} if the field is globally enabled.
     */
    public boolean isFieldEnabled(@NonNull final String dbKey) {
        return getGlobalFieldVisibility().isVisible(dbKey).orElse(true);
    }

    /**
     * Get the cover storage helper.
     *
     * @return singleton
     */
    @NonNull
    public CoverStorage getCoverStorage() {
        synchronized (this) {
            if (coverStorage == null) {
                coverStorage = new CoverStorage(this::getAppContext,
                                                this::getCoverCacheDao);
            }
        }
        return coverStorage;
    }

    /**
     * Get the global Cookie manager.
     *
     * @return manager
     */
    @NonNull
    public CookieManager getCookieManager() {
        synchronized (this) {
            if (cookieManager == null) {
                // 2024-12-21: our private store.... see IsfdbAuth class why...
                cookieManager = new CookieManager(new BiscuitStore(appContext),
                                                  CookiePolicy.ACCEPT_ALL);
                CookieHandler.setDefault(cookieManager);
            }
        }
        return cookieManager;
    }

    /**
     * Get the single/shared OkHttp client initialised with the global CookieManager.
     *
     * @return client
     */
    @NonNull
    public OkHttpClient getOkHttpClient() {
        synchronized (this) {
            if (okHttpClient == null) {
                okHttpClient = new OkHttpClient.Builder()
                        .cookieJar(new JavaNetCookieJar(getCookieManager()))
                        .build();
            }
        }
        return okHttpClient;
    }

    /**
     * Get the current application locale helper.
     *
     * @return singleton
     */
    @NonNull
    public AppLocale getAppLocale() {
        synchronized (this) {
            if (appLocale == null) {
                // Using SYSTEM Locales versus USER Locales, see:
                // https://medium.com/@hectorricardomendez/how-to-get-the-current-locale-in-android-fc12d8be6242
                // Key-Takeaway #5: To get the list of preferred locales of the device
                // (as defined in the Settings), call
                // Resources.getSystem().getConfiguration().getLocales()
                //
                // although, at this point, it seems LocaleList.getDefault() DOES return
                // the correct list, so we could use that. It's not clear if it matters.
                appLocale = new AppLocaleImpl(getSystemLocaleList(), this::getLanguages);
            }
        }
        return appLocale;
    }

    @NonNull
    public Notifier getNotifier() {
        synchronized (this) {
            if (notifier == null) {
                notifier = new NotifierImpl();
                getAppLocale().registerOnLocaleChangedListener(notifier);
            }
        }
        return notifier;
    }

    @NonNull
    public DBHelper getDbHelper() {
        synchronized (this) {
            if (dbHelper == null) {
                dbHelper = new DBHelper(appContext);
            }
        }
        return dbHelper;
    }

    /**
     * Main entry point for clients to get the main database.
     * <p>
     * <strong>Dev. note:</strong> This method always returns the same object for
     * the duration of the apps' life.
     * Our {@link DBHelper} caches a single {@link SynchronizedDb},
     * which in turn caches the database from the underlying SQLiteOpenHelper
     * which in turn caches the actual database.
     * Or in short: it's safe to use this as a singleton.
     *
     * @return the database instance
     */
    @NonNull
    public SynchronizedDb getDb() {
        return getDbHelper().getDb();
    }

    /**
     * Main entry point for clients to get the cache database.
     *
     * @return the database instance
     *
     * @see #getDb()
     */
    @NonNull
    public SynchronizedDb getCacheDb() {
        synchronized (this) {
            if (cacheDbHelper == null) {
                cacheDbHelper = new CacheDbHelper(appContext, getDb().isCollationCaseSensitive());
            }
        }
        return cacheDbHelper.getDb();
    }

    @NonNull
    public AuthorDao getAuthorDao() {
        synchronized (this) {
            if (authorDao == null) {
                authorDao = new AuthorDaoImpl(getDb());
            }
        }
        return authorDao;
    }

    @NonNull
    public BedethequeCacheDao getBedethequeCacheDao() {
        synchronized (this) {
            if (bedethequeCacheDao == null) {
                bedethequeCacheDao = new BedethequeCacheDaoImpl(getCacheDb());
            }
        }
        return bedethequeCacheDao;
    }

    @NonNull
    public BookDao getBookDao() {
        synchronized (this) {
            if (bookDao == null) {
                bookDao = new BookDaoImpl(getDb(), getSystemLocaleList().get(0)
                );
            }
        }
        return bookDao;
    }

    @NonNull
    public DeletedBooksDao getDeletedBooksDao() {
        synchronized (this) {
            if (deletedBooksDao == null) {
                deletedBooksDao = new DeletedBooksDaoImpl(getDb());
            }
        }
        return deletedBooksDao;
    }

    @NonNull
    public BookshelfDao getBookshelfDao() {
        synchronized (this) {
            if (bookshelfDao == null) {
                bookshelfDao = new BookshelfDaoImpl(getDb(), this::getStyles);
            }
        }
        return bookshelfDao;
    }

    @NonNull
    public CalibreDao getCalibreDao() {
        synchronized (this) {
            if (calibreDao == null) {
                calibreDao = new CalibreDaoImpl(getDb());
            }
        }
        return calibreDao;
    }

    @NonNull
    public CalibreLibraryDao getCalibreLibraryDao() {
        synchronized (this) {
            if (calibreLibraryDao == null) {
                calibreLibraryDao = new CalibreLibraryDaoImpl(getDb());
            }
        }
        return calibreLibraryDao;
    }

    @NonNull
    public CalibreCustomFieldDao getCalibreCustomFieldDao() {
        synchronized (this) {
            if (calibreCustomFieldDao == null) {
                calibreCustomFieldDao = new CalibreCustomFieldDaoImpl(getDb());
            }
        }
        return calibreCustomFieldDao;
    }

    @NonNull
    public ColorDao getColorDao() {
        synchronized (this) {
            if (colorDao == null) {
                colorDao = new ColorDaoImpl(getDb());
            }
        }
        return colorDao;
    }

    @NonNull
    public FormatDao getFormatDao() {
        synchronized (this) {
            if (formatDao == null) {
                formatDao = new FormatDaoImpl(getDb());
            }
        }
        return formatDao;
    }

    @NonNull
    public FtsDao getFtsDao() {
        synchronized (this) {
            if (ftsDao == null) {
                ftsDao = new FtsDaoImpl(getDb(), this::getFtsDaoHelper, this::getStyles);
            }
        }
        return ftsDao;
    }

    @NonNull
    public FtsDaoHelper getFtsDaoHelper() {
        synchronized (this) {
            if (ftsDaoHelper == null) {
                ftsDaoHelper = new FtsDaoHelper();
            }
        }
        return ftsDaoHelper;
    }

    /**
     * Get the dao to handle {@code Identifier} keys.
     *
     * @return singleton
     */
    @NonNull
    public IdentifierDao getIdentifierDao() {
        synchronized (this) {
            if (identifierDao == null) {
                identifierDao = new IdentifierDaoImpl(getDb());
            }
        }
        return identifierDao;
    }

    @NonNull
    public IdentifierValueDao getBookIdentifierDao() {
        synchronized (this) {
            if (bookIdentifierDao == null) {
                bookIdentifierDao = new IdentifierValueDaoImpl(
                        getDb(), DBDefinitions.TBL_BOOK_IDENTIFIER, DBKey.FK_BOOK);
            }
        }
        return bookIdentifierDao;
    }

    @NonNull
    public IdentifierValueDao getAuthorIdentifierDao() {
        synchronized (this) {
            if (authorIdentifierDao == null) {
                authorIdentifierDao = new IdentifierValueDaoImpl(
                        getDb(), DBDefinitions.TBL_AUTHOR_IDENTIFIER, DBKey.FK_AUTHOR);
            }
        }
        return authorIdentifierDao;
    }

    @NonNull
    public IdentifierValueDao getSeriesIdentifierDao() {
        synchronized (this) {
            if (seriesIdentifierDao == null) {
                seriesIdentifierDao = new IdentifierValueDaoImpl(
                        getDb(), DBDefinitions.TBL_SERIES_IDENTIFIER, DBKey.FK_SERIES);
            }
        }
        return seriesIdentifierDao;
    }

    @NonNull
    public LanguageDao getLanguageDao() {
        synchronized (this) {
            if (languageDao == null) {
                languageDao = new LanguageDaoImpl(getDb(), this::getLanguages);
            }
        }
        return languageDao;
    }

    @NonNull
    public LoaneeDao getLoaneeDao() {
        synchronized (this) {
            if (loaneeDao == null) {
                loaneeDao = new LoaneeDaoImpl(getDb());
            }
        }
        return loaneeDao;
    }

    @NonNull
    public LocationDao getLocationDao() {
        synchronized (this) {
            if (locationDao == null) {
                locationDao = new LocationDaoImpl(getDb());
            }
        }
        return locationDao;
    }

    @NonNull
    public PublisherDao getPublisherDao() {
        synchronized (this) {
            if (publisherDao == null) {
                publisherDao = new PublisherDaoImpl(getDb());
            }
        }
        return publisherDao;
    }

    @NonNull
    public PublicationFrequencyDao getPublicationFrequencyDao() {
        synchronized (this) {
            if (publicationFrequencyDao == null) {
                publicationFrequencyDao = new PublicationFrequencyDaoImpl(getDb());
            }
        }
        return publicationFrequencyDao;
    }

    @NonNull
    public SeriesDao getSeriesDao() {
        synchronized (this) {
            if (seriesDao == null) {
                seriesDao = new SeriesDaoImpl(getDb());
            }
        }
        return seriesDao;
    }

    @NonNull
    public StripInfoDao getStripInfoDao() {
        synchronized (this) {
            if (stripInfoDao == null) {
                stripInfoDao = new StripInfoDaoImpl(getDb());
            }
        }
        return stripInfoDao;
    }

    /**
     * private; You probably want to use {@link #getStyles()} instead.
     *
     * @return singleton
     */
    @NonNull
    private StyleDao getStyleDao() {
        synchronized (this) {
            if (styleDao == null) {
                styleDao = new StyleDaoImpl(getDb());
            }
        }
        return styleDao;
    }

    /**
     * private; You probably want to use {@link #getLanguages()} instead.
     *
     * @return singleton
     */
    @NonNull
    private IsoLanguageDao getIsoLanguageDao() {
        synchronized (this) {
            if (isoLanguageDao == null) {
                isoLanguageDao = new IsoLanguageDaoImpl(getDb());
            }
        }
        return isoLanguageDao;
    }

    @NonNull
    public TagDao getTagDao() {
        synchronized (this) {
            if (tagDao == null) {
                tagDao = new TagDaoImpl(getDb());
            }
        }
        return tagDao;
    }

    @NonNull
    public TagMappingDao getTagMappingDao() {
        synchronized (this) {
            if (tagMappingDao == null) {
                tagMappingDao = new TagMappingDaoImpl(getDb());
            }
        }
        return tagMappingDao;
    }

    @NonNull
    public TocEntryDao getTocEntryDao() {
        synchronized (this) {
            if (tocEntryDao == null) {
                tocEntryDao = new TocEntryDaoImpl(getDb());
            }
        }
        return tocEntryDao;
    }

    /**
     * You probably want to use {@link #getCoverStorage()} instead.
     *
     * @return singleton
     */
    @NonNull
    public CoverCacheDao getCoverCacheDao() {
        synchronized (this) {
            if (coverCacheDao == null) {
                coverCacheDao = new CoverCacheDaoImpl(getCacheDb());
            }
        }
        return coverCacheDao;
    }
}
