package org.jabref.gui.clipboard;

import java.awt.Toolkit;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.util.List;
import java.util.Optional;

import javafx.application.Platform;
import javafx.scene.control.TextInputControl;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;
import javafx.scene.input.MouseButton;

import org.jabref.architecture.AllowedToUseAwt;
import org.jabref.gui.StateManager;
import org.jabref.logic.bibtex.BibEntryWriter;
import org.jabref.logic.bibtex.FieldWriter;
import org.jabref.logic.importer.util.MediaTypes;
import org.jabref.logic.os.OS;
import org.jabref.logic.preferences.CliPreferences;
import org.jabref.model.TransferInformation;
import org.jabref.model.TransferMode;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.database.BibDatabaseMode;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.BibEntryTypesManager;
import org.jabref.model.entry.BibtexString;

import com.airhacks.afterburner.injection.Injector;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@AllowedToUseAwt("Requires java.awt.datatransfer.Clipboard")
public class ClipBoardManager {
    private static final Logger LOGGER = LoggerFactory.getLogger(ClipBoardManager.class);

    private static final DataFormat DATAFORMAT_JABREF_DATA = new DataFormat(MediaTypes.APPLICATION_JABREF_DATA);

    private static Clipboard clipboard;

    private static java.awt.datatransfer.Clipboard primary;

    private final StateManager stateManager;

    public ClipBoardManager(StateManager stateManager) {
        this(stateManager, Clipboard.getSystemClipboard(), Toolkit.getDefaultToolkit().getSystemSelection());
    }

    public ClipBoardManager(StateManager stateManager, Clipboard clipboard, java.awt.datatransfer.Clipboard primary) {
        ClipBoardManager.clipboard = clipboard;
        ClipBoardManager.primary = primary;
        this.stateManager = stateManager;
    }

    /// Add X11 clipboard support to a text input control. It is necessary to call this method in every input where you
    /// want to use it: `ClipBoardManager.addX11Support(TextInputControl input);`.
    ///
    /// @param input the TextInputControl (e.g., TextField, TextArea, and children) where adding this functionality.
    /// @see <a href="https://www.uninformativ.de/blog/postings/2017-04-02/0/POSTING-en.html">Short summary for X11
    /// clipboards</a>
    /// @see <a href="https://unix.stackexchange.com/questions/139191/whats-the-difference-between-primary-selection-and-clipboard-buffer/139193#139193">Longer
    /// text over clipboards</a>
    public static void addX11Support(TextInputControl input) {
        input.selectedTextProperty().addListener(
                // using InvalidationListener because of https://bugs.openjdk.java.net/browse/JDK-8176270
                observable -> Platform.runLater(() -> {
                    String newValue = input.getSelectedText();
                    if (!newValue.isEmpty() && (primary != null)) {
                        primary.setContents(new StringSelection(newValue), null);
                    }
                }));
        input.setOnMouseClicked(event -> {
            if (event.getButton() == MouseButton.MIDDLE) {
                input.insertText(input.getCaretPosition(), getContentsPrimary());
            }
        });
    }

    /// Get the String residing on the system clipboard.
    ///
    /// @return any text found on the Clipboard; if none found, return an empty String.
    public static @NonNull String getContents() {
        String result = clipboard.getString();
        if (result == null) {
            return "";
        }
        return result;
    }

    public @Nullable TransferInformation getJabRefClipboardTransferData() {
        return Optional.ofNullable(clipboard.getContent(DATAFORMAT_JABREF_DATA))
                       .filter(String.class::isInstance)
                       .map(String.class::cast)
                       .map(JabRefClipboardData::fromJson)
                       .flatMap(data -> data.toTransferInformation(stateManager))
                       .orElse(null);
    }

    public static @NonNull String getHtmlContents() {
        String result = clipboard.getHtml();
        if (result == null) {
            return "";
        }
        return result;
    }

    public static boolean hasHtml() {
        return clipboard.hasHtml();
    }

    /// Get the String residing on the primary clipboard (if it exists).
    ///
    /// @return any text found on the primary Clipboard; if none found, try with the system clipboard.
    public static @NonNull String getContentsPrimary() {
        if (primary != null) {
            Transferable contents = primary.getContents(null);
            if ((contents != null) && contents.isDataFlavorSupported(DataFlavor.stringFlavor)) {
                try {
                    return (String) contents.getTransferData(DataFlavor.stringFlavor);
                } catch (UnsupportedFlavorException | IOException e) {
                    LOGGER.warn("Could not get transfer data", e);
                }
            }
        }
        return getContents();
    }

    /// Puts content onto the system clipboard.
    ///
    /// @param content the ClipboardContent to set as current value of the system clipboard.
    public void setContent(ClipboardContent content) {
        clipboard.setContent(content);
        setPrimaryClipboardContent(content);
    }

    /// Puts content onto the primary clipboard (if it exists).
    ///
    /// @param content the ClipboardContent to set as current value of the primary clipboard.
    public void setPrimaryClipboardContent(ClipboardContent content) {
        if (primary != null) {
            primary.setContents(new StringSelection(content.getString()), null);
        }
    }

    public void setHtmlContent(String html, String fallbackPlain) {
        final ClipboardContent content = new ClipboardContent();
        content.putHtml(html);
        content.putString(fallbackPlain);
        clipboard.setContent(content);
        setPrimaryClipboardContent(content);
    }

    public void setContent(String string) {
        setContent(string, null);
    }

    private void setContent(String string, @Nullable JabRefClipboardData jabRefClipboardData) {
        ClipboardContent content = new ClipboardContent();
        content.putString(string);
        if (jabRefClipboardData != null) {
            content.put(DATAFORMAT_JABREF_DATA, jabRefClipboardData.asJson());
        }
        clipboard.setContent(content);
        setPrimaryClipboardContent(content);
    }

    public void setContent(TransferMode transferMode, BibDatabaseContext bibDatabaseContext, List<BibEntry> entries, BibEntryTypesManager entryTypesManager, List<BibtexString> stringConstants) throws IOException {
        StringBuilder builder = new StringBuilder();
        if (!stringConstants.isEmpty()) {
            stringConstants.forEach(strConst -> {
                builder.append(strConst.getParsedSerialization() == null ? "" : strConst.getParsedSerialization());
                builder.append(OS.NEWLINE);
            });
            builder.append(OS.NEWLINE);
        }
        String serializedEntries = serializeEntries(entries, entryTypesManager);
        builder.append(serializedEntries);
        JabRefClipboardData jabRefClipboardData = new JabRefClipboardData(bibDatabaseContext.getUid(), transferMode);
        setContent(builder.toString(), jabRefClipboardData);
    }

    private String serializeEntries(List<BibEntry> entries, BibEntryTypesManager entryTypesManager) throws IOException {
        CliPreferences preferences = Injector.instantiateModelOrService(CliPreferences.class);
        // BibEntry is not Java serializable. Thus, we need to do the serialization manually
        // At reading of the clipboard in JabRef, we parse the plain string in all cases, so we don't need to flag we put BibEntries here
        // Furthermore, storing a string also enables other applications to work with the data
        BibEntryWriter writer = new BibEntryWriter(new FieldWriter(preferences.getFieldPreferences()), entryTypesManager);
        return writer.write(entries, BibDatabaseMode.BIBTEX);
    }
}
