/* 
* 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 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) {
        }
        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()) {
            Global.principal.msgBox("Retire el DNIe del lector");
            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) {
                        String s = e.getMessage();
                    }
                    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));

        byte[] p1 = responseAPDU.getBytes();
        int p2 = responseAPDU.getSW1();
        int p3 = responseAPDU.getSW2();

        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);
        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);
            }

            if (nombre.length() == 40) {
                nombre = nombre.substring(0, 39);
            }
            //nombre += "*";
        }
    }
}
