/* 
* Copyright 2016 Agencia Estatal de Administración Tibutaria
*
* Licencia con arreglo a la EUPL, Versión 1.1 o –en cuanto
* sean aprobadas por la Comisión Europea– versiones
* posteriores de la EUPL (la «Licencia»);
* Solo podrá usarse esta obra si se respeta la Licencia.
* Puede obtenerse una copia de la Licencia en:
*  
* https://joinup.ec.europa.eu/software/page/eupl
*  
* Salvo cuando lo exija la legislación aplicable o se acuerde
* por escrito, el programa distribuido con arreglo a la
* Licencia se distribuye «TAL CUAL», SIN GARANTÍAS NI CONDICIONES
* DE NINGÚN TIPO, ni expresasni implícitas.
*
* Véase la Licencia en el idioma concreto que rige los permisos y 
* limitaciones que establece la Licencia.
*/ 

package gestor.utiles;

import gestor.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.List;
import javax.smartcardio.*;

import javax.smartcardio.Card;
import javax.smartcardio.CardChannel;
import javax.smartcardio.CardException;
import javax.smartcardio.CardTerminal;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
import javax.smartcardio.TerminalFactory;

public final class LectorSmartCard {

   boolean listo = false;
   CardTerminals terminals;
   CardTerminal presente;
   List<CardTerminal> terminales;
   ThreadCheckCards hiloCheck;
   ISmartCardAvisable formAvisable = null;

   public LectorSmartCard() {
      listo = buscarLectores();
      if (listo) {
         iniciarHiloAviso();
      }
   }

   public void registrarVentanaAvisable(ISmartCardAvisable av) {
      formAvisable = av;
   }

   boolean buscarLectores() {
      try {
         TerminalFactory factory = TerminalFactory.getDefault();
         terminals = factory.terminals();
         terminales = terminals.list();
         return terminales.size() > 0;
      } catch (CardException exc) {
         return false;
      }
   }

   void iniciarHiloAviso() {
      hiloCheck = new ThreadCheckCards();
      hiloCheck.start();
   }

   class ThreadCheckCards extends Thread {

      @Override
      public void run() {
         while (true) {
            try {
               terminals.waitForChange();
               for (final CardTerminal terminal : terminals.list(javax.smartcardio.CardTerminals.State.CARD_INSERTION)) {
                  Runnable runnable = () -> {
                      tarjetaInsertada(terminal);
                  };
                  try {
                     javax.swing.SwingUtilities.invokeLater(runnable);
                  } catch (Exception exc) {
                  }
               }

               if(terminals.list(javax.smartcardio.CardTerminals.State.CARD_REMOVAL).size() > 0) {
                  Runnable runnable = () -> {
                      tarjetaExtraida();
                  };
                  try {
                     javax.swing.SwingUtilities.invokeLater(runnable);
                  } catch (Exception exc) {
                  }
               }
            } catch (CardException cex) {
            }
         }
      }
   }

   public void tarjetaInsertada(CardTerminal terminal) {
      DatosDNIe datos = leerDatosDNIe(terminal);
      if(datos != null && formAvisable != null) {
         formAvisable.avisoSmartCardInsertada(datos.nif, datos.nombre);
      }
   }

   public DatosDNIe leerDatosDNIe(CardTerminal terminal) {
      int intentos = 0;
      DatosDNIe datos = null;
      try {
         while(intentos < 3) {
            datos = leerDNIe(terminal);
            if(datos != null) {
               break;
            }
            intentos++;
         }
      } catch (Exception exc) {
         String s = exc.getMessage();
      }
      return datos;
   }

   public void tarjetaExtraida() {
      try {
         if(formAvisable != null) {
            formAvisable.avisoSmartCardExtraida();
         }
      } catch (Exception exc) {}
   }

   public boolean hayTarjetaPresente() {
      if (!listo) {
         return false;
      }
      presente = tarjetaPresente();
      return presente != null;
   }

   public void mostrarDialogoExtraerTarjeta (javax.swing.JFrame parent) {
      if(hayTarjetaPresente()) {
         DlgMensajeGenerico dlg = new DlgMensajeGenerico(parent);
         dlg.setLinea1(Global.imagenEnHtml("/recursos/logo_dnie.png", "", false));
         dlg.setLinea2(Global.trad.valor("RetirarDnie"));
         dlg.centrarLineas(true, true, true);
         registrarVentanaAvisable(dlg);
         dlg.setVisible(true);
         registrarVentanaAvisable(null);
      }
   }

   CardTerminal tarjetaPresente() {
      if (listo) {
         try {
             for (CardTerminal terminal : terminales) {
                 if (terminal.isCardPresent()) {
                     return terminal;
                 }
             }
         } catch (CardException exc) {
         }
      }
      return null;
   }

   public DatosDNIe leerDNIe() {
      if (listo) {
         try {
            return leerDNIe(presente);
         } catch (Exception exc) {
         }
      }
      return null;
   }

   public DatosDNIe leerDNIe(CardTerminal terminal) throws CardException, IOException {
      DatosDNIe datos = null;
      int intentos = 0;
      if (listo) {
         if (terminal.isCardPresent()) {
            while(datos == null && intentos < 5) {
               try {
                  Card card = terminal.connect("*"); //T=0, T=1 or T=CL(not needed)
                  if (esDNIe(card.getATR().getBytes())) {
                     datos = leerTarjetaDNIe(card.getBasicChannel());
                  }
                  card.disconnect(false);
               } catch (Exception e) {
//                  StringWriter sw = new StringWriter();
//                  PrintWriter pw = new PrintWriter(sw);
//                  e.printStackTrace(pw);
//                  Global.panel.msgBox("En leerDNIe: \n" +  sw.toString());
               }
               intentos++;
            }
         }
      }
      return datos;
   }

   private boolean esDNIe(byte[] atrCard) {
      int j = 0;
      boolean found = false;

      final byte[] dnie_v_1_0_Atr = {
         (byte) 0x3B, (byte) 0x7F, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x6A, (byte) 0x44,
         (byte) 0x4E, (byte) 0x49, (byte) 0x65, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
         (byte) 0x00, (byte) 0x00, (byte) 0x90, (byte) 0x00};

      final byte[] dnie_v_1_0_Mask = {
         (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
         (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
         (byte) 0x00, (byte) 0x00, (byte) 0xFF, (byte) 0xFF};

      //Es una tarjeta DNIe?
      if (atrCard.length == dnie_v_1_0_Atr.length) {
         found = true;
         while (j < dnie_v_1_0_Atr.length && found) {
            if ((atrCard[j] & dnie_v_1_0_Mask[j]) != (dnie_v_1_0_Atr[j] & dnie_v_1_0_Mask[j])) {
               found = false; //No es una tarjeta DNIe
            }
            j++;
         }
      }
      return found;
   }

   private DatosDNIe leerTarjetaDNIe(CardChannel ch) throws CardException, IOException {
      ResponseAPDU responseAPDU;
      byte[] command;

      // El primer paso es posicionarse en el fichero raiz de toda la estructura (Master File)
      command = new byte[]{(byte) 0x00, (byte) 0xa4, (byte) 0x04, (byte) 0x00, (byte) 0x0b, (byte) 0x4D, (byte) 0x61,
                 (byte) 0x73, (byte) 0x74, (byte) 0x65, (byte) 0x72, (byte) 0x2E, (byte) 0x46, (byte) 0x69, (byte) 0x6C, (byte) 0x65};
      responseAPDU = ch.transmit(new CommandAPDU(command));
      if (responseAPDU.getSW() != 0x9000) {
         return null;
      }

      //Seleccionamos el directorio PKCS#15 5015
      command = new byte[]{(byte) 0x00, (byte) 0xA4, (byte) 0x00, (byte) 0x00, (byte) 0x02, (byte) 0x50, (byte) 0x15};
      responseAPDU = ch.transmit(new CommandAPDU(command));

      if (responseAPDU.getSW() != 0x9000) {
         return null;
      }

      //Seleccionamos el Certificate Directory File (CDF) del DNIe 6004
      command = new byte[]{(byte) 0x00, (byte) 0xA4, (byte) 0x00, (byte) 0x00, (byte) 0x02, (byte) 0x60, (byte) 0x04};
      responseAPDU = ch.transmit(new CommandAPDU(command));

      if (responseAPDU.getSW() != 0x9000) {
         return null;
      }
      byte[] data = responseAPDU.getData();
      int lres = bytes2int(data[7], data[8]);   // Se obtiene la longitud de la respuesta.
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      for (int x = 0; x < (lres / 255 + 1); x++) {  // Se obtiene respuesta en bloques de 255 bytes.
         byte[] rec = recupera(ch, lres, x);
         if (rec != null) {
            baos.write(rec);
         }
      }
      byte[] data6004 = baos.toByteArray();
      byte[] blo1 = getAsn1(data6004);   // Se obtiene el primer dato del certificado CertAutenticacion.
      return extraeNombreNif(blo1);
   }

   private DatosDNIe extraeNombreNif(byte[] certiByte) throws UnsupportedEncodingException {
      String certi = new String(certiByte, "UTF-8");
      char[] posNifChar = new char[]{48, 16, 6, 3, 85, 4, 5, 19};
      int posNif = certi.indexOf(new String(posNifChar)) + 9;
      String nif = certi.substring(posNif, posNif + 9);
      char[] posNameChar = new char[]{6, 3, 85, 4, 3};
      int posName2 = certi.indexOf(new String(posNameChar)) + 7;
      int finName = certi.indexOf("(AUTENT");
      if (finName == -1) {
         finName = posName2 + 30;
      }
      String nombre = certi.substring(posName2, finName).trim();
      DatosDNIe ddnie = new DatosDNIe(nif, nombre);
      return ddnie;
   }

   private byte[] getAsn1(byte[] data6004) throws IOException {
      ByteArrayInputStream is = new ByteArrayInputStream(data6004);
      ByteArrayOutputStream baos1 = new ByteArrayOutputStream();
      baos1.write(is.read());  // tipo
      baos1.write(is.read());  // longitud mayor 128
      int l1 = is.read();      // primer byte longitud.
      baos1.write(l1);
      int l2 = is.read();		// segundo byte longitud.
      baos1.write(l2);
      int longitud = bytes2int((byte) l1, (byte) l2);
      for (int i = 0; i < longitud; i++) {
         baos1.write(is.read());
      }
      byte[] asn1 = baos1.toByteArray();
      return asn1;
   }

   private byte[] recupera(CardChannel ch, int longitud, int tramo) throws CardException {
      int tramofinal = longitud / 255;
      int inicio = tramo * 255;
      int fin = inicio + 255;
      int solicitar = 255;
      int offset = fin - 255;
      if (tramo == tramofinal) {
         fin = longitud;
         solicitar = longitud - inicio;
      }

      byte[] offsetBynary = int2byte(offset);
      byte[] solicitarBynary = int2byte(solicitar);
      byte[] command = new byte[]{(byte) 0x00, (byte) 0xB0, offsetBynary[0], offsetBynary[1], solicitarBynary[1]};
      ResponseAPDU responseAPDU = ch.transmit(new CommandAPDU(command));
      byte[] data = null;
      if (responseAPDU.getSW() == 0x9000) {
         data = responseAPDU.getData();
      }
      return data;
   }

   private String byte2Hex(byte[] b) {
      StringBuilder sb = new StringBuilder(b.length * 2);
      for (int i = 0; i < b.length; i++) {
         int v = b[i] & 0xff;
         if (v < 16) {
            sb.append('0');
         }
         sb.append(Integer.toHexString(v));
      }
      return sb.toString().toUpperCase();
   }

   /**
    * Converts a two byte array to an integer
    * @param b a byte array of length 2
    * @return an int representing the unsigned short
    */
   private int bytes2int(byte b1, byte b2) {
      int i = 0;
      i |= b1 & 0xFF;
      i <<= 8;
      i |= b2 & 0xFF;
      return i;
   }

   private byte[] int2byte(int valor) {
      byte[] retorno = new byte[2];
      retorno[1] = (byte) valor;
      valor = valor >> 8;
      retorno[0] = (byte) valor;
      return retorno;
   }

   public class DatosDNIe {

      public String nif;
      public String nombre;

      public DatosDNIe(String _nif, String _nombre) {
         nif = _nif;
         nombre = _nombre.replace(",", "").trim();
         if(nombre.length() > 40) nombre = nombre.substring(0, 40);

         // Se sustituye el asterisco final por un campo especifico en la BD candidatos
//         if(nombre.length() == 40) {
//            nombre = nombre.substring(0,39);
//         }
//         nombre += "*";
      }
   }
} 

