package net.sf.jabref.gui.groups;

import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

import javax.swing.JComponent;
import javax.swing.JTable;
import javax.swing.TransferHandler;

import net.sf.jabref.JabRefExecutorService;
import net.sf.jabref.gui.BasePanel;
import net.sf.jabref.gui.JabRefFrame;
import net.sf.jabref.gui.externalfiles.DroppedFileHandler;
import net.sf.jabref.gui.externalfiles.TransferableFileLinkSelection;
import net.sf.jabref.gui.externalfiletype.ExternalFileType;
import net.sf.jabref.gui.externalfiletype.ExternalFileTypes;
import net.sf.jabref.gui.importer.ImportMenuItem;
import net.sf.jabref.gui.importer.actions.OpenDatabaseAction;
import net.sf.jabref.gui.maintable.MainTable;
import net.sf.jabref.gui.net.MonitoredURLDownload;
import net.sf.jabref.logic.util.io.FileUtil;
import net.sf.jabref.pdfimport.PdfImporter;
import net.sf.jabref.pdfimport.PdfImporter.ImportPdfFilesResult;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class EntryTableTransferHandler extends TransferHandler {

    private final MainTable entryTable;

    private final JabRefFrame frame;

    private final BasePanel panel;

    private DataFlavor urlFlavor;

    private final DataFlavor stringFlavor;

    private static final boolean DROP_ALLOWED = true;

    private static final Log LOGGER = LogFactory.getLog(EntryTableTransferHandler.class);

    private boolean draggingFile;


    /**
     * Construct the transfer handler.
     *
     * @param entryTable The table this transfer handler should operate on. This argument is allowed to equal null, in
     *            which case the transfer handler can assume that it works for a JabRef instance with no databases open,
     *            attached to the empty tabbed pane.
     * @param frame The JabRefFrame instance.
     * @param panel The BasePanel this transferhandler works for.
     */
    public EntryTableTransferHandler(MainTable entryTable, JabRefFrame frame, BasePanel panel) {
        this.entryTable = entryTable;
        this.frame = frame;
        this.panel = panel;
        stringFlavor = DataFlavor.stringFlavor;
        try {
            urlFlavor = new DataFlavor("application/x-java-url; class=java.net.URL");
        } catch (ClassNotFoundException e) {
            LOGGER.info("Unable to configure drag and drop for main table", e);
        }
    }

    /**
     * Overridden to indicate which types of drags are supported (only LINK).
     */
    @Override
    public int getSourceActions(JComponent c) {
        return DnDConstants.ACTION_LINK;
    }

    /**
     * This method is called when dragging stuff *from* the table.
     */
    @Override
    public Transferable createTransferable(JComponent c) {
        if (draggingFile) {
            draggingFile = false;
            return new TransferableFileLinkSelection(panel, entryTable.getSelectedEntries());//.getTransferable();
        } else {
            /* so we can assume it will never be called if entryTable==null: */
            return new TransferableEntrySelection(entryTable.getSelectedEntries());
        }
    }

    /**
     * This method is called when stuff is drag to the component.
     *
     * Imports the dropped URL or plain text as a new entry in the current database.
     *
     */
    @Override
    public boolean importData(JComponent comp, Transferable t) {

        // If the drop target is the main table, we want to record which
        // row the item was dropped on, to identify the entry if needed:
        int dropRow = -1;
        if (comp instanceof JTable) {
            dropRow = ((JTable) comp).getSelectedRow();
        }

        try {

            // This flavor is used for dragged file links in Windows:
            if (t.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
                // JOptionPane.showMessageDialog(null, "Received
                // javaFileListFlavor");
                @SuppressWarnings("unchecked")
                List<File> l = (List<File>) t.getTransferData(DataFlavor.javaFileListFlavor);
                return handleDraggedFiles(l, dropRow);
            } else if (t.isDataFlavorSupported(urlFlavor)) {
                URL dropLink = (URL) t.getTransferData(urlFlavor);
                return handleDropTransfer(dropLink);
            } else if (t.isDataFlavorSupported(stringFlavor)) {
                String dropStr = (String) t.getTransferData(stringFlavor);
                LOGGER.debug("Received stringFlavor: " + dropStr);
                return handleDropTransfer(dropStr, dropRow);
            }
        } catch (IOException ioe) {
            LOGGER.error("Failed to read dropped data", ioe);
        } catch (UnsupportedFlavorException | ClassCastException ufe) {
            LOGGER.error("Drop type error", ufe);
        }

        // all supported flavors failed
        LOGGER.info("Can't transfer input: ");
        DataFlavor[] inflavs = t.getTransferDataFlavors();
        for (DataFlavor inflav : inflavs) {
            LOGGER.info("  " + inflav);
        }

        return false;
    }

    /**
     * This method is called to query whether the transfer can be imported.
     *
     * Will return true for urls, strings, javaFileLists
     */
    @Override
    public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) {
        if (!EntryTableTransferHandler.DROP_ALLOWED) {
            return false;
        }

        // accept this if any input flavor matches any of our supported flavors
        for (DataFlavor inflav : transferFlavors) {
            if (inflav.match(urlFlavor) || inflav.match(stringFlavor) || inflav.match(DataFlavor.javaFileListFlavor)) {
                return true;
            }
        }

        // System.out.println("drop type forbidden");
        // nope, never heard of this type
        return false;
    }



    @Override
    public void exportAsDrag(JComponent comp, InputEvent e, int action) {
        if (e instanceof MouseEvent) {
            int columnIndex = entryTable.columnAtPoint(((MouseEvent) e).getPoint());
            int modelIndex = entryTable.getColumnModel().getColumn(columnIndex).getModelIndex();
            if(entryTable.isFileColumn(modelIndex)) {
                LOGGER.info("Dragging file");
                draggingFile = true;
            }
        }
        super.exportAsDrag(comp, e, DnDConstants.ACTION_LINK);
    }

    @Override
    protected void exportDone(JComponent source, Transferable data, int action) {
        // default implementation is OK
        super.exportDone(source, data, action);
    }

    @Override
    public void exportToClipboard(JComponent comp, Clipboard clip, int action) {
        // default implementation is OK
        super.exportToClipboard(comp, clip, action);
    }

    // add-ons -----------------------

    private boolean handleDropTransfer(String dropStr, final int dropRow) throws IOException {
        if (dropStr.startsWith("file:")) {
            // This appears to be a dragged file link and not a reference
            // format. Check if we can map this to a set of files:
            if (handleDraggedFilenames(dropStr, dropRow)) {
                return true;
                // If not, handle it in the normal way...
            }
        } else if (dropStr.startsWith("http:")) {
            // This is the way URL links are received on OS X and KDE (Gnome?):
            URL url = new URL(dropStr);
            // JOptionPane.showMessageDialog(null, "Making URL:
            // "+url.toString());
            return handleDropTransfer(url);
        }
        File tmpfile = File.createTempFile("jabrefimport", "");
        tmpfile.deleteOnExit();
        try (FileWriter fw = new FileWriter(tmpfile)) {
            fw.write(dropStr);
        }

        // System.out.println("importing from " + tmpfile.getAbsolutePath());

        ImportMenuItem importer = new ImportMenuItem(frame, false);
        importer.automatedImport(Collections.singletonList(tmpfile.getAbsolutePath()));

        return true;
    }

    /**
     * Translate a String describing a set of files or URLs dragged into JabRef into a List of File objects, taking care
     * of URL special characters.
     *
     * @param s String describing a set of files or URLs dragged into JabRef
     * @return a List<File> containing the individual file objects.
     *
     */
    public static List<File> getFilesFromDraggedFilesString(String s) {
        // Split into lines:
        String[] lines = s.replace("\r", "").split("\n");
        List<File> files = new ArrayList<>();
        for (String line1 : lines) {
            String line = line1;

            // Try to use url.toURI() to translate URL specific sequences like %20 into
            // standard characters:
            File fl = null;
            try {
                URL url = new URL(line);
                fl = new File(url.toURI());
            } catch (MalformedURLException | URISyntaxException e) {
                LOGGER.warn("Could not get file", e);
            }

            // Unless an exception was thrown, we should have the sanitized path:
            if (fl != null) {
                line = fl.getPath();
            } else if (line.startsWith("file:")) {
                line = line.substring(5);
            } else {
                continue;
            }
            // Under Gnome, the link is given as file:///...., so we
            // need to strip the extra slashes:
            if (line.startsWith("//")) {
                line = line.substring(2);
            }

            File f = new File(line);
            if (f.exists()) {
                files.add(f);
            }
        }
        return files;
    }

    /**
     * Handle a String describing a set of files or URLs dragged into JabRef.
     *
     * @param s String describing a set of files or URLs dragged into JabRef
     * @param dropRow The row in the table where the files were dragged.
     * @return success status for the operation
     *
     */
    private boolean handleDraggedFilenames(String s, final int dropRow) {

        return handleDraggedFiles(EntryTableTransferHandler.getFilesFromDraggedFilesString(s), dropRow);

    }

    /**
     * Handle a List containing File objects for a set of files to import.
     *
     * @param files A List containing File instances pointing to files.
     * @param dropRow @param dropRow The row in the table where the files were dragged.
     * @return success status for the operation
     */
    private boolean handleDraggedFiles(List<File> files, final int dropRow) {
        final List<String> fileNames = new ArrayList<>();
        for (File file : files) {
            fileNames.add(file.getAbsolutePath());
        }
        // Try to load BIB files normally, and import the rest into the current
        // database.
        // This process must be spun off into a background thread:
        JabRefExecutorService.INSTANCE.execute(() -> {
            final ImportPdfFilesResult importRes = new PdfImporter(frame, panel, entryTable, dropRow)
                    .importPdfFiles(fileNames);
            if (!importRes.getNoPdfFiles().isEmpty()) {
                loadOrImportFiles(importRes.getNoPdfFiles(), dropRow);
            }
        });

        return true;
    }

    /**
     * Take a set of filenames. Those with names indicating BIB files are opened as such if possible. All other files we
     * will attempt to import into the current database.
     *
     * @param fileNames The names of the files to open.
     * @param dropRow success status for the operation
     */
    private void loadOrImportFiles(List<String> fileNames, int dropRow) {

        OpenDatabaseAction openAction = new OpenDatabaseAction(frame, false);
        List<String> notBibFiles = new ArrayList<>();
        List<String> bibFiles = new ArrayList<>();
        for (String fileName : fileNames) {
            // Find the file's extension, if any:
            Optional<String> extension = FileUtil.getFileExtension(fileName);
            Optional<ExternalFileType> fileType;

            if (extension.isPresent() && "bib".equals(extension.get())) {
                // we assume that it is a BibTeX file.
                // When a user wants to import something with file extension "bib", but which is not a BibTeX file, he should use "file -> import"
                bibFiles.add(fileName);
                continue;
            }

            fileType = ExternalFileTypes.getInstance().getExternalFileTypeByExt(extension.orElse(""));
            /*
             * This is a linkable file. If the user dropped it on an entry, we
             * should offer options for autolinking to this files:
             *
             * TODO we should offer an option to highlight the row the user is on too.
             */
            if ((fileType.isPresent()) && (dropRow >= 0)) {

                /*
                 * TODO: make this an instance variable?
                 */
                DroppedFileHandler dfh = new DroppedFileHandler(frame, panel);
                dfh.handleDroppedfile(fileName, fileType.get(), entryTable, dropRow);

                continue;
            }
            notBibFiles.add(fileName);
        }

        openAction.openFilesAsStringList(bibFiles, true);

        if (!notBibFiles.isEmpty()) {
            // Import into new if entryTable==null, otherwise into current
            // database:
            ImportMenuItem importer = new ImportMenuItem(frame, entryTable == null);
            importer.automatedImport(notBibFiles);
        }
    }

    private boolean handleDropTransfer(URL dropLink) throws IOException {
        File tmpfile = File.createTempFile("jabrefimport", "");
        tmpfile.deleteOnExit();

        // System.out.println("Import url: " + dropLink.toString());
        // System.out.println("Temp file: "+tmpfile.getAbsolutePath());

        MonitoredURLDownload.buildMonitoredDownload(entryTable, dropLink).downloadToFile(tmpfile);

        // Import into new if entryTable==null, otherwise into current database:
        ImportMenuItem importer = new ImportMenuItem(frame, entryTable == null);
        importer.automatedImport(Collections.singletonList(tmpfile.getAbsolutePath()));

        return true;
    }

}
