package org.jabref.gui.preferences.ocr;

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

import javafx.beans.property.ListProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;

import org.jabref.gui.DialogService;
import org.jabref.gui.preferences.PreferenceTabViewModel;
import org.jabref.gui.util.FileDialogConfiguration;
import org.jabref.logic.FilePreferences;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.ocr.EngineSelection;
import org.jabref.logic.ocr.OcrPreferences;
import org.jabref.logic.ocr.PagesWithTextHandling;
import org.jabref.logic.util.BackgroundTask;
import org.jabref.logic.util.HeadlessExecutorService;
import org.jabref.logic.util.StreamGobbler;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.logic.util.strings.StringUtil;

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

public class OcrTabViewModel implements PreferenceTabViewModel {
    private static final Logger LOGGER = LoggerFactory.getLogger(OcrTabViewModel.class);
    private static final int CHECKING_TIMEOUT = 10;
    private static final List<String> DEFAULT_OCRMYPDF_PATHS = List.of(
            "ocrmypdf",
            "python -m ocrmypdf",
            "py -m ocrmypdf",
            "python3 -m ocrmypdf"
    );
    private static final List<String> DEFAULT_DOCLING_PATHS = List.of(
            "docling"
    );
    private final ObjectProperty<EngineSelection> selectedEngine = new SimpleObjectProperty<>(EngineSelection.OCRMYPDF);
    private final ListProperty<EngineSelection> engineOptions =
            new SimpleListProperty<>(FXCollections.observableArrayList(EngineSelection.values()));
    private final StringProperty ocrEnginePath = new SimpleStringProperty();
    private final ObjectProperty<PagesWithTextHandling> selectedPagesHaveText = new SimpleObjectProperty<>(PagesWithTextHandling.SKIP);
    private final ListProperty<PagesWithTextHandling> pagesHaveTextOptions =
            new SimpleListProperty<>(FXCollections.observableArrayList(PagesWithTextHandling.values()));

    private final DialogService dialogService;
    private final FilePreferences filePreferences;
    private final OcrPreferences ocrPreferences;
    private final TaskExecutor taskExecutor;

    private boolean isInitializing = true;

    public OcrTabViewModel(DialogService dialogService,
                           FilePreferences filePreferences,
                           OcrPreferences ocrPreferences,
                           TaskExecutor taskExecutor) {
        this.dialogService = dialogService;
        this.filePreferences = filePreferences;
        this.ocrPreferences = ocrPreferences;
        this.taskExecutor = taskExecutor;

        selectedEngine.addListener((_, oldValue, newValue) -> {
            if (isInitializing || oldValue == newValue) {
                return;
            }
            autoDetectEnginePath();
        });
    }

    @Override
    public void setValues() {
        isInitializing = true;
        selectedEngine.setValue(ocrPreferences.getEngineSelection());
        ocrEnginePath.setValue(ocrPreferences.getOcrEnginePath());
        selectedPagesHaveText.setValue(ocrPreferences.getPagesHaveText());
        isInitializing = false;
    }

    @Override
    public void storeSettings() {
        ocrPreferences.setEngineSelection(selectedEngine.getValue());
        ocrPreferences.setOcrEnginePath(ocrEnginePath.getValue());
        ocrPreferences.setPagesHaveText(selectedPagesHaveText.getValue());
    }

    public StringProperty ocrEnginePathProperty() {
        return ocrEnginePath;
    }

    public ObjectProperty<EngineSelection> selectedEngineProperty() {
        return selectedEngine;
    }

    public ReadOnlyListProperty<EngineSelection> engineOptions() {
        return engineOptions;
    }

    public ObjectProperty<PagesWithTextHandling> selectedPagesHaveTextProperty() {
        return selectedPagesHaveText;
    }

    public ReadOnlyListProperty<PagesWithTextHandling> pagesHaveTextOptions() {
        return pagesHaveTextOptions;
    }

    public void browseEnginePath() {
        Optional<Path> selectedPath = dialogService.showFileOpenDialog(
                new FileDialogConfiguration.Builder()
                        .withInitialDirectory(filePreferences.getWorkingDirectory())
                        .build());
        selectedPath.ifPresent(path -> ocrEnginePathProperty().set(path.toString()));
    }

    public Optional<String> autoDetectDefaultEnginePath(EngineSelection engineToDetect) {
        if (engineToDetect == EngineSelection.OCRMYPDF) {
            return DEFAULT_OCRMYPDF_PATHS.stream()
                                         .filter(this::enginePathExists)
                                         .findFirst();
        }
        return DEFAULT_DOCLING_PATHS.stream()
                                    .filter(this::enginePathExists)
                                    .findFirst();
    }

    public void autoDetectEnginePath() {
        EngineSelection selectionAtStart = selectedEngine.get();
        BackgroundTask<Optional<String>> autoDetectTask =
                BackgroundTask.wrap(() -> autoDetectDefaultEnginePath(selectionAtStart));
        String engineSelectionName = selectionAtStart.getDisplayName();

        autoDetectTask.titleProperty().set(Localization.lang("Auto detection of %0 path", engineSelectionName));
        autoDetectTask.showToUser(true);
        autoDetectTask.onSuccess(result -> {
            if (selectedEngine.get() != selectionAtStart) {
                return;
            }
            if (result.isPresent()) {
                String path = result.get();
                ocrEnginePathProperty().set(path);
                dialogService.notify(Localization.lang("%0 detected at: %1", engineSelectionName, path));
            } else {
                dialogService.notify(Localization.lang("%0 could not be detected automatically", engineSelectionName));
            }
        });
        autoDetectTask.onFailure(_ -> {
            if (selectedEngine.get() != selectionAtStart) {
                return;
            }
            dialogService.notify(Localization.lang("Auto detection of %0 path failed", engineSelectionName));
        });
        taskExecutor.execute(autoDetectTask);
    }

    private boolean enginePathExists(String path) {
        ArrayList<String> command = StringUtil.splitRespectingEscapedWhitespace(path);
        command.add("--version");
        try {
            ProcessBuilder processBuilder = new ProcessBuilder(command);
            processBuilder.redirectErrorStream(true);
            Process process = processBuilder.start();

            // Get the output and the errors of the process
            StreamGobbler streamGobblerInput = new StreamGobbler(process.getInputStream(), LOGGER::debug);
            HeadlessExecutorService.INSTANCE.execute(streamGobblerInput);

            boolean finished = process.waitFor(CHECKING_TIMEOUT, TimeUnit.SECONDS);
            if (!finished) {
                process.destroyForcibly();
                LOGGER.debug("Auto-detecting {} as path timed out", path);
                return false;
            }
            return process.exitValue() == 0;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            LOGGER.debug("Auto detection of {} as engine's path was interrupted", path, e);
            return false;
        } catch (IOException e) {
            LOGGER.debug("{} is not available as engine's path: IOException occurred", path, e);
            return false;
        }
    }
}
