/*
 *   
 *
 * Copyright  1990-2006 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version
 * 2 only, as published by the Free Software Foundation. 
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License version 2 for more details (a copy is
 * included at /legal/license.txt). 
 * 
 * You should have received a copy of the GNU General Public License
 * version 2 along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA 
 * 
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions. 
 */

package  com.sun.midp.io.j2me.apdu;

import com.sun.cardreader.*;
import java.io.IOException;
import com.sun.midp.security.*;
import java.util.*;
import java.io.*;
import com.sun.midp.main.Configuration;

/**
 * This class provides virtual device driving the simulator.
 */
public class Simulator extends com.sun.cardreader.CardDevice {
    /** Configuration property name. */
    public static final String PROP_NAME = 
        "com.sun.midp.io.j2me.apdu.hostsandports";
    /** Host names. */
    private static String[] hosts = new String[0];

    /** Port numbers. */
    private static int[] ports = new int[0];

    /** Input streams. */
    private InputStream[] in = new InputStream[0];

    /** Output streams. */
    private OutputStream[] out = new OutputStream[0];

    /**
     * Socket connection to connect with the Java Card reference
     * implementation.
     */
    private com.sun.midp.io.j2me.socket.Protocol[] conn = 
                new com.sun.midp.io.j2me.socket.Protocol[0];

    /**
     * Number of slots supported by the simulator.
     */
    private int slotCount = 0;

    /**
     * Number of the current slot for operations.
     */
    private int currentSlot = 0;

    /**
     * Is the card changed since last reset.
     */
    private boolean cardChanged;

    /** The response message received from the card. */
    private TLP224Message responseMsg;

    /** Message sent to the card. */
    private TLP224Message commandMsg;

    /** Byte array containing current APDU header. */
    private byte[] header;

    /** Current command APDU data. */
    private byte[] command;

    /** Lc value for the current APDU (data size). */
    private int Lc;

    /** Le value for the current APDU (length of expected data). */
    private int Le;

    /** 1st byte of APDU status word. */
    private int sw1;

    /** 2nd byte of APDU status word. */
    private int sw2;

    /** Data received from the card during APDU exchange. */
    private byte[] output;

    /** Current size of received data. */
    private int dataSize;

    /**
     * Initializes the device.
     * 
     * @throws IOException If device initialization failed.
     * @throws CardDeviceException If device configuration is bad.
     */
    public void init() throws IOException, CardDeviceException {

        /* Read configuration, moved from Cad */
        String config = Configuration.getProperty(PROP_NAME);

        boolean ok;
        try {
            Vector list = new Vector();

            int index = config.indexOf(",");
            while (index != -1) {
                list.addElement(config.substring(0, index));
                config = config.substring(index + 1);
                index = config.indexOf(",");
            }
            list.addElement(config);

            this.slotCount = list.size();
            this.hosts = new String[this.slotCount];
            this.ports = new int[this.slotCount];
            this.in = new InputStream[this.slotCount];
            this.out = new OutputStream[this.slotCount];
            this.conn = 
                new com.sun.midp.io.j2me.socket.Protocol[this.slotCount];

            for (int i = 0; i < list.size(); i++) {
                String s = (String) list.elementAt(i);
                index = s.indexOf(":");
                hosts[i] = s.substring(0, index++).trim();
                ports[i] = Integer.parseInt(s.substring(index));
            }
            ok = list.size() > 0;
        } catch (NullPointerException npe) {
            ok = false;
        } catch (IndexOutOfBoundsException iobe) {
            ok = false;
        } catch (NumberFormatException nfe) {
            ok = false;
        }

        if (! ok) {
            throw new CardDeviceException("Simulator configuration is bad");
        }
        header = new byte[4];
        commandMsg = new TLP224Message();
        responseMsg = new TLP224Message();
        cardChanged = false;
    }

    /**
     * Close the specified slot. 
     * @param slot Slot number
     * @throws IOException If slot close failed.
     */
    public void closeSlot(int slot) throws IOException {
        String msg = "";
        if (slot >= 0 && slot <= this.slotCount) {
            
            try {
                if (this.in[slot] != null) {
                    InputStream tmp = this.in[slot];
                    this.in[slot] = null;
                    tmp.close();
                }
            } catch (IOException e) {
                msg += e.getMessage();
            }
            
            try {
                if (this.out[slot] != null) {
                    OutputStream tmp = this.out[slot];
                    this.out[slot] = null;
                    tmp.close();
                }
            } catch (IOException e) {
                msg += e.getMessage();
            }
            
            try {
                if (this.conn[slot] != null) {
                    com.sun.midp.io.j2me.socket.Protocol tmp = this.conn[slot];
                    this.conn[slot] = null;
                    tmp.close();
                }
            } catch (IOException e) {
                msg += e.getMessage();
            }
            if (msg.length() != 0) {
                throw new IOException(msg);
            }
        } else {
            throw new IOException("Invalid slot number");
        }
    }
    

    /**
     * Closes the device.
     * No exceptions thrown to avoid mess in exception handlers trying
     * to clean up and close the device.
     */
    public void close() {
        for (int i = 0; i < this.slotCount; i++) {
            try {
                closeSlot(i);
            }
            catch (IOException e) { // ignored
            }
        }
    }
    
    /**
     * Performs platform lock of the device. This is intended to make
     * sure that no other native application
     * uses the same device during a transaction.
     *
     * @throws IOException If a device locking failed.
     */
    public void lock() throws IOException {
    }
    
    /**
     * Unlocks the device.
     *
     * @throws IOException If a device unlocking failed.
     */
    public void unlock() throws IOException {
    }
    
    /**
     * Open the specified slot. 
     *
     * @param slot Slot number
     * @param token Security token
     * @throws IOException If slot opening failed.
     */
    public void openSlot(int slot, SecurityToken token) throws IOException {
        // IMPL_NOTE: The CREF starts very slow. TCK's port checking is not enough:
        // the CREF can accept connection a little later than port 
        // is not available. Two seconds is enough on my computer
        try {
            Thread.sleep(2000);
        } catch (InterruptedException ie) {} // ignored

        // Here we open socket to the simulator
        this.conn[slot] = new com.sun.midp.io.j2me.socket.Protocol();
        String url = "//" + this.hosts[slot] + ":" + this.ports[slot];
        try {
            this.conn[slot].openPrim(token, url);
            this.in[slot] = this.conn[slot].openInputStream();
            this.out[slot] = this.conn[slot].openOutputStream();
        } catch (IOException ie) {
            throw new IOException("Cannot open '" + url + "':" + ie);
        }
    }

    /**
     * Selects the current slot for the subsequent transfer operations.
     * For the one-slot devices the default slot number is 0.
     *
     * @param slot Slot number
     * @throws IOException If a slot selection failed.
     */
    public void selectSlot(int slot) throws IOException {
        if (this.conn[slot] == null) {
            throw new IOException("Connection for simulator slot " +
                                  Integer.toString(slot) + " not open");
        }

        this.currentSlot = slot;
        cardChanged = false;
    }

    /**
     * Checks if this slot is SAT slot.
     *
     * @param slotNumber Slot number
     * @return <code>true</code> if the slot is dedicated for SAT,
     *         <code>false</code> otherwise
     * @throws IOException If an error occured.
     */
    public boolean isSatSlot(int slotNumber) throws IOException {
        // IMPL_NOTE: This method must return always false if SAT support is disabled
        return slotNumber == 0;
    }

    /**
     * Performs reset of device.
     * Returns ATR into provided buffer
     *
     * @param atr Buffer for ATR bytes
     * @return Length of ATR
     * @throws IOException If a reset failed.
     */
    public int cmdReset(byte[] atr) throws IOException {
        byte[] data = commandMsg.getData();
        int atrLen;

        data[0] = TLP224Message.ACK;
        data[1] = 4;
        data[2] = TLP224Message.POWER_UP;
        data[3] = 0;
        data[4] = 0;
        data[5] = 0;
        data[6] = commandMsg.computeLRC(6);
        commandMsg.setLength(7);
        sendTLP224Message(commandMsg);
        receiveTLP224Message(responseMsg);
        
        data = responseMsg.getData();
        if (data[2] == TLP224Message.STATUS_CARD_REMOVED) {
            // the card has been just inserted, try again
            sendTLP224Message(commandMsg);
            receiveTLP224Message(responseMsg);
        }
        if (data[2] != 0) {
            throw new IOException("TLP224Error " + (data[2] & 0xff));
        }
        if (data[1] < 5 || (data[1] == 1 && data[2] == 0)) {
            throw new IOException("CREF is not ready");
        }
        // card on-line
        atrLen = data[5];
        if (atrLen < 0 || atrLen > atr.length) {
            throw new IOException("Invalid length of ATR (" + atrLen + ")");
        }
        System.arraycopy(data, 6, atr, 0, atrLen);

        return atrLen;
    }

    /**
     * Performs 'POWER DOWN' command.
     *
     * @throws IOException If a reset failed.
     */
    public void cmdPowerDown() throws IOException {
        byte[] data = commandMsg.getData();
        int atrLen;

        data[0] = TLP224Message.ACK;
        data[1] = 1;
        data[2] = 0x4D; // 'POWER_DOWN'
        data[3] = commandMsg.computeLRC(3);
        commandMsg.setLength(4);
        sendTLP224Message(commandMsg);
        receiveTLP224Message(responseMsg);
    }

    /**
     * Checks if the card in the selected slot was changed 
     * since last call or since last reset.
     * Always called after any transfer operation, so
     * implementation can check and store that status during 
     * this operation.
     *
     * @return true if was changed, false otherwise.
     * @throws IOException If something fails.
     */
    public boolean isCardChanged() throws IOException {
        boolean retValue = cardChanged;
        cardChanged = false;
        return retValue;
    }
    
    /**
     * Performs data transfer to the device.
     *
     * @param commandAPDU Request bytes
     * @param response Response bytes
     * @return Length of response
     * @throws IOException If a data transfer failed.
     */
    public int cmdXfer(byte[] commandAPDU, byte[] response) 
	throws IOException {

        System.arraycopy(commandAPDU, 0, header, 0, 4);
    	command = commandAPDU;
   	    dataSize = 0;
   	    
    	if (command.length == 4) { // case 1 -> ISO_IN
    	    Lc = 0;
    	    Le = -1;
    	    isoIn();
    	} else
    	if (command.length == 5) { // case 2 -> ISO_OUT
    	    Lc = 0;
    	    Le = command[4] & 0xFF;
    	    isoOut();
	    } else
    	if (command.length == (command[4] & 0xFF) + 5) { // case 3 -> ISO_IN
    	    Lc = (command[4] & 0xFF);
    	    Le = -1;
    	    isoIn();
	    } else
    	if (command.length > (command[4] & 0xFF) + 5) { // case 4 -> ISO_IN
    	    Lc = (command[4] & 0xFF);
    	    Le = (command[Lc + 5] & 0xFF);
    	    isoIn();
	    } else {
    	    throw new IOException("Malformed APDU");
	    }
	    if (dataSize > 0) {
    	    System.arraycopy(output, 0, response, 0, dataSize);
	    }
	    response[dataSize] = (byte)sw1;
	    response[dataSize+1] = (byte)sw2;

	    return dataSize + 2;
    }

    /**
     * Format and send an ISO_IN command to the CAD.
     * @exception InterruptedIOException if connection was closed in the
     * other thread
     * @exception IOException if a communication error happens
     */
    private void isoIn() throws IOException {

        byte[] data = commandMsg.getData();
        int length = Lc;

        data[0] = TLP224Message.ACK;
        data[1] = (byte) (length + 6);
        data[2] = TLP224Message.ISO_INPUT;
        System.arraycopy(header, 0, data, 3, 4);
        data[7] = (byte) length;
        if (length > 0)
            System.arraycopy(command, 5, data, 8, length);
        data[length + 8] = commandMsg.computeLRC(length + 8);
        commandMsg.setLength(length + 9);

        sendTLP224Message(commandMsg);
        receiveTLP224Message(responseMsg);
        data = responseMsg.getData();
        sw1 = data[3] & 0xff;
        sw2 = data[4] & 0xff;

        byte status = data[2];
        if (status != TLP224Message.STATUS_SUCCESS &&
            status != TLP224Message.STATUS_CARD_ERROR &&
            status != TLP224Message.STATUS_INTERRUPTED_EXCHANGE) {
            throw new IOException("TLP224Error " + status);
        }
    }

    /**
     * Format and send an ISO_OUT command to the CAD.
     * @exception InterruptedIOException if connection was closed in the
     * other thread
     * @exception IOException if a communication error happens
     */
    private void isoOut() throws IOException {

        byte[] data = commandMsg.getData();
        data[0] = TLP224Message.ACK;
        data[1] = 6;
        data[2] = TLP224Message.ISO_OUTPUT;
        System.arraycopy(header, 0, data, 3, 4);
        data[7] = (byte) Le;
        data[8] = commandMsg.computeLRC(8);
        commandMsg.setLength(9);

        sendTLP224Message(commandMsg);
        receiveTLP224Message(responseMsg);

        data = responseMsg.getData();

        int received = (data[1] == 3) ? 0 : responseMsg.getLength() - 6;

        if (received > 0) {
            if (output == null || output.length < received) {
                output = new byte[received];
            }
            System.arraycopy(data, 3, output, dataSize, received);
            dataSize = received;
        }

        sw1 = data[3 + received] & 0xff;
        sw2 = data[4 + received] & 0xff;

        byte status = data[2];
        if (status != TLP224Message.STATUS_SUCCESS &&
            status != TLP224Message.STATUS_CARD_ERROR &&
            status != TLP224Message.STATUS_INTERRUPTED_EXCHANGE) {
            throw new IOException("TLP224Error " + status);
        }
    }

    /**
     * Receive a TLP224 formatted message from the input stream.
     * This method reads bytes from the input stream until an EOT (0x03)
     * character is received. The resulting message is decoded and stored
     * in the TL224Message msg. In the event of a transmission error, this
     * method will send attempt error recovery by sending a TLP224 NACK
     * message to the sender. Up to 5 retries will be performed.
     * @param msg The object to store the received message in.
     * @exception IOException If an error occurs while reading from the input
     * stream.
     */
    private void receiveTLP224Message(TLP224Message msg) throws IOException {

        byte[] data = msg.getData();
        int tries = 0;

        try {
            while (true) {
                // Only retry link level errors 5 times before giving up.
                if (tries++ > 5) {
                    throw new IOException("TLP224Error " + 1);
                }
    
                // loop reading characters until EOT is received.
                boolean messageTooLong = false;
                boolean xmitError = false;
                int got = 0;
    
                int hiNibble, lowNibble;
    
                while ((hiNibble = 
                        in[currentSlot].read()) != TLP224Message.EOT) {
    
                    if (hiNibble == -1) {
                        throw new EOFException();
                    }
    
                    if ((lowNibble = in[currentSlot].read()) == -1) {
                        throw new EOFException();
                    }
    
                    xmitError |= (hiNibble < '0' || hiNibble > 'F' ||
                                  (hiNibble > '9' && hiNibble < 'A')) ||
                                 (lowNibble < '0' || lowNibble > 'F' ||
                                  (lowNibble > '9' && lowNibble < 'A'));
    
                    if (lowNibble == TLP224Message.EOT) {
                        break;
                    }
    
                    if (xmitError)
                        continue;
    
                    hiNibble -= hiNibble > '9' ? 0x37 : 0x30;
                    lowNibble -= lowNibble > '9' ? 0x37 : 0x30;
    
                    try {
                        data[got++] = (byte) ((hiNibble << 4) | lowNibble);
                    } catch (ArrayIndexOutOfBoundsException e) {
                        messageTooLong = true;
                    }
                }
    
                if (xmitError || got < 3) {
                    transmissionError();
                    continue;
                }
    
                if (messageTooLong) {
                    statusResponse(TLP224Message.STATUS_MESSAGE_TOO_LONG);
                    continue;
                }
    
                if (data[got - 1] != msg.computeLRC(got - 1) ||
                    data[1] != (byte) (got - 3)) {
                    // the message must contain a valid LRC, the second byte
                    // of the message is the command length. The total message
                    // length includes the ACK/NACK, the length and the LRC
                    transmissionError();
                    continue;
                }
    
                // The first byte of the message must be either an ACK or a NACK
                if (data[0] != TLP224Message.ACK &&
                    data[0] != TLP224Message.NACK) {
                    statusResponse(TLP224Message.STATUS_PROTOCOL_ERROR);
                    continue;
                }
    
                msg.setLength(got);
                break;
            }
        } catch (IOException e) {
            cardChanged = true;
            throw e;
        }
    }

    /**
     * Formats a TLP224Message into it's ASCII representation and
     * sends the message on the output stream.
     * @param msg TLP224 encoded message to be sent
     * @exception IOException is thrown in case there are any problems.
    */
    private void sendTLP224Message(TLP224Message msg) throws IOException {
        byte[] data = msg.getData();

        try {
            for (int i = 0; i < msg.getLength(); i++) {
                int nibble = data[i] >> 4 & 0xf;
                out[currentSlot].write(nibble + (nibble < 10 ? 0x30 : 0x37));
                nibble = data[i] & 0xf;
                out[currentSlot].write(nibble + (nibble < 10 ? 0x30 : 0x37));
            }
    
            out[currentSlot].write(TLP224Message.EOT);
            out[currentSlot].flush();
        } catch (IOException e) {
            cardChanged = true;
            throw e;
        }
    }

    /**
     * Send a one byte TLP224 status response.
     * @param code the status response
     * @throws IOException if IO error occurs
     */
    private void statusResponse(int code) throws IOException {
        TLP224Message msg = new TLP224Message();
        byte[] data = msg.getData();
        data[0] = TLP224Message.ACK;
        data[1] = 1;
        data[2] = (byte) code;
        data[3] = msg.computeLRC(3);
        msg.setLength(4);
        sendTLP224Message(msg);
    }

    /**
     * Send a TLP224 Transmission Error response.
     * @throws IOException if IO error occurs
     */
    private void transmissionError() throws IOException {
        TLP224Message msg = new TLP224Message();
        byte[] data = msg.getData();
        data[0] = TLP224Message.NACK;
        data[1] = 0;
        data[2] = msg.computeLRC(2);
        msg.setLength(3);
        sendTLP224Message(msg);
    }

    /**
     * Gets number of slots on a device. Default implementation returns 1 which
     * is ok for most devices.
     *
     * @return Number of slots on a device
     */
    public int getSlotCount() {
        return this.slotCount;
    }

    /* 
     * DEBUG: delete till end of file after debugging
     * /
    /** 
     * Formates byte array as hex string.
     * @param buf array to format
     * @param offset start offset in buffer
     * @param length length of array
     * @return result string
     * /
    public static String bytesToHex(byte[] buf, int offset, int length) {
        StringBuffer sb = new StringBuffer();
        
        for (int i = 0; i < length; i++) {
            int val = (buf[offset + i] & 0xFF);
            if (i != 0) {
                sb.append(' ');
            }
            sb.append(hexDigit(val >> 4));
            sb.append(hexDigit(val & 0x0F));
        }
        return new String(sb);
    }
    
    /**
     * Converts integer value (0-16) into hex representation.
     * @param val value to be converted
     * @return result hex char
     * /
    private static char hexDigit(int val) {
        val &= 0x0F;
        if (val <= 9) {
            return (char)(val + (int)'0');
        }
        return (char)(val - 0x0A + (int)'A');
    }
    
    /**
     * Calculates length of APDU. (For printing purpose.)
     * @param apdu formatted APDU 
     * @return length of APDU command
     * /
    public static int getAPDULength(byte[] apdu) {
        if (apdu == null) {
            return 0;
        }
        if (apdu.length > 5) {
            int Lc = apdu[4] & 0xFF;
            if (apdu.length == Lc + 5) {
                return Lc + 5;
            } else
            if (apdu.length > Lc + 5) {
                return Lc + 6;
            } else {
                return apdu.length;
            }
        } else {
            return apdu.length;
        }
    }
/* */
}
