package org.jabref.logic.importer;

import java.util.Comparator;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.jabref.logic.FilePreferences;
import org.jabref.logic.importer.fetcher.ACMPortalFetcher;
import org.jabref.logic.importer.fetcher.ACS;
import org.jabref.logic.importer.fetcher.ApsFetcher;
import org.jabref.logic.importer.fetcher.ArXivFetcher;
import org.jabref.logic.importer.fetcher.AstrophysicsDataSystem;
import org.jabref.logic.importer.fetcher.BiodiversityLibrary;
import org.jabref.logic.importer.fetcher.BvbFetcher;
import org.jabref.logic.importer.fetcher.CompositeSearchBasedFetcher;
import org.jabref.logic.importer.fetcher.CrossRef;
import org.jabref.logic.importer.fetcher.CustomizableKeyFetcher;
import org.jabref.logic.importer.fetcher.DBLPFetcher;
import org.jabref.logic.importer.fetcher.DOABFetcher;
import org.jabref.logic.importer.fetcher.DOAJFetcher;
import org.jabref.logic.importer.fetcher.DiVA;
import org.jabref.logic.importer.fetcher.DoiFetcher;
import org.jabref.logic.importer.fetcher.DoiResolution;
import org.jabref.logic.importer.fetcher.EuropePmcFetcher;
import org.jabref.logic.importer.fetcher.GvkFetcher;
import org.jabref.logic.importer.fetcher.IEEE;
import org.jabref.logic.importer.fetcher.INSPIREFetcher;
import org.jabref.logic.importer.fetcher.ISIDOREFetcher;
import org.jabref.logic.importer.fetcher.IacrEprintFetcher;
import org.jabref.logic.importer.fetcher.IssnFetcher;
import org.jabref.logic.importer.fetcher.LOBIDFetcher;
import org.jabref.logic.importer.fetcher.LibraryOfCongress;
import org.jabref.logic.importer.fetcher.MathSciNet;
import org.jabref.logic.importer.fetcher.MedlineFetcher;
import org.jabref.logic.importer.fetcher.Medra;
import org.jabref.logic.importer.fetcher.OpenAccessDoi;
import org.jabref.logic.importer.fetcher.OpenAlex;
import org.jabref.logic.importer.fetcher.ResearchGate;
import org.jabref.logic.importer.fetcher.RfcFetcher;
import org.jabref.logic.importer.fetcher.ScholarArchiveFetcher;
import org.jabref.logic.importer.fetcher.ScienceDirect;
import org.jabref.logic.importer.fetcher.Scopus;
import org.jabref.logic.importer.fetcher.SemanticScholar;
import org.jabref.logic.importer.fetcher.SpringerNatureFullTextFetcher;
import org.jabref.logic.importer.fetcher.SpringerNatureWebFetcher;
import org.jabref.logic.importer.fetcher.SsrnFetcher;
import org.jabref.logic.importer.fetcher.TitleFetcher;
import org.jabref.logic.importer.fetcher.UnpaywallFetcher;
import org.jabref.logic.importer.fetcher.WileyFetcher;
import org.jabref.logic.importer.fetcher.ZbMATH;
import org.jabref.logic.importer.fetcher.isbntobibtex.IsbnFetcher;
import org.jabref.logic.importer.fileformat.pdf.PdfMergeMetadataImporter;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.field.Field;
import org.jabref.model.entry.field.StandardField;
import org.jabref.model.entry.identifier.ArXivIdentifier;
import org.jabref.model.entry.identifier.DOI;
import org.jabref.model.entry.identifier.ISBN;
import org.jabref.model.entry.identifier.ISSN;
import org.jabref.model.entry.identifier.IacrEprint;
import org.jabref.model.entry.identifier.Identifier;
import org.jabref.model.entry.identifier.RFC;
import org.jabref.model.entry.identifier.SSRN;

import static org.jabref.model.entry.field.StandardField.DOI;
import static org.jabref.model.entry.field.StandardField.EPRINT;
import static org.jabref.model.entry.field.StandardField.ISBN;
import static org.jabref.model.entry.field.StandardField.ISSN;

public class WebFetchers {

    private static SortedSet<SearchBasedFetcher> searchBasedFetchers;

    private WebFetchers() {
    }

    /// @implNote Needs to be consistent with [#getIdBasedFetcherFoIdentifier(Identifier, ImportFormatPreferences) ]
    public static Optional<IdBasedFetcher> getIdBasedFetcherForField(Field field, ImportFormatPreferences importFormatPreferences) {
        IdBasedFetcher fetcher;

        switch (field) {
            case DOI ->
                    fetcher = new DoiFetcher(importFormatPreferences);
            case EPRINT ->
                    fetcher = new ArXivFetcher(importFormatPreferences);
            case ISBN ->
                    fetcher = new IsbnFetcher(importFormatPreferences);
            case ISSN ->
                    fetcher = new IssnFetcher();
            case null,
                 default -> {
                return Optional.empty();
            }
        }
        return Optional.of(fetcher);
    }

    /// @implNote Needs to be consistent with [#getIdBasedFetcherForField(Field, ImportFormatPreferences) ]
    public static Optional<IdBasedFetcher> getIdBasedFetcherForIdentifier(Identifier identifier, ImportFormatPreferences importFormatPreferences) {
        IdBasedFetcher fetcher;

        // All identifiers listed here should also appear at {@link org.jabref.gui.mergeentries.FetchAndMergeEntry.SUPPORTED_FIELDS} and vice versa.

        return Optional.ofNullable(
                switch (identifier) {
                    case ArXivIdentifier _ ->
                            new ArXivFetcher(importFormatPreferences);
                    case DOI _ ->
                            new DoiFetcher(importFormatPreferences);
                    case IacrEprint _ ->
                            new IacrEprintFetcher(importFormatPreferences);
                    case ISBN _ ->
                            new IsbnFetcher(importFormatPreferences);
                    case ISSN _ ->
                            new IssnFetcher();
                    case RFC _ ->
                            new RfcFetcher(importFormatPreferences);
                    case SSRN _ ->
                            new SsrnFetcher(importFormatPreferences);
                    // No fetcher for ARK and MathSciNet
                    default ->
                            null;
                });
    }

    @SuppressWarnings("unchecked")
    public static <T extends Identifier> IdFetcher<T> getIdFetcherForIdentifier(Class<T> clazz, ImporterPreferences importerPreferences) {
        if (clazz == DOI.class) {
            return (IdFetcher<T>) new CrossRef(importerPreferences);
        } else {
            throw new IllegalArgumentException("No fetcher found for identifier" + clazz.getCanonicalName());
        }
    }

    public static Optional<IdFetcher<? extends Identifier>> getIdFetcherForField(Field field, ImporterPreferences importerPreferences) {
        if (field == StandardField.DOI) {
            return Optional.of(new CrossRef(importerPreferences));
        }
        return Optional.empty();
    }

    /// @return sorted set containing search based fetchers
    public static synchronized SortedSet<SearchBasedFetcher> getSearchBasedFetchers(ImportFormatPreferences importFormatPreferences, ImporterPreferences importerPreferences) {
        // Caching is allowed as the properties work with observables -> any update of the preferences will be used by the fetchers at the next call
        if (searchBasedFetchers != null) {
            return searchBasedFetchers;
        }

        searchBasedFetchers = new TreeSet<>(new CompositeSearchFirstComparator());

        searchBasedFetchers.add(new ArXivFetcher(importFormatPreferences));
        searchBasedFetchers.add(new ISIDOREFetcher());
        searchBasedFetchers.add(new INSPIREFetcher(importFormatPreferences));
        searchBasedFetchers.add(new GvkFetcher(importFormatPreferences));
        searchBasedFetchers.add(new BvbFetcher());
        searchBasedFetchers.add(new MedlineFetcher(importerPreferences));
        searchBasedFetchers.add(new AstrophysicsDataSystem(importFormatPreferences, importerPreferences));
        searchBasedFetchers.add(new MathSciNet(importFormatPreferences));
        searchBasedFetchers.add(new ZbMATH(importFormatPreferences));
        searchBasedFetchers.add(new ACMPortalFetcher());
        // set.add(new GoogleScholar(importFormatPreferences));
        searchBasedFetchers.add(new DBLPFetcher(importFormatPreferences));
        searchBasedFetchers.add(new SpringerNatureWebFetcher(importerPreferences));
        searchBasedFetchers.add(new CrossRef(importerPreferences));
        searchBasedFetchers.add(new OpenAlex(importerPreferences));
        searchBasedFetchers.add(new DOAJFetcher(importFormatPreferences));
        searchBasedFetchers.add(new IEEE(importFormatPreferences, importerPreferences));
        // set.add(new CollectionOfComputerScienceBibliographiesFetcher(importFormatPreferences));
        searchBasedFetchers.add(new DOABFetcher());
        // set.add(new JstorFetcher(importFormatPreferences));
        searchBasedFetchers.add(new SemanticScholar(importerPreferences));
        searchBasedFetchers.add(new ResearchGate(importFormatPreferences));
        searchBasedFetchers.add(new BiodiversityLibrary(importerPreferences));
        searchBasedFetchers.add(new LOBIDFetcher());
        searchBasedFetchers.add(new ScholarArchiveFetcher());
        searchBasedFetchers.add(new EuropePmcFetcher());
        searchBasedFetchers.add(new Scopus(importerPreferences));
        // Even though Unpaywall is used differently, adding it here enables "smooth" setting of the email (as fetcher key) in the preferences UI
        searchBasedFetchers.add(new UnpaywallFetcher(importerPreferences));

        // Make all search-based fetchers available for pre-selection
        searchBasedFetchers.add(new CompositeSearchBasedFetcher(searchBasedFetchers, importerPreferences, 30));

        return searchBasedFetchers;
    }

    /// @return sorted set containing id based fetchers
    public static SortedSet<IdBasedFetcher> getIdBasedFetchers(ImportFormatPreferences importFormatPreferences,
                                                               ImporterPreferences importerPreferences) {
        SortedSet<IdBasedFetcher> set = new TreeSet<>(Comparator.comparing(WebFetcher::getName, String.CASE_INSENSITIVE_ORDER));
        set.add(new ArXivFetcher(importFormatPreferences));
        set.add(new AstrophysicsDataSystem(importFormatPreferences, importerPreferences));
        set.add(new IsbnFetcher(importFormatPreferences));
        // .addRetryFetcher(new EbookDeIsbnFetcher(importFormatPreferences)));
        // .addRetryFetcher(new DoiToBibtexConverterComIsbnFetcher(importFormatPreferences)));
        set.add(new DiVA(importFormatPreferences));
        set.add(new DoiFetcher(importFormatPreferences));
        set.add(new SsrnFetcher(importFormatPreferences));
        set.add(new EuropePmcFetcher());
        set.add(new MedlineFetcher(importerPreferences));
        set.add(new TitleFetcher(importFormatPreferences, importerPreferences));
        set.add(new MathSciNet(importFormatPreferences));
        set.add(new ZbMATH(importFormatPreferences));
        set.add(new CrossRef(importerPreferences));
        set.add(new LibraryOfCongress(importFormatPreferences));
        set.add(new LOBIDFetcher());
        set.add(new IacrEprintFetcher(importFormatPreferences));
        set.add(new RfcFetcher(importFormatPreferences));
        set.add(new Medra());
        // set.add(new JstorFetcher(importFormatPreferences));
        return set;
    }

    public static SortedSet<EntryBasedFetcher> getEntryBasedFetchers(ImporterPreferences importerPreferences,
                                                                     ImportFormatPreferences importFormatPreferences,
                                                                     FilePreferences filePreferences,
                                                                     BibDatabaseContext databaseContext) {
        SortedSet<EntryBasedFetcher> set = new TreeSet<>(Comparator.comparing(WebFetcher::getName, String.CASE_INSENSITIVE_ORDER));

        set.add(new AstrophysicsDataSystem(importFormatPreferences, importerPreferences));
        set.add(new CrossRef(importerPreferences));
        set.add(new DoiFetcher(importFormatPreferences));
        set.add(new INSPIREFetcher(importFormatPreferences));
        set.add(new IsbnFetcher(importFormatPreferences));

        set.add(new IssnFetcher());
        // .addRetryFetcher(new EbookDeIsbnFetcher(importFormatPreferences)));
        // .addRetryFetcher(new DoiToBibtexConverterComIsbnFetcher(importFormatPreferences)));

        set.add(new MathSciNet(importFormatPreferences));
        set.add(new OpenAlex(importerPreferences));
        set.add(new ResearchGate(importFormatPreferences));
        set.add(new SemanticScholar(importerPreferences));
        set.add(new SsrnFetcher(importFormatPreferences));
        set.add(new ZbMATH(importFormatPreferences));

        // Uses the PDFs - and then uses the parsed DOI. Makes it 10% a web fetcher.
        // We list it here, because otherwise, it would be much more effort (other UI button, ...)
        set.add(new PdfMergeMetadataImporter.EntryBasedFetcherWrapper(importFormatPreferences, filePreferences, databaseContext));

        return set;
    }

    /// @return sorted set containing id fetchers
    public static SortedSet<IdFetcher<? extends Identifier>> getIdFetchers(ImportFormatPreferences importFormatPreferences) {
        return getIdFetchers(importFormatPreferences, ImporterPreferences.getDefault());
    }

    public static SortedSet<IdFetcher<? extends Identifier>> getIdFetchers(ImportFormatPreferences importFormatPreferences, ImporterPreferences importerPreferences) {
        SortedSet<IdFetcher<?>> set = new TreeSet<>(Comparator.comparing(WebFetcher::getName, String.CASE_INSENSITIVE_ORDER));

        set.add(new ArXivFetcher(importFormatPreferences));
        set.add(new CrossRef(importerPreferences));

        return set;
    }

    /// @return set containing fulltext fetchers
    public static Set<FulltextFetcher> getFullTextFetchers(ImportFormatPreferences importFormatPreferences, ImporterPreferences importerPreferences) {
        Set<FulltextFetcher> fetchers = new HashSet<>();

        // Original
        fetchers.add(new DoiResolution(importFormatPreferences.doiPreferences()));

        // Publishers
        fetchers.add(new ACS());
        fetchers.add(new ApsFetcher());
        fetchers.add(new ArXivFetcher(importFormatPreferences));
        fetchers.add(new IacrEprintFetcher(importFormatPreferences));
        fetchers.add(new IEEE(importFormatPreferences, importerPreferences));
        fetchers.add(new ScienceDirect(importerPreferences));
        fetchers.add(new SpringerNatureFullTextFetcher(importerPreferences));
        fetchers.add(new WileyFetcher(importerPreferences));

        // Meta search
        // fetchers.add(new JstorFetcher(importFormatPreferences));
        // fetchers.add(new GoogleScholar(importFormatPreferences));
        fetchers.add(new OpenAccessDoi());
        // OpenAlex provides OA locations and direct PDF links via its API
        fetchers.add(new OpenAlex(importerPreferences));
        fetchers.add(new ResearchGate(importFormatPreferences));
        fetchers.add(new SemanticScholar(importerPreferences));
        fetchers.add(new UnpaywallFetcher(importerPreferences));

        return fetchers;
    }

    /// @return set containing customizable api key fetchers
    public static Set<CustomizableKeyFetcher> getCustomizableKeyFetchers(ImportFormatPreferences importFormatPreferences, ImporterPreferences importerPreferences) {
        Set<CustomizableKeyFetcher> fetchers = Set.of(
                new AstrophysicsDataSystem(importFormatPreferences, importerPreferences),
                new BiodiversityLibrary(importerPreferences),
                new CrossRef(importerPreferences),
                new IEEE(importFormatPreferences, importerPreferences),
                new MedlineFetcher(importerPreferences),
                new OpenAlex(importerPreferences),
                new SemanticScholar(importerPreferences),
                new Scopus(importerPreferences),
                new SpringerNatureWebFetcher(importerPreferences),
                new UnpaywallFetcher(importerPreferences),
                new WileyFetcher(importerPreferences)
        );

        return fetchers;
    }
}

/// Places "Search pre-configured" to the first of the set
class CompositeSearchFirstComparator implements Comparator<SearchBasedFetcher> {
    @Override
    public int compare(SearchBasedFetcher s1, SearchBasedFetcher s2) {
        if (Objects.equals(s1.getName(), CompositeSearchBasedFetcher.FETCHER_NAME)) {
            return -1;
        } else {
            return String.CASE_INSENSITIVE_ORDER.compare(s1.getName(), s2.getName());
        }
    }
}
