/*
    BEEM is a videoconference application on the Android Platform.

    Copyright (C) 2009 by Frederic-Charles Barthelery,
                          Jean-Manuel Da Silva,
                          Nikita Kozlov,
                          Philippe Lago,
                          Jean Baptiste Vergely,
                          Vincent Veronis.

    This file is part of BEEM.

    BEEM is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    BEEM 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 BEEM.  If not, see <http://www.gnu.org/licenses/>.

    Please send bug reports with examples or suggestions to
    contact@beem-project.com or http://dev.beem-project.com/

    Epitech, hereby disclaims all copyright interest in the program "Beem"
    written by Frederic-Charles Barthelery,
               Jean-Manuel Da Silva,
               Nikita Kozlov,
               Philippe Lago,
               Jean Baptiste Vergely,
               Vincent Veronis.

    Nicolas Sadirac, November 26, 2009
    President of Epitech.

    Flavien Astraud, November 26, 2009
    Head of the EIP Laboratory.

*/
package com.beem.project.beem.ui;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.jivesoftware.smack.util.StringUtils;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.text.util.Linkify;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnClickListener;
import android.view.View.OnKeyListener;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

import com.beem.project.beem.R;
import com.beem.project.beem.service.Contact;
import com.beem.project.beem.service.Message;
import com.beem.project.beem.service.PresenceAdapter;
import com.beem.project.beem.service.aidl.IBeemRosterListener;
import com.beem.project.beem.service.aidl.IChat;
import com.beem.project.beem.service.aidl.IChatManager;
import com.beem.project.beem.service.aidl.IMessageListener;
import com.beem.project.beem.service.aidl.IRoster;
import com.beem.project.beem.service.aidl.IXmppFacade;
import com.beem.project.beem.utils.BeemBroadcastReceiver;
import com.beem.project.beem.utils.Status;

/**
 * This class represents an activity which allows the user to chat with his/her contacts.
 * @author Jean-Manuel Da Silva <dasilvj at beem-project dot com>
 */
public class Chat extends Activity implements OnKeyListener {

    private static final String TAG = "Chat";
    private static final Intent SERVICE_INTENT = new Intent();
    static {
	SERVICE_INTENT.setComponent(new ComponentName("com.beem.project.beem", "com.beem.project.beem.BeemService"));
    }
    private Handler mHandler = new Handler();

    private IRoster mRoster;
    private Contact mContact;

    private TextView mContactNameTextView;
    private TextView mContactStatusMsgTextView;
    private ImageView mContactStatusIcon;
    private ListView mMessagesListView;
    private EditText mInputField;
    private Button mSendButton;
    private final Map<Integer, Bitmap> mStatusIconsMap = new HashMap<Integer, Bitmap>();

    private final List<MessageText> mListMessages = new ArrayList<MessageText>();

    private IChat mChat;
    private IChatManager mChatManager;
    private final IMessageListener mMessageListener = new OnMessageListener();
    private MessagesListAdapter mMessagesListAdapter = new MessagesListAdapter();

    private final ServiceConnection mConn = new BeemServiceConnection();
    private final BeemBroadcastReceiver mBroadcastReceiver = new BeemBroadcastReceiver();
    private final BeemRosterListener mBeemRosterListener = new BeemRosterListener();
    private IXmppFacade mXmppFacade;

    /**
     * Constructor.
     */
    public Chat() {
	super();
    }

    /**
     * {@inheritDoc}.
     */
    @Override
    protected void onCreate(Bundle savedBundle) {
	super.onCreate(savedBundle);
	setContentView(R.layout.chat);
	this.registerReceiver(mBroadcastReceiver, new IntentFilter(BeemBroadcastReceiver.BEEM_CONNECTION_CLOSED));

	// UI
	mContactNameTextView = (TextView) findViewById(R.id.chat_contact_name);
	mContactStatusMsgTextView = (TextView) findViewById(R.id.chat_contact_status_msg);
	mContactStatusIcon = (ImageView) findViewById(R.id.chat_contact_status_icon);
	mMessagesListView = (ListView) findViewById(R.id.chat_messages);
	mMessagesListView.setAdapter(mMessagesListAdapter);
	mInputField = (EditText) findViewById(R.id.chat_input);
	mInputField.setOnKeyListener(this);
	mSendButton = (Button) findViewById(R.id.chat_send_message);
	mSendButton.setOnClickListener(new OnClickListener() {
	    @Override
	    public void onClick(View v) {
		sendMessage();
	    }
	});

	prepareIconsStatus();
    }

    @Override
    protected void onResume() {
	super.onResume();
	if (mXmppFacade == null)
	    bindService(SERVICE_INTENT, mConn, BIND_AUTO_CREATE);
    }

    /**
     * {@inheritDoc}.
     */
    @Override
    protected void onDestroy() {
	super.onDestroy();
	this.unregisterReceiver(mBroadcastReceiver);
    }

    /**
     * {@inheritDoc}.
     */
    @Override
    protected void onPause() {
	super.onPause();
	try {
	    if (mChat != null) {
		mChat.setOpen(false);
		mChat.removeMessageListener(mMessageListener);
	    }
	    if (mRoster != null)
		mRoster.removeRosterListener(mBeemRosterListener);
	} catch (RemoteException e) {
	    Log.e(TAG, e.getMessage());
	}
	unbindService(mConn);
	mXmppFacade = null;
	mRoster = null;
	mChat = null;
	mChatManager = null;
    }

    /**
     * {@inheritDoc}.
     */
    @Override
    protected void onNewIntent(Intent intent) {
	super.onNewIntent(intent);
	setIntent(intent);
    }

    /**
     * {@inheritDoc}.
     */
    @Override
    protected void onSaveInstanceState(Bundle savedInstanceState) {
	// TODO
	super.onSaveInstanceState(savedInstanceState);
    }

    /**
     * {@inheritDoc}.
     */
    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
	super.onRestoreInstanceState(savedInstanceState);
	// TODO
    }

    /**
     * {@inheritDoc}.
     */
    @Override
    public final boolean onCreateOptionsMenu(Menu menu) {
	super.onCreateOptionsMenu(menu);
	MenuInflater inflater = getMenuInflater();
	inflater.inflate(R.menu.chat, menu);
	return true;
    }

    /**
     * {@inheritDoc}.
     */
    @Override
    public final boolean onOptionsItemSelected(MenuItem item) {
	switch (item.getItemId()) {
	    case R.id.chat_menu_contacts_list:
		Intent contactListIntent = new Intent(this, ContactList.class);
		contactListIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
		startActivity(contactListIntent);
		break;
	    case R.id.chat_menu_change_chat:
		try {
		    final List<Contact> openedChats = mChatManager.getOpenedChatList();
		    if (openedChats.size() > 0)
			createChatSwitcherDialog(openedChats);
		    else
			createNoActiveChatsDialog();
		} catch (RemoteException e) {
		    Log.e(TAG, e.getMessage());
		}
		break;
	    case R.id.chat_menu_close_chat:
		try {
		    mChatManager.destroyChat(mChat);
		} catch (RemoteException e) {
		    Log.e(TAG, e.getMessage());
		}
		this.finish();
		break;
	    default:
		return false;
	}
	return true;
    }

    /**
     * Create the change chat dialog.
     * @param openedChats A list containing the JID of participants of the opened chats.
     */
    private void createChatSwitcherDialog(final List<Contact> openedChats) {
	CharSequence[] items = new CharSequence[openedChats.size()];

	int i = 0;
	for (Contact c : openedChats) {
	    items[i++] = c.getName();
	}

	AlertDialog.Builder builder = new AlertDialog.Builder(this);
	builder.setTitle(getString(R.string.chat_dialog_change_chat_title));
	builder.setItems(items, new DialogInterface.OnClickListener() {
	    public void onClick(DialogInterface dialog, int item) {
		Intent chatIntent = new Intent(getApplicationContext(), com.beem.project.beem.ui.Chat.class);
		chatIntent.setData((openedChats.get(item)).toUri());
		startActivity(chatIntent);
	    }
	});
	AlertDialog chatSwitcherDialog = builder.create();
	chatSwitcherDialog.show();
    }

    /**
     * Create a dialog which notify the user that there're not another one active chat.
     */
    private void createNoActiveChatsDialog() {
	AlertDialog.Builder builder = new AlertDialog.Builder(this);
	builder.setMessage(getString(R.string.chat_no_more_chats));
	AlertDialog noActiveChatsDialog = builder.create();
	noActiveChatsDialog.show();
    }

    /**
     * Change the displayed chat.
     * @param contact the targeted contact of the new chat
     * @throws RemoteException If a Binder remote-invocation error occurred.
     */
    private void changeCurrentChat(Contact contact) throws RemoteException {
	if (mChat != null) {
	    mChat.setOpen(false);
	    mChat.removeMessageListener(mMessageListener);
	}
	mChat = mChatManager.createChat(contact, mMessageListener);
	mChat.setOpen(true);
	mChatManager.deleteChatNotification(mChat);

	mContact = mRoster.getContact(contact.getJID());
	if (mContact == null)
	    mContact = contact;
	updateContactInformations();
	updateContactStatusIcon();

	playRegisteredTranscript();
    }

    /**
     * Get all messages from the current chat and refresh the activity with them.
     * @throws RemoteException If a Binder remote-invocation error occurred.
     */
    private void playRegisteredTranscript() throws RemoteException {
	mListMessages.clear();
	List<MessageText> msgList = convertMessagesList(mChat.getMessages());
	mListMessages.addAll(msgList);
	mMessagesListAdapter.notifyDataSetChanged();
    }

    /**
     * Convert a list of Mesage coming from the service to a list of MessageText that can be displayed in UI.
     * @param chatMessages the list of Message
     * @return a list of message that can be displayed.
     */
    private List<MessageText> convertMessagesList(List<Message> chatMessages) {
	List<MessageText> result = new ArrayList<MessageText>(chatMessages.size());
	String remoteName = mContact.getName();
	String localName = getString(R.string.chat_self);
	MessageText lastMessage = null;

	for (Message m : chatMessages) {
	    String name = remoteName;
	    String fromBareJid = StringUtils.parseBareAddress(m.getFrom());
	    if (m.getType() == Message.MSG_TYPE_ERROR) {
		lastMessage = null;
		result.add(new MessageText(fromBareJid, name, m.getBody(), true));
		continue;
	    } else if (m.getType() == Message.MSG_TYPE_CHAT) {
		if (fromBareJid == null) { //nofrom or from == yours
		    name = localName;
		}
		if (m.getBody() != null) {
		    if (lastMessage == null || !lastMessage.getBareJid().equals(fromBareJid)) {
			lastMessage = new MessageText(fromBareJid, name, m.getBody());
			result.add(lastMessage);
		    } else {
			lastMessage.setMessage(lastMessage.getMessage().concat("\n" + m.getBody()));
		    }
		}
	    }
	}
	return result;
    }

    /**
     * {@inheritDoc}.
     */
    private final class BeemServiceConnection implements ServiceConnection {

	/**
	 * Constructor.
	 */
	public BeemServiceConnection() { }

	/**
	 * {@inheritDoc}.
	 */
	@Override
	public void onServiceConnected(ComponentName name, IBinder service) {
	    mXmppFacade = IXmppFacade.Stub.asInterface(service);
	    try {
		if ((mRoster = mXmppFacade.getRoster()) != null)
		    mRoster.addRosterListener(mBeemRosterListener);
		mContact = new Contact(getIntent().getData());
		if ((mChatManager = mXmppFacade.getChatManager()) != null)
		    changeCurrentChat(mContact);
	    } catch (RemoteException e) {
		Log.e(TAG, e.getMessage());
	    }
	}

	/**
	 * {@inheritDoc}.
	 */
	@Override
	public void onServiceDisconnected(ComponentName name) {
	    mXmppFacade = null;
	    try {
		mRoster.removeRosterListener(mBeemRosterListener);
	    } catch (RemoteException e) {
		Log.e(TAG, e.getMessage());
	    }
	}
    }

    /**
     * {@inheritDoc}.
     */
    private class BeemRosterListener extends IBeemRosterListener.Stub {

	/**
	 * Constructor.
	 */
	public BeemRosterListener() {
	}

	/**
	 * {@inheritDoc}.
	 */
	@Override
	public void onEntriesAdded(List<String> addresses) throws RemoteException {
	}

	/**
	 * {@inheritDoc}.
	 */
	@Override
	public void onEntriesDeleted(List<String> addresses) throws RemoteException {
	}

	/**
	 * {@inheritDoc}.
	 */
	@Override
	public void onEntriesUpdated(List<String> addresses) throws RemoteException {
	}

	/**
	 * {@inheritDoc}.
	 */
	@Override
	public void onPresenceChanged(final PresenceAdapter presence) throws RemoteException {
	    if (mContact.getJID().equals(StringUtils.parseBareAddress(presence.getFrom()))) {
		mHandler.post(new Runnable() {
		    @Override
		    public void run() {
			mContact.setStatus(presence.getStatus());
			mContact.setMsgState(presence.getStatusText());
			updateContactInformations();
			updateContactStatusIcon();
		    }
		});
	    }
	}
    }

    /**
     * {@inheritDoc}.
     */
    private class OnMessageListener extends IMessageListener.Stub {

	/**
	 * Constructor.
	 */
	public OnMessageListener() {
	}

	/**
	 * {@inheritDoc}.
	 */
	@Override
	public void processMessage(IChat chat, final Message msg) throws RemoteException {
	    final String fromBareJid = StringUtils.parseBareAddress(msg.getFrom());

	    if (mContact.getJID().equals(fromBareJid)) {
		mHandler.post(new Runnable() {

		    @Override
		    public void run() {
			//TODO gerer les messages de types error
			if (msg.getBody() != null && msg.getType() != Message.MSG_TYPE_ERROR) {
			    MessageText lastMessage = mListMessages.size() != 0 ? mListMessages.get(mListMessages
				.size() - 1) : null;

			    if (lastMessage != null && lastMessage.getBareJid().equals(fromBareJid)) {
				lastMessage.setMessage(lastMessage.getMessage().concat("\n" + msg.getBody()));
				mListMessages.set(mListMessages.size() - 1, lastMessage);
			    } else if (msg.getBody() != null)
				mListMessages.add(new MessageText(fromBareJid, mContact.getName(), msg.getBody()));
			    mMessagesListAdapter.notifyDataSetChanged();
			} else if (msg.getType() == Message.MSG_TYPE_ERROR) {
			    mListMessages.add(new MessageText(fromBareJid, mContact.getName(), msg.getBody(), true));
			    mMessagesListAdapter.notifyDataSetChanged();
			}
		    }
		});
	    }
	}

	/**
	 * {@inheritDoc}.
	 */
	@Override
	public void stateChanged(IChat chat) throws RemoteException {
	    //TODO
	}
    }

    /**
     * Update the contact informations.
     */
    private void updateContactInformations() {
	// Check for a contact name update
	if (!(mContactNameTextView.getText().toString().equals(mContact.getName())))
	    mContactNameTextView.setText(mContact.getName());

	// Check for a contact status message update
	if (!(mContactStatusMsgTextView.getText().toString().equals(mContact.getMsgState()))) {
	    mContactStatusMsgTextView.setText(mContact.getMsgState());
	    Linkify.addLinks(mContactStatusMsgTextView, Linkify.WEB_URLS);
	}
    }

    /**
     * Update the contact status icon.
     */
    private void updateContactStatusIcon() {
	mContactStatusIcon.setImageBitmap(mStatusIconsMap.get(mContact.getStatus()));
    }

    /**
     * Prepare the status icons map.
     */
    private void prepareIconsStatus() {
	mStatusIconsMap.put(Status.CONTACT_STATUS_AVAILABLE, BitmapFactory.decodeResource(getResources(),
	    android.R.drawable.presence_online));
	mStatusIconsMap.put(Status.CONTACT_STATUS_AVAILABLE_FOR_CHAT, BitmapFactory.decodeResource(getResources(),
	    android.R.drawable.presence_online));
	mStatusIconsMap.put(Status.CONTACT_STATUS_AWAY, BitmapFactory.decodeResource(getResources(),
	    android.R.drawable.presence_away));
	mStatusIconsMap.put(Status.CONTACT_STATUS_BUSY, BitmapFactory.decodeResource(getResources(),
	    android.R.drawable.presence_busy));
	mStatusIconsMap.put(Status.CONTACT_STATUS_DISCONNECT, BitmapFactory.decodeResource(getResources(),
	    android.R.drawable.presence_offline));
	mStatusIconsMap.put(Status.CONTACT_STATUS_UNAVAILABLE, BitmapFactory.decodeResource(getResources(),
	    R.drawable.status_requested));
    }

    /**
     * {@inheritDoc}.
     */
    private class MessagesListAdapter extends BaseAdapter {

	/**
	 * Constructor.
	 */
	public MessagesListAdapter() {
	}

	/**
	 * Returns the number of messages contained in the messages list.
	 * @return The number of messages contained in the messages list.
	 */
	@Override
	public int getCount() {
	    return mListMessages.size();
	}

	/**
	 * Return an item from the messages list that is positioned at the position passed by parameter.
	 * @param position The position of the requested item.
	 * @return The item from the messages list at the requested position.
	 */
	@Override
	public Object getItem(int position) {
	    return mListMessages.get(position);
	}

	/**
	 * Return the id of an item from the messages list that is positioned at the position passed by parameter.
	 * @param position The position of the requested item.
	 * @return The id of an item from the messages list at the requested position.
	 */
	@Override
	public long getItemId(int position) {
	    return position;
	}

	/**
	 * Return the view of an item from the messages list.
	 * @param position The position of the requested item.
	 * @param convertView The old view to reuse if possible.
	 * @param parent The parent that this view will eventually be attached to.
	 * @return A View corresponding to the data at the specified position.
	 */
	public View getView(int position, View convertView, ViewGroup parent) {
	    View sv;
	    if (convertView == null) {
		LayoutInflater inflater = Chat.this.getLayoutInflater();
		sv = inflater.inflate(R.layout.chat_msg_row, null);
	    } else {
		sv = convertView;
	    }
	    MessageText msg = mListMessages.get(position);
	    TextView msgName = (TextView) sv.findViewById(R.id.chatmessagename);
	    msgName.setText(msg.getName());
	    msgName.setTextColor(Color.WHITE);
	    msgName.setError(null);
	    TextView msgText = (TextView) sv.findViewById(R.id.chatmessagetext);
	    msgText.setText(msg.getMessage());
	    if (msg.isError()) {
		String err = getString(R.string.chat_error);
		msgName.setText(err);
		msgName.setTextColor(Color.RED);
		msgName.setError(err);
	    }
	    return sv;
	}
    }

    /**
     * Class which simplify an Xmpp text message.
     * @author Jean-Manuel Da Silva <dasilvj at beem-project dot com>
     */
    private class MessageText {
	private String mBareJid;
	private String mName;
	private String mMessage;
	private boolean mIsError;

	/**
	 * Constructor.
	 * @param bareJid A String containing the bare JID of the message's author.
	 * @param name A String containing the name of the message's author.
	 * @param message A String containing the message.
	 */
	public MessageText(final String bareJid, final String name, final String message) {
	    mBareJid = bareJid;
	    mName = name;
	    mMessage = message;
	    mIsError = false;
	}

	/**
	 * Constructor.
	 * @param bareJid A String containing the bare JID of the message's author.
	 * @param name A String containing the name of the message's author.
	 * @param message A String containing the message.
	 * @param isError if the message is an error message.
	 */
	public MessageText(final String bareJid, final String name, final String message, final boolean isError) {
	    mBareJid = bareJid;
	    mName = name;
	    mMessage = message;
	    mIsError = isError;
	}

	/**
	 * JID attribute accessor.
	 * @return A String containing the bare JID of the message's author.
	 */
	public String getBareJid() {
	    return mBareJid;
	}

	/**
	 * Name attribute accessor.
	 * @return A String containing the name of the message's author.
	 */
	public String getName() {
	    return mName;
	}

	/**
	 * Message attribute accessor.
	 * @return A String containing the message.
	 */
	public String getMessage() {
	    return mMessage;
	}

	/**
	 * JID attribute mutator.
	 * @param bareJid A String containing the author's bare JID of the message.
	 */
	@SuppressWarnings("unused")
	public void setBareJid(String bareJid) {
	    mBareJid = bareJid;
	}

	/**
	 * Name attribute mutator.
	 * @param name A String containing the author's name of the message.
	 */
	@SuppressWarnings("unused")
	public void setName(String name) {
	    mName = name;
	}

	/**
	 * Message attribute mutator.
	 * @param message A String containing a message.
	 */
	public void setMessage(String message) {
	    mMessage = message;
	}

	/**
	 * Get the message type.
	 * @return true if the message is an error message.
	 */
	public boolean isError() {
	    return mIsError;
	}
    }

    /**
     * {@inheritDoc}.
     */
    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
	if (event.getAction() == KeyEvent.ACTION_DOWN) {
	    switch (keyCode) {
		case KeyEvent.KEYCODE_ENTER:
		    sendMessage();
		    return true;
		default:
		    return false;
	    }
	}
	return false;
    }

    /**
     * Send an XMPP message.
     */
    private void sendMessage() {
	final String inputContent = mInputField.getText().toString();

	if (!"".equals(inputContent)) {
	    Message msgToSend = new Message(mContact.getJID(), Message.MSG_TYPE_CHAT);
	    msgToSend.setBody(inputContent);

	    try {
		mChat.sendMessage(msgToSend);
	    } catch (RemoteException e) {
		Log.e(TAG, e.getMessage());
	    }

	    final String self = getString(R.string.chat_self);
	    MessageText lastMessage = mListMessages.size() != 0 ? mListMessages.get(mListMessages.size() - 1) : null;

	    if (lastMessage != null && lastMessage.getName().equals(self)) {
		lastMessage.setMessage(lastMessage.getMessage().concat("\n" + inputContent));
	    } else
		mListMessages.add(new MessageText(self, self, inputContent));
	    mMessagesListAdapter.notifyDataSetChanged();
	    mInputField.setText(null);
	}
    }
}
