/**
 * Copyright (C) 2004-2011 Jive Software. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jivesoftware.spark.filetransfer;

import java.awt.AWTException;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Desktop;
import java.awt.FileDialog;
import java.awt.Frame;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;

import javax.imageio.ImageIO;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.UIManager;

import org.jivesoftware.MainWindow;
import org.jivesoftware.Spark;
import org.jivesoftware.resource.Default;
import org.jivesoftware.resource.Res;
import org.jivesoftware.resource.SparkRes;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.FromMatchesFilter;
import org.jivesoftware.smack.filter.StanzaTypeFilter;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;
import org.jivesoftware.smackx.filetransfer.FileTransferManager;
import org.jivesoftware.smackx.filetransfer.FileTransferRequest;
import org.jivesoftware.smackx.filetransfer.OutgoingFileTransfer;
import org.jivesoftware.spark.ChatManager;
import org.jivesoftware.spark.PresenceManager;
import org.jivesoftware.spark.SparkManager;
import org.jivesoftware.spark.filetransfer.preferences.FileTransferPreference;
import org.jivesoftware.spark.preference.PreferenceManager;
import org.jivesoftware.spark.ui.*;
import org.jivesoftware.spark.ui.rooms.ChatRoomImpl;
import org.jivesoftware.spark.ui.transctipt.TranscriptWindow;
import org.jivesoftware.spark.util.ResourceUtils;
import org.jivesoftware.spark.util.SwingWorker;
import org.jivesoftware.spark.util.log.Log;
import org.jivesoftware.sparkimpl.plugin.filetransfer.transfer.Downloads;
import org.jivesoftware.sparkimpl.plugin.filetransfer.transfer.ui.ReceiveFileTransfer;
import org.jivesoftware.sparkimpl.plugin.filetransfer.transfer.ui.SendFileTransfer;
import org.jivesoftware.sparkimpl.plugin.filetransfer.transfer.ui.TransferUtils;
import org.jivesoftware.sparkimpl.plugin.manager.Enterprise;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.EntityFullJid;
import org.jxmpp.jid.Jid;

import static org.jivesoftware.spark.ChatManager.ERROR_COLOR;
import static org.jivesoftware.spark.util.BrowserLauncher.openFolder;

/**
 * Responsible for the handling of File Transfer within Spark. You would use the SparkManager
 * for sending of images, files, multiple files and adding your own transfer listeners for plugin work.
 *
 * @author Derek DeMoro
 */
public class SparkTransferManager {
    private final CopyOnWriteArrayList<FileTransferListener> listeners = new CopyOnWriteArrayList<>();
    private File defaultDirectory;

    private static SparkTransferManager singleton;
    private static final Object LOCK = new Object();

    private FileTransferManager transferManager;
    private final Map<EntityBareJid, ArrayList<File>> waitMap = new HashMap<>();
    private BufferedImage bufferedImage;
    private ImageSelectionPanel selectionPanel;
    private Robot robot;

    public static SparkTransferManager getInstance() {
        synchronized (LOCK) {
            if (singleton == null) {
                singleton = new SparkTransferManager();
                return singleton;
            }
        }
        return singleton;
    }

    private SparkTransferManager() {
        // See if we should disable the option to transfer files and images
        if (Default.getBoolean(Default.DISABLE_FILE_TRANSFER) || !Enterprise.containsFeature(Enterprise.FILE_TRANSFER_FEATURE))
            return;
        SparkManager.getConnection().addConnectionListener(new ConnectionListener() {
            @Override
            public void authenticated(XMPPConnection xmppConnection, boolean b) {
                transferManager = FileTransferManager.getInstanceFor(SparkManager.getConnection());
            }
        });
        final ContactList contactList = SparkManager.getWorkspace().getContactList();
        // Create the listener
        transferManager = FileTransferManager.getInstanceFor(SparkManager.getConnection());
        transferManager.addFileTransferListener(request -> handleTransferRequest(request, contactList));
        //SPARK-1869
        InBandBytestreamManager ibbmanager = InBandBytestreamManager.getByteStreamManager(SparkManager.getConnection());
        ibbmanager.setDefaultBlockSize(61_440);
        // Add Send File to Chat Room
        addSendFileButton();

        contactList.addFileDropListener((files, component) -> {
            if (!(component instanceof ContactItem)) {
                return;
            }
            ContactItem item = (ContactItem) component;
            ChatRoom chatRoom = null;
            for (File file : files) {
                chatRoom = sendFile(file, item.getJid());
            }
            if (chatRoom != null) {
                SparkManager.getChatManager().getChatContainer().activateChatRoom(chatRoom);
            }
        });

        try {
            robot = new Robot();
            selectionPanel = new ImageSelectionPanel();
        } catch (AWTException e) {
            Log.error(e);
        }

        // Register Preferences
        PreferenceManager prefManager = SparkManager.getPreferenceManager();
        prefManager.addPreference(new FileTransferPreference());

        final JMenu actionsMenu = SparkManager.getMainWindow().getMenuByName(Res.getString("menuitem.actions"));
        JMenuItem downloadsMenu = new JMenuItem("", SparkRes.getImageIcon(SparkRes.Icon.DOWNLOAD_16x16));
        ResourceUtils.resButton(downloadsMenu, Res.getString("menuitem.view.downloads"));
        actionsMenu.addSeparator();
        actionsMenu.add(downloadsMenu);
        downloadsMenu.addActionListener(e -> openFolder(Downloads.getDownloadDirectory()));

        if (defaultDirectory == null) {
            defaultDirectory = new File(System.getProperty("user.home"));
        }

        addPresenceListener();

//        // Add View Downloads to Command Panel
//        final JPanel commandPanel = SparkManager.getWorkspace().getCommandPanel();
//
//        RolloverButton viewDownloads = new RolloverButton(SparkRes.getImageIcon(SparkRes.Icons.DOWNLOAD_16x16));
//        viewDownloads.setToolTipText(Res.getString("menuitem.view.downloads"));
//        commandPanel.add(viewDownloads);
//        viewDownloads.addActionListener(new ActionListener() {
//            public void actionPerformed(ActionEvent e) {              
//                BrowserLauncher.openFolder(Downloads.getDownloadDirectory());
//            }
//        });
    }


    private void handleTransferRequest(FileTransferRequest request, ContactList contactList) {
        // Check if a listener handled this request
        if (fireTransferListeners(request)) {
            return;
        }
        Jid requestor = request.getRequestor();
        EntityBareJid bareJID = requestor.asEntityBareJidOrThrow();
        String fileName = request.getFileName();

        ContactItem contactItem = contactList.getContactItemByJID(bareJID);
        String chatName = contactItem != null ? contactItem.getDisplayName() : bareJID.asUnescapedString();
        ChatRoom chatRoom = SparkManager.getChatManager().createChatRoom(bareJID, chatName, chatName);

        TranscriptWindow transcriptWindow = chatRoom.getTranscriptWindow();
        transcriptWindow.insertNotificationMessage(Res.getString("message.file.transfer.chat.window"), Color.BLACK);

        final ReceiveFileTransfer receivingMessageUI = new ReceiveFileTransfer(chatRoom);
        receivingMessageUI.acceptFileTransfer(request);
        chatRoom.addClosingListener(receivingMessageUI::cancelTransfer);
        transcriptWindow.addComponent(receivingMessageUI);
        chatRoom.increaseUnreadMessageCount();
        chatRoom.scrollToBottom();
        String fileTransMsg = chatName + " " + Res.getString("message.file.transfer.short.message") + " " + fileName;
        SparkManager.getChatManager().getChatContainer().fireNotifyOnMessage(chatRoom, true, fileTransMsg, Res.getString("message.file.transfer.notification"));
    }

    public void sendFileTo(ContactItem item) {
        FileDialog fileChooser = getFileChooser(SparkManager.getMainWindow(), Res.getString("title.select.file.to.send"));
        if (defaultDirectory != null) {
            fileChooser.setDirectory(defaultDirectory.getAbsolutePath());
        }
        fileChooser.setVisible(true);
        final File[] files = fileChooser.getFiles();
        if (files.length == 0) {
            // no selection
            return;
        }
        File file = files[0]; // Single-file selection is used. Using the first array item is safe.
        if (file.exists()) {
            defaultDirectory = file.getParentFile();
            sendFile(file, item.getJid());
        }
    }

    private void addSendFileButton() {
        final ChatManager chatManager = SparkManager.getChatManager();
        chatManager.addChatRoomListener(new ChatRoomListener() {
            @Override
            public void chatRoomOpened(final ChatRoom room) {
                if (!(room instanceof ChatRoomImpl)) {
                    return;
                }
                // Otherwise,
                new ChatRoomTransferDecorator(room);
            }
        });
    }

    public void sendScreenshot(final ChatRoomButton button, final ChatRoom room) {
        button.setEnabled(false);
        final MainWindow mainWindow = SparkManager.getMainWindow();
        final ChatFrame chatFrame = SparkManager.getChatManager().getChatContainer().getChatFrame();
        final boolean mainWindowVisible = mainWindow.isVisible();
        final boolean chatFrameVisible = chatFrame.isVisible();
        if (mainWindowVisible) {
            mainWindow.setVisible(false);
        }
        if (chatFrameVisible) {
            chatFrame.setVisible(false);
        }

        final SwingWorker worker = new SwingWorker() {
            @Override
            public Object construct() {
                try {
                    Thread.sleep(1000);
                    Rectangle area = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
                    return robot.createScreenCapture(area);
                } catch (Throwable e) {
                    Log.error(e);
                    if (mainWindowVisible) {
                        mainWindow.setVisible(true);
                    }
                    if (chatFrameVisible) {
                        chatFrame.setVisible(true);
                    }
                }
                return null;
            }

            @Override
            public void finished() {
                bufferedImage = (BufferedImage) get();
                if (bufferedImage == null) {
                    JOptionPane.showMessageDialog(null, Res.getString("title.error"), "Unable to process screenshot.", JOptionPane.ERROR_MESSAGE);
                    return;
                }

                final Frame frame = new Frame();
                frame.setCursor(new Cursor(Cursor.CROSSHAIR_CURSOR));

                selectionPanel.setImage(bufferedImage);
                selectionPanel.validate();
                selectionPanel.addMouseListener(new MouseAdapter() {
                    @Override
                    public void mouseReleased(MouseEvent e) {
                        Rectangle clip = selectionPanel.getClip();
                        BufferedImage newImage = null;
                        try {
                            newImage = bufferedImage.getSubimage((int) clip.getX(), (int) clip.getY(), (int) clip.getWidth(), (int) clip.getHeight());
                        } catch (Exception ignored) {
                        }
                        if (newImage != null) {
                            sendImage(newImage, room);
                            bufferedImage = null;
                            selectionPanel.clear();
                        }

                        frame.dispose();
                        frame.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));

                        if (mainWindowVisible) {
                            mainWindow.setVisible(true);
                        }
                        if (chatFrameVisible) {
                            chatFrame.setVisible(true);
                        }
                        selectionPanel.removeMouseListener(this);
                    }
                });

                frame.addKeyListener(new KeyAdapter() {
                    @Override
                    public void keyReleased(KeyEvent e) {
                        if (e.getKeyChar() == KeyEvent.VK_ESCAPE) {
                            frame.dispose();
                            frame.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
                            if (mainWindowVisible) {
                                mainWindow.setVisible(true);
                            }
                            if (chatFrameVisible) {
                                chatFrame.setVisible(true);
                            }
                        }
                    }
                });
                frame.setSize(bufferedImage.getWidth(null), bufferedImage.getHeight());
                frame.add(selectionPanel);
                frame.setUndecorated(true);
                // Determine if full-screen mode is supported directly
                GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
                GraphicsDevice gs = ge.getDefaultScreenDevice();
                if (gs.isFullScreenSupported()) {
                    gs.setFullScreenWindow(frame);
                } else {
                    // Full-screen mode will be simulated
                    frame.setVisible(true);
                }
                button.setEnabled(true);
            }
        };
        worker.start();
    }

    private void addPresenceListener() {
        SparkManager.getConnection().addAsyncStanzaListener(stanza -> {
            Presence presence = (Presence) stanza;
            if (presence.isAvailable()) {
                EntityBareJid bareJID = presence.getFrom().asEntityBareJidIfPossible();
                // Iterate through map.
                ArrayList<File> list = waitMap.get(bareJID);
                if (list != null) {
                    // Iterate through list and send.
                    ChatRoom room = null;
                    for (File file : list) {
                        room = sendFile(file, bareJID);
                    }
                    if (room != null) {
                        room.sendMessage(Res.getString("message.sent.offline.files"));
                    }
                }
                waitMap.remove(bareJID);
            }
        }, new StanzaTypeFilter(Presence.class));
    }

    /**
     * Send a file to a user.
     *
     * @param file the file to send.
     * @param jid  the jid of the user to send the file to.
     * @return the ChatRoom of the user.
     */
    public ChatRoom sendFile(File file, Jid jid) {
        long maxSize = Long.parseLong(Default.getString(Default.FILE_TRANSFER_MAXIMUM_SIZE));
        long warningSize = Long.parseLong(Default.getString(Default.FILE_TRANSFER_WARNING_SIZE));
        if (file.length() >= maxSize && maxSize != -1) {
            String maxsizeString = TransferUtils.getAppropriateByteWithSuffix(maxSize);
            String yoursizeString = TransferUtils.getAppropriateByteWithSuffix(file.length());
            String output = Res.getString("message.file.transfer.file.too.big.error", maxsizeString, yoursizeString);
            JOptionPane.showMessageDialog(null, output, Res.getString("title.error"), JOptionPane.ERROR_MESSAGE);
            return null;
        }

        if (file.length() >= warningSize && warningSize != -1) {
            int result = JOptionPane.showConfirmDialog(null, Res.getString("message.file.transfer.file.too.big.warning"), Res.getString("title.error"), JOptionPane.YES_NO_OPTION);
            if (result != 0) {
                return null;
            }
        }

        final ContactList contactList = SparkManager.getWorkspace().getContactList();
        EntityBareJid bareJid = jid.asEntityBareJidIfPossible();
        if (!PresenceManager.isOnline(bareJid)) {
            ArrayList<File> list = waitMap.get(bareJid);
            if (list == null) {
                list = new ArrayList<>();
            }
            list.add(file);
            waitMap.put(bareJid, list);

            ContactItem contactItem = contactList.getContactItemByJID(bareJid);
            String chatName = contactItem != null ? contactItem.getDisplayName() : bareJid.asUnescapedString();
            ChatRoom chatRoom = SparkManager.getChatManager().createChatRoom(bareJid, chatName, chatName);
            chatRoom.getTranscriptWindow().insertNotificationMessage(Res.getString("message.user.offline.autosend", file.getName()), ERROR_COLOR);
            return null;
        }

        // Create the outgoing file transfer
        EntityFullJid fullJID = PresenceManager.getFullyQualifiedJID(bareJid);
        final OutgoingFileTransfer transfer = transferManager.createOutgoingFileTransfer(fullJID);

        ContactItem contactItem = contactList.getContactItemByJID(bareJid);
        String chatName = contactItem != null ? contactItem.getDisplayName() : bareJid.asUnescapedString();
        ChatRoom chatRoom = SparkManager.getChatManager().createChatRoom(bareJid, chatName, chatName);
        TranscriptWindow transcriptWindow = chatRoom.getTranscriptWindow();
        SendFileTransfer sendingUI = new SendFileTransfer(chatRoom);
        try {
            transfer.sendFile(file, "Sending file");
        } catch (SmackException e) {
            Log.error(e);
        }

        // Add listener to cancel transfer is sending file to user who just went offline.
        AndFilter presenceFilter = new AndFilter(new StanzaTypeFilter(Presence.class), FromMatchesFilter.createBare(bareJid));
        final StanzaListener packetListener = stanza -> {
            Presence presence = (Presence) stanza;
            if (!presence.isAvailable()) {
                transfer.cancel();
            }
        };

        // Add presence listener to check if user is offline and cancel sending.
        SparkManager.getConnection().addAsyncStanzaListener(packetListener, presenceFilter);
        chatRoom.addClosingListener(() -> {
            SparkManager.getConnection().removeAsyncStanzaListener(packetListener);
            if (!transfer.isDone()) {
                transfer.cancel();
            }
        });

        try {
            sendingUI.sendFile(transfer, transferManager, fullJID, chatName);
        } catch (NullPointerException e) {
            Log.error(e);
        }

        transcriptWindow.addComponent(sendingUI);

        chatRoom.scrollToBottom();
        return chatRoom;
    }

    /**
     * Send an image to a user.
     *
     * @param image the image to send.
     * @param room  the ChatRoom of the user you wish to send the image to.
     */
    public void sendImage(final BufferedImage image, final ChatRoom room) {
        File tmpDirectory = new File(Spark.getCacheDir(), "/tempImages");
        //noinspection ResultOfMethodCallIgnored
        tmpDirectory.mkdirs();

        String imageName = "image_" + StringUtils.randomString(2) + ".png";
        final File imageFile = new File(tmpDirectory, imageName);
        // Write image to system.
        room.setCursor(new Cursor(Cursor.WAIT_CURSOR));

        SwingWorker writeImageThread = new SwingWorker() {
            @Override
            public Object construct() {
                try {
                    // Write out file in separate thread.
                    ImageIO.write(image, "png", imageFile);
                } catch (IOException e) {
                    Log.error(e);
                }
                return true;
            }

            @Override
            public void finished() {
                ChatRoomImpl roomImpl = (ChatRoomImpl) room;
                sendFile(imageFile, roomImpl.getParticipantJID());
                SparkManager.getChatManager().getChatContainer().activateChatRoom(room);
                room.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
            }
        };
        writeImageThread.start();
    }

    /**
     * Returns an image if one is found in the clipboard, otherwise null is returned.
     */
    public static BufferedImage getClipboard() {
        Transferable t = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null);
        try {
            if (t != null && t.isDataFlavorSupported(DataFlavor.imageFlavor)) {
                return (BufferedImage) t.getTransferData(DataFlavor.imageFlavor);
            }
        } catch (UnsupportedFlavorException | IOException e) {
            // Nothing to do
        }
        return null;
    }

    /**
     * Adds a new TransferListener to the SparkManager. FileTransferListeners can be used
     * to intercept incoming file transfers for own customizations. You may wish to not
     * allow certain file transfers, or have your own UI to handle incoming files.
     */
    public void addTransferListener(FileTransferListener listener) {
        listeners.addIfAbsent(listener);
    }

    /**
     * Removes the FileTransferListener.
     */
    public void removeTransferListener(FileTransferListener listener) {
        listeners.remove(listener);
    }

    private boolean fireTransferListeners(FileTransferRequest request) {
        for (FileTransferListener listener : listeners) {
            boolean accepted = listener.handleTransfer(request);
            if (accepted) {
                return true;
            }
        }
        return false;
    }

    /**
     * Sets the current default directory to store files.
     *
     * @param directory the default directory.
     */
    public void setDefaultDirectory(File directory) {
        if (directory == null) {
            defaultDirectory = null;
        } else if (directory.isDirectory()) {
            defaultDirectory = directory;
        } else {
            File parent = defaultDirectory.getParentFile();
            if (parent != null) {
                defaultDirectory = parent;
            }
        }
    }

    /**
     * Returns the current default directory to store files.
     *
     * @return the default directory.
     */
    public File getDefaultDirectory() {
        return defaultDirectory;
    }

    /**
     * Return the File Chooser to user.
     * @param parent the parent component.
     * @param title the title.
     * @return the FileChooser. (Native Widget)
     */
    public FileDialog getFileChooser(Frame parent, String title) {
        return new FileDialog(parent, title, FileDialog.LOAD);
    }
}
