package madynes.flowoid.exporter;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Scanner;
import android.content.ContentResolver;
import android.provider.Settings.Secure;
import android.util.Log;


public class ExportPacket implements Exportable {
    private final short version = 9;
    private ContentResolver contentResolver;
    private static int bootEpoch = 0;
    
	// RFC3954 5.1 > format
    private final short headerSize = 20;
	private TemplateFlowSet templateFlowSet;
	private HashMap<Short, DataFlowSet> dataFlowSets;
	private static int sequenceNumber = 0;

	/**
	 * Defines a NetFlow v9 export packet. See RFC3954 5. 
	 * 
	 * @param contentResolver the Android content resolver
	 */
    public ExportPacket(ContentResolver contentResolver) {
    	dataFlowSets = new HashMap<Short, DataFlowSet>();
		this.contentResolver = contentResolver;
	}

    /**
     * Sets the template FlowSet if this export packet has to
     * (re)define it.
     * 
     * @param templateFlowSet template FlowSet
     */
	public void setTemplateFlowSet(TemplateFlowSet templateFlowSet) {
		this.templateFlowSet = templateFlowSet;
	}
	
	/**
	 * Adds a data FlowSet to include in this export packet.
	 * 
	 * @param dataFlowSet data FlowSet to add
	 */
	public void addDataFlowSet(DataFlowSet dataFlowSet) {
		dataFlowSets.put(dataFlowSet.getFlowSetID(), dataFlowSet);
	}
    
	/**
	 * Returns the NetFlow version used.
	 * 
	 * @return NetFlow version (v9)
	 */
    private short getVersion() {
    	return version;
    }

    /**
     * Returns the total number of records in this export packet. 
     * 
     * @return number of records
     */
    private short getCount() {
    	int count = 0;
    	
    	for(Short flowSetID : dataFlowSets.keySet()) {
    		count += dataFlowSets.get(flowSetID).getCount();
    	}
    		
    	if(templateFlowSet != null)
    		count += templateFlowSet.getCount();
    	
    	return (short) count;
    }
    
    /**
     * Returns the timestamp corresponding to the last device boot.
     * 
     * @return timestamp in milliseconds
     */
    public static int getBootEpoch() {
    	if(bootEpoch == 0) {
        	double uptime;
        	
    		try {
    			String[] uptimeStr = new Scanner(new FileInputStream("/proc/uptime")).nextLine().split("\\s+");
    			uptime = Double.parseDouble(uptimeStr[0]) * 1000;

    		} catch (FileNotFoundException e) {
    			uptime = 0;
    		}
    		
    		bootEpoch = (int) (new Date().getTime() - uptime);
    	}

    	return bootEpoch;
    }
    
    /**
     * Returns the time in milliseconds since this device was first booted.
     * See RFC3954 5.1 > sysUpTime. 
     * 
     * @return uptime in milliseconds
     */
    private int getSysUpTime() {
    	return (int) (new Date().getTime() - getBootEpoch());
    }
    
    /**
     * Returns the current time in seconds since the January 1st 1970.
     * See RFC3954 5.1 > UNIX Secs.
     * 
     * @return current time
     */
    private int getUNIXsecs() {
    	return (int) ((new Date().getTime()) / 1000);
    }

    /**
     * Returns the incremental sequence counter of all Export Packets sent.
     * See RFC3954 5.1 > .Sequence Number
     * 
     * @return sequence number
     */
    public static int getSequenceNumber() {
    	return sequenceNumber;
    }
    
    /**
     * Increments the sequence number (each time a new Export Packet is sent).
     */
    public static void incSequenceNumber() {
    	synchronized (ExportPacket.class) {
    			sequenceNumber++;
    	}
    }
    
    /**
     * Returns the Android ID as domain source ID.
     * See RFC3954 5.1 > Source ID.
     * 
     * @return domain source ID
     */
    private int getSourceID() {
    	String androidID = Secure.getString(contentResolver, Secure.ANDROID_ID);

    	return Math.abs(androidID.hashCode());
    }
    
    /**
     * Returns the data FlowSet having a given ID. 
     * 
     * @param flowSetID FlowSet ID to match
     * @return data FlowSet found 
     */
    public DataFlowSet getDataFlowSet(short flowSetID) {
    	return dataFlowSets.get(flowSetID); 
    }

    /**
     * Returns true if a data FlowSet with the given ID was already
     * created.
     * 
     * @param flowSetID FlowSet ID to match
     * @return true if the corresponding data FlowSet exists
     */
    public boolean containsDataFlowSet(short flowSetID) {
    	return dataFlowSets.containsKey(flowSetID);
    }
    
	@Override
	public short getTotalLength() {
		short length = 0;

		if(templateFlowSet != null) {
			length += templateFlowSet.getTotalLength();
		}
		
    	for(Short flowSetID : dataFlowSets.keySet()) {
			length += dataFlowSets.get(flowSetID).getTotalLength();
		}
		
		return (short) (headerSize + length);
	}
    
    @Override
    public ArrayList<Byte> toBytes() throws TemplateMismatchException {
    	ArrayList<Byte> bytes = new ArrayList<Byte>();
    	
    	ByteBuffer headers = ByteBuffer.allocate(headerSize);
    	headers.order(ByteOrder.BIG_ENDIAN);
    	headers.putShort(getVersion());
    	headers.putShort(getCount());
    	headers.putInt(getSysUpTime());
    	headers.putInt(getUNIXsecs());
    	headers.putInt(getSequenceNumber());
    	headers.putInt(getSourceID());
    	
    	for(byte b : headers.array()) {
    		bytes.add((Byte) b);
    	}
    	
		if(templateFlowSet != null) {
			bytes.addAll(templateFlowSet.toBytes());
		}
		
    	for(Short flowSetID : dataFlowSets.keySet()) {
			bytes.addAll(dataFlowSets.get(flowSetID).toBytes());
		}
		
    	return bytes;
    }
    
    /**
     * Returns the raw bytes value of this export packet. 
     * 
     * @return this export packet in primitives bytes
     * @throws TemplateMismatchException a field doesn't return a value's size corresponding
     * to its associated template 
     */
    public byte[] toPrimitiveBytes() throws TemplateMismatchException {
    	ArrayList<Byte> bytes = toBytes();
    	byte[] pBytes = new byte[bytes.size()];
    	
    	for(int i = 0; i < pBytes.length; i++) {
    		pBytes[i] = (byte) bytes.get(i);
    	}
    	
    	return pBytes;
    }
}