/*
 * @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.database.dao.impl;

import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;

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

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;

import com.hardbacknutter.nevertoomanybooks.BuildConfig;
import com.hardbacknutter.nevertoomanybooks.backup.json.coders.StyleCoder;
import com.hardbacknutter.nevertoomanybooks.booklist.style.BuiltinStyle;
import com.hardbacknutter.nevertoomanybooks.booklist.style.FieldVisibility;
import com.hardbacknutter.nevertoomanybooks.booklist.style.GlobalStyle;
import com.hardbacknutter.nevertoomanybooks.booklist.style.Style;
import com.hardbacknutter.nevertoomanybooks.booklist.style.UserStyle;
import com.hardbacknutter.nevertoomanybooks.core.database.DaoInsertException;
import com.hardbacknutter.nevertoomanybooks.core.database.DaoUpdateException;
import com.hardbacknutter.nevertoomanybooks.core.database.ExtSQLiteStatement;
import com.hardbacknutter.nevertoomanybooks.core.database.SynchronizedDb;
import com.hardbacknutter.nevertoomanybooks.core.database.SynchronizedStatement;
import com.hardbacknutter.nevertoomanybooks.core.database.Synchronizer;
import com.hardbacknutter.nevertoomanybooks.database.CursorRow;
import com.hardbacknutter.nevertoomanybooks.database.DBDefinitions;
import com.hardbacknutter.nevertoomanybooks.database.DBKey;
import com.hardbacknutter.nevertoomanybooks.database.dao.StyleDao;
import com.hardbacknutter.nevertoomanybooks.entities.DataHolder;
import com.hardbacknutter.util.logger.LoggerFactory;

public class StyleDaoImpl
        extends BaseDaoImpl
        implements StyleDao {

    private static final String TAG = "StyleDaoImpl";

    private static final String ERROR_UPDATE_FROM = "Update from\n";
    private static final String ERROR_INSERT_FROM = "Insert from\n";

    /**
     * Constructor.
     *
     * @param db Database Access
     */
    public StyleDaoImpl(@NonNull final SynchronizedDb db) {
        super(db, TAG);
    }

    /**
     * Run at <strong>installation</strong> time to add the built-in style ID's to the database.
     *
     * @param db Underlying database
     */
    public static void onPostCreate(@NonNull final SQLiteDatabase db) {
        insertGlobalDefaults(db, GlobalStyle.createDefault());

        // insert the built-in styles so foreign key rules are possible.
        // Other than the id/uuid/type and the menu options, the settings are never
        // read/written from/to the database.
        try (SQLiteStatement stmt = db.compileStatement(Sql.INSERT_BUILTIN_STYLE)) {
            int menuPos = 1;
            for (final BuiltinStyle.Definition styleDef : BuiltinStyle.getAll()) {
                int c = 0;
                stmt.bindLong(++c, styleDef.getId());
                stmt.bindString(++c, styleDef.getUuid());
                stmt.bindLong(++c, Style.Type.Builtin.getId());
                // preferred: false
                stmt.bindLong(++c, 0);
                // menu position, initially just in the order defined.
                stmt.bindLong(++c, menuPos++);

                // after inserting the id '-1' debug logging will claim that the insert failed.
                if (BuildConfig.DEBUG /* always */) {
                    if (styleDef.getId() == -1) {
                        LoggerFactory.getLogger().d(TAG, "onPostCreate",
                                                    "Ignore debug message inserting -1 ^^^");
                    }
                }
                stmt.executeInsert();
            }
        }
    }

    /**
     * Create and insert the single global/defaults style.
     * Used during app installation (and upgrade).
     *
     * @param db    DUnderlying database
     * @param style the defaults
     */
    public static void insertGlobalDefaults(@NonNull final SQLiteDatabase db,
                                            @NonNull final Style style) {
        try (ExtSQLiteStatement stmt = new ExtSQLiteStatement(
                db.compileStatement(Sql.INSERT_STYLE))) {
            doInsert(style, null, stmt);
        } catch (@NonNull final SQLException e) {
            // log, but just rethrow insert errors... we're in a real mess now
            LoggerFactory.getLogger().e(TAG, e);
            throw e;
        }
    }

    @NonNull
    private static String getGroupIdsAsCsv(@NonNull final Style style) {
        return style.getGroupList()
                    .stream()
                    .map(group -> String.valueOf(group.getId()))
                    .collect(Collectors.joining(","));
    }

    /**
     * Insert the style.
     * <strong>Exception handling and {@code -1} returns MUST be done by the caller</strong>
     *
     * @param style     to insert
     * @param styleName the name
     * @param stmt      statement to run
     *
     * @return the row id of the newly inserted row, or {@code -1} if an error occurred
     */
    private static long doInsert(@NonNull final Style style,
                                 @Nullable final String styleName,
                                 @NonNull final ExtSQLiteStatement stmt) {
        int c = 0;
        stmt.bindString(++c, style.getUuid());
        stmt.bindLong(++c, style.getType().getId());
        stmt.bindBoolean(++c, style.isPreferred());
        stmt.bindLong(++c, style.getMenuPosition());
        stmt.bindString(++c, styleName);

        stmt.bindLong(++c, style.getLayout().getId());
        stmt.bindLong(++c, style.getCoverClickAction().getId());
        stmt.bindLong(++c, style.getCoverLongClickAction().getId());
        stmt.bindLong(++c, style.getCoverScale().getId());
        stmt.bindLong(++c, style.getTextScale().getId());
        stmt.bindBoolean(++c, style.isGroupRowUsesPreferredHeight());

        stmt.bindLong(++c, style.getHeaderFieldVisibilityValue());
        stmt.bindLong(++c, style.getFieldVisibilityValue(FieldVisibility.Screen.List));
        stmt.bindString(++c, StyleCoder.getBookLevelFieldsOrderByAsJsonString(style));
        stmt.bindBoolean(++c, style.isSortAuthorByGivenName());

        stmt.bindBoolean(++c, style.isShowAuthorByGivenName());
        stmt.bindBoolean(++c, style.isShowReorderedTitle());
        stmt.bindBoolean(++c, style.isShowGroupBookCount());

        stmt.bindBoolean(++c, style.useReadProgress());
        stmt.bindLong(++c, style.getCitationType().getId());

        stmt.bindLong(++c, style.getFieldVisibilityValue(FieldVisibility.Screen.Detail));

        stmt.bindLong(++c, style.getExpansionLevel());
        stmt.bindString(++c, getGroupIdsAsCsv(style));
        stmt.bindLong(++c, style.getPrimaryAuthorRole());
        for (final Style.UnderEach item : Style.UnderEach.values()) {
            stmt.bindBoolean(++c, style.isShowBooksUnderEachGroup(item.getGroupId()));
        }

        //NEWTHINGS: style option: add to the inserted values

        return stmt.executeInsert();
    }

    @Override
    public long getStyleIdByUuid(@NonNull final String uuid) {
        try (SynchronizedStatement stmt = db.compileStatement(Sql.FIND_STYLE_ID_BY_UUID)) {
            stmt.bindString(1, uuid);
            return stmt.simpleQueryForLongOrZero();
        }
    }

    @Override
    @NonNull
    public Map<String, Style> getUserStyles() {
        final Map<String, Style> map = new LinkedHashMap<>();

        try (Cursor cursor = db.rawQuery(Sql.FIND_BY_TYPE, new String[]{
                String.valueOf(Style.Type.User.getId())})) {
            final DataHolder rowData = new CursorRow(cursor);
            while (cursor.moveToNext()) {
                final Style style = UserStyle.createFromDatabase(rowData);
                map.put(style.getUuid(), style);
            }
        }

        return map;
    }

    @Override
    @NonNull
    public Map<String, Style> getBuiltinStyles() {
        final Map<String, Style> map = new LinkedHashMap<>();

        try (Cursor cursor = db.rawQuery(Sql.FIND_BY_TYPE, new String[]{
                String.valueOf(Style.Type.Builtin.getId())})) {
            final DataHolder rowData = new CursorRow(cursor);
            while (cursor.moveToNext()) {
                BuiltinStyle.createFromDatabase(rowData).ifPresent(
                        style -> map.put(style.getUuid(), style));
            }
        }

        return map;
    }

    @Override
    @NonNull
    public Style getGlobalStyle() {
        try (Cursor cursor = db.rawQuery(Sql.FIND_BY_TYPE, new String[]{
                String.valueOf(Style.Type.Global.getId())})) {
            final DataHolder rowData = new CursorRow(cursor);
            if (cursor.moveToFirst()) {
                return GlobalStyle.createFromDatabase(rowData);
            }
        }

        throw new IllegalStateException("No Global style!");
    }

    @Override
    @IntRange(from = 1)
    public long insert(@NonNull final Context context,
                       @NonNull final Style style)
            throws DaoInsertException {

        final long iId;
        try (SynchronizedStatement stmt = db.compileStatement(Sql.INSERT_STYLE)) {
            iId = doInsert(style, style.getLabel(context), stmt);
        }

        if (iId != -1) {
            style.setId(iId);
            return iId;
        }

        // The insert failed with -1
        throw new DaoInsertException(ERROR_INSERT_FROM + style);
    }

    @Override
    public void update(@NonNull final Context context,
                       @NonNull final Style style)
            throws DaoUpdateException {

        // Note that the Style.Type is NEVER updated.

        final int rowsAffected;
        if (style.getType() == Style.Type.Builtin) {
            try (SynchronizedStatement stmt = db.compileStatement(Sql.UPDATE_BUILTIN_STYLE)) {
                int c = 0;
                stmt.bindBoolean(++c, style.isPreferred());
                stmt.bindLong(++c, style.getMenuPosition());

                stmt.bindLong(++c, style.getId());
                rowsAffected = stmt.executeUpdateDelete();
            }
        } else {
            try (SynchronizedStatement stmt = db.compileStatement(Sql.UPDATE_STYLE)) {
                int c = 0;
                stmt.bindBoolean(++c, style.isPreferred());
                stmt.bindLong(++c, style.getMenuPosition());
                stmt.bindString(++c, style.getLabel(context));

                stmt.bindLong(++c, style.getLayout().getId());
                stmt.bindLong(++c, style.getCoverClickAction().getId());
                stmt.bindLong(++c, style.getCoverLongClickAction().getId());
                stmt.bindLong(++c, style.getCoverScale().getId());
                stmt.bindLong(++c, style.getTextScale().getId());
                stmt.bindBoolean(++c, style.isGroupRowUsesPreferredHeight());

                stmt.bindLong(++c, style.getHeaderFieldVisibilityValue());
                stmt.bindLong(++c, style.getFieldVisibilityValue(FieldVisibility.Screen.List));
                stmt.bindString(++c, StyleCoder.getBookLevelFieldsOrderByAsJsonString(style));
                stmt.bindBoolean(++c, style.isSortAuthorByGivenName());

                stmt.bindBoolean(++c, style.isShowAuthorByGivenName());
                stmt.bindBoolean(++c, style.isShowReorderedTitle());
                stmt.bindBoolean(++c, style.isShowGroupBookCount());

                stmt.bindBoolean(++c, style.useReadProgress());
                stmt.bindLong(++c, style.getCitationType().getId());

                stmt.bindLong(++c, style.getFieldVisibilityValue(FieldVisibility.Screen.Detail));

                stmt.bindLong(++c, style.getExpansionLevel());
                stmt.bindString(++c, getGroupIdsAsCsv(style));
                stmt.bindLong(++c, style.getPrimaryAuthorRole());

                //NEWTHINGS: style option: add to the UPDATE

                for (final Style.UnderEach item : Style.UnderEach.values()) {
                    stmt.bindBoolean(++c, style.isShowBooksUnderEachGroup(item.getGroupId()));
                }

                stmt.bindLong(++c, style.getId());
                rowsAffected = stmt.executeUpdateDelete();
            }
        }

        if (rowsAffected > 0) {
            return;
        }

        throw new DaoUpdateException(ERROR_UPDATE_FROM + style);
    }

    @Override
    public void update(@NonNull final Context context,
                       @NonNull final Collection<Style> styles)
            throws DaoUpdateException {

        Synchronizer.SyncLock txLock = null;
        try {
            if (!db.inTransaction()) {
                txLock = db.beginTransaction(true);
            }

            for (final Style style : styles) {
                if (BuildConfig.DEBUG /* always */) {
                    if (style.getUuid().isEmpty()) {
                        throw new IllegalStateException("ERROR_MISSING_UUID");
                    }
                    // Reminder: do NOT require a positive value here!
                    // It's perfectly fine to update built-in styles;
                    // ONLY NEW styles must be rejected
                    if (style.getId() == 0) {
                        throw new IllegalStateException("A new Style cannot be updated");
                    }
                }

                update(context, style);
            }

            if (txLock != null) {
                db.setTransactionSuccessful();
            }
        } finally {
            if (txLock != null) {
                db.endTransaction(txLock);
            }
        }
    }

    @Override
    public boolean delete(@NonNull final Style style) {
        Synchronizer.SyncLock txLock = null;
        try {
            if (!db.inTransaction()) {
                txLock = db.beginTransaction(true);
            }

            purgeNodeStates(style);

            final int rowsAffected;
            try (SynchronizedStatement stmt = db.compileStatement(Sql.DELETE_STYLE_BY_ID)) {
                stmt.bindLong(1, style.getId());
                rowsAffected = stmt.executeUpdateDelete();
            }
            if (rowsAffected > 0) {
                style.setId(0);

                if (txLock != null) {
                    db.setTransactionSuccessful();
                }
                return true;
            }
            return false;
        } catch (@NonNull final DaoUpdateException e) {
            return false;
        } finally {
            if (txLock != null) {
                db.endTransaction(txLock);
            }
        }
    }

    @Override
    public void purgeNodeStates(@NonNull final Style style)
            throws DaoUpdateException {
        try (SynchronizedStatement stmt = db.compileStatement(Sql.DELETE_NODE_STATE_BY_STYLE_ID)) {
            stmt.bindLong(1, style.getId());
            stmt.executeUpdateDelete();
        }
    }

    private static final class Sql {

        /** Insert a {@link BuiltinStyle}. */
        static final String INSERT_BUILTIN_STYLE =
                INSERT_INTO_ + DBDefinitions.TBL_BOOKLIST_STYLES.getName()
                + '(' + DBKey.PK_ID
                + ',' + DBKey.STYLE.UUID
                + ',' + DBKey.STYLE.TYPE
                + ',' + DBKey.STYLE.IS_PREFERRED
                + ',' + DBKey.STYLE.MENU_POSITION
                + ") VALUES(?,?,?,?,?)";

        /** Update a {@link BuiltinStyle}. */
        static final String UPDATE_BUILTIN_STYLE =
                UPDATE_ + DBDefinitions.TBL_BOOKLIST_STYLES.getName()
                + _SET_ + DBKey.STYLE.IS_PREFERRED + "=?"
                + ',' + DBKey.STYLE.MENU_POSITION + "=?"
                + _WHERE_ + DBKey.PK_ID + "=?";

        /** Insert a {@link Style}. */
        static final String INSERT_STYLE;
        /** Insert a {@link Style}. */
        static final String UPDATE_STYLE;

        /** Delete a {@link Style}. */
        static final String DELETE_STYLE_BY_ID =
                DELETE_FROM_ + DBDefinitions.TBL_BOOKLIST_STYLES.getName()
                + _WHERE_ + DBKey.PK_ID + "=?";

        /**
         * Find a {@link Style} by its {@link Style.Type}.
         * <p>
         * We order by the id, i.e. in the order the styles were created.
         * This is only done to get a reproducible and consistent order.
         */
        static final String FIND_BY_TYPE =
                SELECT_ + "*" + _FROM_ + DBDefinitions.TBL_BOOKLIST_STYLES.getName()
                + _WHERE_ + DBKey.STYLE.TYPE + "=?"
                + _ORDER_BY_ + DBKey.PK_ID;

        /** Find the id of a {@link Style} by its UUID. */
        static final String FIND_STYLE_ID_BY_UUID =
                SELECT_ + DBKey.PK_ID + _FROM_ + DBDefinitions.TBL_BOOKLIST_STYLES.getName()
                + _WHERE_ + DBKey.STYLE.UUID + "=?";

        static final String DELETE_NODE_STATE_BY_STYLE_ID =
                DELETE_FROM_ + DBDefinitions.TBL_BOOK_LIST_NODE_STATE.getName()
                + _WHERE_ + DBKey.FK_STYLE + "=?";

        static {
            StringBuilder tmp = new StringBuilder(
                    INSERT_INTO_ + DBDefinitions.TBL_BOOKLIST_STYLES.getName()
                    + '(' + DBKey.STYLE.UUID
                    + ',' + DBKey.STYLE.TYPE
                    + ',' + DBKey.STYLE.IS_PREFERRED
                    + ',' + DBKey.STYLE.MENU_POSITION
                    + ',' + DBKey.STYLE.NAME

                    + ',' + DBKey.STYLE.LAYOUT
                    + ',' + DBKey.STYLE.COVER_CLICK_ACTION
                    + ',' + DBKey.STYLE.COVER_LONG_CLICK_ACTION
                    + ',' + DBKey.STYLE.COVER_SCALE
                    + ',' + DBKey.STYLE.TEXT_SCALE
                    + ',' + DBKey.STYLE.ROW_USES_PREF_HEIGHT

                    + ',' + DBKey.STYLE.LIST_HEADER
                    + ',' + DBKey.STYLE.BOOK_LIST_FIELD_VISIBILITY
                    + ',' + DBKey.STYLE.BOOK_LIST_FIELD_ORDER_BY
                    + ',' + DBKey.STYLE.AUTHOR_SORT_BY_GIVEN_NAME

                    + ',' + DBKey.STYLE.AUTHOR_SHOW_BY_GIVEN_NAME
                    + ',' + DBKey.STYLE.TITLE_SHOW_REORDERED
                    + ',' + DBKey.STYLE.SHOW_GROUP_BOOK_COUNT

                    + ',' + DBKey.STYLE.READ_STATUS_WITH_PROGRESS
                    + ',' + DBKey.STYLE.CITATION_TYPE

                    + ',' + DBKey.STYLE.BOOK_DETAIL_FIELD_VISIBILITY

                    + ',' + DBKey.STYLE.EXP_LEVEL
                    + ',' + DBKey.STYLE.GROUPS
                    + ',' + DBKey.STYLE.GROUPS_AUTHOR_PRIMARY_ROLE
            );

            //NEWTHINGS: style option: add to the INSERT

            for (final Style.UnderEach item : Style.UnderEach.values()) {
                tmp.append(',').append(item.getDbKey());
            }

            tmp.append(") VALUES (");

            final int count = (int) (tmp.chars().filter(ch -> ch == ',').count() + 1);

            tmp.append(String.join(",", Collections.nCopies(count, "?"))).append(")");

            INSERT_STYLE = tmp.toString();

            tmp = new StringBuilder(
                    UPDATE_ + DBDefinitions.TBL_BOOKLIST_STYLES.getName()
                    + _SET_ + DBKey.STYLE.IS_PREFERRED + "=?"
                    + ',' + DBKey.STYLE.MENU_POSITION + "=?"
                    + ',' + DBKey.STYLE.NAME + "=?"

                    + ',' + DBKey.STYLE.LAYOUT + "=?"
                    + ',' + DBKey.STYLE.COVER_CLICK_ACTION + "=?"
                    + ',' + DBKey.STYLE.COVER_LONG_CLICK_ACTION + "=?"
                    + ',' + DBKey.STYLE.COVER_SCALE + "=?"
                    + ',' + DBKey.STYLE.TEXT_SCALE + "=?"
                    + ',' + DBKey.STYLE.ROW_USES_PREF_HEIGHT + "=?"

                    + ',' + DBKey.STYLE.LIST_HEADER + "=?"
                    + ',' + DBKey.STYLE.BOOK_LIST_FIELD_VISIBILITY + "=?"
                    + ',' + DBKey.STYLE.BOOK_LIST_FIELD_ORDER_BY + "=?"
                    + ',' + DBKey.STYLE.AUTHOR_SORT_BY_GIVEN_NAME + "=?"

                    + ',' + DBKey.STYLE.AUTHOR_SHOW_BY_GIVEN_NAME + "=?"
                    + ',' + DBKey.STYLE.TITLE_SHOW_REORDERED + "=?"
                    + ',' + DBKey.STYLE.SHOW_GROUP_BOOK_COUNT + "=?"

                    + ',' + DBKey.STYLE.READ_STATUS_WITH_PROGRESS + "=?"
                    + ',' + DBKey.STYLE.CITATION_TYPE + "=?"

                    + ',' + DBKey.STYLE.BOOK_DETAIL_FIELD_VISIBILITY + "=?"

                    + ',' + DBKey.STYLE.EXP_LEVEL + "=?"
                    + ',' + DBKey.STYLE.GROUPS + "=?"
                    + ',' + DBKey.STYLE.GROUPS_AUTHOR_PRIMARY_ROLE + "=?"
            );

            //NEWTHINGS: style option: add to the UPDATE

            for (final Style.UnderEach item : Style.UnderEach.values()) {
                tmp.append(',').append(item.getDbKey()).append("=?");
            }

            tmp.append(_WHERE_ + DBKey.PK_ID + "=?");

            UPDATE_STYLE = tmp.toString();
        }
    }
}
