/**
 * Copyright (c) 2013, Redsolution LTD. All rights reserved.
 *
 * This file is part of Xabber project; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License, Version 3.
 *
 * Xabber is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License,
 * along with this program. If not, see http://www.gnu.org/licenses/.
 */
package com.xabber.android.data.message;

import android.database.Cursor;
import android.os.Environment;

import com.xabber.android.R;
import com.xabber.android.data.Application;
import com.xabber.android.data.NetworkException;
import com.xabber.android.data.OnLoadListener;
import com.xabber.android.data.SettingsManager;
import com.xabber.android.data.SettingsManager.ChatsShowStatusChange;
import com.xabber.android.data.account.AccountItem;
import com.xabber.android.data.account.AccountManager;
import com.xabber.android.data.account.ArchiveMode;
import com.xabber.android.data.account.OnAccountArchiveModeChangedListener;
import com.xabber.android.data.account.OnAccountDisabledListener;
import com.xabber.android.data.account.OnAccountRemovedListener;
import com.xabber.android.data.account.StatusMode;
import com.xabber.android.data.connection.ConnectionItem;
import com.xabber.android.data.connection.OnDisconnectListener;
import com.xabber.android.data.connection.OnPacketListener;
import com.xabber.android.data.entity.BaseEntity;
import com.xabber.android.data.entity.NestedMap;
import com.xabber.android.data.extension.archive.MessageArchiveManager;
import com.xabber.android.data.extension.muc.RoomChat;
import com.xabber.android.data.roster.OnRosterReceivedListener;
import com.xabber.android.data.roster.OnStatusChangeListener;
import com.xabber.android.data.roster.RosterManager;
import com.xabber.android.utils.StringUtils;
import com.xabber.xmpp.address.Jid;
import com.xabber.xmpp.carbon.CarbonManager.Direction;
import com.xabber.xmpp.delay.Delay;

import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smackx.muc.packet.MUCUser;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

/**
 * Manage chats and its messages.
 * <p/>
 * Warning: message processing using chat instances should be changed.
 *
 * @author alexander.ivanov
 */
public class MessageManager implements OnLoadListener, OnPacketListener, OnDisconnectListener,
        OnAccountRemovedListener, OnAccountDisabledListener, OnRosterReceivedListener,
        OnAccountArchiveModeChangedListener, OnStatusChangeListener {

    private final static MessageManager instance;

    static {
        instance = new MessageManager();
        Application.getInstance().addManager(instance);
    }

    /**
     * Registered chats for bareAddresses in accounts.
     */
    private final NestedMap<AbstractChat> chats;
    /**
     * Visible chat.
     * <p/>
     * Will be <code>null</code> if there is no one.
     */
    private AbstractChat visibleChat;

    private MessageManager() {
        chats = new NestedMap<>();
    }

    public static MessageManager getInstance() {
        return instance;
    }

    /**
     * @param messageItems
     * @param clearId      Whether message id must be set to the <code>null</code>.
     * @return Collection with ids for specified messages.
     */
    static Collection<Long> getMessageIds(Collection<MessageItem> messageItems, boolean clearId) {
        ArrayList<Long> ids = new ArrayList<>();
        for (MessageItem messageItem : messageItems) {
            Long id = messageItem.getId();
            if (id == null) {
                continue;
            }
            ids.add(id);
            if (clearId) {
                messageItem.setId(null);
            }
        }
        return ids;
    }

    @Override
    public void onLoad() {
        final Set<BaseEntity> loadChats = new HashSet<BaseEntity>();
        Cursor cursor;
        cursor = MessageTable.getInstance().messagesToSend();
        try {
            if (cursor.moveToFirst()) {
                do {
                    loadChats.add(new BaseEntity(MessageTable.getAccount(cursor), MessageTable.getUser(cursor)));
                } while (cursor.moveToNext());
            }
        } finally {
            cursor.close();
        }
        Application.getInstance().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                onLoaded(loadChats);
            }
        });
    }

    private void onLoaded(Set<BaseEntity> loadChats) {
        for (BaseEntity baseEntity : loadChats) {
            if (getChat(baseEntity.getAccount(), Jid.getBareAddress(baseEntity.getUser())) == null) {
                createChat(baseEntity.getAccount(), baseEntity.getUser());
            }
        }
    }

    /**
     * @param account
     * @param user
     * @return <code>null</code> if there is no such chat.
     */
    public AbstractChat getChat(String account, String user) {
        return chats.get(account, user);
    }

    public Collection<AbstractChat> getChats() {
        return Collections.unmodifiableCollection(chats.values());
    }

    /**
     * Creates and adds new regular chat to be managed.
     *
     * @param account
     * @param user
     * @return
     */
    private RegularChat createChat(String account, String user) {
        RegularChat chat = new RegularChat(account, Jid.getBareAddress(user));
        addChat(chat);
        return chat;
    }

    /**
     * Adds chat to be managed.
     *
     * @param chat
     */
    public void addChat(AbstractChat chat) {
        if (getChat(chat.getAccount(), chat.getUser()) != null) {
            throw new IllegalStateException();
        }
        chats.put(chat.getAccount(), chat.getUser(), chat);
    }

    /**
     * Removes chat from managed.
     *
     * @param chat
     */
    public void removeChat(AbstractChat chat) {
        chats.remove(chat.getAccount(), chat.getUser());
    }

    /**
     * Sends message. Creates and registers new chat if necessary.
     *
     * @param account
     * @param user
     * @param text
     */
    public void sendMessage(String account, String user, String text) {
        AbstractChat chat = getChat(account, user);
        if (chat == null) {
            chat = createChat(account, user);
        }
        MessageItem messageItem = chat.newMessage(text);
        chat.sendQueue(messageItem);
    }

    /**
     * @param account
     * @param user
     * @return Where there is active chat.
     */
    public boolean hasActiveChat(String account, String user) {
        AbstractChat chat = getChat(account, user);
        return chat != null && chat.isActive();
    }

    /**
     * @return Collection with active chats.
     */
    public Collection<AbstractChat> getActiveChats() {
        Collection<AbstractChat> collection = new ArrayList<>();
        for (AbstractChat chat : chats.values()) {
            if (chat.isActive()) {
                collection.add(chat);
            }
        }
        return Collections.unmodifiableCollection(collection);
    }

    /**
     * Returns existed chat or create new one.
     *
     * @param account
     * @param user
     * @return
     */
    public AbstractChat getOrCreateChat(String account, String user) {
        AbstractChat chat = getChat(account, user);
        if (chat == null) {
            chat = createChat(account, user);
        }
        return chat;
    }

    /**
     * Force open chat (make it active).
     *
     * @param account
     * @param user
     */
    public void openChat(String account, String user) {
        getOrCreateChat(account, user).openChat();
    }

    /**
     * Closes specified chat (make it inactive).
     *
     * @param account
     * @param user
     */
    public void closeChat(String account, String user) {
        AbstractChat chat = getChat(account, user);
        if (chat == null) {
            return;
        }
        chat.closeChat();
    }

    public void requestToLoadLocalHistory(String account, String user) {
        AbstractChat chat = getChat(account, user);
        if (chat == null) {
            chat = createChat(account, user);
        }
        chat.requestToLoadLocalHistory();
    }

    /**
     * @param account
     * @param user
     * @return Last incoming message's text. Empty string if last message is
     * outgoing.
     */
    public String getLastText(String account, String user) {
        AbstractChat chat = getChat(account, user);
        if (chat == null) {
            return "";
        }
        return chat.getLastText();
    }

    /**
     * @param account
     * @param user
     * @return Time of last message in chat. Can be <code>null</code>.
     */
    public Date getLastTime(String account, String user) {
        AbstractChat chat = getChat(account, user);
        if (chat == null) {
            return null;
        }
        return chat.getLastTime();
    }

    /**
     * Sets currently visible chat.
     */
    public void setVisibleChat(BaseEntity visibleChat) {
        final boolean remove = !AccountManager.getInstance().getArchiveMode(visibleChat.getAccount()).saveLocally();
        AbstractChat chat = getChat(visibleChat.getAccount(), visibleChat.getUser());
        if (chat == null) {
            chat = createChat(visibleChat.getAccount(), visibleChat.getUser());
        } else {
            // Mark messages as read and them delete from db if necessary.
            final ArrayList<MessageItem> messageItems = new ArrayList<MessageItem>();
            for (MessageItem messageItem : chat.getMessages()) {
                if (!messageItem.isRead()) {
                    messageItem.markAsRead();
                    messageItems.add(messageItem);
                }
            }
            Application.getInstance().runInBackground(new Runnable() {
                @Override
                public void run() {
                    Collection<Long> ids = getMessageIds(messageItems, remove);
                    if (remove) {
                        MessageTable.getInstance().removeMessages(ids);
                    } else {
                        MessageTable.getInstance().markAsRead(ids);
                    }
                }
            });
        }
        this.visibleChat = chat;
    }

    /**
     * All chats become invisible.
     */
    public void removeVisibleChat() {
        visibleChat = null;
    }

    /**
     * @param chat
     * @return Whether specified chat is currently visible.
     */
    boolean isVisibleChat(AbstractChat chat) {
        return visibleChat == chat;
    }

    /**
     * Removes all messages from chat.
     *
     * @param account
     * @param user
     */
    public void clearHistory(String account, String user) {
        AbstractChat chat = getChat(account, user);
        if (chat == null) {
            return;
        }
        chat.removeAllMessages();
        onChatChanged(chat.getAccount(), chat.getUser(), false);
    }

    /**
     * Removes message from history.
     *
     * @param messageItem
     */
    public void removeMessage(MessageItem messageItem) {
        AbstractChat chat = messageItem.getChat();
        chat.removeMessage(messageItem);
        onChatChanged(chat.getAccount(), chat.getUser(), false);
    }

    /**
     * @param account
     * @param user
     * @return List of messages or empty list.
     */
    public Collection<MessageItem> getMessages(String account, String user) {
        AbstractChat chat = getChat(account, user);
        if (chat == null) {
            return Collections.emptyList();
        }
        return chat.getMessages();
    }

    /**
     * Called on action settings change.
     */
    public void onSettingsChanged() {
        ChatsShowStatusChange showStatusChange = SettingsManager.chatsShowStatusChange();
        Collection<BaseEntity> changedEntities = new ArrayList<>();
        for (AbstractChat chat : chats.values()) {
            if ((chat instanceof RegularChat && showStatusChange != ChatsShowStatusChange.always)
                    || (chat instanceof RoomChat && showStatusChange == ChatsShowStatusChange.never)) {
                // Remove actions with status change.
                ArrayList<MessageItem> remove = new ArrayList<>();
                for (MessageItem messageItem : chat.getMessages()) {
                    if (messageItem.getAction() != null && messageItem.getAction().isStatusChage()) {
                        remove.add(messageItem);
                    }
                }
                if (remove.isEmpty()) {
                    continue;
                }
                for (MessageItem messageItem : remove) {
                    chat.removeMessage(messageItem);
                }
                changedEntities.add(chat);
            }
        }
        RosterManager.getInstance().onContactsChanged(changedEntities);
    }

    @Override
    public void onAccountArchiveModeChanged(AccountItem accountItem) {
        final ArchiveMode archiveMode = AccountManager.getInstance().getArchiveMode(accountItem.getAccount());
        if (archiveMode.saveLocally()) {
            return;
        }
        final String account = accountItem.getAccount();
        final ArrayList<MessageItem> removeMessageItems = new ArrayList<MessageItem>();
        for (AbstractChat chat : chats.getNested(account).values()) {
            for (MessageItem messageItem : chat.getMessages()) {
                if (archiveMode == ArchiveMode.dontStore || ((messageItem.isRead()
                        || archiveMode != ArchiveMode.unreadOnly) && messageItem.isSent())) {
                    removeMessageItems.add(messageItem);
                }
            }
        }
        Application.getInstance().runInBackground(new Runnable() {
            @Override
            public void run() {
                // If message was read or received after removeMessageItems
                // was created then it's ID will be not null. DB actions with
                // such message will have no effect as if it was removed.
                // History ids becomes invalid and will be cleared on next
                // history load.
                MessageTable.getInstance().removeMessages(getMessageIds(removeMessageItems, true));
                if (archiveMode == ArchiveMode.dontStore) {
                    MessageTable.getInstance().removeAccount(account);
                } else if (archiveMode == ArchiveMode.unreadOnly) {
                    MessageTable.getInstance().removeReadAndSent(account);
                } else {
                    MessageTable.getInstance().removeSent(account);
                }
            }
        });
        AccountManager.getInstance().onAccountChanged(accountItem.getAccount());
    }

    @Override
    public void onPacket(ConnectionItem connection, String bareAddress, Stanza packet) {
        if (!(connection instanceof AccountItem)) {
            return;
        }
        String account = ((AccountItem) connection).getAccount();
        if (bareAddress == null) {
            return;
        }
        if (packet instanceof Message
                && MessageArchiveManager.getInstance().isModificationsSucceed(account)
                && Delay.isOfflineMessage(Jid.getServer(account), packet)) {
            // Ignore offline message if modification from server side message
            // archive have been received.
            return;
        }
        final String user = packet.getFrom();
        boolean processed = false;
        for (AbstractChat chat : chats.getNested(account).values()) {
            if (chat.onPacket(bareAddress, packet)) {
                processed = true;
                break;
            }
        }
        if (getChat(account, user) != null) {
            return;
        }
        if (!processed && packet instanceof Message) {
            final Message message = (Message) packet;
            final String body = message.getBody();
            if (body == null) {
                return;
            }
            for (ExtensionElement packetExtension : message.getExtensions()) {
                if (packetExtension instanceof MUCUser) {
                    return;
                }
            }
            createChat(account, user).onPacket(bareAddress, packet);
        }
    }

    public void displayForwardedMessage(ConnectionItem connection, Message message, Direction direction) {

        if (!(connection instanceof AccountItem))
            return;
        String account = ((AccountItem) connection).getAccount();
        if (MessageArchiveManager.getInstance().isModificationsSucceed(account)
                && Delay.isOfflineMessage(Jid.getServer(account), message)) {
            // Ignore offline message if modification from server side message
            // archive have been received.
            return;
        }

        if (direction == Direction.sent) {
            String companion = Jid.getBareAddress(message.getTo());
            if (companion == null) {
                return;
            }
            AbstractChat chat = getChat(account, companion);
            if (chat == null) {
                chat = createChat(account, companion);
            }
            String body = message.getBody();
            if (body == null) {
                return;
            }
            chat.newMessage(body);
            return;
        }

        String companion = Jid.getBareAddress(message.getFrom());
        boolean processed = false;
        for (AbstractChat chat : chats.getNested(account).values()) {
            if (chat.onPacket(companion, message)) {
                processed = true;
                break;
            }
        }
        if (getChat(account, companion) != null) {
            return;
        }
        if (processed) {
            return;
        }
        final String body = message.getBody();
        if (body == null) {
            return;
        }
        createChat(account, companion).onPacket(companion, message);

    }
    @Override
    public void onRosterReceived(AccountItem accountItem) {
        String account = accountItem.getAccount();
        for (AbstractChat chat : chats.getNested(account).values()) {
            chat.onComplete();
        }
    }

    @Override
    public void onDisconnect(ConnectionItem connection) {
        if (!(connection instanceof AccountItem)) {
            return;
        }
        String account = ((AccountItem) connection).getAccount();
        for (AbstractChat chat : chats.getNested(account).values()) {
            chat.onDisconnect();
        }
    }

    @Override
    public void onAccountRemoved(AccountItem accountItem) {
        chats.clear(accountItem.getAccount());
    }

    @Override
    public void onAccountDisabled(AccountItem accountItem) {
        chats.clear(accountItem.getAccount());
    }

    /**
     * Export chat to file with specified name.
     *
     * @param account
     * @param user
     * @param fileName
     * @throws NetworkException
     */
    public File exportChat(String account, String user, String fileName) throws NetworkException {
        final File file = new File(Environment.getExternalStorageDirectory(), fileName);
        try {
            BufferedWriter out = new BufferedWriter(new FileWriter(file));
            final String titleName = RosterManager.getInstance().getName(account, user) + " (" + user + ")";
            out.write("<html><head><title>");
            out.write(StringUtils.escapeHtml(titleName));
            out.write("</title></head><body>");
            final AbstractChat abstractChat = getChat(account, user);
            if (abstractChat != null) {
                final boolean isMUC = abstractChat instanceof RoomChat;
                final String accountName = AccountManager.getInstance().getNickName(account);
                final String userName = RosterManager.getInstance().getName(account, user);
                for (MessageItem messageItem : abstractChat.getMessages()) {
                    if (messageItem.getAction() != null) {
                        continue;
                    }
                    final String name;
                    if (isMUC) {
                        name = messageItem.getResource();
                    } else {
                        if (messageItem.isIncoming()) {
                            name = userName;
                        } else {
                            name = accountName;
                        }
                    }
                    out.write("<b>");
                    out.write(StringUtils.escapeHtml(name));
                    out.write("</b>&nbsp;(");
                    out.write(StringUtils.getDateTimeText(messageItem.getTimestamp()));
                    out.write(")<br />\n<p>");
                    out.write(StringUtils.escapeHtml(messageItem.getText()));
                    out.write("</p><hr />\n");
                }
            }
            out.write("</body></html>");
            out.close();
        } catch (IOException e) {
            throw new NetworkException(R.string.FILE_NOT_FOUND);
        }
        return file;
    }

    /**
     * Notifies registered {@link OnChatChangedListener}.
     *
     * @param account
     * @param user
     * @param incoming
     */
    public void onChatChanged(final String account, final String user,
                              final boolean incoming) {
        Application.getInstance().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                for (OnChatChangedListener onChatChangedListener
                        : Application.getInstance().getUIListeners(OnChatChangedListener.class)) {
                    onChatChangedListener.onChatChanged(account, user, incoming);
                }
            }
        });
    }

    private boolean isStatusTrackingEnabled(String account, String bareAddress) {
        if (SettingsManager.chatsShowStatusChange() != ChatsShowStatusChange.always) {
            return false;
        }
        AbstractChat abstractChat = getChat(account, bareAddress);
        return abstractChat != null && abstractChat instanceof RegularChat && abstractChat.isStatusTrackingEnabled();
    }

    @Override
    public void onStatusChanged(String account, String bareAddress, String resource, String statusText) {
        if (isStatusTrackingEnabled(account, bareAddress)) {
            getChat(account, bareAddress).newAction(resource, statusText, ChatAction.status);
        }
    }

    @Override
    public void onStatusChanged(String account, String bareAddress, String resource,
                                StatusMode statusMode, String statusText) {
        if (isStatusTrackingEnabled(account, bareAddress)) {
            getChat(account, bareAddress).newAction(resource, statusText, ChatAction.getChatAction(statusMode));
        }
    }

}