package org.jabref.gui.fieldeditors.identifier;

import java.io.IOException;
import java.util.Optional;

import javax.swing.undo.UndoManager;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;

import org.jabref.gui.DialogService;
import org.jabref.gui.StateManager;
import org.jabref.gui.autocompleter.SuggestionProvider;
import org.jabref.gui.desktop.os.NativeDesktop;
import org.jabref.gui.fieldeditors.AbstractEditorViewModel;
import org.jabref.gui.mergeentries.FetchAndMergeEntry;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.logic.importer.FetcherClientException;
import org.jabref.logic.importer.FetcherServerException;
import org.jabref.logic.importer.IdFetcher;
import org.jabref.logic.importer.util.IdentifierParser;
import org.jabref.logic.integrity.FieldCheckers;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.field.Field;
import org.jabref.model.entry.field.FieldTextMapper;
import org.jabref.model.entry.identifier.Identifier;

import com.tobiasdiez.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class BaseIdentifierEditorViewModel<T extends Identifier> extends AbstractEditorViewModel {
    private static final Logger LOGGER = LoggerFactory.getLogger(BaseIdentifierEditorViewModel.class);
    protected BooleanProperty isInvalidIdentifier = new SimpleBooleanProperty();
    protected final BooleanProperty canShortenIdentifier = new SimpleBooleanProperty(false);
    protected final BooleanProperty identifierLookupInProgress = new SimpleBooleanProperty(false);
    protected final BooleanProperty canLookupIdentifier = new SimpleBooleanProperty(true);
    protected final BooleanProperty canFetchBibliographyInformationById = new SimpleBooleanProperty(false);
    protected final BooleanProperty canSyncWithBrowser = new SimpleBooleanProperty(false);
    protected IdentifierParser identifierParser;
    protected final ObjectProperty<Optional<T>> identifier = new SimpleObjectProperty<>(Optional.empty());
    protected DialogService dialogService;
    protected TaskExecutor taskExecutor;
    protected GuiPreferences preferences;
    protected final StateManager stateManager;

    public BaseIdentifierEditorViewModel(Field field,
                                         SuggestionProvider<?> suggestionProvider,
                                         FieldCheckers fieldCheckers,
                                         DialogService dialogService,
                                         TaskExecutor taskExecutor,
                                         GuiPreferences preferences,
                                         UndoManager undoManager,
                                         StateManager stateManager) {
        super(field, suggestionProvider, fieldCheckers, undoManager);
        this.dialogService = dialogService;
        this.taskExecutor = taskExecutor;
        this.preferences = preferences;
        this.stateManager = stateManager;
    }

    /// Since it's not possible to perform the same actions on all identifiers, specific implementations can call the `configure`
    /// method to tell the actions they can perform and the actions they can't. Based on this configuration, the view will enable/disable or
    /// show/hide certain UI elements for certain identifier editors.
    ///
    /// **NOTE: This method MUST be called by all the implementation view models in their principal constructor**
    protected final void configure(boolean canFetchBibliographyInformationById, boolean canLookupIdentifier, boolean canShortenIdentifier) {
        configure(canFetchBibliographyInformationById, canLookupIdentifier, canShortenIdentifier, false);
    }

    /// Overload adding `canSyncWithBrowser`, currently only used by the MathSciNet editor. Kept separate from the
    /// 3-arg overload so the far more common call sites don't all need a trailing `false`.
    protected final void configure(boolean canFetchBibliographyInformationById, boolean canLookupIdentifier, boolean canShortenIdentifier, boolean canSyncWithBrowser) {
        this.canFetchBibliographyInformationById.set(canFetchBibliographyInformationById);
        this.canLookupIdentifier.set(canLookupIdentifier);
        this.canShortenIdentifier.set(canShortenIdentifier);
        this.canSyncWithBrowser.set(canSyncWithBrowser);
    }

    protected Optional<T> updateIdentifier() {
        if (identifierParser == null) {
            return Optional.empty();
        }

        identifier.set((Optional<T>) identifierParser.parse(field));
        return identifier.get();
    }

    protected void handleIdentifierFetchingError(Exception exception, IdFetcher<T> fetcher) {
        LOGGER.error("Error while fetching identifier", exception);
        if (exception instanceof FetcherClientException) {
            dialogService.showInformationDialogAndWait(Localization.lang("Look up %0", fetcher.getName()), Localization.lang("No data was found for the identifier"));
        } else if (exception instanceof FetcherServerException) {
            dialogService.showInformationDialogAndWait(Localization.lang("Look up %0", fetcher.getName()), Localization.lang("Server not available"));
        } else if (exception.getCause() != null) {
            dialogService.showWarningDialogAndWait(Localization.lang("Look up %0", fetcher.getName()), Localization.lang("Error occurred %0", exception.getCause().getMessage()));
        } else {
            dialogService.showWarningDialogAndWait(Localization.lang("Look up %0", fetcher.getName()), Localization.lang("Error occurred %0", exception.getCause().getMessage()));
        }
    }

    public BooleanProperty canShortenIdentifierProperty() {
        return canShortenIdentifier;
    }

    public boolean getCanShortenIdentifier() {
        return canShortenIdentifierProperty().get();
    }

    public BooleanProperty canFetchBibliographyInformationByIdProperty() {
        return canFetchBibliographyInformationById;
    }

    public boolean getCanFetchBibliographyInformationById() {
        return canFetchBibliographyInformationById.get();
    }

    public BooleanProperty canSyncWithBrowserProperty() {
        return canSyncWithBrowser;
    }

    public boolean getCanSyncWithBrowser() {
        return canSyncWithBrowser.get();
    }

    /// Whether this identifier should currently be synced to the browser extension. Only meaningful when
    /// {@link #getCanSyncWithBrowser()} is true; overridden by the (currently only) editor that supports it.
    public boolean getSyncWithBrowser() {
        return false;
    }

    public void setSyncWithBrowser(boolean syncWithBrowser) {
        // No-op: only the MathSciNet editor currently supports browser sync.
    }

    public BooleanProperty canLookupIdentifierProperty() {
        return canLookupIdentifier;
    }

    public boolean getCanLookupIdentifier() {
        return canLookupIdentifier.get();
    }

    public BooleanProperty isInvalidIdentifierProperty() {
        return isInvalidIdentifier;
    }

    public boolean getIsInvalidIdentifier() {
        return isInvalidIdentifier.get();
    }

    public boolean getIdentifierLookupInProgress() {
        return identifierLookupInProgress.get();
    }

    public BooleanProperty identifierLookupInProgressProperty() {
        return identifierLookupInProgress;
    }

    public boolean getIdentifierLookupNotInProgress() {
        return identifierLookupInProgress.not().get();
    }

    public void fetchBibliographyInformation(BibEntry bibEntry) {
        stateManager.getActiveDatabase().ifPresentOrElse(
                databaseContext -> new FetchAndMergeEntry(databaseContext, taskExecutor, preferences, dialogService, undoManager, stateManager)
                        .fetchAndMerge(entry, field),
                () -> dialogService.notify(Localization.lang("No library selected"))
        );
    }

    public void lookupIdentifier(BibEntry bibEntry) {
        LOGGER.warn("Lookup not implemented yet for identifier '{}'", FieldTextMapper.getDisplayName(field));
    }

    public void openExternalLink() {
        identifier.get().flatMap(Identifier::getExternalURI).ifPresent(url -> {
                    try {
                        NativeDesktop.openBrowser(url, preferences.getExternalApplicationsPreferences());
                    } catch (IOException ex) {
                        dialogService.showErrorDialogAndWait(Localization.lang("Unable to open link."), ex);
                    }
                }
        );
    }

    public void shortenID() throws UnsupportedOperationException {
        throw new UnsupportedOperationException("Shortening of identifiers is not supported by this identifier editor.");
    }

    @Override
    public void bindToEntry(BibEntry entry) {
        super.bindToEntry(entry);
        identifierParser = new IdentifierParser(entry);
        EasyBind.subscribe(textProperty(), ignored -> updateIdentifier());
        EasyBind.subscribe(identifier, newIdentifier -> isInvalidIdentifier.set(newIdentifier.isEmpty()));
    }
}
