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

import android.content.Context;

import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;

import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;

import com.hardbacknutter.nevertoomanybooks.BuildConfig;
import com.hardbacknutter.nevertoomanybooks.R;
import com.hardbacknutter.nevertoomanybooks.ServiceLocator;
import com.hardbacknutter.nevertoomanybooks.core.network.CredentialsException;
import com.hardbacknutter.nevertoomanybooks.core.network.NetworkUnavailableException;
import com.hardbacknutter.nevertoomanybooks.core.storage.StorageException;
import com.hardbacknutter.nevertoomanybooks.core.tasks.ASyncExecutor;
import com.hardbacknutter.nevertoomanybooks.core.tasks.LTask;
import com.hardbacknutter.nevertoomanybooks.core.tasks.TaskListener;
import com.hardbacknutter.nevertoomanybooks.entities.Book;

/**
 * Searches a single {@link SearchEngine}.
 */
final class SearchTask
        extends LTask<Book> {

    /** Log tag. */
    private static final String TAG = "SearchTask";

    private static final AtomicInteger TASK_ID = new AtomicInteger();

    private final int searchId;
    @NonNull
    private final SearchEngine searchEngine;
    /** Search criteria. Usage depends on {@link #searchBy}. */
    @NonNull
    private final BookSearchCriteria criteria;
    /** What criteria to search by. */
    private final SearchEngine.SearchBy searchBy;

    /**
     * Constructor.
     *
     * @param context      Current context
     * @param taskId       a unique task identifier, returned with each message
     * @param searchId     a unique search identifier, to which this task belongs
     * @param searchEngine the search site engine
     * @param searchBy     how to search
     * @param criteria     to use
     * @param taskListener for the results
     */
    private SearchTask(@NonNull final Context context,
                       final int taskId,
                       final int searchId,
                       @NonNull final SearchEngine searchEngine,
                       @NonNull final SearchEngine.SearchBy searchBy,
                       @NonNull final BookSearchCriteria criteria,
                       @NonNull final TaskListener<Book> taskListener) {
        super(taskId, TAG + ' ' + searchEngine.getEngineId().getName(context), taskListener);
        this.searchId = searchId;
        this.searchEngine = searchEngine;
        this.criteria = criteria;
        this.searchBy = searchBy;
    }

    /**
     * Constructor. Will search according to passed {@link BookSearchCriteria}.
     *
     * @param context      Current context
     * @param searchId     a unique search identifier, to which this task will belong
     * @param searchEngine the search site engine
     * @param searchBy     how to search
     * @param criteria     to use
     * @param taskListener for the results
     *
     * @return task
     */
    @NonNull
    static SearchTask createSearchTask(@NonNull final Context context,
                                       final int searchId,
                                       @NonNull final SearchEngine searchEngine,
                                       @NonNull final SearchEngine.SearchBy searchBy,
                                       @NonNull final BookSearchCriteria criteria,
                                       @NonNull final TaskListener<Book> taskListener) {

        final SearchTask task = new SearchTask(context, TASK_ID.incrementAndGet(),
                                               searchId, searchEngine, searchBy, criteria,
                                               taskListener);
        task.setExecutor(ASyncExecutor.NETWORK);

        searchEngine.setCaller(task);
        return task;
    }


    public int getSearchId() {
        return searchId;
    }

    @NonNull
    public SearchEngine getSearchEngine() {
        return searchEngine;
    }

    @NonNull
    SearchEngine.SearchBy getSearchBy() {
        return searchBy;
    }

    void startSearch() {
        execute();
    }

    @Override
    @AnyThread
    public void cancel() {
        super.cancel();
        synchronized (searchEngine) {
            searchEngine.cancel();
        }
    }

    @Override
    @WorkerThread
    @NonNull
    protected Book doWork()
            throws StorageException,
                   SearchException,
                   CredentialsException,
                   IOException {
        Objects.requireNonNull(criteria);

        final Context context = ServiceLocator.getInstance().getLocalizedAppContext();

        // Checking this each time a search starts is not needed...
        // But it makes error handling slightly easier and doing
        // it here offloads it from the UI thread.
        if (!ServiceLocator.getInstance().getNetworkChecker().isNetworkAvailable()) {
            throw new NetworkUnavailableException(this.getClass().getName());
        }

        // can we reach the site ?
        searchEngine.ping();

        if (searchEngine instanceof SearchEngine.Login) {
            final SearchEngine.Login sel = (SearchEngine.Login) searchEngine;
            if (sel.isLoginToSearch(context)) {
                publishProgress(1, context.getString(R.string.progress_msg_authenticating_to_site,
                                                     searchEngine.getEngineId().getName(context)));
                sel.login(context);
            }
        }

        publishProgress(1, context.getString(R.string.progress_msg_searching_site,
                                             searchEngine.getEngineId().getName(context)));

        final Book book;
        switch (searchBy) {
            case ExternalId: {
                if (BuildConfig.DEBUG /* always */) {
                    criteria.requireSid(searchEngine.getEngineId());
                }
                book = ((SearchEngine.ByExternalId) searchEngine)
                        .searchByExternalId(context, criteria);
                break;
            }
            case Issn: {
                if (BuildConfig.DEBUG /* always */) {
                    criteria.requireProductCode();
                }
                book = ((SearchEngine.ByIssn) searchEngine).searchByIssn(context, criteria);
                break;
            }
            case Isbn: {
                if (BuildConfig.DEBUG /* always */) {
                    criteria.requireProductCode();
                }
                book = ((SearchEngine.ByIsbn) searchEngine).searchByIsbn(context, criteria);
                break;
            }
            case Barcode: {
                if (BuildConfig.DEBUG /* always */) {
                    criteria.requireProductCode();
                }
                book = ((SearchEngine.ByBarcode) searchEngine).searchByBarcode(context, criteria);
                break;
            }
            case Text: {
                book = ((SearchEngine.ByText) searchEngine).search(context, criteria);
                break;
            }
            default: {
                // we should never get here...
                throw new IllegalArgumentException("SearchEngine "
                                                   + searchEngine.getEngineId().getName(context)
                                                   + " does not implement " + searchBy);
            }
        }

        return book;
    }

    @Override
    @NonNull
    public String toString() {
        return "SearchTask{"
               + "searchEngine=" + searchEngine.getEngineId()
               + ", searchId=" + searchId
               + ", searchBy=" + searchBy
               + ", criteria=`" + criteria + '`'
               + '}';
    }
}
