package com.simplytapp.cardagent;

import java.io.IOException;

import javacard.framework.APDU;
import javacard.framework.ISO7816;
import javacard.framework.ISOException;
import com.simplytapp.virtualcard.Agent;
import com.simplytapp.virtualcard.CardAgentConnector;
import com.simplytapp.virtualcard.TransceiveData;

public class PayPassAgent extends Agent {

	private static final long serialVersionUID = 1L;
	private final static byte sentApdu = 0x00;
	private final static byte sendingRrApdu = 0x01;
	private final static byte sendingGpoApdu = 0x02;
	private final static byte sendingAcApdu = 0x03;
	private final static byte sendingSelectApdu = 0x04;
	private final static byte sendingApdu = 0x05;
	private final static byte RR = 0x00;
	private final static byte GPO = 0x01;
	transient boolean selected = false;
	transient boolean transactionFailed = false;
	transient byte state = sentApdu;
	transient Thread tLoadCache = null;
	transient Thread connectTimer = null;
	

	private Cache cache = new Cache();
	
	public PayPassAgent() {
		allowSoftTransactions();
		allowNfcTransactions();
		denySocketTransactions();
	}
	
	public static void install(CardAgentConnector cardAgentConnector) {
		new PayPassAgent().register(cardAgentConnector);
	}

	private void loadLocalCache()
	{
		//add static cmd/rsp if needed
		if(cache.getRsp(new byte[]{0x00,(byte)0xA4,0x04,0x00,0x05,(byte)0x32,0x50,0x41,0x59,(byte)0x2E})==null)
			cache.addCmd(	new byte[]{0x00,(byte)0xA4,0x04,0x00,0x05,(byte)0x32,0x50,0x41,0x59,(byte)0x2E}, 
							new byte[]{0x6F, 0x23, (byte)0x84, 0x0E, 0x32, 0x50, 0x41, 0x59, 0x2E, 0x53, 0x59, 0x53, 0x2E, 0x44, 0x44, 0x46,   
				    			0x30, 0x31, (byte)0xA5, 0x11, (byte)0xBF, 0x0C, 0x0E, 0x61, 0x0C, 0x4F, 0x07, (byte)0xA0, 0x00, 0x00, 0x00, 0x04,   
				    			0x10, 0x10, (byte)0x87, 0x01, 0x01});  //PPSE
	}
	
	private void loadCache(final byte[] apduc, final byte flag)
	{
		
		if(tLoadCache!=null)
			return;
		
		tLoadCache = new Thread(new Runnable(){
			
			public void run()
			{
				//create a cache from data
				TransceiveData apdus = new TransceiveData(TransceiveData.NFC_CHANNEL);
				apdus.setTimeout((short)5000);
				
				//read record cache!
				boolean rrCache = true;
				if(flag==GPO || flag==RR)
				{
					if(flag==GPO)
						apdus.packApdu(apduc, false);
					if(	cache.getRsp(new byte[]{0x00,(byte)0xB2,0x01,0x0C,0x00})==null) 
					{
						rrCache = false;
						apdus.packApdu(new byte[]{0x00,(byte)0xB2,0x01,0x0C,0x00}, true);
					}
				}
				
				try {
					transceive(apdus);
				} catch (IOException e) {
					tLoadCache = null;
					return;
				}
				if(rrCache && (flag==GPO || flag==RR))
				{
					tLoadCache = null;
					return;
				}
				
				for(short i=0;i<1;i++)
				{
					byte[] rsp = apdus.getNextResponse();
					if(rsp==null || rsp.length<2)
						continue;
					else if((short)(rsp[rsp.length-2]&0xFF)!=(short)(0x90&0xFF) || rsp[rsp.length-1]!=0x00)
						continue;
					
					//don't store the SW in the cache
					byte[] tmp = new byte[rsp.length-2];
					for(short j=0;j<tmp.length;j++)
						tmp[j] = rsp[j];
					rsp = tmp;
						
					byte[] cmd = null;
					if(i==0)
						cmd = new byte[]{0x00,(byte)0xB2,0x01,0x0C,0x00};
					cache.addCmd(cmd, rsp);
				}
				tLoadCache=null;
			}
		});
		
		tLoadCache.start();
	}
	
	@Override
	public void create() {
	}

	@Override
	public void activated(){ //this happens when the card is activated
		try {
			setBusy();
			connect();
		} catch (IOException e) {
			try {
				clearBusy();
				postMessage("No Connection Available!",false,null);
				deactivate();
			} catch (IOException e1) {
			}
			return;
		}
		connectTimer = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					Thread.sleep(120000);
				} catch (InterruptedException e) {
				}
				try {
					disconnect();
				} catch (IOException e) {
				}
				connectTimer = null;
			}});
		connectTimer.start();
		
		try {
			clearBusy();
		} catch (IOException e) {
		}
	}

	@Override
	public void deactivated(){ //this happens when the card is deactivated
		if(connectTimer!=null)
			connectTimer.interrupt();
		try {
			disconnect();
		} catch (IOException e) {
		}
	}
	
	@Override
	public void disconnected(){
	}

	@Override
	public void transactionStarted()
	{
	}
	
	@Override
	public void transactionFinished()
	{
		selected = false;
		state = sentApdu;
		transactionFailed = false;
		//update the state of the class
		try {
			saveState();
		} catch (IOException e1) {
		}
	}
	
	@Override
	public void sentApdu()
	{
		switch(state)
		{
		case sendingAcApdu:
			selected = false;
			break;
		case sendingGpoApdu:
			break;
		default:
			break;
		}
		state = sentApdu;
	}
	
	void sendApduCFailure() throws ISOException
	{
		state = sendingApdu;
		try {transactionFailure();} catch (IOException e) {}
		transactionFailed = true;
		throw new ISOException(ISO7816.SW_COMMAND_NOT_ALLOWED);
	}

	private byte[] queryCache(APDU apdu, short len)
	{
		//check the cache for a response
		byte[] cmd = new byte[len];
		for(short i=0;i<len;i++)
			cmd[i] = apdu.getBuffer()[i];
		byte[] rsp = null;
		if(cache!=null)
		{
			rsp = cache.getRsp(cmd);
			if(rsp==null)
				sendApduCFailure();
		}
		else
			sendApduCFailure();
		return rsp;
	}
	
	@Override
	public void process(APDU apdu) throws ISOException {
		
		while(state!=sentApdu)  //wait for previous one to complete (thread safe)
		{
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
			}
			try {
				if(getTransactionFinished())
				{
					state = sendingApdu;
					throw new ISOException(ISO7816.SW_UNKNOWN);
				}
			} catch (IOException e) {
			}
		}
		
		if(transactionFailed)
		{
			state = sendingApdu;
			throw new ISOException(ISO7816.SW_UNKNOWN);
		}
		
		if((short)(APDU.getProtocol()&0xFF)!=(short)(0xFF&APDU.PROTOCOL_MEDIA_CONTACTLESS_TYPE_A) && 
			(short)(APDU.getProtocol()&0xFF)!=(short)(0xFF&APDU.PROTOCOL_MEDIA_SOCKET) && 
			(short)(APDU.getProtocol()&0xFF)!=(short)(0xFF&APDU.PROTOCOL_MEDIA_SOFT))
			sendApduCFailure();
		
		//receive APDU-C
		short len = apdu.setIncomingAndReceive();
		len+=5;
		
		//validate command format
		if((short)(apdu.getBuffer()[ISO7816.OFFSET_LC]&0xFF)+5!=len)
			throw new ISOException(ISO7816.SW_WRONG_LENGTH);

		//respond to this APDU-C
		switch(apdu.getBuffer()[ISO7816.OFFSET_INS])
		{
		case (byte) 0xA4:  //select
			if(apdu.getBuffer()[ISO7816.OFFSET_LC]>4 && apdu.getBuffer()[ISO7816.OFFSET_LC+1]==(byte)0xA0 &&
					apdu.getBuffer()[ISO7816.OFFSET_LC+2]==(byte)0x00 && apdu.getBuffer()[ISO7816.OFFSET_LC+3]==(byte)0x00 && 
					apdu.getBuffer()[ISO7816.OFFSET_LC+4]==(byte)0x00 && apdu.getBuffer()[ISO7816.OFFSET_LC+5]==(byte)0x04)
			{
				byte[] cmd = new byte[len];
				for(short i=0;i<len;i++)
					cmd[i] = apdu.getBuffer()[i];
				try {
					connect();
					TransceiveData reset = new TransceiveData(TransceiveData.NFC_CHANNEL);
					reset.packCardReset(false);
					byte[] rsp = cache.getRsp(cmd);
					if(rsp==null)
					{
						reset.packApdu(cmd, true);
						try{
							transceive(reset);
							rsp = reset.getNextResponse();
							if(rsp!=null && rsp.length>1 && (short)(rsp[rsp.length-2]&0xFF)==(short)(0x90&0xFF) && rsp[rsp.length-1]==0x00)
							{
								//don't store the SW in the cache
								byte[] tmp = new byte[rsp.length-2];
								for(short j=0;j<tmp.length;j++)
									tmp[j] = rsp[j];
								rsp = tmp;
								cache.addCmd(	new byte[]{0x00,(byte)0xA4,0x04,0x00,0x05,(byte)0xA0,0x00,0x00,0x00,0x04}, 
												rsp);  //AID
							}
						} catch (IOException e){
						}
					}
					else
					{
						reset.packApdu(cmd, false);
						try {
							transceive(reset);
						} catch (IOException e){
						}
					}
				} catch (IOException e) {
					if(e.getMessage().equals("ALREADY_CONNECTED"))
					{
						TransceiveData reset = new TransceiveData(TransceiveData.NFC_CHANNEL);
						reset.packCardReset(false);
						byte[] rsp = cache.getRsp(cmd);
						if(rsp==null)
						{
							reset.packApdu(cmd, true);
							try {
								transceive(reset);
								rsp = reset.getNextResponse();
								if(rsp!=null && rsp.length>1 && (short)(rsp[rsp.length-2]&0xFF)==(short)(0x90&0xFF) && rsp[rsp.length-1]==0x00)
								{
									//don't store the SW in the cache
									byte[] tmp = new byte[rsp.length-2];
									for(short j=0;j<tmp.length;j++)
										tmp[j] = rsp[j];
									rsp = tmp;
									cache.addCmd(	new byte[]{0x00,(byte)0xA4,0x04,0x00,0x05,(byte)0xA0,0x00,0x00,0x00,0x04}, 
													rsp);  //AID
								}
							} catch (IOException e1) {
							}
						}
						else
						{
							reset.packApdu(cmd, false);
							try {
								transceive(reset);
							} catch (IOException e1) {
							}
						}
					}
				}
				selected = true;
			}
			else
				selected = false;
			loadLocalCache();		
			byte[] rsp = queryCache(apdu,len);
			for(short i=0;i<rsp.length;i++)
				apdu.getBuffer()[i] = rsp[i];
			state = sendingSelectApdu;
			apdu.setOutgoingAndSend((short)0, (short)rsp.length);
			break;
		case (byte) 0xA8:  //gpo
			if(selected)
			{
				byte[] cmd = new byte[len];
				for(short i=0;i<len;i++)
					cmd[i] = apdu.getBuffer()[i];
				rsp = cache.getRsp(cmd);
				if(rsp==null)
				{
					TransceiveData gpo = new TransceiveData(TransceiveData.NFC_CHANNEL);
					gpo.packApdu(cmd, true);
					try {
						transceive(gpo);
						
						rsp = gpo.getNextResponse();
						if(rsp!=null && rsp.length>1 && (short)(rsp[rsp.length-2]&0xFF)==(short)(0x90&0xFF) && rsp[rsp.length-1]==0x00)
						{
							//don't store the SW in the cache
							byte[] tmp = new byte[rsp.length-2];
							for(short j=0;j<tmp.length;j++)
								tmp[j] = rsp[j];
							rsp = tmp;
							cache.addCmd(	new byte[]{(byte)0x80,(byte)0xA8,0x00,0x00,0x00}, 
											rsp);  //GPO
						}
						loadCache(cmd,RR);
					} catch (IOException e) {
					}
				}
				else
				{
					loadCache(cmd,GPO);
				}
				rsp = queryCache(apdu,len);
				for(short i=0;i<rsp.length;i++)
					apdu.getBuffer()[i] = rsp[i];
				state = sendingGpoApdu;  //success triggers cache clearing and get new cache after transaction is over
				apdu.setOutgoingAndSend((short)0, (short)rsp.length);
			}
			else
				sendApduCFailure();
			break;
		case (byte) 0xB2:  //read record
			if(selected)
			{
				byte[] cmd = new byte[len];
				for(short i=0;i<len;i++)
					cmd[i] = apdu.getBuffer()[i];
				rsp  = cache.getRsp(cmd);
				while(rsp==null && tLoadCache!=null)
				{
					try {
						Thread.sleep(1);
					} catch (InterruptedException e) {
					}
				}
				rsp = queryCache(apdu,len);
				for(short i=0;i<rsp.length;i++)
					apdu.getBuffer()[i] = rsp[i];
				state = sendingRrApdu;  
				apdu.setOutgoingAndSend((short)0, (short)rsp.length);
			}
			else
				sendApduCFailure();
			break;
		case (byte) 0x2A:  //application cryptogram
			if(selected)
			{
				while(tLoadCache!=null)
				{
					try {
						Thread.sleep(1);
					} catch (InterruptedException e) {
					}
				}
				byte[] cmd = new byte[len];
				for(short i=0;i<len;i++)
					cmd[i] = apdu.getBuffer()[i];

				TransceiveData genAc = new TransceiveData(TransceiveData.NFC_CHANNEL);
				genAc.packApdu(cmd, true);
				try {
					transceive(genAc);
				} catch (IOException e){
				}
				rsp = genAc.getNextResponse();
				if(rsp!=null && rsp.length>1 && (short)(rsp[rsp.length-2]&0xFF)==(short)(0x90&0xFF) && rsp[rsp.length-1]==0x00)
				{
					//don't store the SW in the cache
					byte[] tmp = new byte[rsp.length-2];
					for(short j=0;j<tmp.length;j++)
						tmp[j] = rsp[j];
					rsp = tmp;
				}
				else
					sendApduCFailure();
				
				for(short i=0;i<rsp.length;i++)
					apdu.getBuffer()[i] = rsp[i];
				state = sendingAcApdu;  //success triggers a successful transaction
				apdu.setTransactionSuccess();
				apdu.setOutgoingAndSend((short)0, (short)rsp.length);
			}
			else
				sendApduCFailure();
			break;
		default:
			sendApduCFailure();
		}
	}
}