/**
 * @author      ETSI / STF481 / Yann Garcia
 * @version     $URL$
 *              $Id$
 */
package org.etsi.certificates.io;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.etsi.certificates.Helpers;
import org.etsi.common.ByteHelper;

import de.fraunhofer.sit.c2x.CryptoLib;

public class CertificatesIO implements ICertificatesIO {
    
    /**
     * Relative path for to extract certificates
     */
    private static final String CERTS_PATH = "/certs_store";
    
    /**
     *  Extension file for private keys
     */
    private static final String KEYS_EXT = "bin";
    
    /**
     *  Relative path for to extract private keys
     */
    private static final String KEYS_PATH = "/priv_keys";
    
    /**
     * Full path to access certificate files
     */
    private String _fullPathCerts;
    
    /**
     * Full path to access key files
     */
    private String _fullPathKeys;
    
    /**
     * Memory cache for the certificates
     */
    private Map<String, byte[]> _cachedCertificates;
    
    private Map<String, byte[]> _cachedCertificatesDigest;
    
    private Map<String, String> _cachedReverseCertificatesDigest;
    
    /**
     * Memory cache for the signing private keys
     */
    private Map<String, byte[]> _cachedSigningPrivateKey;
    
    /**
     * Memory cache for the encrypt private keys
     */
    private Map<String, byte[]> _cachedEncryptPrivateKey;
    
    /**
     * Default constructor
     */
    public CertificatesIO() {
        _cachedCertificates = new ConcurrentHashMap<String, byte[]>();
        _cachedCertificatesDigest = new ConcurrentHashMap<String, byte[]>();
        _cachedSigningPrivateKey = new ConcurrentHashMap<String, byte[]>();
        _cachedEncryptPrivateKey = new ConcurrentHashMap<String, byte[]>();
        _cachedReverseCertificatesDigest = new ConcurrentHashMap<String, String>();
    } // End of Constructor
    
    /**
     * @desc    Load in memory cache the certificates available in the specified directory
     * @param   rootDirectory Root directory to access to the certificates identified by the certificate ID
     * @param   configId      A configuration identifier
     * @return  true on success, false otherwise
     */
    @Override
    public boolean loadCertificates(final String rootDirectory, final String configId) { // E.g. <rootDirectory path>, cfg01
        // Initialise the memory caches
        unloadCertificates();
        
        // Build full path
        if ((rootDirectory == null) || (rootDirectory.length() == 0)) {
            _fullPathCerts = System.getProperty("user.dir").replace("\\", "/");
        } else {
            _fullPathCerts = rootDirectory.replace("\\", "/");
        }
        if (!_fullPathCerts.endsWith("/")) {
            _fullPathCerts += "/";
        }
        if ((configId != null) && (configId.length() != 0)) {
            _fullPathCerts += configId.replace('.', '/'); 
        }
        _fullPathCerts = _fullPathCerts.toLowerCase();
        _fullPathKeys = _fullPathCerts + KEYS_PATH;
        _fullPathCerts += CERTS_PATH;
        // Check the paths
        File keysPath = new File(_fullPathKeys);
        if (!keysPath.exists()) {
            System.err.println("CertificatesIO.readCertificate: path '" + _fullPathKeys + "' does not found");
            return false;
        }
        File certsPath = new File(_fullPathCerts);
        if (!certsPath.exists()) {
            System.err.println("CertificatesIO.readCertificate: path '" + _fullPathCerts + "' does not found");
            return false;
        }
        
        return loadMemoryCache(keysPath); // Load certificates and keys and return
    }
    
    /**
     * @desc    Unload from memory cache the certificates available
     * @return  true on success, false otherwise
     */
    @Override
    public boolean unloadCertificates() {
        _fullPathKeys = null;
        _fullPathCerts = null;
        _cachedCertificates.clear();
        _cachedCertificatesDigest.clear();
        _cachedSigningPrivateKey.clear();
        _cachedEncryptPrivateKey.clear();
        _cachedReverseCertificatesDigest.clear();
        
        return true;
    }
    
    /**
     * @desc    Read the specified certificate
     * @param   certificateId the certificate identifier
     * @param   certificate   the expected certificate
     * @return  true on success, false otherwise
     */
    @Override
    public boolean readCertificate(final String key, final ByteArrayOutputStream certificate) {
        // Sanity check
        if (!_cachedCertificates.containsKey(key)) {
            System.err.println("CertificatesIO.readCertificate: key '" + key + "' not found");
            return false;
        }
        
        try {
            certificate.write(_cachedCertificates.get(key));
            
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }
    
    @Override
    public boolean readCertificateDigest(final String certificateId, final ByteArrayOutputStream digest) {
        // Sanity check
        String key = certificateId + ".DIGEST";
        if (!_cachedCertificatesDigest.containsKey(key)) {
            System.err.println("CertificatesIO.readCertificate: key '" + key + "' not found");
            return false;
        }
        
        try {
            digest.write(_cachedCertificatesDigest.get(key));
            
            return true;
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return false;
    }
    
    /**
     * @desc    Read the private keys for the specified certificate
     * @param   keysId            the keys identifier
     * @param   signingPrivateKey the signing private key
     * @param   encryptPrivateKey the encrypt private key
     * @return  true on success, false otherwise
     */
    @Override
    public boolean readPrivateKeys(final String key, final ByteArrayOutputStream signingPrivateKey, final ByteArrayOutputStream encryptPrivateKey) {
        // Sanity check
        if (!_cachedSigningPrivateKey.containsKey(key) || !_cachedEncryptPrivateKey.containsKey(key)) {
            System.err.println("CertificatesIO.readPrivateKeys: key '" + key + "' not found");
            return false;
        }
        
        try {
            signingPrivateKey.write(_cachedSigningPrivateKey.get(key));
            encryptPrivateKey.write(_cachedEncryptPrivateKey.get(key));
            
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }
    
    /**
     * @desc Load certificates based on existing keys file, excluding xxx_at.bin files 
     * @param p_keysPath    path for private key files
     * @param p_certsPath   Path for certificate files
     * @return true on success, false otherwise
     */
    private boolean loadMemoryCache(final File p_keysPath) { // E.g. <path>/keys, <path>/certs
        // Retrieve the list of the files in the p_keysPath
        try {
            List<File> files = Helpers.getInstance().getFileListing(p_keysPath, KEYS_EXT, new String[] { "_at", "_aa", /*for debug: */".svn", "._.DS_Store", ".DS_Store"});
            // Create the memory cache
            for (File file : files) {
                addItem(file);
            } // End of 'for' statement
            
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        return false;
    }
    
    private void addItem(final File p_keysFile)  throws FileNotFoundException, IOException {
        // Sanity checks
        if (p_keysFile.length() != 32) { // FIXME Should have both encrypt and singing keys
            return;
        }
        
        // Load the keys file name
        String filename = p_keysFile.getName().toLowerCase();
        String keysFileName = filename.substring(0, filename.indexOf("_ca"));
        File caFile = new File(_fullPathCerts + "/" + keysFileName + "_ca.crt");
        File aaFile = new File(_fullPathCerts + "/" + keysFileName + "_aa.crt");
        File atFile = new File(_fullPathCerts + "/" + keysFileName + "_at.crt");
        
        String key = keysFileName.toUpperCase();
        if (!_cachedCertificates.containsKey(key)) {
            // Open CA keys file
            FileInputStream fsKeys = new FileInputStream(p_keysFile.getCanonicalPath());
            // Load keys
            byte bytes[] = new byte[(int) p_keysFile.length()];
            fsKeys.read(bytes);
            _cachedSigningPrivateKey.put(key + ".CA_PRIVATE_KEYS", ByteHelper.extract(bytes, 0, 32));
            // FIXME Should have both encrypt and singing keys            _cachedEncryptPrivateKey.put(key + ".PRIVATE_KEYS", ByteHelper.extract(bytes, 32, 32));
            _cachedEncryptPrivateKey.put(key + ".CA_PRIVATE_KEYS", ByteHelper.extract(bytes, 0, 32));
            // Close file stream
            fsKeys.close();
            // Open CA keys file
            fsKeys = new FileInputStream(p_keysFile.getCanonicalPath().toLowerCase().replaceAll("_ca", "_at"));
            // Load keys
            bytes = new byte[(int) p_keysFile.length()];
            fsKeys.read(bytes);
            _cachedSigningPrivateKey.put(key + ".AT_PRIVATE_KEYS", ByteHelper.extract(bytes, 0, 32));
            // FIXME Should have both encrypt and singing keys            _cachedEncryptPrivateKey.put(key + ".PRIVATE_KEYS", ByteHelper.extract(bytes, 32, 32));
            _cachedEncryptPrivateKey.put(key + ".AT_PRIVATE_KEYS", ByteHelper.extract(bytes, 0, 32));
            // Close file stream
            fsKeys.close();
            
            // Open CA certificate
            FileInputStream fsCaFile = new FileInputStream(caFile.getCanonicalPath());
            bytes = new byte[(int) caFile.length()];
            fsCaFile.read(bytes);
            _cachedCertificates.put(key + ".CA_CERT", bytes);
            // Close file streams
            fsCaFile.close();
            
            // Open AA certificate
            FileInputStream fsAaFile = new FileInputStream(aaFile.getCanonicalPath());
            bytes = new byte[(int) aaFile.length()];
            fsAaFile.read(bytes);
            _cachedCertificates.put(key + ".AA_CERT", bytes);
            // Close file streams
            fsAaFile.close();
            
            // Open AT certificate
            FileInputStream fsAtFile = new FileInputStream(atFile.getCanonicalPath());
            bytes = new byte[(int) atFile.length()];
            fsAtFile.read(bytes);
            _cachedCertificates.put(key + ".AT_CERT", bytes);
            byte[] digest = calculateDigestFromCertificate(bytes);
            _cachedCertificatesDigest.put(key + ".AT_CERT.DIGEST", digest);
            _cachedReverseCertificatesDigest.put(ByteHelper.byteArrayToString(digest), key + ".AT_CERT.DIGEST");
            // Close file streams
            fsAtFile.close();
            
        } // else, ignore it
        
    }
    
    @Override
    public String getKeyIdFromHashedId8(byte[] p_hashedId8ToBeUsed) {
        String key = ByteHelper.byteArrayToString(p_hashedId8ToBeUsed);
        if (!_cachedReverseCertificatesDigest.containsKey(key)) {
            return null;
        }
        
        return _cachedReverseCertificatesDigest.get(key).substring(0, key.length() - 7/*.DIGEST*/);
    }
    
    private byte[] calculateDigestFromCertificate(final byte[] p_toBeHashedData) {
        byte[] hash = CryptoLib.hashWithSha256(p_toBeHashedData);
        return ByteHelper.extract(hash, hash.length - 8, 8);
    }
    
} // End of class CertificatesIO
