import java.awt.EventQueue;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;

import java.awt.Font;
import javax.swing.JComboBox;
import javax.swing.JButton;
import javax.swing.JTextArea;
import javax.swing.JScrollPane;
import javax.swing.JPanel;
import javax.swing.border.TitledBorder;
import javax.swing.text.DefaultFormatterFactory;
import javax.swing.text.MaskFormatter;

import AcsCommon.Chain3DES;
import AcsCommon.Helper;
import Pcsc.CardSelector;
import Pcsc.PcscReader;
import Pcsc.ReaderEvent;
import Pcsc.ReaderEvent.TransmitApduEventArg;

import javax.swing.border.EtchedBorder;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;

import javax.swing.SwingConstants;
import javax.swing.JTextField;
import javax.swing.JCheckBox;
import java.awt.event.ActionListener;
import java.nio.charset.Charset;
import java.text.ParseException;
import java.awt.event.ActionEvent;

import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.smartcardio.CardException;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JFormattedTextField;

import java.util.*;

public class Mifare_Ultralight_C extends JFrame implements ReaderEvent.TransmitApduHandler {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	private JFrame frame;
	private JTextField textFieldCLA;
	private JTextField textFieldINS;
	private JTextField textFieldP1;
	private JTextField textFieldP2;
	private JTextField textFieldLc;
	private JTextField textFieldLe;
	private JTextField textFieldText;
	private JTextField textFieldPageNum;
	private JTextField textFieldLength;
	private JComboBox<String> comboBoxReader;
	private JButton btnInitialize;
	private JButton btnConnect;
	private JButton buttonClear;
	private JButton buttonReset;
	private JButton buttonQuit;
	private JTextArea textArea;
	private JPanel panelSecurity;
	private JButton btnSend;
	private JComboBox<Object> comboBox;
	private JPanel panelAuthentication;
	private JButton btnEnd;
	private JButton btnAuthenticate;
	private JButton btnStart;
	private JCheckBox chckbxDefaultKey;
	private JPanel panelPage;
	private JButton btnRead;
	private JButton btnWrite;
	private JPanel panelCardProperty;
	private JButton btnGetSerialNumber;
	private JFormattedTextField textFieldKey;

	private PcscReader readerControl = new PcscReader();
	private CardSelector cardSelector = new CardSelector(readerControl);
	private ReaderFunctions readerFunctions = new ReaderFunctions(readerControl);
	private MifareUltralight mifareUltralight = new MifareUltralight(readerControl);
	private Charset characterSet = Charset.forName("US-ASCII");
	private int maxPage;
	private byte[] data;
	private MaskFormatter mask;

	final static public int FOUR_BYTES = 4;
	final static public int TRANSPARENT_SESSION_EXPECTED_LENGTH = 7;
	final static public int RANDOM_NUMBER_LENGTH = 8;
	final static public int ACTIVATE_TYPE_A_SWITCH_EXPECTED_LENGTH = 10;
	final static public int RECEIVED_RANDOM_NUMBER_INDEX = 15;
	final static public int SUM_OF_RANDOM_NUMBERS = 16;
	final static public int SIXTEEN_BYTES = 16;
	final static public int GET_RANDOM_NUMBER_EXPECTED_LENGTH = 25;
	final static public int VERIFY_RANDOM_NUMBER_EXPECTED_LENGTH = 26;
	final static public String HEX_FORMATTER_SIXTEEN_BYTES = "HH HH HH HH HH HH HH HH HH HH HH HH HH HH HH HH";
	private JFormattedTextField textFieldHex;

	/**
	 * Launch the application.
	 */
	public static void main(String[] args) {
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				try {
					Mifare_Ultralight_C window = new Mifare_Ultralight_C();
					window.frame.setVisible(true);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		});
	}

	/**
	 * Create the application.
	 */
	public Mifare_Ultralight_C() {
		// Instantiate an event handler object
		readerControl.setEventHandler(new ReaderEvent());
		// Register the event handler implementation of this class
		readerControl.getEventHandler().addEventListener(this);
		initialize();
		resetControls();
	}

	/**
	 * Initialize the contents of the frame.
	 */
	private void initialize() {
		frame = new JFrame();
		frame.setBounds(100, 100, 856, 615);
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.getContentPane().setLayout(null);

		JLabel lblSelectReader = new JLabel("Select Reader");
		lblSelectReader.setFont(new Font("Verdana", Font.PLAIN, 12));
		lblSelectReader.setBounds(10, 11, 87, 16);
		frame.getContentPane().add(lblSelectReader);

		comboBoxReader = new JComboBox<String>();
		comboBoxReader.setFont(new Font("Verdana", Font.PLAIN, 12));
		comboBoxReader.setEnabled(false);
		comboBoxReader.setBounds(107, 9, 273, 20);
		frame.getContentPane().add(comboBoxReader);

		btnInitialize = new JButton("Initialize");
		btnInitialize.setFont(new Font("Verdana", Font.PLAIN, 12));
		btnInitialize.setEnabled(true);
		btnInitialize.setBounds(212, 40, 168, 25);
		frame.getContentPane().add(btnInitialize);

		btnConnect = new JButton("Connect");
		btnConnect.setFont(new Font("Verdana", Font.PLAIN, 12));
		btnConnect.setEnabled(false);
		btnConnect.setBounds(212, 67, 168, 25);
		frame.getContentPane().add(btnConnect);

		JLabel lblApduLogs = new JLabel("APDU Logs:");
		lblApduLogs.setFont(new Font("Verdana", Font.PLAIN, 12));
		lblApduLogs.setBounds(390, 263, 87, 16);
		frame.getContentPane().add(lblApduLogs);

		buttonClear = new JButton("Clear");
		buttonClear.setFont(new Font("Verdana", Font.PLAIN, 12));
		buttonClear.setBounds(390, 540, 140, 25);
		frame.getContentPane().add(buttonClear);

		buttonReset = new JButton("Reset");
		buttonReset.setFont(new Font("Verdana", Font.PLAIN, 12));
		buttonReset.setEnabled(false);
		buttonReset.setBounds(540, 540, 140, 25);
		frame.getContentPane().add(buttonReset);

		buttonQuit = new JButton("Quit");
		buttonQuit.setFont(new Font("Verdana", Font.PLAIN, 12));
		buttonQuit.setBounds(690, 540, 140, 25);
		frame.getContentPane().add(buttonQuit);

		JScrollPane scrollPane = new JScrollPane();
		scrollPane.setBounds(390, 290, 440, 239);
		frame.getContentPane().add(scrollPane);

		textArea = new JTextArea();
		scrollPane.setViewportView(textArea);
		textArea.setText("");
		textArea.setLineWrap(true);

		panelSecurity = new JPanel();
		panelSecurity.setBorder(
				new TitledBorder(null, "Security Functions", TitledBorder.LEADING, TitledBorder.TOP, null, null));
		panelSecurity.setBounds(390, 11, 440, 241);
		frame.getContentPane().add(panelSecurity);
		panelSecurity.setLayout(null);

		btnSend = new JButton("Send Command");
		btnSend.setFont(new Font("Verdana", Font.PLAIN, 12));
		btnSend.setEnabled(true);
		btnSend.setBounds(262, 199, 168, 25);
		panelSecurity.add(btnSend);

		JLabel lblSelectReader_1 = new JLabel("Select Reader");
		lblSelectReader_1.setFont(new Font("Verdana", Font.PLAIN, 12));
		lblSelectReader_1.setBounds(10, 22, 87, 16);
		panelSecurity.add(lblSelectReader_1);

		comboBox = new JComboBox<Object>();
		comboBox.setBounds(10, 49, 420, 25);
		panelSecurity.add(comboBox);

		JLabel lblCLA_1 = new JLabel("CLA");
		lblCLA_1.setHorizontalAlignment(SwingConstants.CENTER);
		lblCLA_1.setFont(new Font("Verdana", Font.PLAIN, 12));
		lblCLA_1.setBounds(10, 85, 40, 16);
		panelSecurity.add(lblCLA_1);

		textFieldCLA = new JTextField();
		textFieldCLA.setEditable(false);
		textFieldCLA.setHorizontalAlignment(SwingConstants.CENTER);
		textFieldCLA.setColumns(2);
		textFieldCLA.setDocument(new JTextFieldLimit(2, true));
		textFieldCLA.setBounds(10, 105, 40, 20);
		panelSecurity.add(textFieldCLA);

		textFieldINS = new JTextField();
		textFieldINS.setEditable(false);
		textFieldINS.setHorizontalAlignment(SwingConstants.CENTER);
		textFieldINS.setColumns(2);
		textFieldINS.setDocument(new JTextFieldLimit(2, true));
		textFieldINS.setBounds(60, 105, 40, 20);
		panelSecurity.add(textFieldINS);

		JLabel lblINS = new JLabel("INS");
		lblINS.setHorizontalAlignment(SwingConstants.CENTER);
		lblINS.setFont(new Font("Verdana", Font.PLAIN, 12));
		lblINS.setBounds(60, 85, 40, 16);
		panelSecurity.add(lblINS);

		JLabel lblP1 = new JLabel("P1");
		lblP1.setHorizontalAlignment(SwingConstants.CENTER);
		lblP1.setFont(new Font("Verdana", Font.PLAIN, 12));
		lblP1.setBounds(110, 85, 40, 16);
		panelSecurity.add(lblP1);

		textFieldP1 = new JTextField();
		textFieldP1.setEditable(false);
		textFieldP1.setHorizontalAlignment(SwingConstants.CENTER);
		textFieldP1.setColumns(2);
		textFieldP1.setDocument(new JTextFieldLimit(2, true));
		textFieldP1.setBounds(110, 105, 40, 20);
		panelSecurity.add(textFieldP1);

		JLabel lblP2 = new JLabel("P2");
		lblP2.setHorizontalAlignment(SwingConstants.CENTER);
		lblP2.setFont(new Font("Verdana", Font.PLAIN, 12));
		lblP2.setBounds(160, 85, 40, 16);
		panelSecurity.add(lblP2);

		textFieldP2 = new JTextField();
		textFieldP2.setEditable(false);
		textFieldP2.setHorizontalAlignment(SwingConstants.CENTER);
		textFieldP2.setColumns(2);
		textFieldP2.setDocument(new JTextFieldLimit(2, true));
		textFieldP2.setBounds(160, 105, 40, 20);
		panelSecurity.add(textFieldP2);

		textFieldLc = new JTextField();
		textFieldLc.setEditable(false);
		textFieldLc.setHorizontalAlignment(SwingConstants.CENTER);
		textFieldLc.setColumns(2);
		textFieldLc.setDocument(new JTextFieldLimit(2, true));
		textFieldLc.setBounds(210, 105, 40, 20);
		panelSecurity.add(textFieldLc);

		JLabel lblLc = new JLabel("Lc");
		lblLc.setHorizontalAlignment(SwingConstants.CENTER);
		lblLc.setFont(new Font("Verdana", Font.PLAIN, 12));
		lblLc.setBounds(210, 85, 40, 16);
		panelSecurity.add(lblLc);

		JLabel lblLe = new JLabel("Le");
		lblLe.setHorizontalAlignment(SwingConstants.CENTER);
		lblLe.setFont(new Font("Verdana", Font.PLAIN, 12));
		lblLe.setBounds(260, 85, 40, 16);
		panelSecurity.add(lblLe);

		textFieldLe = new JTextField();
		textFieldLe.setEditable(false);
		textFieldLe.setHorizontalAlignment(SwingConstants.CENTER);
		textFieldLe.setColumns(2);
		textFieldLe.setDocument(new JTextFieldLimit(2, true));
		textFieldLe.setBounds(260, 105, 40, 20);
		panelSecurity.add(textFieldLe);

		JLabel lblDataHex = new JLabel("Data (Hex)");
		lblDataHex.setFont(new Font("Verdana", Font.PLAIN, 12));
		lblDataHex.setBounds(10, 136, 87, 16);
		panelSecurity.add(lblDataHex);

		panelAuthentication = new JPanel();
		panelAuthentication.setBorder(new TitledBorder(
				new EtchedBorder(EtchedBorder.LOWERED, new Color(255, 255, 255), new Color(160, 160, 160)),
				"Authentication Function", TitledBorder.LEADING, TitledBorder.TOP, null, new Color(0, 0, 0)));
		panelAuthentication.setBounds(10, 159, 370, 173);
		frame.getContentPane().add(panelAuthentication);
		panelAuthentication.setLayout(null);

		btnAuthenticate = new JButton("Authenticate");
		btnAuthenticate.setFont(new Font("Verdana", Font.PLAIN, 12));
		btnAuthenticate.setEnabled(true);
		btnAuthenticate.setBounds(192, 99, 168, 25);
		panelAuthentication.add(btnAuthenticate);

		btnStart = new JButton("Start Session");
		btnStart.setFont(new Font("Verdana", Font.PLAIN, 12));
		btnStart.setEnabled(true);
		btnStart.setBounds(192, 11, 168, 25);
		panelAuthentication.add(btnStart);

		JLabel lbldesKeyhex = new JLabel("3DES Key (Hex)");
		lbldesKeyhex.setFont(new Font("Verdana", Font.PLAIN, 12));
		lbldesKeyhex.setBounds(10, 36, 118, 16);
		panelAuthentication.add(lbldesKeyhex);

		chckbxDefaultKey = new JCheckBox("Use Default Key");
		chckbxDefaultKey.setBounds(10, 95, 133, 23);
		panelAuthentication.add(chckbxDefaultKey);

		mask = null;
		try {
			mask = new MaskFormatter(HEX_FORMATTER_SIXTEEN_BYTES);
		} catch (ParseException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
		mask.setPlaceholderCharacter('_');
		textFieldKey = new JFormattedTextField(mask);
		textFieldKey.setBounds(10, 63, 350, 27);
		panelAuthentication.add(textFieldKey);

		textFieldHex = new JFormattedTextField(mask);
		textFieldHex.setBounds(10, 163, 420, 25);
		panelSecurity.add(textFieldHex);

		btnEnd = new JButton("End Session");
		btnEnd.setBounds(192, 135, 168, 25);
		panelAuthentication.add(btnEnd);
		btnEnd.setFont(new Font("Verdana", Font.PLAIN, 12));
		btnEnd.setEnabled(true);

		btnEnd.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				try {
					displayMessage("\nEnd Transparent Session");
					mifareUltralight.endSession();
					displayMessage("End Transparent Session success");
					enableSession(false);
					setDefaultKey(false);
				} catch (CardException cardException) {
					displayErrorMessage(Pcsc.PcscProvider.getScardErrorMessage(cardException)
							+ "\nPlease re-connect using Connect button.");
				} catch (Exception exception) {
					displayErrorMessage(exception.getMessage());
				}
			}
		});

		panelPage = new JPanel();
		panelPage.setBorder(new TitledBorder(
				new EtchedBorder(EtchedBorder.LOWERED, new Color(255, 255, 255), new Color(160, 160, 160)),
				"PageFunctions", TitledBorder.LEADING, TitledBorder.TOP, null, new Color(0, 0, 0)));
		panelPage.setBounds(10, 343, 370, 222);
		frame.getContentPane().add(panelPage);
		panelPage.setLayout(null);

		btnRead = new JButton("Read Page");
		btnRead.setFont(new Font("Verdana", Font.PLAIN, 12));
		btnRead.setEnabled(true);
		btnRead.setBounds(192, 149, 168, 25);
		panelPage.add(btnRead);

		btnWrite = new JButton("Write Page");
		btnWrite.setFont(new Font("Verdana", Font.PLAIN, 12));
		btnWrite.setEnabled(true);
		btnWrite.setBounds(192, 185, 168, 25);
		panelPage.add(btnWrite);

		textFieldText = new JTextField();
		textFieldText.setColumns(10);
		textFieldText.setDocument(new JTextFieldLimit(16));
		textFieldText.setBounds(10, 113, 350, 25);
		panelPage.add(textFieldText);

		JLabel lblDataText = new JLabel("Data (Text)");
		lblDataText.setFont(new Font("Verdana", Font.PLAIN, 12));
		lblDataText.setBounds(10, 86, 350, 16);
		panelPage.add(lblDataText);

		JLabel lblPageNum = new JLabel("Page Number (Hex)");
		lblPageNum.setFont(new Font("Verdana", Font.PLAIN, 12));
		lblPageNum.setBounds(10, 25, 145, 16);
		panelPage.add(lblPageNum);

		textFieldPageNum = new JTextField();
		textFieldPageNum.setHorizontalAlignment(SwingConstants.CENTER);
		textFieldPageNum.setColumns(2);
		textFieldPageNum.setDocument(new JTextFieldLimit(2, true));
		textFieldPageNum.setBounds(165, 24, 40, 20);
		panelPage.add(textFieldPageNum);

		textFieldLength = new JTextField();
		textFieldLength.setHorizontalAlignment(SwingConstants.CENTER);
		textFieldLength.setColumns(2);
		textFieldLength.setDocument(new JTextFieldLimit(2, true));
		textFieldLength.setBounds(165, 55, 40, 20);
		panelPage.add(textFieldLength);

		JLabel lblLength = new JLabel("Length (Hex)");
		lblLength.setFont(new Font("Verdana", Font.PLAIN, 12));
		lblLength.setBounds(10, 56, 145, 16);
		panelPage.add(lblLength);

		panelCardProperty = new JPanel();
		panelCardProperty
				.setBorder(new TitledBorder(null, "Card Property", TitledBorder.LEADING, TitledBorder.TOP, null, null));
		panelCardProperty.setBounds(10, 103, 370, 45);
		frame.getContentPane().add(panelCardProperty);
		panelCardProperty.setLayout(null);

		btnGetSerialNumber = new JButton("Get Serial Number");
		btnGetSerialNumber.setBounds(192, 11, 168, 25);
		panelCardProperty.add(btnGetSerialNumber);
		btnGetSerialNumber.setFont(new Font("Verdana", Font.PLAIN, 12));
		btnGetSerialNumber.setEnabled(true);

		btnInitialize.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				String[] readerList;
				comboBoxReader.setEnabled(true);
				comboBoxReader.removeAllItems();

				try {
					readerList = readerControl.listTerminals();

					for (int i = 0; i < readerList.length; i++)
						comboBoxReader.addItem(readerList[i]);

					if (readerList.length > 1)
						comboBoxReader.setSelectedIndex(0);

					btnConnect.setEnabled(true);
				} catch (CardException cardException) {
					displayErrorMessage(Pcsc.PcscProvider.getScardErrorMessage(cardException));
				} catch (Exception exception) {
					displayErrorMessage(exception.getMessage() + "\r\n");
				}
			}
		});

		btnConnect.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				try {
					String cardName;
					final int MAX_ULTRALIGHT_PAGE_SIZE = 15;
					final int MAX_ULTRALIGHT_C_PAGE_SIZE = 47;

					if (readerControl.isConnectionActive())
						readerControl.disconnect();

					readerControl.connect(comboBoxReader.getSelectedIndex(), "*");

					displayMessage("\r\nSuccessfully connected to "
							+ comboBoxReader.getItemAt(comboBoxReader.getSelectedIndex()));

					cardName = cardSelector.getCardNameOnRid(readerControl.getAtr());

					if (cardName == "Mifare Ultralight") {
						maxPage = MAX_ULTRALIGHT_PAGE_SIZE;
						enableAuthenticateFunction(false);
						displayMessage("\r\nCard Type: Mifare Ultralight");
					} else if (cardName == "Mifare Ultralight C"
							&& !comboBoxReader.getItemAt(comboBoxReader.getSelectedIndex())
									.contains("ACS ACR1222 3S PICC")
							&& !comboBoxReader.getItemAt(comboBoxReader.getSelectedIndex()).contains("ACS ACR122")) {

						maxPage = MAX_ULTRALIGHT_C_PAGE_SIZE;
						enableAuthenticateFunction(true);
						enableSession(false);
						displayMessage("\r\nCard Type: Mifare Ultralight C");
					} else {
						displayErrorMessage("Card not supported. Please present Mifare Ultralight card.\r\n");
						return;
					}

					btnInitialize.setEnabled(false);
					btnConnect.setEnabled(false);
					buttonReset.setEnabled(true);
					comboBoxReader.setEnabled(false);

					enablePageFunctions(true);
					enableSecurityFunctions(true);
					setEnabled(panelCardProperty, true);
					createCommandList(cardName);
					textFieldHex.setEnabled(false);
				} catch (Exception exception) {
					displayErrorMessage(exception.getMessage() + "\r\n");
				}

			}
		});

		buttonClear.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				textArea.setText("");
			}
		});

		buttonReset.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				try {
					if (readerControl.isConnectionActive())
						readerControl.disconnect();

					resetControls();
				} catch (CardException cardException) {
					displayErrorMessage(Pcsc.PcscProvider.getScardErrorMessage(cardException));
				} catch (Exception exception) {
					displayErrorMessage(exception.getMessage());
				}
			}
		});

		buttonQuit.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				try {
					if (readerControl.isConnectionActive())
						readerControl.disconnect();

					System.gc();
					frame.dispose();
				} catch (CardException cardException) {
					displayErrorMessage(Pcsc.PcscProvider.getScardErrorMessage(cardException));
				} catch (Exception exception) {
					displayErrorMessage(exception.getMessage());
				}
			}
		});

		btnGetSerialNumber.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				try {
					displayMessage("\nGet Card Serial Number");
					readerFunctions.getSerialNumber();
					displayMessage("Get Card Serial Number success");
				} catch (CardException cardException) {
					displayErrorMessage(Pcsc.PcscProvider.getScardErrorMessage(cardException)
							+ "\nPlease re-connect using Connect button.");
				} catch (Exception exception) {
					displayErrorMessage(exception.getMessage());
				}
			}
		});

		comboBox.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {

				String command = comboBox.getSelectedItem().toString();
				switch (command) {
				case "Read Lock Bytes Page (02h)":
					data = new byte[] { (byte) 0xFF, (byte) 0xB0, (byte) 0x00, (byte) 0x02, (byte) 0x04 };
					setTextBoxApduCommand(data, false);
					break;

				case "Write Lock Bytes Page (02h)":
					data = new byte[] { (byte) 0xFF, (byte) 0xD6, (byte) 0x00, (byte) 0x02, (byte) 0x04 };
					setTextBoxApduCommand(data, true);
					break;

				case "Read OTP Page (03h)":
					data = new byte[] { (byte) 0xFF, (byte) 0xB0, (byte) 0x00, (byte) 0x03, (byte) 0x04 };
					setTextBoxApduCommand(data, false);
					break;

				case "Write OTP Page (03h)":
					data = new byte[] { (byte) 0xFF, (byte) 0xD6, (byte) 0x00, (byte) 0x03, (byte) 0x04 };
					setTextBoxApduCommand(data, true);
					break;

				case "Read Lock Bytes Page (28h)":
					data = new byte[] { (byte) 0xFF, (byte) 0xB0, (byte) 0x00, (byte) 0x28, (byte) 0x04 };
					setTextBoxApduCommand(data, false);
					break;

				case "Write Lock Bytes Page (28h)":
					data = new byte[] { (byte) 0xFF, (byte) 0xD6, (byte) 0x00, (byte) 0x28, (byte) 0x04 };
					setTextBoxApduCommand(data, true);
					break;

				case "Read Counter Page (29h)":
					data = new byte[] { (byte) 0xFF, (byte) 0xB0, (byte) 0x00, (byte) 0x29, (byte) 0x04 };
					setTextBoxApduCommand(data, false);
					break;

				case "Write Counter Page (29h)":
					data = new byte[] { (byte) 0xFF, (byte) 0xD6, (byte) 0x00, (byte) 0x29, (byte) 0x04 };
					setTextBoxApduCommand(data, true);
					break;

				case "Read Authentication 0 Page (2Ah)":
					data = new byte[] { (byte) 0xFF, (byte) 0xB0, (byte) 0x00, (byte) 0x2A, (byte) 0x04 };
					setTextBoxApduCommand(data, false);
					break;

				case "Write Authentication 0 Page (2Ah)":
					data = new byte[] { (byte) 0xFF, (byte) 0xD6, (byte) 0x00, (byte) 0x2A, (byte) 0x04 };
					setTextBoxApduCommand(data, true);
					break;

				case "Read Authentication 1 Page (2Bh)":
					data = new byte[] { (byte) 0xFF, (byte) 0xB0, (byte) 0x00, (byte) 0x2B, (byte) 0x04 };
					setTextBoxApduCommand(data, false);
					break;

				case "Write Authentication 1 Page (2Bh)":
					data = new byte[] { (byte) 0xFF, (byte) 0xD6, (byte) 0x00, (byte) 0x2B, (byte) 0x04 };
					setTextBoxApduCommand(data, true);
					break;

				case "Write 3DES Key Page (2Ch - 2Fh)":
					data = new byte[] { (byte) 0xFF, (byte) 0xD6, (byte) 0x00, (byte) 0x2C, (byte) 0x10 };
					setTextBoxApduCommand(data, true);
					break;

				default:
					break;
				}
			}
		});

		btnSend.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				byte[] sendData = null;

				try {
					if (data[1] == 0xB0) {
						sendData = null;
					} else if (data[1] == 0xD6) {
						sendData = Helper.getBytes(textFieldHex.getText());
						if (sendData.length == 0) {
							displayErrorMessage("Please key-in numeric value for Page Number.");
							return;
						}

						if (sendData.length < data[4]) {
							displayErrorMessage("Invalid Data. Length of Data does not match Lc Length.");
							return;
						}
					}

					displayMessage("\nSend Command");

					mifareUltralight.sendCommand(data, sendData);

					displayMessage("Send success");

				} catch (CardException exception) {
					displayErrorMessage(Pcsc.PcscProvider.getScardErrorMessage(exception)
							+ "\nSession is still active. Please End the Session first.");
				} catch (Exception exception) {
					displayErrorMessage(exception.getMessage());
				}
			}
		});

		btnAuthenticate.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				byte[] key;

				key = Helper.getBytes(textFieldKey.getText());

				if (key.length == 0) {
					displayErrorMessage("Please key-in numeric value for the 3DES Key.");
					return;
				}
				if (key.length < 16) {
					displayErrorMessage("Please fill all the fields in the 3DES Key.");
					return;
				}

				mifareUltralight.setAuthenticationKey(key);

				try {
					displayMessage("Mutual Authentication");

					displayMessage("ISO 14443-3 Type A switch");
					mifareUltralight.switchAntennaField();
					displayMessage("ISO 14443-3 Type A switch enabled");

					// Authentication Part 1: Get Encrypted Random Number from CARD
					displayMessage("Authentication Part 1");
					byte[] cardRandomNumber = new byte[RANDOM_NUMBER_LENGTH];
					cardRandomNumber = mifareUltralight.getCardRandomNumber();
					displayMessage("Authentication Part 1 success");

					byte[] formattedKey = new byte[16];
					formattedKey = reformat3DesKey(key);
					SecretKey secretKey = Chain3DES.createKey(formattedKey);

					// Create IV
					IvParameterSpec iv = new IvParameterSpec(new byte[8]);

					// Decrypt Ek(RndB)
					byte[] decryptedCardRandomNumber = Chain3DES.decrypt(cardRandomNumber, iv, secretKey);

					// Shift decrypted RndB (MSB->LSB)
					byte[] shiftedCardRandomNumber = shiftCardRandomNumber(decryptedCardRandomNumber);

					// Generate RndA
					byte[] generatedTerminalRandomNumber = generateTerminalRandomNumber();

					// Encrypt RndA
					iv = new IvParameterSpec(cardRandomNumber);
					byte[] encryptedTerminalRandomNumber = Chain3DES.encrypt(generatedTerminalRandomNumber, iv,
							secretKey);

					// Encrypt RndB'
					iv = new IvParameterSpec(encryptedTerminalRandomNumber);
					byte[] encryptedCardRandomNumberComplement = Chain3DES.encrypt(shiftedCardRandomNumber, iv,
							secretKey);

					// Create Ek(RndA) + Ek(RndB')
					byte[] encryptedRandomNumbers = new byte[SUM_OF_RANDOM_NUMBERS];
					System.arraycopy(encryptedTerminalRandomNumber, 0, encryptedRandomNumbers, 0, 8);
					System.arraycopy(encryptedCardRandomNumberComplement, 0, encryptedRandomNumbers, 8, 8);

					// Authentication Part 2 -> Get data from card
					displayMessage("\nAuthentication Part 2");
					byte[] rndAComplement = new byte[RANDOM_NUMBER_LENGTH];
					rndAComplement = mifareUltralight.getDataFromCard(encryptedRandomNumbers);
					displayMessage("Authentication Part 2 success");

					// Decrypt RndA'
					iv = new IvParameterSpec(encryptedCardRandomNumberComplement);
					byte[] decryptedRndAComplement = Chain3DES.decrypt(rndAComplement, iv, secretKey);

					// Create RndA
					byte[] extractedTerminalRandomNumber = extractTerminalRandomNumber(decryptedRndAComplement);

					// Verify RndA from card response data
					if (Arrays.equals(extractedTerminalRandomNumber, generatedTerminalRandomNumber)) {
						displayMessage("\nMutual Authentication success");
					} else {
						displayMessage("\nMutual Authentication failed");
					}
				} catch (CardException exception) {
					displayErrorMessage(Pcsc.PcscProvider.getScardErrorMessage(exception)
							+ "\nSession is still active. Please End the Session first.");
				} catch (Exception exception) {
					displayErrorMessage(exception.getMessage());
				}
			}
		});

		btnStart.addActionListener(new ActionListener() {

			public void actionPerformed(ActionEvent e) {
				try {
					displayMessage("\nStart Transparent Session");
					mifareUltralight.startSession();
					displayMessage("Start Transparent Session success");
					enableSession(true);
				} catch (CardException cardException) {
					displayErrorMessage(Pcsc.PcscProvider.getScardErrorMessage(cardException)
							+ "\nPlease re-connect using Connect button.");
				} catch (Exception exception) {
					displayErrorMessage(exception.getMessage());
				}
			}
		});

		btnRead.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				int MIN_INPUT_DATA_SIZE = 4;
				int MAX_INPUT_DATA_SIZE = 16;

				byte[] tempStr;
				byte pageNumber;
				byte length;

				if (textFieldPageNum.getText().length() == 0) {
					displayErrorMessage("Please key-in numeric value for Page Number.");
					return;
				}
				if (textFieldLength.getText().length() == 0) {
					displayErrorMessage("Please key-in numeric value for Length.");
					return;
				}

				pageNumber = Helper.getBytes(textFieldPageNum.getText())[0];
				length = Helper.getBytes(textFieldLength.getText())[0];

				if (pageNumber > maxPage) {
					displayErrorMessage("Please key-in valid Page Number. Valid value: 0 - ");
					return;
				}
				if (length < MIN_INPUT_DATA_SIZE || length > MAX_INPUT_DATA_SIZE) {
					displayErrorMessage("Invalid Length. Valid value: 4 - 16.");
					return;
				}
				if (length % MIN_INPUT_DATA_SIZE != 0) {
					displayErrorMessage("Invalid Length. Value must be a multiple of 4.");
					return;
				}

				if (mifareUltralight.isPageNotUserData(pageNumber)) {
					displayErrorMessage("Read failed. Page Number is not a User Data Page.");
					return;
				}

				try {

					displayMessage("\nRead Binary");
					tempStr = mifareUltralight.readBinaryPage(pageNumber, length);

					textFieldText.setText(Helper.byteArrayToString(tempStr, tempStr.length));

					displayMessage("Read success");
				} catch (CardException exception) {
					displayErrorMessage(Pcsc.PcscProvider.getScardErrorMessage(exception)
							+ "\nSession is still active. Please End the Session first.");
				} catch (Exception exception) {
					displayErrorMessage(exception.getMessage());
				}
			}
		});

		btnWrite.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				int MIN_INPUT_DATA_SIZE = 4;
				int MAX_INPUT_DATA_SIZE = 16;

				byte[] data = null;
				byte pageNumber;
				byte length;

				if (textFieldPageNum.getText().length() == 0) {
					displayErrorMessage("Please key-in numeric value for Page Number.");
					return;
				}
				if (textFieldLength.getText().length() == 0) {
					displayErrorMessage("Please key-in numeric value for Length.");
					return;
				}

				pageNumber = Helper.getBytes(textFieldPageNum.getText())[0];
				length = Helper.getBytes(textFieldLength.getText())[0];

				if (pageNumber > maxPage) {
					displayErrorMessage("Please key-in valid Page Number. Valid value: 0 - ");
					return;
				}
				if (length < MIN_INPUT_DATA_SIZE || length > MAX_INPUT_DATA_SIZE) {
					displayErrorMessage("Invalid Length. Valid value: 4 - 16.");
					return;
				}
				if (length % MIN_INPUT_DATA_SIZE != 0) {
					displayErrorMessage("Invalid Length. Value must be a multiple of 4.");
					return;
				}

				if (textFieldText.getText().length() == 0) {
					displayErrorMessage("Please key-in the data to write.");
					return;
				}

				if (textFieldText.getText().length() > length) {
					displayErrorMessage("Length does not match.");
					return;
				}

				data = textFieldText.getText().getBytes(characterSet);

				if (mifareUltralight.isPageNotUserData(pageNumber)) {
					displayErrorMessage("Read failed. Page Number is not a User Data Page.");
					return;
				}

				try {

					displayMessage("\nRead Binary");

					mifareUltralight.writeBinaryPage(pageNumber, data, length);

					textFieldText.setText("");
					displayMessage("Write success");

				} catch (CardException exception) {
					displayErrorMessage(Pcsc.PcscProvider.getScardErrorMessage(exception)
							+ "\nSession is still active. Please End the Session first.");
				} catch (Exception exception) {
					displayErrorMessage(exception.getMessage());
				}
			}
		});

		chckbxDefaultKey.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent event) {
				JCheckBox cb = (JCheckBox) event.getSource();

				setDefaultKey(cb.isSelected());
			}
		});
	}

	void resetControls() {
		textArea.setText("");
		btnInitialize.setEnabled(true);
		btnConnect.setEnabled(false);
		buttonReset.setEnabled(false);
		comboBoxReader.removeAllItems();
		comboBoxReader.setEnabled(false);

		setEnabled(panelCardProperty, false);
		enableAuthenticateFunction(false);
		enablePageFunctions(false);
		enableSecurityFunctions(false);

		displayMessage("Program Ready");
	}

	void setEnabled(Component component, boolean enable) {
		component.setEnabled(enable);
		// Check for instance of container.
		if (component instanceof Container) {
			// Continue setEnabled if there are more components.
			Component components[] = ((Container) component).getComponents();
			for (int i = 0; i < components.length; i++) {
				if (components[i] != null) {
					setEnabled(components[i], enable); // <- Here is the recursive call.
				}
			}
		}
	}

	void displayMessage(String message) {
		textArea.append(message + "\r\n");
	}

	void displayErrorMessage(String message) {
		JOptionPane.showMessageDialog(null, message, "Failed", JOptionPane.ERROR_MESSAGE);
		displayMessage(message);
	}

	@Override
	public void onSendCommand(TransmitApduEventArg event) {
		// TODO Auto-generated method stub
		displayMessage("<< " + event.getAsString(true));
	}

	@Override
	public void onReceiveCommand(TransmitApduEventArg event) {
		// TODO Auto-generated method stub
		displayMessage(">> " + event.getAsString(true));
	}

	private void enableAuthenticateFunction(boolean enable) {
		setEnabled(panelAuthentication, enable);
		setDefaultKey(false);
	}

	private void enablePageFunctions(boolean enable) {
		setEnabled(panelPage, enable);
		textFieldPageNum.setText("");
		textFieldLength.setText("");
		textFieldText.setText("");
	}

	private void enableSession(boolean enable) {
		btnStart.setEnabled(!enable);

		textFieldKey.setEnabled(enable);

		btnAuthenticate.setEnabled(enable);
		btnEnd.setEnabled(enable);
		chckbxDefaultKey.setEnabled(enable);
	}

	private void enableSecurityFunctions(boolean enable) {
		setEnabled(panelSecurity, enable);

		clearSecurityCommands();
	}

	private void clearSecurityCommands() {
		textFieldCLA.setText("");
		textFieldINS.setText("");
		textFieldP1.setText("");
		textFieldP2.setText("");
		textFieldLc.setText("");
		textFieldLe.setText("");
	}

	private void setDefaultKey(boolean enable) {
		if (enable == true)
			textFieldKey.setText("42 52 45 41 4B 4D 45 49 46 59 4F 55 43 41 4E 21");
		else
			textFieldKey.setText("");

		textFieldKey.requestFocus();
		chckbxDefaultKey.setSelected(enable);
	}

	private void setTextBoxApduCommand(byte[] cmd, boolean writeData) {
		textFieldCLA.setText(String.format("%02X", cmd[0]));
		textFieldINS.setText(String.format("%02X", cmd[1]));
		textFieldP1.setText(String.format("%02X", cmd[2]));
		textFieldP2.setText(String.format("%02X", cmd[3]));

		textFieldHex.setText("");

		if (writeData) {
			textFieldLc.setText(String.format("%02X", cmd[4]));
			textFieldLe.setText("00");
			textFieldHex.setEnabled(writeData);
			setMaskedTextBoxDataSize(cmd[4]);
		} else {
			textFieldLe.setText(String.format("%02X", cmd[4]));
			textFieldLc.setText("00");
			textFieldHex.setEnabled(writeData);
		}
	}

	private void setMaskedTextBoxDataSize(int size) {
		String maskFormat = ">>";

		for (int index = 0; index < size; index++) {
			maskFormat += "HH ";

			if ((index + 1) == size) {
				maskFormat = maskFormat.substring(0, maskFormat.length() - 1);
			}
		}

		try {
			MaskFormatter mf1 = new MaskFormatter(maskFormat);
			mf1.setPlaceholderCharacter('_');
			textFieldHex.setFormatterFactory(new DefaultFormatterFactory(mf1));
		} catch (ParseException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	private void createCommandList(String cardType) {
		comboBox.removeAllItems();

		switch (cardType) {
		case "Mifare Ultralight":
			comboBox.setModel(new DefaultComboBoxModel<Object>(new String[] { "Read Lock Bytes Page (02h)",
					"Write Lock Bytes Page (02h)", "Read OTP Page (03h)", "Write OTP Page (03h)" }));
			break;

		case "Mifare Ultralight C":
			comboBox.setModel(new DefaultComboBoxModel<Object>(
					new String[] { "Read Lock Bytes Page (02h)", "Write Lock Bytes Page (02h)", "Read OTP Page (03h)",
							"Write OTP Page (03h)", "Read Lock Bytes Page (28h)", "Write Lock Bytes Page (28h)",
							"Read Counter Page (29h)", "Write Counter Page (29h)", "Read Authentication 0 Page (2Ah)",
							"Write Authentication 0 Page (2Ah)", "Read Authentication 1 Page (2Bh)",
							"Write Authentication 1 Page (2Bh)", "Write 3DES Key Page (2Ch - 2Fh)" }));
			break;
		}
	}

	private byte[] generateTerminalRandomNumber() {
		Random randomNumber = new Random();
		byte[] terminalRandomNumber = new byte[8];
		for (int index = 0; index < 8; index++)
			terminalRandomNumber[index] = (byte) randomNumber.nextInt(0xFF);
		return terminalRandomNumber;
	}

	private byte[] extractTerminalRandomNumber(byte[] decryptedTerminalNumberComplement) {
		byte[] terminalRandomNumber = new byte[RANDOM_NUMBER_LENGTH];
		for (int randomNumberIndex = 1,
				complementRandomNumberIndex = 0; randomNumberIndex < RANDOM_NUMBER_LENGTH; randomNumberIndex++, complementRandomNumberIndex++) {
			terminalRandomNumber[randomNumberIndex] = decryptedTerminalNumberComplement[complementRandomNumberIndex];
		}
		terminalRandomNumber[0] = decryptedTerminalNumberComplement[RANDOM_NUMBER_LENGTH - 1];
		return terminalRandomNumber;
	}

	public byte[] reverseKey(byte[] key) {
		int index = 0;
		int reverseIndex = key.length - 1;
		byte tmp;
		while (reverseIndex > index) {
			tmp = key[reverseIndex];
			key[reverseIndex] = key[index];
			key[index] = tmp;
			reverseIndex--;
			index++;
		}

		return key;
	}

	private byte[] shiftCardRandomNumber(byte[] decryptedTerminalNumber) {
		byte[] shiftedRnd = new byte[RANDOM_NUMBER_LENGTH];
		for (int index = 0, index2 = 1; index < (RANDOM_NUMBER_LENGTH - 1); index++, index2++) {
			shiftedRnd[index] = decryptedTerminalNumber[index2];
		}
		shiftedRnd[RANDOM_NUMBER_LENGTH - 1] = decryptedTerminalNumber[0];
		return shiftedRnd;
	}

	public byte[] reformat3DesKey(byte[] actual3DesKey) {
		// String desKey = "IEMK AERB !NAC UOYF";
		// key1: KMEI -> IEMK
		// key2: BREA -> AERB
		// key3: CAN! -> !NAC
		// key4: FYOU -> UOYF

		byte[] key1 = new byte[FOUR_BYTES];
		byte[] key2 = new byte[FOUR_BYTES];
		byte[] key3 = new byte[FOUR_BYTES];
		byte[] key4 = new byte[FOUR_BYTES];
		byte[] convertedKey = new byte[SIXTEEN_BYTES];

		System.arraycopy(actual3DesKey, 0, key1, 0, FOUR_BYTES);
		System.arraycopy(actual3DesKey, 4, key2, 0, FOUR_BYTES);
		System.arraycopy(actual3DesKey, 8, key3, 0, FOUR_BYTES);
		System.arraycopy(actual3DesKey, 12, key4, 0, FOUR_BYTES);

		key1 = reverseKey(key1);
		key2 = reverseKey(key2);
		key3 = reverseKey(key3);
		key4 = reverseKey(key4);

		System.arraycopy(key1, 0, convertedKey, 4, FOUR_BYTES);
		System.arraycopy(key2, 0, convertedKey, 0, FOUR_BYTES);
		System.arraycopy(key3, 0, convertedKey, 12, FOUR_BYTES);
		System.arraycopy(key4, 0, convertedKey, 8, FOUR_BYTES);

		return convertedKey;
	}
}
