/*
 *   
 *
 * 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.midp.security.SecurityToken;
import javax.microedition.io.*;
import com.sun.midp.main.Configuration;

import java.io.*;

/**
 * This class provides an interface to the low level APDU protocol 
 * details as described below:
 * 
 * 1. Manage Card power up/reset. 
 * 2. APDU scheduler-Can assign priorities to APDUs to synchronize card 
 * access. Synchronization is required between APDUs being sent by native 
 * telecom software and APDUs sent by J2ME application. The reference 
 * implementation will only have APDUs sent by the J2ME application which 
 * would all have the same priority.  
 * 3. APDU Dispatcher-Support APDU exchange with the card. It has transaction 
 * capabilities to receive the APDUs from applications on the consumer device 
 * and return the responses to the corresponding application. 
 * 4. J2ME applications are oblivious of the logical channel information. 
 * Getting the logical channel for communicating with the smart card 
 * application is the responsibility of the APDUManager which sends 
 * appropriate APDUs to the card to get the logical channels 
 * assigned for a new connection or close channels when the connection is 
 * closed.
 */
public class APDUManager {

    /** SAT selection APDU for testing purposes. */
    private static final String SAT_APDU_PROP = 
        "com.sun.midp.io.j2me.apdu.satselectapdu";
    
    /** Contains references to all supported slots. */
    static Slot[] slots;
    
    /**
     * Creates SAT connection.
     *
     * @param slot Slot number
     * @param securityToken Security token for this class
     * @return new connection handle
     * @exception IOException when SIM is not present or
     * connection cannot be established with the card.
     */
    static Handle openSATConnection(int slot, SecurityToken securityToken)
        throws IOException {
        
        Handle h = null;
        
        checkSlotNumber(slot);
        Slot cardSlot = slots[slot];
        
        String satAPDU = Configuration.getProperty(SAT_APDU_PROP);
        if (satAPDU != null) {
            byte[] apdu = new byte[24];
            boolean ok;
            try {
                int len = parseDottedBytes(satAPDU, apdu, 0);
                ok = len >= 10 && len <= 22; // 5 bytes hdr + AID + Le
                int Lc = (apdu[4] & 0xFF);
                if (ok && (len < Lc + 5 || 
                           len > Lc + 5 + 1 ||
                           apdu[0] != (byte)0x00 ||
                           apdu[1] != (byte)0xA4 ||
                           apdu[2] != (byte)0x04)) {
                    ok = false;
                }
                if (ok && len == Lc + 5) {
                    apdu[len] = 0x7F;
                }
            } catch (NullPointerException npe) {
                ok = false;
            } catch (IndexOutOfBoundsException iobe) {
                ok = false;
            } catch (IllegalArgumentException iae) {
                ok = false;
            }

            if (ok) {
                h = selectApplication(true, apdu, slot, securityToken);
            }
        }
        if (h == null) {
            if (!isAlive(cardSlot)) {
                throw new ConnectionNotFoundException("SIM not found");
            }
            h = new Handle(slot, 0, securityToken);
        }
        return h;
    }
    
    
    /**
     * Opens a connection to a smart card for reading of ACL.
     * This method is called from <code>reset</code>
     * method, so it does not need <code>synchronized</code>
     * statement.
     *
     * @param apdu The APDU that will be used for opening
     * @param slot Slot number
     * @param securityToken Security token for this class
     * @return new connection handle
     * @exception IOException when a card is not present or
     * connection cannot be established with the card.
     */
    public static Handle openACLConnection(byte[] apdu, int slot, 
                SecurityToken securityToken)
        throws IOException {
        
        checkSlotNumber(slot);
        return selectApplication(true, apdu, slot, securityToken);
    }
    
    /**
     * Initializes ACL for the slot (if needed). This method is invoked
     * when an establishment of new connection is being performed. 
     * @param slot The slot number
     * @param securityToken Security token for this class
     */
    public static void initACL(int slot, SecurityToken securityToken) {
        
        try {
            checkSlotNumber(slot);
            Slot cardSlot = slots[slot];
            if (!cardSlot.powered) {
                reset(cardSlot); // here a reading of ACL is being performed
            }
        } catch (IOException e) {} // ignored
    }

    /**
     * Verifies that slot number is correct. Invokes init method
     * if necessary.
     * @param slot the slot number
     * @throws ConnectionNotFoundException if slot number is wrong
     */
    public static void checkSlotNumber(int slot) throws
            ConnectionNotFoundException {

        try {
            init();
        } catch (IOException e) {
            throw new ConnectionNotFoundException(
                "Invalid configuration: " + e);
        }
        if (slot < 0 || slot >= getSlotCount()) {
            throw new ConnectionNotFoundException(
                    "Invalid slot identifier: " + slot);
        }
    }
    
    /**
     * Checks if this slot is SAT slot.
     * @param slot the slot number
     * @return SAT check result
     * @throws IOException If an error occured.
     */
    public static boolean isSatSlot(int slot) throws IOException {
        checkSlotNumber(slot);
        return isSAT(slot);
    }

    /**
     * This method returns the ATR received from the card.
     * @param slot the slot number
     * @return ATR information received from the card at startup or
     * reset. In case of I/O troubles returns null.
     */
    public static byte[] getATR(int slot) {
        byte[] result = null;
        Slot cardSlot = slots[slot];
        
        if (isAlive(cardSlot) && cardSlot.atr != null) {
            int len = cardSlot.atr.length;
            result = new byte[len];
            System.arraycopy(cardSlot.atr, 0, result, 0, len);
        }
        return result;
    }

    /**
     * Returns the number of slots.
     * @return the number of slots. If error occured it returns 0.
     */
    public static int getSlotCount() {
        try {
            init();
        }
        catch (IOException e) {
            return 0;
        }
        return slots.length;
    }


    /**
     * Checks if the the connection is still live or not. It sends special
     * <code>isAliveAPDU</code> to the card and checks if an error occured.
     *
     * @param slot The slot object.
     * @return <code>true</code> if the connection is alive, <false> otherwise
     */
    private static boolean isAlive(Slot slot) {
        int tries = 2;
        do {
            if (!slot.powered) {
                try {
                    clean(slot);
                    reset(slot);
                } catch (IOException e) {} // ignored
            }
            try {
                exchangeAPDU(slot, slot.isAliveAPDU);
                return true;
            } catch (IOException e) {} // ignored
        } while (--tries > 0);
        return false;
    }
    
    /**
     * Cleans the slot object before a reset.
     *
     * @param slot The slot object.
     */
    private static void clean(Slot slot) {
        slot.basicChannelInUse = false;
        slot.SIMPresent = false;
        slot.FCI = null;
    }

    /**
     * This method is used to close connection with the card. If the
     * channel number passed to this method is for basic channel then
     * the basic channel is marked as available and nothing is
     * communicated with the card.
     * If the channel is not the basic channel, a request to close the
     * channel is sent to the card.
     * @param slot the slot object
     * @param channel channel number
     * @exception IOException if a communication error happens
     */
    private static void closeChannel(Slot slot, int channel) 
            throws IOException {
        
        if (channel == 0) {
            slot.basicChannelInUse = false;
            return;
        }

        try {
            slot.closeChannelAPDU[3] = (byte) channel;
            exchangeAPDU(slot, slot.closeChannelAPDU);
        } catch (IOException ioException) {
            throw new IOException("Error closing connection: " + ioException);
        }
    }

    /**
     * The public method which should be called when application
     * selection is required. Calls an internal method.
     *
     * @param selectAPDU byte encoded selection APDU
     * @param slot slot number
     * @param securityToken Security token for this class
     * @return new connection handle
     * @exception IOException when selection is not successful
     */
    public static Handle selectApplication(byte[] selectAPDU, int slot,
                  SecurityToken securityToken) throws IOException {
        checkSlotNumber(slot);
        return selectApplication(false, selectAPDU, slot, securityToken);
    }
     
    /**
     * This method is called when there is a connection creation is in 
     * progress
     * and specifically card application selection is required. If the card 
     * application selection is successful
     * this method gets the channel information from the CAD which it returns
     * to the APDUConnection object.
     *
     * @param forSAT is this selection is making for SAT?
     * @param selectAPDU byte encoded selection APDU
     * @param slot slot number
     * @param securityToken Security token for this class
     * @return new connection handle
     * @exception IOException when selection is not successful
     */
    private static Handle selectApplication(boolean forSAT,
                  byte[] selectAPDU, int slot,
                  SecurityToken securityToken) throws IOException {

        int channel;
        
        Slot cardSlot = slots[slot]; // we have checked slot number earlier

        // Test if 'POWER UP' is needed or a card was changed
        if (!isAlive(cardSlot)) {
            throw new ConnectionNotFoundException("SmartCard not found");
        }

        if (!forSAT && (cardSlot.basicChannelInUse || cardSlot.SIMPresent)) {
            // get another channel for communication.
            byte[] response = exchangeAPDU(cardSlot, cardSlot.getChannelAPDU);
            if (response.length  == 2) {
                // just got back the status word
                throw new IOException("No logical channel available");
            }
            // new channel number is in the first byte of response
            channel = response[0];
        } else {
            cardSlot.basicChannelInUse = true;
            channel = 0;
        }
        
        selectAPDU[0] = (byte)((selectAPDU[0] & 0xFC) | channel);

        byte[] result = exchangeAPDU(cardSlot, selectAPDU);

        int sw1 = result[result.length - 2] & 0xFF;
        int sw2 = result[result.length - 1] & 0xFF;
        if ((sw1 << 8) + sw2 != 0x9000) {
            closeChannel(cardSlot, channel);
            throw new ConnectionNotFoundException(
                    "Card application selection failed");
        }
        cardSlot.FCI = result;
        return new Handle(slot, channel, securityToken);
    }

    /**
     * This method reads the configuration file for number of slots
     * and their parameters and performs necessary initialization.
     * @exception IOException if there are any config problem
     */
    synchronized private static void init() throws IOException {

        if (slots != null) {
            return;
        }
        int slotCount = init0();

        slots = new Slot[slotCount];
        for (int i = 0; i < slotCount; i++) {
            slots[i] = new Slot(i);
        }
    }

    /**
     * This public method takes in the command APDU in the form of 
     * a byte array and calls the native exchangeAPDU0 method 
     * to send the APDU to the card. If there are no errors, this method
     * gets the response APDU data from the card and returns that.
     * @param h connection handle
     * @param apduData APDU data in byte array form
     * @return response APDU data in byte array form
     * @exception IOException if there are any IO problems
     */
    public static byte[] exchangeAPDU(Handle h, byte[] apduData)
            throws IOException {
        byte[] retData;
        
        Slot slot = h.cardSlot;
        try {
            synchronized (slot) {
                byte[] respBuffer = slot.respBuffer;
                int respLen = exchangeAPDU0(h, null, apduData, respBuffer);
                retData = new byte[respLen];
                System.arraycopy(respBuffer, 0, retData, 0, respLen);
            }
            return retData;
        } catch (IOException e) {
            if (!slot.powered) {
                // power up the slot after removal/insertion
                isAlive(slot);
            }
            throw e;
        }
    }
    
    /**
     * This internal method takes in the command APDU in the form of 
     * a byte array and calls the native exchangeAPDU0 method 
     * to send the APDU to the card. If there are no errors, this method
     * gets the response APDU data from the card and returns that.
     * @param slot the slot object
     * @param apduData APDU data in byte array form
     * @return response APDU data in byte array form
     * @exception IOException if there are any IO problems
     */
    private static byte[] exchangeAPDU(Slot slot, byte[] apduData)
            throws IOException {
        byte[] retData;
        synchronized (slot) {
            byte[] respBuffer = slot.respBuffer;
            int respLen = exchangeAPDU0(null, slot, apduData, respBuffer);
            retData = new byte[respLen];
            System.arraycopy(respBuffer, 0, retData, 0, respLen);
        }
        return retData;
    }
    /**
     * Performs reset of device.
     * Saves ATR into provided slot object. After the reset invokes 
     * an ACL loading for this slot.
     *
     * @param cardSlot the slot object to be reset
     * @throws IOException If a reset failed.
     */
    private static void reset(Slot cardSlot) throws IOException {
        synchronized (cardSlot) {
            byte[] atr = reset0(cardSlot);
            cardSlot.atr = atr;
            // after reset we must reload access control file
            com.sun.satsa.acl.AccessControlManager.init(cardSlot.slot);
        }
    }
    
    /**
     * Closes the connection.
     * @param h connection handle
     * @exception IOException if there are any IO problems
     */
    public static void closeConnection(Handle h) throws IOException {
        Slot cardSlot = h.cardSlot;
        synchronized (cardSlot) {
            h.opened = false;
            if (h.cardSessionId != cardSlot.cardSessionId) {
                throw new IOException();
            }
            closeChannel(cardSlot, h.channel);
        }
    }
    
    /**
     * Parses string that contains hexadecimal byte values separated by
     * dots. May throw runtime exceptions.
     * @param src source string
     * @param dest destination array
     * @param offset target offset
     * @return number of bytes parsed
     */
    public static int parseDottedBytes(String src, byte[] dest,
                                       int offset) {

        int i = 0;
        int len = 0;
        int j;

        while (i != src.length() + 1) {

            if ((j = src.indexOf('.', i)) == -1) {
                j = src.length();
            }

            int l = Integer.parseInt(src.substring(i, j), 16);
            if (l != (l & 0xff)) {
                throw new IllegalArgumentException();
            }
            dest[offset + len++] = (byte) l;
            i = j + 1;
        }
        return len;
    }
    
    /**
     * Performs data transfer to the device. This method must be called within
     * <code>synchronize</code> block with the Slot object.
     *
     * @param h Connection handle. Can be null for internal purposes
     * @param slot Slot object. Unused when <code>h</code> is not null. 
     * Must be provided if <code>h</code> is <code>null</code>.
     * @param request Buffer with request data
     * @param response Buffer for response data
     * @return Length of response data
     * @exception NullPointerException if any needed parameter is null
     * @exception IllegalArgumentException if request does not contain proper 
     * APDU
     * @exception InterruptedIOException if the connection handle is suddenly 
     * closed
     * in the middle of exchange or the card was removed and inserted again
     * @exception IOException if any I/O troubles occured
     */
    private static native int exchangeAPDU0(Handle h, Slot slot, 
                          byte[] request, byte[] response) throws IOException;
    
    /**
     * Performs reset of the card in the slot. This method must be called within
     * <code>synchronize</code> block with the Slot object.
     *
     * @param slot Slot object
     * @return byte array with ATR
     * @exception NullPointerException if parameter is null
     * @exception IOException if any i/o troubles occured
     */
    private static native byte[] reset0(Slot slot) throws IOException;
    
    /**
     * Initializes the device.
     *
     * @return number of supported slots 
     * @exception CardDeviceException If configuration failed.
     * @exception IOException in case of I/O problems.
     */
    private static native int init0() throws IOException;
    
    /**
     * Checks if this slot is SAT slot. This method is invoked once after 
     * a reset of the card.
     * @param slot Slot number
     * @return <code>true</code> if the slot is dedicated for SAT,
     *         <code>false</code> if not
     * @exception IOException in case of error
     */
    private static native boolean isSAT(int slot);

}
