/*
 * Copyright 2011 Federal Chancellery Austria and
 * Graz University of Technology
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package at.gv.util;

import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Date;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;

import at.gv.util.data.BPK;
import at.gv.util.data.WBPK;
import at.gv.util.ex.InternalErrorException;

/**
 * Utility class for sector specific PINs (Bereichsspezifische
 * Personenkennzeichen).
 * 
 * @author <a href="mailto:Arne.Tauber@egiz.gv.at">Arne Tauber</a>
 * 
 */
public final class BpkUtil {

	public static final String SECTOR_DELIVERY = "ZU";
	public static final String PUBLIC_KEY_ZUSEKOPF_SN01_BASE64 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDfGFFEzWl1kSbzmk4pWVeftyD2aWTVQ8xSIwb6ECLdFTy4zE9LI6a87zlPbOOzvvdO+Nv1VCvT+WqD9HOCtPwealwwRKS2cHI9aqYyozqDOIGBRIz7MDBKuqaTwCvonFe0MUBxVWDDTmqhDIjkN0uDQidQtqqjzupEzuQ59lVpBQIDAQAB";

	public static final String PREFIX_BPK_TYPE = "urn:publicid:gv.at:cdid+";
	public static final String PREFIX_WBPK_TYPE = "urn:publicid:gv.at:wbpk+";
	public static final String PREFIX_STORK_TYPE = "urn:publicid:gv.at:storkid+";
	 
	
	
	private static String calcDigest(String data)
	    throws NoSuchAlgorithmException, UnsupportedEncodingException {
		MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
		byte[] digest = sha1.digest(data.getBytes("ISO-8859-1"));
		return new String(Base64.encodeBase64(digest));
	}

	
	public static String calcbPKorwbPKorSTORKID(String baseID, String sector) {
		MiscUtil.assertNotEmpty(baseID, "BaseID");
		MiscUtil.assertNotEmpty(sector, "Sector");
		
		if (sector.startsWith(PREFIX_BPK_TYPE))
			return calcBPK(baseID, sector);
		
		else if (sector.startsWith(PREFIX_WBPK_TYPE))
			return calcWBPK(baseID, sector);
		
		else if (sector.startsWith(PREFIX_STORK_TYPE))
			return calcWBPK(baseID, sector);
		
		else
			throw new InternalErrorException("Sector is not of bPK or wbPK or stork type");
		
	}
	
	
	public static String calcBPK(String baseID, String sector) {
		MiscUtil.assertNotEmpty(baseID, "BaseID");
		MiscUtil.assertNotEmpty(sector, "Sector");
		// prefix according to specification
	
		// concatenation according to algorithm
		String data;
		if (sector.startsWith(PREFIX_BPK_TYPE))
			data = baseID + "+" + sector;
		else
			data = baseID + "+" + PREFIX_BPK_TYPE + sector;

		String hash = null;
		try {
			hash = calcDigest(data);
		} catch (NoSuchAlgorithmException noSuchAlgorithmException) {
			throw new InternalErrorException(noSuchAlgorithmException);
		} catch (UnsupportedEncodingException unsupportedEncodingException) {
			throw new InternalErrorException(unsupportedEncodingException);
		}
		return hash;
	}

	public static BPK createBPK(String baseID, String sector) {
		
		if (sector.startsWith(PREFIX_BPK_TYPE))
			sector = sector.substring(PREFIX_BPK_TYPE.length());
		
		return new BPK(sector, calcBPK(baseID, sector));
	}

	public static String calcZBPK(String baseID) {
		return calcBPK(baseID, SECTOR_DELIVERY);
	}

	public static BPK createZBPK(String baseID) {
		return new BPK(SECTOR_DELIVERY, calcZBPK(baseID));
	}

	public static String calcWBPK(String baseID, String nameQualifier) {
		MiscUtil.assertNotEmpty(baseID, "BaseID");
		MiscUtil.assertNotEmpty(nameQualifier, "Name qualifier");
		// concatenation according to algorithm
		String data = baseID + "+" + nameQualifier;

		String wBPK = null;
		try {
			wBPK = calcDigest(data);
		} catch (NoSuchAlgorithmException e) {
			throw new InternalErrorException(e);
		} catch (UnsupportedEncodingException e) {
			throw new InternalErrorException(e);
		}
		return wBPK;
	}

	public static WBPK createWBPK(String baseID, String nameQualifier) {
		return new WBPK(calcWBPK(baseID, nameQualifier), nameQualifier);
	}

	public static byte[] encrypt(byte[] inputBytes, PublicKey publicKey) {
		byte[] result;
		try {
			Cipher cipher = null;
			try {
				cipher = Cipher.getInstance("RSA/ECB/OAEPPadding"); // try with bouncycastle
			} catch(NoSuchAlgorithmException e) {
				cipher = Cipher.getInstance("RSA/ECB/OAEP"); // try with iaik provider
			}
			cipher.init(Cipher.ENCRYPT_MODE, publicKey);
			result = cipher.doFinal(inputBytes);
		} catch (InvalidKeyException e) {
			throw new InternalErrorException(e);
		} catch (IllegalBlockSizeException e) {
			throw new InternalErrorException(e);
		} catch (BadPaddingException e) {
			throw new InternalErrorException(e);
		} catch (NoSuchAlgorithmException e) {
			throw new InternalErrorException(e);
		} catch (NoSuchPaddingException e) {
			throw new InternalErrorException(e);
		}
		return result;
	}

	public static byte[] decrypt(byte[] encryptedBytes, PrivateKey privateKey) {
		byte[] result;
		try {
			Cipher cipher = null;
			try {
				cipher = Cipher.getInstance("RSA/ECB/OAEPPadding"); // try with bouncycastle
			} catch(NoSuchAlgorithmException e) {
				cipher = Cipher.getInstance("RSA/ECB/OAEP"); // try with iaik provider
			}
			cipher.init(Cipher.DECRYPT_MODE, privateKey);
			result = cipher.doFinal(encryptedBytes);
		} catch (InvalidKeyException e) {
			throw new InternalErrorException(e);
		} catch (IllegalBlockSizeException e) {
			throw new InternalErrorException(e);
		} catch (BadPaddingException e) {
			throw new InternalErrorException(e);
		} catch (NoSuchAlgorithmException e) {
			throw new InternalErrorException(e);
		} catch (NoSuchPaddingException e) {
			throw new InternalErrorException(e);
		}
		return result;
	}

	public static String encryptBPK(BPK bpk, PublicKey publicKey) {
		MiscUtil.assertNotNull(bpk, "BPK");
		MiscUtil.assertNotNull(publicKey, "publicKey");
		String input = "V1::urn:publicid:gv.at:cdid+" + bpk.getSector() + "::"
		    + bpk.getBpk() + "::"
		    + DateTimeUtil.formatDate(new Date(), "yyyy-MM-dd'T'HH:mm:ss");
		System.out.println(input);
		byte[] result;
		try {
			byte[] inputBytes = input.getBytes("ISO-8859-1");
			result = encrypt(inputBytes, publicKey);
		} catch (UnsupportedEncodingException e) {
			throw new InternalErrorException(e);
		}
		return new String(Base64.encodeBase64(result)).replaceAll("\r\n", "");
	}

	public static BPK decryptBPK(String encryptedBpk, PrivateKey privateKey) {
		MiscUtil.assertNotEmpty(encryptedBpk, "Encrypted BPK");
		MiscUtil.assertNotNull(privateKey, "Private key");
		String decryptedString;
		try {
			byte[] encryptedBytes = Base64.decodeBase64(encryptedBpk
			    .getBytes("ISO-8859-1"));
			byte[] decryptedBytes = decrypt(encryptedBytes, privateKey);
			decryptedString = new String(decryptedBytes, "ISO-8859-1");
		} catch (UnsupportedEncodingException e) {
			throw new InternalErrorException(e);
		}
		String tmp = decryptedString.substring(decryptedString.indexOf('+') + 1);
		String sector = tmp.substring(0, tmp.indexOf("::"));
		tmp = tmp.substring(tmp.indexOf("::") + 2);
		String bPK = tmp.substring(0, tmp.indexOf("::"));

		return new BPK(sector, bPK);
	}

	public static String calcVZBPK(String baseID, PublicKey publicKey) {
		MiscUtil.assertNotEmpty(baseID, "BaseID");
		MiscUtil.assertNotNull(publicKey, "Public key");
		String bpk = calcZBPK(baseID);
		String input = "Ver:1::urn:publicid:gv.at:cdid+" + SECTOR_DELIVERY + "::"
		    + bpk + "::"
		    + DateTimeUtil.formatDate(new Date(), "yyyy-MM-dd'T'HH:mm:ss");
		byte[] result;
		try {
			byte[] inputBytes = input.getBytes("ISO-8859-1");
			result = encrypt(inputBytes, publicKey);
		} catch (UnsupportedEncodingException e) {
			throw new InternalErrorException(e);
		}
		return new String(Base64.encodeBase64(result)).replaceAll("\r\n", "");
	}

	public static String calcVZBPK(String baseID) {
		MiscUtil.assertNotEmpty(baseID, "BaseID");
		PublicKey publicKey;
		try {
			byte[] publicKeyBytes = Base64
			    .decodeBase64(PUBLIC_KEY_ZUSEKOPF_SN01_BASE64.getBytes("ISO-8859-1"));
			KeyFactory rsaKeyFac = KeyFactory.getInstance("RSA");
			X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
			publicKey = (RSAPublicKey) rsaKeyFac.generatePublic(keySpec);
		} catch (UnsupportedEncodingException e) {
			throw new InternalErrorException(e);
		} catch (NoSuchAlgorithmException e) {
			throw new InternalErrorException(e);
		} catch (InvalidKeySpecException e) {
			throw new InternalErrorException(e);
		}
		return calcVZBPK(baseID, publicKey);
	}

	/**
	 * @deprecated Use {@link BpkUtil#calcVZBPK(String, PublicKey)} instead.
	 */
	public static String calcVzbpk(String baseID, String sector,
	    byte[] rsaPublicKey) {
		String zbpk = calcZBPK(baseID);
		return encryptBPK(zbpk, sector, new Date(), rsaPublicKey);
	}

	/**
	 * Calculates the check digit for commercial register digits.<br/>
	 * A commercial register number is build by concatinating the commercial
	 * register digits with the check digit (e.g. 123456 and i results in
	 * 123456i).
	 * 
	 * @param commercialRegisterDigits
	 *          The commercial register number consisting of 6 digits.
	 * @return The check digit.
	 */
	public static char calcCheckDigitFromCommercialRegisterNumber(
	    String commercialRegisterDigits) {
		final int[] WEIGHT = { 6, 4, 14, 15, 10, 1 };
		final char[] CHECKDIGIT = { 'a', 'b', 'd', 'f', 'g', 'h', 'i', 'k', 'm',
		    'p', 's', 't', 'v', 'w', 'x', 'y', 'z' };
		if (commercialRegisterDigits == null) {
			throw new NullPointerException("Commercial register number missing.");
		}
		commercialRegisterDigits = StringUtils.leftPad(commercialRegisterDigits, 6,
		    '0');
		if (!commercialRegisterDigits.matches("\\d{6}")) {
			throw new IllegalArgumentException(
			    "Invalid commercial register number provided.");
		}
		int sum = 0;
		for (int i = 0; i < commercialRegisterDigits.length(); i++) {
			int value = commercialRegisterDigits.charAt(i) - '0';
			sum += WEIGHT[i] * value;
		}
		return CHECKDIGIT[sum % 17];
	}

	/**
	 * Checks the validity of a commercial register number.
	 * 
	 * @param commercialRegisterNumber
	 *          The commercial register number.
	 * @return {@code true} if the given commercial register number is valid,
	 *         {@code false} if not.
	 */
	public static boolean checkCommercialRegisterNumber(
	    String commercialRegisterNumber) {
		if (commercialRegisterNumber == null) {
			return false;
		}
		commercialRegisterNumber = StringUtils.leftPad(commercialRegisterNumber, 7,
		    '0');
		if (!commercialRegisterNumber.matches("\\d{6}[abdfghikmpstvwxzy]")) {
			return false;
		}
		String digits = commercialRegisterNumber.substring(0,
		    commercialRegisterNumber.length() - 1);
		char checkDigit = commercialRegisterNumber.charAt(commercialRegisterNumber
		    .length() - 1);
		boolean result = calcCheckDigitFromCommercialRegisterNumber(digits) == checkDigit;
		return result;
	}

	/**
	 * @deprecated Use {@link BpkUtil#encryptBPK(BPK, PublicKey)} instead.
	 */
	public static String encryptBPK(String bpk, String context, Date date,
	    byte[] binaryPublicKey) {
		String inputData = "Ver:1::urn:publicid:gv.at:cdid+" + context + "::" + bpk
		    + "::" + DateTimeUtil.formatDate(date, "yyyy-MM-dd'T'HH:mm:ss");
		byte[] plain = new String(inputData).getBytes();
		// log.debug("inputData = " + inputData);
		byte[] encrypted = null;

		try {
			KeyFactory rsaKeyFac = KeyFactory.getInstance("RSA");
			X509EncodedKeySpec keySpec = new X509EncodedKeySpec(binaryPublicKey);
			PublicKey rsaPublicKey = (RSAPublicKey) rsaKeyFac.generatePublic(keySpec);
			Cipher rsa = null;
			try {
				rsa = Cipher.getInstance("RSA/ECB/OAEPPadding"); // try with bouncycastle
			} catch(NoSuchAlgorithmException e) {
				rsa = Cipher.getInstance("RSA/ECB/OAEP"); // try with iaik provider
			}
			rsa.init(Cipher.ENCRYPT_MODE, rsaPublicKey);
			encrypted = rsa.doFinal(plain);
		} catch (InvalidKeyException e) {
			throw new InternalErrorException(e);
		} catch (NoSuchAlgorithmException e) {
			throw new InternalErrorException(e);
		} catch (NoSuchPaddingException e) {
			throw new InternalErrorException(e);
		} catch (IllegalArgumentException e) {
			throw new InternalErrorException(e);
		} catch (IllegalBlockSizeException e) {
			throw new InternalErrorException(e);
		} catch (BadPaddingException e) {
			throw new InternalErrorException(e);
		} catch (InvalidKeySpecException e) {
			throw new InternalErrorException(e);
		}

		return new String(Base64.encodeBase64(encrypted)).replaceAll("\r\n", "");
	}

	private BpkUtil() {
	}

}