/*
 *   
 *
 * 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.cardreader;

import java.io.IOException;
import java.io.InterruptedIOException;
import com.sun.midp.security.*;

/**
 * This class represents card slot abstraction.
 */
public class CardSlot {
    /**
     * Device object.
     */
    private CardDevice device;

    /**
     * Local slot number (inside the device).
     */
    private int slotNumber;

    /**
     * Security token for this slot.
     */
    SecurityToken securityToken;

    /**
     * Indicates that the slot has been opened.
     */
    private boolean opened;

    /** Unique card session identifier. */
    private int cardSessionId;

     /** This field is used to assign unique card session identifiers. */
    private static int sessionId = 1;

    /**
     * ATR bytes kept since last reset.
     */
    private byte[] ATR;

    /**
     * A flag which shows if the card was changed.
     */
    private boolean cardChanged;

    /**
     * isAlive command APDU.
     */
    private byte[] isAliveAPDU;

    /**
     * isAlive command response buffer.
     */
    private byte[] isAliveResponse;

    /**
     * Creates card slot on the specified device.
     *
     * @param device Card device for which the slot is created
     * @param slot Local device slot number
     * @param token Security token for this slot
     * @throws IOException If slot creation failed.
     */
    public CardSlot(CardDevice device, int slot, SecurityToken token)
	throws IOException {
        this.device = device;
        this.slotNumber = slot;
        this.securityToken = token;
        opened = false;
        cardChanged = true;
        isAliveAPDU = new byte[] {0, 0x70, (byte) 0x80, 0};
        isAliveResponse = new byte[2];
    }

    /**
     * Performs data transfer to the device slot.
     * It must be called where slot is locked.
     *
     * @param request Request APDU bytes
     * @param response Response bytes
     * @return Length of response
     * @throws IOException If a data transfer failed or
     *         the card was changed.
     */
    public int xferData(byte[] request, byte[] response)
            throws IOException {
        return xferData(false, request, response);
    }

    /**
     * Performs data transfer to the device slot.
     * It must be called where slot is locked.
     *
     * @param aliveChecking true if isAlive command is performed,
     *                      false otherwise
     * @param request Request APDU bytes
     * @param response Response bytes
     * @return Length of response
     * @throws IOException If a data transfer failed or
     *         the card was changed.
     */
    private int xferData(boolean aliveChecking,
            byte[] request, byte[] response)
            throws IOException {
        int bytes_read;

        if (isCardChanged() && !aliveChecking) {
            unlockSlot();
            throw new InterruptedIOException("Card changed");
        }

        try {
            bytes_read = device.xfer(request, response);
        }
        catch (IOException e) {
            if (isCardChanged() && !aliveChecking) {
                unlockSlot();
                throw new InterruptedIOException("Card changed: " + e);
            }
            unlockSlot();
            throw e;
        }
        catch (RuntimeException e) { // We must unlock slot
            unlockSlot();
            throw e;
        }
        if (isCardChanged() && !aliveChecking) {
            unlockSlot();
            throw new InterruptedIOException("Card changed");
        }
        return bytes_read;
    }

    /**
     * Checks if the the connection is still live or not. It
     * is a public wrapper for <code>doIsAlive</code>. Also
     * checks if a card was changed. Slot must not be locked.
     * @return <code>true</code> if the connection is alive
     */
    public boolean isAlive() {
        boolean alive = doIsAlive();
        if (!alive) {
            try {
                if (!alive) {
                    lockSlot();
                    boolean changed = isCardChanged();
                    unlockSlot();
                    if (changed) {
                        alive = doIsAlive();
                    }
                }
            } catch (IOException e) {
                alive = false;
            }
        }
        return alive;
    }

    /**
     * Checks if the the connection is still live or not by trying to
     * send an APDU to close channel 0. If we get an IOException back
     * that means a card or cref is gone.
     * @return <code>true</code> if the connection is alive
     */
    private boolean doIsAlive() {
        try {
            lockSlot();
            xferData(true, isAliveAPDU, isAliveResponse);
            unlockSlot();
            return true;
        } catch (IOException e) {
            return false;
        }
    }

    /**
     * Gets ATR bytes from the device.
     *
     * @return ATR bytes.
     */
    public byte[] getATR() {
        byte[] result;

        if (opened && ATR.length > 0) {
            result = new byte[ATR.length];
            System.arraycopy(ATR, 0, result, 0, ATR.length);
        } else {
            result = null;
        }
        return result;
    }

    /**
     * Gets local device slot number.
     *
     * @return Local slot number.
     */
    public int getSlotNumber() {
        return slotNumber;
    }

    /**
     * Returns the card session identifier.
     * This number is different for different card sessions.
     * @return the card session identifier or -1 when slot is closed
     */
    public int getCardSessionId() {
        return (opened && !cardChanged) ? cardSessionId : -1;
    }

    /**
     * Starts operations with the slot.
     * All the transfer operations (<code>xferData</code>)
     * should be performed when the device is locked.
     * If any problem occurs an unlock must be done.
     *
     * @throws IOException If something fails.
     */
    public void lockSlot() throws IOException {
        try {
            device.lock();
            open();
            device.selectSlot(slotNumber);
        }
        catch (IOException e) {
            unlockSlot();
            throw e;
        }
    }

    /**
     * Ends operations with the slot.
     *
     * IOException is ignored.
     */
    public void unlockSlot() {
        try {
            device.unlock();
        }
        catch (IOException e) {}
    }

    /**
     * Makes sure that slot is ready for connection.
     *
     * @return true if this is first connection to inserted card
     * @throws IOException If something fails.
     */
    public boolean initConnection() throws IOException {
        boolean prevCardChanged;
        lockSlot();
        prevCardChanged = isCardChanged();
        unlockSlot();
        cardChanged = false;
        return prevCardChanged;
    }

    /**
     * Initializes ACL for the slot.
     */
    public void initACL() {
        boolean changed;
        try {
            lockSlot();
            changed = isCardChanged();
            unlockSlot();
            if (changed) {
                com.sun.satsa.acl.AccessControlManager.init(slotNumber);
            }
        } catch (IOException e) {} // ignored
    }

    /**
     * Closes the slot in case of error.
     * <code>device.closeSlot</code> usually
     * does nothing.
     * Used for socket-like 'devices'.
     *
     * @throws IOException If something fails.
     */
    public void closeSlot() throws IOException {
        if (opened) {
            device.closeSlot(slotNumber);
        }
        opened = false;
    }

    /**
     * Gets device of this slot. For using in derived classes.
     *
     * @return Slot device
     */
    protected CardDevice getDevice() {
        return this.device;
    }

    /**
     * Checks if this slot is SAT slot.
     * @return SAT check result
     * @throws IOException If an error occured.
     */
    public boolean isSAT() throws IOException {
        return device.isSatSlot(slotNumber);
    }

    /**
     * Checks if the card was withdrawn or inserted.
     * The device must be locked.
     *
     * @return true if a card was changed, false otherwise
     * @throws IOException If something wrong.
     */
    private boolean isCardChanged() throws IOException {
        try {
            if (device.isCardChanged()) {
                closeSlot();
            }
            open();
        } catch (IOException e) {
            unlockSlot();
            throw e;
        }
        if (cardChanged) {
            return true;
        }
        return false;
    }

    /**
     * Tries to open slot if it closed. Performes the 'RESET' command.
     * The device must be locked.
     *
     * @throws IOException If open fails.
     */
    private void open() throws IOException {
        boolean openedSuccessfuly = false;
        if (!opened) {
            try {
                device.openSlot(slotNumber, securityToken);
                openedSuccessfuly = true;
                device.selectSlot(slotNumber);
                device.reset();
                cardSessionId = sessionId++;
            } catch (IOException ie) {
                if (openedSuccessfuly) {
                    try {
                        device.closeSlot(slotNumber);
                    } catch (IOException ign) {} // ignored
                }
                throw new IOException("Cannot open slot #" +
                            slotNumber + ": " + ie);
            }
            ATR = device.getATR();
            cardChanged = true;
            opened = true;
        }
    }
}
