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

import android.content.Context;

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

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import com.hardbacknutter.nevertoomanybooks.R;
import com.hardbacknutter.nevertoomanybooks.booklist.RebuildBooklist;
import com.hardbacknutter.nevertoomanybooks.booklist.grouping.BooklistGroup;
import com.hardbacknutter.nevertoomanybooks.booklist.header.BooklistHeader;
import com.hardbacknutter.nevertoomanybooks.citations.CitationType;
import com.hardbacknutter.nevertoomanybooks.core.database.Sort;
import com.hardbacknutter.nevertoomanybooks.database.DBKey;
import com.hardbacknutter.nevertoomanybooks.entities.AuthorRole;

public interface Style {

    /**
     * The (arbitrary) position for a Style which is not on the users preferred Style list.
     * i.e. it's at the very end.
     */
    int MENU_POSITION_NOT_PREFERRED = 1000;

    /**
     * A Style <strong>UUID</strong>.
     * <p>
     * <br>type: {@code String}
     */
    String BKEY_UUID = "Style:uuid";

    @SuppressWarnings("ClassReferencesSubclass")
    @NonNull
    WritableStyle clone(@NonNull Context context);

    /**
     * Get the id.
     * <ul>
     *      <li>Positive ID's: user-defined Styles</li>
     *      <li>Negative ID's: built-in Styles</li>
     *      <li>0: a user-defined Style which has not been saved yet</li>
     * </ul>
     *
     * @return id
     */
    long getId();

    /**
     * Set the id.
     *
     * @param id to set
     */
    void setId(long id);

    /**
     * Get the UUID for this Style.
     * <p>
     * Formatted as a 20 character UUID string, i.e. with 4 '-' separators.
     *
     * @return the UUID
     */
    @NonNull
    String getUuid();

    /**
     * Get the type of this Style.
     *
     * @return type
     */
    @NonNull
    Type getType();

    /**
     * Get the label to use. This is for <strong>displaying only</strong>.
     *
     * @param context Current context
     *
     * @return the label to use.
     */
    @NonNull
    String getLabel(@NonNull Context context);

    /**
     * Get the menu position of this Style as sorted by the user.
     *
     * @return menuPosition
     */
    int getMenuPosition();

    /**
     * Set the menu position of this Style as sorted by the user.
     *
     * @param menuPosition to set
     */
    void setMenuPosition(int menuPosition);

    /**
     * Check if this is a user preferred Style.
     *
     * @return flag
     */
    boolean isPreferred();

    /**
     * Set this Style as a user preferred Style.
     *
     * @param preferred flag
     */
    void setPreferred(boolean preferred);

    /**
     * Get the default visible level for the list.
     * i.e. the level which will be visible but not expanded.
     * i.o.w. the top-level where items above will be expanded/visible,
     * and items below will be hidden.
     * <p>
     * Reminder: during building/displaying the book list, this
     * setting will only be applied immediately if the {@link RebuildBooklist}
     * is set to {@link RebuildBooklist#Preferred}.
     * Otherwise, it's up to the user to apply it using the menu option.
     *
     * @return level
     */
    @IntRange(from = 1)
    int getExpansionLevel();

    /**
     * Get the layout as set on the Style.
     *
     * @return layout
     */
    @NonNull
    ScreenLayout getLayout();

    /**
     * Set the layout.
     *
     * @param layout to set
     */
    void setLayout(@NonNull ScreenLayout layout);

    /**
     * Get the layout as set on the Style, potentially overriding it
     * depending on the given flag.
     * <p>
     * <strong>Do not use this method for storing the layout!</strong>
     *
     * @param hasEmbeddedDetailsFrame whether the display Activity is showing
     *                                the embedded details-frame.
     *
     * @return layout
     */
    @NonNull
    default ScreenLayout getLayout(final boolean hasEmbeddedDetailsFrame) {
        if (hasEmbeddedDetailsFrame) {
            // We tested with using the grid when having the embedded frame,
            // but it was just too confusing, and a mess to look at.
            // Hence, just use the list layout in this setup.
            return ScreenLayout.List;
        } else {
            return getLayout();
        }
    }

    /**
     * Get the action to take when the user taps the cover image in the BoB list.
     *
     * @return action
     */
    @NonNull
    CoverClickAction getCoverClickAction();

    /**
     * Get the action to take when the user long-clicks a cover image in the BoB list.
     *
     * @return action
     */
    @NonNull
    CoverLongClickAction getCoverLongClickAction();

    /**
     * Whether the user prefers the Author names displayed by Given names, or by Family name first.
     *
     * @return {@code true} if the Given name should be displayed before the Family name
     */
    boolean isShowAuthorByGivenName();

    /**
     * Whether the user prefers titles of books, series,... to be displayed
     * as "Title, The" or as the original "The Title". Not applicable to all languages though.
     * <p>
     * Note this is also used for the Publisher name.
     *
     * @return {@code true} if titles should have the article/prefix (if present) moved to the end.
     */
    boolean isShowReorderedTitle();

    /**
     * Whether the user prefers the Author names sorted by Given names, or by Family name first.
     *
     * @return {@code true} if the Given name should be sorted before then the Family name
     */
    boolean isSortAuthorByGivenName();

    /**
     * Get the text scale <strong>identifier</strong> used by the Style.
     *
     * @return scale
     */
    @NonNull
    TextScale getTextScale();

    /**
     * Get the cover scale <strong>identifier</strong> used by the Style.
     * <p>
     * Used for user preferences and stored in the database.
     *
     * @return scale
     */
    @NonNull
    CoverScale getCoverScale();

    /**
     * Get the citation type.
     *
     * @return type
     */
    @NonNull
    CitationType getCitationType();

    /**
     * Check if the Style wants the specified header to be displayed.
     *
     * @param bit to check
     *
     * @return {@code true} if the header should be shown
     */
    boolean isShowHeaderField(@BooklistHeader.Option int bit);

    /**
     * Get the bitmap value with the list header fields to show.
     *
     * @return bitmask
     */
    @BooklistHeader.Option
    int getHeaderFieldVisibilityValue();

    /**
     * Check if the given field should be displayed.
     *
     * @param screen to get the setting for
     * @param dbKey  to check - one of the {@link DBKey} constants.
     *
     * @return {@code true} if in use
     */
    boolean isShowField(@NonNull FieldVisibility.Screen screen,
                        @NonNull String dbKey);

    /**
     * Get the visibility keys for the given screen.
     *
     * @param screen to get the setting for
     * @param all    set to {@code true} to get all keys regardless of visibility;
     *               Use {@code false} to get only the currently visible fields (keys).
     *
     * @return a new unmodifiable Set
     *
     * @see FieldVisibility#getKeys(boolean)
     */
    @NonNull
    Set<String> getFieldVisibilityKeys(@NonNull FieldVisibility.Screen screen,
                                       boolean all);

    /**
     * Get the visibility bitmask value for the given screen.
     *
     * @param screen to get the setting for
     *
     * @return bitmask
     *
     * @see FieldVisibility#getBitValue()
     */
    long getFieldVisibilityValue(@NonNull FieldVisibility.Screen screen);

    /**
     * Get the fields and how to sort on them for the lowest level (books) in the BoB.
     *
     * @return an <strong>unmodifiable</strong> map
     */
    @NonNull
    Map<String, Sort> getBookLevelFieldsOrderBy();

    /**
     * Should rows be shown using the system's preferred height or minimum height.
     *
     * @return {@code true} for "?attr/listPreferredItemHeightSmall"
     *         or {@code false} for {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
     */
    boolean isGroupRowUsesPreferredHeight();

    /**
     * Get the number of groups in this Style.
     *
     * @return the number of groups
     */
    int getGroupCount();

    /**
     * Check if the given group is present, using the given group id.
     *
     * @param id group id
     *
     * @return {@code true} if present
     */
    boolean hasGroup(@BooklistGroup.Id int id);

    /**
     * Get the group for the given id.
     *
     * @param id to get
     *
     * @return Optional with the group
     */
    @NonNull
    Optional<BooklistGroup> getGroupById(@BooklistGroup.Id int id);

    /**
     * Get the group for the given id.
     *
     * @param id to get
     *
     * @return group
     *
     * @throws NullPointerException on bug
     */
    @NonNull
    default BooklistGroup requireGroupById(@BooklistGroup.Id final int id)
            throws NullPointerException {
        return getGroupById(id).orElseThrow(() -> new NullPointerException(
                "Missing group: id=" + id + ", " + getUuid()));
    }

    /**
     * Get the group at the given level.
     *
     * @param level to get
     *
     * @return group
     *
     * @throws IndexOutOfBoundsException on bug
     */
    @NonNull
    BooklistGroup getGroupByLevel(@IntRange(from = 1) int level)
            throws IndexOutOfBoundsException;

    /**
     * Get all groups assigned to this Style.
     *
     * @return an <strong>unmodifiable</strong> List
     */
    @NonNull
    List<BooklistGroup> getGroupList();

    /**
     * Get the bitmask representing the role of author we consider the primary author.
     * <p>
     * {@link BooklistGroup#AUTHOR} property.
     *
     * @return bitmask
     */
    @AuthorRole.Role
    int getPrimaryAuthorRole();

    /**
     * Should books be shown under each of the given row type.
     *
     * @param groupId the {@link BooklistGroup} id
     *
     * @return flag
     */
    boolean isShowBooksUnderEachGroup(@BooklistGroup.Id int groupId);

    /**
     * Whether we should use the extended "Read progress" settings,
     * or the simple "Read/Unread" status.
     *
     * @return {@code true} to use the extended options,
     *         {@code false} to use the traditional "Read/Unread" status.
     */
    boolean useReadProgress();

    /**
     * Should a summation of the number of books on each level be done/shown.
     *
     * @return flag
     */
    boolean isShowGroupBookCount();

    enum Type {
        /**
         * User defined.
         */
        User(0),
        /**
         * A predefined Style.
         */
        Builtin(1),
        /**
         * The global Style, i.e. the defaults as set by the user.
         */
        Global(2);

        private final int id;

        Type(final int id) {
            this.id = id;
        }

        /**
         * Lookup by id.
         * <p>
         * Import/Export and database usage only.
         *
         * @param id to lookup
         *
         * @return type
         *
         * @throws IllegalArgumentException for any undefined id
         */
        @NonNull
        public static Type byId(final int id) {
            switch (id) {
                case 0:
                    return User;
                case 1:
                    return Builtin;
                case 2:
                    return Global;
                default:
                    throw new IllegalArgumentException(String.valueOf(id));
            }
        }

        /**
         * Get the internal id.
         * <p>
         * Import/Export and database usage only.
         *
         * @return id
         */
        public int getId() {
            return id;
        }

        /**
         * Get a short description of this type.
         *
         * @param context Current context
         *
         * @return the label
         */
        @NonNull
        public String getLabel(@NonNull final Context context) {
            return context.getResources().getStringArray(R.array.lbl_style_type)[id];
        }
    }

    enum CoverClickAction {
        /** Tapping the cover image will invoke a zoom dialog with the cover. */
        Zoom(0),
        /** Tapping the cover image will trigger opening the book detail page. */
        OpenBookDetails(1);

        private final int id;

        CoverClickAction(final int id) {
            this.id = id;
        }

        /**
         * Lookup by id.
         * <p>
         * Import/Export and database usage only.
         *
         * @param id to lookup
         *
         * @return type; or {@link #OpenBookDetails} for any invalid id.
         */
        @NonNull
        public static CoverClickAction byId(final int id) {
            if (id == 0) {
                return Zoom;
            }
            return OpenBookDetails;
        }

        /**
         * Get the internal id.
         * <p>
         * Import/Export and database usage only.
         *
         * @return id
         */
        public int getId() {
            return id;
        }
    }

    enum CoverLongClickAction {
        /** Ignore long-click on a cover. */
        Ignore(0),
        /** Open the book menu as a popup. */
        PopupMenu(1);

        private final int id;

        CoverLongClickAction(final int id) {
            this.id = id;
        }

        /**
         * Lookup by id.
         * <p>
         * Import/Export and database usage only.
         *
         * @param id to lookup
         *
         * @return type; or {@link #PopupMenu} for any invalid id.
         */
        @NonNull
        public static CoverLongClickAction byId(final int id) {
            if (id == 0) {
                return Ignore;
            }
            return PopupMenu;
        }

        /**
         * Get the internal id.
         * <p>
         * Import/Export and database usage only.
         *
         * @return id
         */
        public int getId() {
            return id;
        }
    }

    /**
     * See {@link #isShowBooksUnderEachGroup(int)}.
     */
    enum UnderEach {
        Author(BooklistGroup.AUTHOR,
               DBKey.STYLE.GROUPS_AUTHOR_SHOW_UNDER_EACH,
               "style.booklist.group.authors.show.all"
        ),

        Series(BooklistGroup.SERIES,
               DBKey.STYLE.GROUPS_SERIES_SHOW_UNDER_EACH,
               "style.booklist.group.series.show.all"
        ),

        Publisher(BooklistGroup.PUBLISHER,
                  DBKey.STYLE.GROUPS_PUBLISHER_SHOW_UNDER_EACH,
                  "style.booklist.group.publisher.show.all"
        ),

        Bookshelf(BooklistGroup.BOOKSHELF,
                  DBKey.STYLE.GROUPS_BOOKSHELF_SHOW_UNDER_EACH,
                  "style.booklist.group.bookshelf.show.all"
        );

        @NonNull
        private final String dbKey;
        @NonNull
        private final String prefKey;
        @BooklistGroup.Id
        private final int groupId;

        UnderEach(@BooklistGroup.Id final int groupId,
                  @NonNull final String dbKey,
                  @NonNull final String prefKey) {
            this.groupId = groupId;
            this.dbKey = dbKey;
            this.prefKey = prefKey;
        }

        @Nullable
        static UnderEach findByPrefKey(@NonNull final String prefKey) {
            return Arrays.stream(values())
                         .filter(v -> v.getPrefKey().equals(prefKey))
                         .findFirst()
                         .orElse(null);
        }

        /**
         * Get the {@link DBKey} for this element.
         *
         * @return key
         */
        @NonNull
        public String getDbKey() {
            return dbKey;
        }

        /**
         * Get the Preferences key for this element.
         *
         * @return key
         */
        @NonNull
        public String getPrefKey() {
            return prefKey;
        }

        /**
         * Get the {@link BooklistGroup} id for this element.
         *
         * @return id
         */
        @SuppressWarnings("WeakerAccess")
        @BooklistGroup.Id
        public int getGroupId() {
            return groupId;
        }
    }
}
