package org.jabref.gui.exporter;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.DialogPane;
import javafx.scene.control.TableColumn;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;

import org.jabref.gui.DialogService;
import org.jabref.gui.LibraryTab;
import org.jabref.gui.StateManager;
import org.jabref.gui.autosaveandbackup.AutosaveManager;
import org.jabref.gui.autosaveandbackup.BackupManager;
import org.jabref.gui.maintable.BibEntryTableViewModel;
import org.jabref.gui.maintable.columns.MainTableColumn;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.gui.util.FileDialogConfiguration;
import org.jabref.logic.exporter.AtomicFileWriter;
import org.jabref.logic.exporter.BibDatabaseWriter;
import org.jabref.logic.exporter.BibWriter;
import org.jabref.logic.exporter.SaveException;
import org.jabref.logic.exporter.SelfContainedSaveConfiguration;
import org.jabref.logic.journals.JournalAbbreviationRepository;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.os.OS;
import org.jabref.logic.shared.DatabaseLocation;
import org.jabref.logic.shared.prefs.SharedDatabasePreferences;
import org.jabref.logic.util.StandardFileType;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.database.event.ChangePropagation;
import org.jabref.model.entry.BibEntryTypesManager;
import org.jabref.model.metadata.SaveOrder;
import org.jabref.model.metadata.SelfContainedSaveOrder;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/// Action for the "Save" and "Save as" operations called from BasePanel. This class is also used for save operations
/// when closing a database or quitting the applications.
///
/// The save operation is loaded off of the GUI thread using {@link org.jabref.logic.util.BackgroundTask}. Callers can query whether the
/// operation was canceled, or whether it was successful.
public class SaveDatabaseAction {
    private static final Logger LOGGER = LoggerFactory.getLogger(SaveDatabaseAction.class);

    private final LibraryTab libraryTab;
    private final DialogService dialogService;
    private final GuiPreferences preferences;
    private final BibEntryTypesManager entryTypesManager;
    private final StateManager stateManager;
    private final JournalAbbreviationRepository journalAbbreviationRepository;

    public enum SaveDatabaseMode {
        SILENT, NORMAL
    }

    public SaveDatabaseAction(LibraryTab libraryTab,
                              DialogService dialogService,
                              GuiPreferences preferences,
                              BibEntryTypesManager entryTypesManager,
                              StateManager stateManager,
                              JournalAbbreviationRepository journalAbbreviationRepository) {
        this.libraryTab = libraryTab;
        this.dialogService = dialogService;
        this.preferences = preferences;
        this.entryTypesManager = entryTypesManager;
        this.stateManager = stateManager;
        this.journalAbbreviationRepository = journalAbbreviationRepository;
    }

    public boolean save() {
        return save(libraryTab.getBibDatabaseContext(), SaveDatabaseMode.NORMAL);
    }

    public boolean save(SaveDatabaseMode mode) {
        return save(libraryTab.getBibDatabaseContext(), mode);
    }

    /// Asks the user for the path and saves afterward
    public void saveAs() {
        askForSavePath().ifPresent(this::saveAs);
    }

    public boolean saveAs(Path file) {
        return this.saveAs(file, SaveDatabaseMode.NORMAL);
    }

    private SelfContainedSaveOrder getSaveOrder() {
        return libraryTab.getBibDatabaseContext()
                         .getMetaData().getSaveOrder()
                         .map(so -> {
                             if (so.getOrderType() == SaveOrder.OrderType.TABLE) {
                                 // We need to "flatten out" SaveOrder.OrderType.TABLE as BibWriter does not have access to preferences
                                 List<TableColumn<BibEntryTableViewModel, ?>> sortOrder = libraryTab.getMainTable().getSortOrder();
                                 return new SelfContainedSaveOrder(
                                         SaveOrder.OrderType.SPECIFIED,
                                         sortOrder.stream()
                                                  .filter(col -> col instanceof MainTableColumn<?>)
                                                  .map(column -> ((MainTableColumn<?>) column).getModel())
                                                  .flatMap(model -> model.getSortCriteria().stream())
                                                  .toList());
                             } else {
                                 return SelfContainedSaveOrder.of(so);
                             }
                         })
                         .orElse(SaveOrder.getDefaultSaveOrder());
    }

    public void saveSelectedAsPlain() {
        askForSavePath().ifPresent(path -> {
            try {
                saveDatabase(path, true, StandardCharsets.UTF_8, BibDatabaseWriter.SaveType.PLAIN_BIBTEX, getSaveOrder());
                preferences.getLastFilesOpenedPreferences().getFileHistory().newFile(path);
                dialogService.notify(Localization.lang("Saved selected to '%0'.", path.toString()));
            } catch (SaveException ex) {
                LOGGER.error("A problem occurred when trying to save the file", ex);
                dialogService.showErrorDialogAndWait(Localization.lang("Save library"), Localization.lang("Could not save file."), ex);
            }
        });
    }

    /// @param file the new file name to save the database to. This is stored in the database context of the panel upon successful save.
    /// @return true on successful save
    boolean saveAs(Path file, SaveDatabaseMode mode) {
        BibDatabaseContext context = libraryTab.getBibDatabaseContext();

        Optional<Path> databasePath = context.getDatabasePath();
        if (databasePath.isPresent()) {
            // Close AutosaveManager, BackupManager, and IndexManager for original library
            AutosaveManager.shutdown(context);
            BackupManager.shutdown(context, this.preferences.getFilePreferences().getBackupDirectory(), preferences.getFilePreferences().shouldCreateBackup());
            libraryTab.closeSearchContext();
        }

        // Set new location
        if (context.getLocation() == DatabaseLocation.SHARED) {
            // Save all properties dependent on the ID. This makes it possible to restore them.
            new SharedDatabasePreferences(context.getDatabase().generateSharedDatabaseID())
                    .putAllDBMSConnectionProperties(context.getDBMSSynchronizer().getConnectionProperties());
        }

        boolean saveResult = save(file, mode);

        if (saveResult) {
            // we managed to successfully save the file
            // thus, we can store the path into the context
            context.setDatabasePath(file);
            stateManager.setActiveDatabase(context);
            libraryTab.updateTabTitle(false);

            // Reset (here: uninstall and install again) AutosaveManager, BackupManager and IndexManager for the new file name
            libraryTab.resetChangeMonitor();
            libraryTab.installAutosaveManagerAndBackupManager();
            libraryTab.createSearchContext();

            preferences.getLastFilesOpenedPreferences().getFileHistory().newFile(file);
        }
        return saveResult;
    }

    /// Asks the user for the path to save to. Stores the directory to the preferences, which is used next time when
    /// opening the dialog.
    ///
    /// @return the path set by the user
    private Optional<Path> askForSavePath() {
        FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder()
                .addExtensionFilter(StandardFileType.BIBTEX_DB)
                .withDefaultExtension(StandardFileType.BIBTEX_DB)
                .withInitialDirectory(preferences.getFilePreferences().getWorkingDirectory())
                .build();
        Optional<Path> selectedPath = dialogService.showFileSaveDialog(fileDialogConfiguration);
        selectedPath.ifPresent(path -> preferences.getFilePreferences().setWorkingDirectory(path.getParent()));
        if (selectedPath.isPresent()) {
            Path savePath = selectedPath.get();
            // Workaround for linux systems not adding file extension
            if (!savePath.getFileName().toString().toLowerCase().endsWith(".bib")) {
                savePath = Path.of(savePath + ".bib");
                if (!Files.notExists(savePath) && !dialogService.showConfirmationDialogAndWait(
                        Localization.lang("Overwrite file"),
                        Localization.lang("'%0' exists. Overwrite file?", savePath.getFileName()))) {
                    return Optional.empty();
                }

                selectedPath = Optional.of(savePath);
            }
        }
        return selectedPath;
    }

    private boolean save(BibDatabaseContext bibDatabaseContext, SaveDatabaseMode mode) {
        Optional<Path> databasePath = bibDatabaseContext.getDatabasePath();
        if (databasePath.isEmpty()) {
            Optional<Path> savePath = askForSavePath();
            return savePath.filter(path -> saveAs(path, mode)).isPresent();
        }

        return save(databasePath.get(), mode);
    }

    private boolean save(Path targetPath, SaveDatabaseMode mode) {
        if (mode == SaveDatabaseMode.NORMAL && libraryTab.getBibDatabaseContext().getEntries().size() > 2_000) {
            dialogService.notify("%s...".formatted(Localization.lang("Saving library")));
        }

        synchronized (libraryTab) {
            if (libraryTab.isSaving()) {
                // if another thread is saving, we do not need to save
                return true;
            }
            libraryTab.setSaving(true);
        }

        libraryTab.suspendChangeMonitor();

        try {
            Charset encoding = libraryTab.getBibDatabaseContext()
                                         .getMetaData()
                                         .getEncoding()
                                         .orElse(StandardCharsets.UTF_8);

            // Make sure to remember which encoding we used
            libraryTab.getBibDatabaseContext().getMetaData().setEncoding(encoding, ChangePropagation.DO_NOT_POST_EVENT);

            boolean success = saveDatabase(targetPath, false, encoding, BibDatabaseWriter.SaveType.WITH_JABREF_META_DATA, getSaveOrder());

            if (success) {
                libraryTab.getUndoManager().markUnchanged();
                libraryTab.resetChangedProperties();
            }
            dialogService.notify(Localization.lang("Library saved"));
            return success;
        } catch (SaveException ex) {
            LOGGER.error("A problem occurred when trying to save the file {}", targetPath, ex);
            dialogService.showErrorDialogAndWait(Localization.lang("Save library"), Localization.lang("Could not save file."), ex);
            return false;
        } finally {
            libraryTab.resumeChangeMonitor();
            // release panel from save status
            libraryTab.setSaving(false);
        }
    }

    private boolean saveDatabase(Path file, boolean selectedOnly, Charset encoding, BibDatabaseWriter.SaveType saveType, SelfContainedSaveOrder saveOrder) throws SaveException {
        // if this code is adapted, please also adapt org.jabref.logic.autosaveandbackup.BackupManager.performBackup
        SelfContainedSaveConfiguration saveConfiguration
                = new SelfContainedSaveConfiguration(saveOrder, false, saveType, preferences.getLibraryPreferences().shouldAlwaysReformatOnSave());
        BibDatabaseContext bibDatabaseContext = libraryTab.getBibDatabaseContext();
        synchronized (bibDatabaseContext) {
            try (AtomicFileWriter fileWriter = new AtomicFileWriter(file, encoding, saveConfiguration.shouldMakeBackup())) {
                BibWriter bibWriter = new BibWriter(fileWriter, bibDatabaseContext.getDatabase().getNewLineSeparator());
                BibDatabaseWriter databaseWriter = new BibDatabaseWriter(
                        bibWriter,
                        saveConfiguration,
                        preferences.getFieldPreferences(),
                        preferences.getCitationKeyPatternPreferences(),
                        entryTypesManager)
                        .withJournalAbbreviationRepository(
                                journalAbbreviationRepository,
                                preferences.getAbbreviationPreferences().shouldUseFJournalField());

                if (selectedOnly) {
                    databaseWriter.writePartOfDatabase(bibDatabaseContext, libraryTab.getSelectedEntries());
                } else {
                    databaseWriter.writeDatabase(bibDatabaseContext);
                }

                libraryTab.registerUndoableChanges(databaseWriter.getSaveActionsFieldChanges());

                if (fileWriter.hasEncodingProblems()) {
                    saveWithDifferentEncoding(file, selectedOnly, encoding, fileWriter.getEncodingProblems(), saveType, saveOrder);
                }
            } catch (UnsupportedCharsetException ex) {
                throw new SaveException(Localization.lang("Character encoding '%0' is not supported.", encoding.displayName()), ex);
            } catch (IOException ex) {
                throw new SaveException("Problems saving: " + ex, ex);
            }
            return true;
        }
    }

    private void saveWithDifferentEncoding(Path file, boolean selectedOnly, Charset encoding, Set<Character> encodingProblems, BibDatabaseWriter.SaveType saveType, SelfContainedSaveOrder saveOrder) throws SaveException {
        DialogPane pane = new DialogPane();
        VBox vbox = new VBox();
        vbox.getChildren().addAll(
                new Text(Localization.lang("The chosen encoding '%0' could not encode the following characters:", encoding.displayName())),
                new Text(encodingProblems.stream().map(Object::toString).collect(Collectors.joining("."))),
                new Text(Localization.lang("What do you want to do?"))
        );
        pane.setContent(vbox);

        ButtonType tryDifferentEncoding = new ButtonType(Localization.lang("Try different encoding"), ButtonBar.ButtonData.OTHER);
        ButtonType ignore = new ButtonType(Localization.lang("Ignore"), ButtonBar.ButtonData.APPLY);
        boolean saveWithDifferentEncoding = dialogService
                .showCustomDialogAndWait(Localization.lang("Save library"), pane, ignore, tryDifferentEncoding)
                .filter(buttonType -> buttonType.equals(tryDifferentEncoding))
                .isPresent();
        if (saveWithDifferentEncoding) {
            Optional<Charset> newEncoding = dialogService.showChoiceDialogAndWait(
                    Localization.lang("Save library"),
                    Localization.lang("Select new encoding"),
                    Localization.lang("Save library"),
                    encoding,
                    OS.ENCODINGS);
            if (newEncoding.isPresent()) {
                // Make sure to remember which encoding we used.
                libraryTab.getBibDatabaseContext().getMetaData().setEncoding(newEncoding.get(), ChangePropagation.DO_NOT_POST_EVENT);

                saveDatabase(file, selectedOnly, newEncoding.get(), saveType, saveOrder);
            }
        }
    }
}
