/**
 * 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.sparkimpl.plugin.alerts;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.Set;

import javax.swing.*;
import javax.swing.event.HyperlinkEvent;

import org.jivesoftware.resource.Default;
import org.jivesoftware.resource.Res;
import org.jivesoftware.resource.SparkRes;
import org.jivesoftware.smack.StanzaListener;
import org.jivesoftware.smack.filter.StanzaFilter;
import org.jivesoftware.smack.filter.StanzaTypeFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Message.Type;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.packet.StanzaBuilder;
import org.jivesoftware.smackx.delay.packet.DelayInformation;
import org.jivesoftware.smackx.jiveproperties.packet.JivePropertiesExtension;
import org.jivesoftware.spark.ChatManager;
import org.jivesoftware.spark.SparkManager;
import org.jivesoftware.spark.component.RolloverButton;
import org.jivesoftware.spark.component.tabbedPane.SparkTab;
import org.jivesoftware.spark.plugin.ContextMenuListener;
import org.jivesoftware.spark.plugin.Plugin;
import org.jivesoftware.spark.ui.ChatContainer;
import org.jivesoftware.spark.ui.ChatRoom;
import org.jivesoftware.spark.ui.ChatRoomNotFoundException;
import org.jivesoftware.spark.ui.ContactGroup;
import org.jivesoftware.spark.ui.ContactItem;
import org.jivesoftware.spark.ui.ContactList;
import org.jivesoftware.spark.ui.MessageListener;
import org.jivesoftware.spark.ui.SparkTabHandler;
import org.jivesoftware.spark.ui.rooms.ChatRoomImpl;
import org.jivesoftware.spark.ui.status.StatusBar;
import org.jivesoftware.spark.util.BrowserLauncher;
import org.jivesoftware.spark.util.ModelUtil;
import org.jivesoftware.spark.util.ResourceUtils;
import org.jivesoftware.spark.util.log.Log;
import org.jivesoftware.sparkimpl.plugin.manager.Enterprise;
import org.jivesoftware.sparkimpl.settings.local.LocalPreferences;
import org.jivesoftware.sparkimpl.settings.local.SettingsManager;
import org.jxmpp.jid.DomainBareJid;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.jid.parts.Localpart;
import org.jxmpp.jid.parts.Resourcepart;
import org.jxmpp.util.XmppStringUtils;
import org.jivesoftware.spark.ui.BroadcastHistoryFrame;

import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.countMatches;
import static org.jivesoftware.spark.Event.MSG_INCOMING;

/**
 * Handles broadcasts from a server and allows for roster wide broadcasts.
 */
public class BroadcastPlugin extends SparkTabHandler implements Plugin, StanzaListener {

    private final Set<ChatRoom> broadcastRooms = new HashSet<>();

    @Override
    public void initialize() {
        // See if we should disable all "Broadcast" menu items
        if (Default.getBoolean(Default.DISABLE_BROADCAST_MENU_ITEM) || !Enterprise.containsFeature(Enterprise.BROADCAST_FEATURE)) {
            return;
        }

        // Add as ContainerDecorator
        SparkManager.getChatManager().addSparkTabHandler(this);

        StanzaFilter serverFilter = new StanzaTypeFilter(Message.class);
        SparkManager.getConnection().addAsyncStanzaListener(this, serverFilter);

        // Register with action menu
        final JMenu actionsMenu = SparkManager.getMainWindow().getMenuByName(Res.getString("menuitem.actions"));
        JMenuItem broadcastHistoryMenu = new JMenuItem(Res.getString("title.broadcast.history"), SparkRes.getImageIcon(SparkRes.Icon.HISTORY_16x16));
        JMenuItem broadcastMenu = new JMenuItem(Res.getString("title.broadcast.message"), SparkRes.getImageIcon(SparkRes.Icon.MEGAPHONE_16x16));
        ResourceUtils.resButton(broadcastMenu, Res.getString("title.broadcast.message"));
        actionsMenu.add(broadcastHistoryMenu);
        actionsMenu.add(broadcastMenu);

        broadcastMenu.addActionListener(e -> broadcastToRoster());
        broadcastHistoryMenu.addActionListener(e -> BroadcastHistoryFrame.invokeDialog());
        // Register with action menu
        JMenuItem startConversationMenu = new JMenuItem("", SparkRes.getImageIcon(SparkRes.Icon.SMALL_MESSAGE_IMAGE));
        ResourceUtils.resButton(startConversationMenu, Res.getString("menuitem.start.a.chat"));
        if (!Default.getBoolean(Default.HIDE_START_A_CHAT) && Enterprise.containsFeature(Enterprise.START_A_CHAT_FEATURE)) {
            actionsMenu.add(startConversationMenu, 0);
        }
        // Start chat with user entered in dialog
        startConversationMenu.addActionListener(e -> {
            ContactList contactList = SparkManager.getWorkspace().getContactList();
            java.util.List<ContactItem> selectedUsers = contactList.getSelectedUsers();
            String selectedUser = "";
            if (!selectedUsers.isEmpty()) {
                ContactItem contactItem = selectedUsers.get(0);
                selectedUser = contactItem.getJid().toString();
            }

            String jid = (String) JOptionPane.showInputDialog(SparkManager.getMainWindow(), Res.getString("label.enter.address"), Res.getString("title.start.chat"), JOptionPane.QUESTION_MESSAGE, null, null, selectedUser);
            if (ModelUtil.hasLength(jid) && ModelUtil.hasLength(XmppStringUtils.parseDomain(jid))) {
                if (!isBlank(jid) && !jid.contains("@")) {
                    // Append server address
                    jid = jid + "@" + SparkManager.getConnection().getXMPPServiceDomain();
                }
                EntityBareJid entityBareJid = JidCreate.entityBareFromUnescapedOrThrowUnchecked(jid);
                String nickname = SparkManager.getUserManager().getUserNicknameFromJID(entityBareJid);
                ChatRoom chatRoom = SparkManager.getChatManager().createChatRoom(entityBareJid, nickname, nickname);
                SparkManager.getChatManager().getChatContainer().activateChatRoom(chatRoom);
            }
        });

        // Add Send to selected users.
        final ContactList contactList = SparkManager.getWorkspace().getContactList();
        contactList.addContextMenuListener(new ContextMenuListener() {
            @Override
            public void poppingUp(Object component, JPopupMenu popup) {
                if (component instanceof ContactGroup) {
                    final ContactGroup group = (ContactGroup) component;
                    Action broadcastMessageAction = new AbstractAction() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            broadcastToGroup(group);
                        }
                    };

                    broadcastMessageAction.putValue(Action.NAME, Res.getString("menuitem.broadcast.to.group"));
                    broadcastMessageAction.putValue(Action.SMALL_ICON, SparkRes.getImageIcon(SparkRes.Icon.MEGAPHONE_16x16));

                    popup.add(broadcastMessageAction);
                }
            }

            @Override
            public boolean handleDefaultAction(MouseEvent e) {
                return false;
            }
        });

        // Add Broadcast to roaster
        StatusBar statusBar = SparkManager.getWorkspace().getStatusBar();

        RolloverButton broadcastToRosterButton = new RolloverButton(SparkRes.getImageIcon(SparkRes.Icon.MEGAPHONE_16x16));
        broadcastToRosterButton.setToolTipText(Res.getString("message.send.a.broadcast"));

        statusBar.invalidate();
        statusBar.validate();
        statusBar.repaint();

        broadcastToRosterButton.addActionListener(e -> broadcastToRoster());
    }

    @Override
    public void shutdown() {
    }

    @Override
    public boolean canShutDown() {
        return false;
    }

    @Override
    public void processStanza(final Stanza stanza) {
        SwingUtilities.invokeLater(() -> {
            try {
                final Message message = (Message) stanza;
                // Do not handle errors or offline messages
                if (message.hasExtension(DelayInformation.class) || message.getError() != null) {
                    return;
                }

                final JivePropertiesExtension extension = message.getExtension(JivePropertiesExtension.class);
                final boolean broadcast = extension != null && extension.getProperty("broadcast") != null;
                // Shows alert if broadcast or normal/headline message
                if ((broadcast || message.getType() == Type.normal
                    || message.getType() == Type.headline) && message.getBody() != null) {
                    showAlert(message);
                } else {
                    DomainBareJid host = SparkManager.getSessionManager().getServerAddress();
                    Jid from = stanza.getFrom();
                    if (host.equals(from)) {
                        showAlert(message);
                    }
                }
            } catch (Exception e) {
                Log.error(e);
            }
        });
    }

    /**
     * Show Server Alert.
     *
     * @param message the message to show.
     */
    private void showAlert(Message message) {
        Type type = message.getType();
        // Do not show alert if the message is an error.
        if (message.getError() != null) {
            return;
        }
        String body = message.getBody();
        // If the message is blank, then it should not be displayed.
        if (isBlank(body)) {
            return;
        }
        Jid from = message.getFrom();
        if (!from.hasLocalpart()) {
            // if there's no "@" it means the message came from the server
            if (Default.getBoolean(Default.BROADCAST_IN_CHAT_WINDOW)
                || body.length() > 1000 || countMatches(body, '\n') > 20) {
                // If the message has more than 20 linebreaks or is longer than 1000 characters
                // we should broadcast in a normal chat window.
                broadcastInChat(message);
            } else {
                broadcastWithPanel(message);
            }
        } else if (message.getFrom() != null) {
            userToUserBroadcast(message, type, from);
        }
    }

    /**
     * Handles Broadcasts made from a user to another user
     *
     * @param message the message
     * @param type the message type
     * @param from the sender
     */
    private void userToUserBroadcast(Message message, Type type, Jid from) {
        EntityBareJid jid = from.asEntityBareJidOrThrow();
        Resourcepart nickname = SparkManager.getUserManager().getUserNicknameAsResourcepartFromJID(jid);
        ChatManager chatManager = SparkManager.getChatManager();
        ChatContainer container = chatManager.getChatContainer();

        ChatRoomImpl chatRoom;
        try {
            chatRoom = (ChatRoomImpl) container.getChatRoom(jid);
        } catch (ChatRoomNotFoundException e) {
            chatRoom = new ChatRoomImpl(jid, nickname, nickname);
            SparkManager.getChatManager().getChatContainer().addChatRoom(chatRoom);
        }

        Message m = StanzaBuilder.buildMessage()
            .setBody(message.getBody())
            .to(message.getTo())
            .from(message.getFrom())
            .build();

        String broadcastType = type == Message.Type.normal ? Res.getString("broadcast") : Res.getString("message.alert.notify");

        chatRoom.getTranscriptWindow().insertMessage((nickname + " - " + broadcastType), message, ChatManager.FROM_COLOR);
        chatRoom.addToTranscript(m, true);
        chatRoom.increaseUnreadMessageCount();
        broadcastRooms.add(chatRoom);

        showToaster(message, nickname, broadcastType);

        SparkManager.getChatManager().fireGlobalMessageReceivedListeners(chatRoom, message);

        if (message.hasExtension(DelayInformation.class)) {
            SparkManager.getSoundManager().playClip(MSG_INCOMING);
        }

        chatRoom.addMessageListener(new MessageListener() {
            boolean waiting = true;

            @Override
            public void messageReceived(ChatRoom room, Message message) {
                removeAsBroadcast(room);
            }

            @Override
            public void messageSent(ChatRoom room, Message message) {
                removeAsBroadcast(room);
            }

            private void removeAsBroadcast(ChatRoom room) {
                if (waiting) {
                    broadcastRooms.remove(room);
                    // Notify decorators
                    SparkManager.getChatManager().notifySparkTabHandlers(room);
                    waiting = false;
                }
            }
        });
    }

    private static void showToaster(Message message, Resourcepart nickname, String broadcastType) {
        LocalPreferences pref = SettingsManager.getLocalPreferences();
        if (pref.isShowToasterPopup()) {
            SparkToaster toaster = new SparkToaster();
            toaster.setDisplayTime(30000);
            toaster.setBorder(BorderFactory.createLineBorder(Color.lightGray, 1, true));
            toaster.setTitle(nickname + " - " + broadcastType);
            toaster.showToaster(message.getBody());
        }
    }

    /**
     * Broadcasts a message to all in the roster.
     */
    private void broadcastToRoster() {
        final BroadcastDialog broadcastDialog = new BroadcastDialog();
        broadcastDialog.invokeDialog();
    }

    /**
     * Broadcasts a message to all selected users.
     *
     * @param group the Contact Group to send the messages to.
     */
    private void broadcastToGroup(ContactGroup group) {
        final BroadcastDialog broadcastDialog = new BroadcastDialog();
        broadcastDialog.invokeDialog(group);
    }

    @Override
    public void uninstall() {
    }

    @Override
    public boolean isTabHandled(SparkTab tab, Component component, boolean isSelectedTab, boolean chatFrameFocused) {
        if (component instanceof ChatRoom) {
            ChatRoom chatroom = (ChatRoom) component;
            if (broadcastRooms.contains(chatroom)) {
                final ChatRoomImpl room = (ChatRoomImpl) component;
                tab.setIcon(SparkRes.getImageIcon(SparkRes.Icon.INFORMATION_IMAGE));
                String nickname = room.getTabTitle();
                nickname = Res.getString("message.broadcast.from", nickname);
                tab.setTabTitle(nickname);

                if ((!chatFrameFocused || !isSelectedTab) && room.getUnreadMessageCount() > 0) {
                    // Make tab red.
                    tab.setTitleColor(Color.red);
                    tab.setTabBold(true);
                } else {
                    tab.setTitleColor(Color.black);
                    tab.setTabFont(tab.getDefaultFont());
                    room.clearUnreadMessageCount();
                }
                return true;
            }
        }
        return false;
    }

    /**
     * Displays the Serverbroadcast like all other messages in its on
     * chat container with transcript history
     */
    private void broadcastInChat(Message message) {
        String windowtitle = message.getSubject() != null ? message.getSubject() : Res.getString("administrator");
        Localpart serverAlertPart = Localpart.fromOrThrowUnchecked("serveralert");
        EntityBareJid jid = JidCreate.entityBareFrom(serverAlertPart, message.getFrom().asDomainBareJid());
        Resourcepart resourcepart = Resourcepart.fromOrThrowUnchecked(Res.getString("broadcast"));
        ChatRoomImpl chatRoom = new ChatRoomImpl(jid, resourcepart, windowtitle);
        chatRoom.getBottomPanel().setVisible(false);
        chatRoom.hideToolbar();
        SparkManager.getChatManager().getChatContainer().addChatRoom(chatRoom);

        chatRoom.getTranscriptWindow().insertNotificationMessage(message.getBody(), ChatManager.NOTIFICATION_COLOR);
        broadcastRooms.add(chatRoom);
    }

    private String linkCreator(String message) {
        message = message.replace("\n", "<br/>");
        String[] lines = message.split("\\s+");
        StringBuilder html = new StringBuilder(message.length() + 200);
        for (String line : lines) {
            if (line.startsWith("www")) {
                line = "https://" + line;
            }
            if (line.startsWith("https:") || line.startsWith("http:")) {
                line = "<a href='" + line + "'>" + line + "</a>";
            }
            html.append(" ").append(line);
        }
        return html.toString();
    }

    /**
     * Displays a Server Broadcast within a JFrame<br>
     * Messages can contain html-tags
     */
    private void broadcastWithPanel(Message message) {
        String title = Res.getString("message.broadcast.from",
            Res.getString("administrator"));
        final JFrame alert = new JFrame(title);
        alert.setLayout(new GridBagLayout());
        alert.setIconImage(SparkRes.getImageIcon(SparkRes.Icon.MAIN_IMAGE).getImage());
        String mylink = linkCreator(message.getBody());
        JLabel icon = new JLabel(SparkRes.getImageIcon(SparkRes.Icon.ALERT));
        JButton close = new JButton(Res.getString("close"));

        close.addActionListener(new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                alert.setVisible(false);
                alert.dispose();
            }
        });

        String subject = message.getSubject();
        StringBuilder buf = new StringBuilder();
        if (subject != null) {
            buf.append(Res.getString("subject")).append(": ").append(subject);
            buf.append("<br/><br/>");
        }
        buf.append(mylink);

        final JEditorPane textPane = new JEditorPane();
        textPane.setEditable(false);
        textPane.setContentType("text/html");
        textPane.setText(buf.toString());

        textPane.addHyperlinkListener(e -> {
            if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
                try {
                    Log.warning("Open the URL in default browser" + e.getURL());
                    BrowserLauncher.openURL(e.getURL().toURI().toString());
                } catch (URISyntaxException ex) {
                    Log.error(ex);
                }
            }
        });
        JScrollPane scrollPane = new JScrollPane(textPane);
        scrollPane.setBorder(BorderFactory.createEmptyBorder());
        textPane.setCaretPosition(0);

        alert.add(icon, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(5, 5, 5, 5), 0, 0));
        alert.add(scrollPane, new GridBagConstraints(1, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(5, 5, 5, 5), 0, 0));
        alert.add(close, new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(5, 5, 5, 5), 0, 0));

        alert.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        alert.setVisible(true);

        alert.setMinimumSize(new Dimension(340, 200));
        alert.setPreferredSize(new Dimension(340, 200));
        alert.pack();
        Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
        int x = (dim.width - alert.getSize().width) / 2;
        int y = (dim.height - alert.getSize().height) / 2;
        alert.setLocation(x, y);
        alert.toFront();
        alert.requestFocus();
    }
}
