/*
 * Copyright (c) 2005, 2010, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code 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.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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 in the LICENSE file that
 * accompanied this code).
 *
 * 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package org.openecard.scio.osx;

import static org.openecard.scio.osx.PCSC.SCARD_LEAVE_CARD;
import static org.openecard.scio.osx.PCSC.SCARD_RESET_CARD;
import static org.openecard.scio.osx.PCSC.SCARD_SHARE_DIRECT;
import static org.openecard.scio.osx.PCSC.SCARD_SHARE_SHARED;
import static org.openecard.scio.osx.PCSC.SCARD_W_REMOVED_CARD;
import static org.openecard.scio.osx.PCSC.SCardBeginTransaction;
import static org.openecard.scio.osx.PCSC.SCardConnect;
import static org.openecard.scio.osx.PCSC.SCardControl;
import static org.openecard.scio.osx.PCSC.SCardDisconnect;
import static org.openecard.scio.osx.PCSC.SCardEndTransaction;
import static org.openecard.scio.osx.PCSC.SCardStatus;
import static org.openecard.scio.osx.PCSC.SCardTransmit;
import static org.openecard.scio.osx.PlatformPCSC.SCARD_PROTOCOL_T0;
import static org.openecard.scio.osx.PlatformPCSC.SCARD_PROTOCOL_T1;

import javax.smartcardio.ATR;
import javax.smartcardio.Card;
import javax.smartcardio.CardChannel;
import javax.smartcardio.CardException;
import javax.smartcardio.CardPermission;

/**
 * Card implementation.
 * 
 * @since 1.6
 * @author Andreas Sterbenz
 */
final class CardImpl extends Card {

  private static enum State {
    OK, REMOVED, DISCONNECTED
  };

  // the terminal that created this card
  private final TerminalImpl terminal;

  // the native SCARDHANDLE
  final long cardId;

  // atr of this card
  private final ATR atr;

  // protocol in use, one of SCARD_PROTOCOL_T0 and SCARD_PROTOCOL_T1
  final int protocol;

  // the basic logical channel (channel 0)
  private final ChannelImpl basicChannel;

  // state of this card connection
  private volatile State state;

  // thread holding exclusive access to the card, or null
  private volatile Thread exclusiveThread;

  CardImpl(TerminalImpl terminal, String protocol) throws PCSCException {
    this.terminal = terminal;
    int sharingMode = SCARD_SHARE_SHARED;
    int connectProtocol;
    if (protocol.equals("*")) {
      connectProtocol = SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1;
    } else if (protocol.equalsIgnoreCase("T=0")) {
      connectProtocol = SCARD_PROTOCOL_T0;
    } else if (protocol.equalsIgnoreCase("T=1")) {
      connectProtocol = SCARD_PROTOCOL_T1;
    } else if (protocol.equalsIgnoreCase("direct")) {
      // testing
      connectProtocol = 0;
      sharingMode = SCARD_SHARE_DIRECT;
    } else {
      throw new IllegalArgumentException("Unsupported protocol " + protocol);
    }
    cardId = SCardConnect(terminal.contextId, terminal.name, sharingMode,
        connectProtocol);
    byte[] status = new byte[2];
    byte[] atrBytes = SCardStatus(cardId, status);
    atr = new ATR(atrBytes);
    this.protocol = status[1] & 0xff;
    basicChannel = new ChannelImpl(this, 0);
    state = State.OK;
  }

  void checkState() {
    State s = state;
    if (s == State.DISCONNECTED) {
      throw new IllegalStateException("Card has been disconnected");
    } else if (s == State.REMOVED) {
      throw new IllegalStateException("Card has been removed");
    }
  }

  boolean isValid() {
    if (state != State.OK) {
      return false;
    }
    // ping card via SCardStatus
    try {
      SCardStatus(cardId, new byte[2]);
      return true;
    } catch (PCSCException e) {
      state = State.REMOVED;
      return false;
    }
  }

  private void checkSecurity(String action) {
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      sm.checkPermission(new CardPermission(terminal.name, action));
    }
  }

  void handleError(PCSCException e) {
    if (e.code == SCARD_W_REMOVED_CARD) {
      state = State.REMOVED;
    }
  }

  public ATR getATR() {
    return atr;
  }

  public String getProtocol() {
    switch (protocol) {
    case SCARD_PROTOCOL_T0:
      return "T=0";
    case SCARD_PROTOCOL_T1:
      return "T=1";
    default:
      // should never occur
      return "Unknown protocol " + protocol;
    }
  }

  public CardChannel getBasicChannel() {
    checkSecurity("getBasicChannel");
    checkState();
    return basicChannel;
  }

  private static int getSW(byte[] b) {
    if (b.length < 2) {
      return -1;
    }
    int sw1 = b[b.length - 2] & 0xff;
    int sw2 = b[b.length - 1] & 0xff;
    return (sw1 << 8) | sw2;
  }

  private static byte[] commandOpenChannel = new byte[] { 0, 0x70, 0, 0, 1 };

  public CardChannel openLogicalChannel() throws CardException {
    checkSecurity("openLogicalChannel");
    checkState();
    checkExclusive();
    try {
      byte[] response = SCardTransmit(cardId, protocol, commandOpenChannel, 0,
          commandOpenChannel.length);
      if ((response.length != 3) || (getSW(response) != 0x9000)) {
        throw new CardException("openLogicalChannel() failed, card response: "
            + PCSC.toString(response));
      }
      return new ChannelImpl(this, response[0]);
    } catch (PCSCException e) {
      handleError(e);
      throw new CardException("openLogicalChannel() failed", e);
    }
  }

  void checkExclusive() throws CardException {
    Thread t = exclusiveThread;
    if (t == null) {
      return;
    }
    if (t != Thread.currentThread()) {
      throw new CardException("Exclusive access established by another Thread");
    }
  }

  public synchronized void beginExclusive() throws CardException {
    checkSecurity("exclusive");
    checkState();
    if (exclusiveThread != null) {
      throw new CardException(
          "Exclusive access has already been assigned to Thread "
              + exclusiveThread.getName());
    }
    try {
      SCardBeginTransaction(cardId);
    } catch (PCSCException e) {
      handleError(e);
      throw new CardException("beginExclusive() failed", e);
    }
    exclusiveThread = Thread.currentThread();
  }

  public synchronized void endExclusive() throws CardException {
    checkState();
    if (exclusiveThread != Thread.currentThread()) {
      throw new IllegalStateException(
          "Exclusive access not assigned to current Thread");
    }
    try {
      SCardEndTransaction(cardId, SCARD_LEAVE_CARD);
    } catch (PCSCException e) {
      handleError(e);
      throw new CardException("endExclusive() failed", e);
    } finally {
      exclusiveThread = null;
    }
  }

  public byte[] transmitControlCommand(int controlCode, byte[] command)
      throws CardException {
    checkSecurity("transmitControl");
    checkState();
    checkExclusive();
    if (command == null) {
      throw new NullPointerException();
    }
    try {
      byte[] r = SCardControl(cardId, controlCode, command);
      return r;
    } catch (PCSCException e) {
      handleError(e);
      throw new CardException("transmitControlCommand() failed", e);
    }
  }

  public void disconnect(boolean reset) throws CardException {
    if (reset) {
      checkSecurity("reset");
    }
    if (state != State.OK) {
      return;
    }
    checkExclusive();
    try {
      SCardDisconnect(cardId, (reset ? SCARD_LEAVE_CARD : SCARD_RESET_CARD));
    } catch (PCSCException e) {
      throw new CardException("disconnect() failed", e);
    } finally {
      state = State.DISCONNECTED;
      exclusiveThread = null;
    }
  }

  public String toString() {
    return "PC/SC card in " + terminal.getName() + ", protocol "
        + getProtocol() + ", state " + state;
  }

  protected void finalize() throws Throwable {
    try {
      if (state == State.OK) {
        SCardDisconnect(cardId, SCARD_LEAVE_CARD);
      }
    } finally {
      super.finalize();
    }
  }

}