/**
 * 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 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;

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

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

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.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.OnStatusChangeListener;
import com.xabber.android.data.roster.OnRosterReceivedListener;
import com.xabber.android.data.roster.RosterManager;
import com.xabber.android.utils.StringUtils;
import com.xabber.androiddev.R;
import com.xabber.xmpp.address.Jid;
import com.xabber.xmpp.delay.Delay;

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

	/**
	 * Registered chats for bareAddresses in accounts.
	 */
	private final NestedMap<AbstractChat> chats;

	/**
	 * Visible chat.
	 * 
	 * Will be <code>null</code> if there is no one.
	 */
	private AbstractChat visibleChat;

	private final static MessageManager instance;

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

	public static MessageManager getInstance() {
		return instance;
	}

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

	@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);
		if (chat == null)
			return false;
		return chat.isActive();
	}

	/**
	 * @return Collection with active chats.
	 */
	public Collection<AbstractChat> getActiveChats() {
		Collection<AbstractChat> collection = new ArrayList<AbstractChat>();
		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.
	 * 
	 * @param account
	 * @param user
	 */
	public void setVisibleChat(String account, String user) {
		final boolean remove = !AccountManager.getInstance()
				.getArchiveMode(account).saveLocally();
		AbstractChat chat = getChat(account, user);
		if (chat == null)
			chat = createChat(account, user);
		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);
				}
			});
		}
		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<BaseEntity>();
		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<MessageItem>();
				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,
			Packet 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 (PacketExtension packetExtension : message.getExtensions())
				if (packetExtension instanceof MUCUser)
					return;
			createChat(account, user).onPacket(bareAddress, packet);
		}
	}

	@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());
	}

	/**
	 * 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);
			}
		});
	}

	/**
	 * @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<Long>();
		for (MessageItem messageItem : messageItems) {
			Long id = messageItem.getId();
			if (id == null)
				continue;
			ids.add(id);
			if (clearId)
				messageItem.setId(null);
		}
		return ids;
	}

	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));
	}

}