/*******************************************************************************
 *  Copyright 2017-2018 Centre for Development of Advanced Computing(C-DAC), Pune
 *  
 * 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 cdac.medinfo.sdk.dcm30_04.services.basicapplicationlevelconfidentialityprofile;

import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Locale;

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


import cdac.medinfo.sdk.dcm30_04.baselibs.DataSet;
import cdac.medinfo.sdk.dcm30_04.commoninterface.IDataElement;
import cdac.medinfo.sdk.dcm30_04.commoninterface.IDataSet;
import cdac.medinfo.sdk.dcm30_04.commoninterface.ITag;
import cdac.medinfo.sdk.dcm30_04.exceptions.ConfidentialityAttributeException;
import cdac.medinfo.sdk.dcm30_04.exceptions.InvalidDataSetException;
import cdac.medinfo.sdk.dcm30_04.utils.DicomConfig;
import cdac.medinfo.sdk.dcm30_04.utils.DicomLogger;
import cdac.medinfo.sdk.dcm30_04.utils.IPairedCollection;
import cdac.medinfo.sdk.dcm30_04.utils.PairedCollection;
import cdac.medinfo.sdk.dcm30_04.utils.ResourceBundleWrapper;

/**
 * This Basic Application Level Confidentiality Profile addresses the following aspects of security:
 * Data Confidentiality at the application level.
 * This Profile is targeted toward creating a special purpose, de-identified version of an already-existing Data Set.
 * This class provide methods to encrypt the Confidential Attributes present in the Data Set.
 * Once the encryption algorithm and the secret key is specified by the user then Confidential Attributes
 * in the data set are removed either by null values or the dummy values specified by the user.
 * The original values of these data elements is added to the EncryptedAttributeSequence.
 * This class also provides method to decrypt the Confidential Data Elements present in the Data Set i.e. re -identifying the Data Elements
 * if the decrption algorithm and secret key is provided.
 */
public class BasicAppLevelConfidentialityProfile 
{

	private IPairedCollection<ITag, Object> objPairedCollection = new PairedCollection<ITag, Object>();

	/**
	 * This method adds Dummy values of the Data Elements i.e. is to be protected.
	 * @param ITag Tag oo the Data Element that is to be protected.
	 * @param objValue Dummy value for the Data Element.
	 */
	public void addDummyValues(ITag objITag, Object objValue)
	{
		objPairedCollection.put(objITag, objValue);
	}

	/**
	 * This method encrypts the Confidential Data Elements present in the Data Set.
	 * @param objOriginalIDataSet instance of Data Set containing Confidential Data Elements i.e. to be encrypted.
	 * @param enumProfileAlgorithm - Encryption Algorithm that is to be used.
	 * @param objKey - Key that is to be used for the encryption of Data ELements.
	 * @param objIV - salt parameters.
	 * @return IDataSet - instance of Data Set containing  Confidential Attributes that are encrypted.
	 * @throws IOException Signals if IO Operation Fails.
	 * @throws NoSuchAlgorithmException Signal is Encryption of the Data Set Fails.
	 * @throws NoSuchPaddingException Signal is Encryption of the Data Set Fails.
	 * @throws InvalidKeyException Signal is Encryption of the Data Set Fails.
	 * @throws IllegalBlockSizeException Signal is Encryption of the Data Set Fails.
	 * @throws BadPaddingException Signal is Encryption of the Data Set Fails.
	 */
	public IDataSet encryptDataSet(IDataSet objOriginalIDataSet,EnumProfileAlgorithm enumProfileAlgorithm, byte[] objKey, byte[] objIV)throws IOException,NoSuchAlgorithmException, NoSuchPaddingException,InvalidKeyException,IllegalBlockSizeException,BadPaddingException, InvalidAlgorithmParameterException
	{
		if(objOriginalIDataSet == null)
		{
			if(DicomConfig.createInstance().isLoggingOn())
			{
				DicomLogger objDicomLogger = DicomLogger.getLogger();
				objDicomLogger.log(ResourceBundleWrapper.getValue("warning","DataSetNull",new Locale(DicomConfig.createInstance().getLocale())));
			}
			return null;
		}
		
		// if the Encryption Algorithm or the Key is not specified then exception should be thrown as the data set cannot be encrypted then.
		if(enumProfileAlgorithm == null || objKey == null)
		{
			String strValue = null;
			if(enumProfileAlgorithm == null)
			{
				strValue = ResourceBundleWrapper.getValue("exceptions","BasicAppConfProfile.NoAlgorithmSpecified",new Locale(DicomConfig.createInstance().getLocale()));
			}
			else
			{
				strValue = ResourceBundleWrapper.getValue("exceptions","BasicAppConfProfile.KeyNotSpecified",new Locale(DicomConfig.createInstance().getLocale()));
			}

			if(DicomConfig.createInstance().isLoggingOn())
			{
				DicomLogger objDicomLogger = DicomLogger.getLogger();
				objDicomLogger.log(strValue);
			}
			throw new ConfidentialityAttributeException(strValue);
		}

		// Presence of (0400,0500) Encrypted Attributes Sequence depicts dataset is already encrypted.
		
		if(objOriginalIDataSet.isPresent(0x0400,0x0500))
		{
			if(DicomConfig.createInstance().isLoggingOn())
			{
				DicomLogger objDicomLogger = DicomLogger.getLogger();
				objDicomLogger.log(ResourceBundleWrapper.getValue("warning","BasicAppConfProfile.REENCRYPTION",new Locale(DicomConfig.createInstance().getLocale())));
			}
		}
		
		//instance od Basic Application Agent.
		BasicAppAgent objBasicAppAgent = new BasicAppAgent();
		objBasicAppAgent.setProfileAlgorithm(enumProfileAlgorithm);
		objBasicAppAgent.setSecretKey(objKey);
		objBasicAppAgent.setSaltParameter(objIV);

		//Create instance of Data Set which will contain Encrypted Data Elements.
		IDataSet objEncryptDataSet = new DataSet(objOriginalIDataSet.getTransferSyntax(), objOriginalIDataSet.getDictionaryReader());

		//Iterate through the original Data Set. If the Data Element is confidential add it to Confidentiality list
		//in Basic App Agent. Otherwise add it to Encrypted Data Set.
		//Few Data Elements cannot have null value. For then Dummy value is generated and they are added 
		//in encrypted data set with the value generated.
		for(IDataElement objIDataElement : objOriginalIDataSet)
		{
			if(objBasicAppAgent.isConfidential(objIDataElement.getTag()))
			{
				if(objIDataElement.getTag().equals(EnumConfidentialityProfileAttributes.SOP_INSTANCE_UID.getValue()))
				{
					if(objPairedCollection.get(EnumConfidentialityProfileAttributes.SOP_INSTANCE_UID.getValue()) == null)
						objPairedCollection.put(EnumConfidentialityProfileAttributes.SOP_INSTANCE_UID.getValue(), generateSOPInstance(objOriginalIDataSet.getSOPClassUID()));
				}
				if(objIDataElement.getTag().equals(EnumConfidentialityProfileAttributes.STUDY_INSTANCE_UID.getValue()))
				{
					if(objPairedCollection.get(EnumConfidentialityProfileAttributes.STUDY_INSTANCE_UID.getValue()) == null)
						objPairedCollection.put(EnumConfidentialityProfileAttributes.STUDY_INSTANCE_UID.getValue(), generateSOPInstance(objOriginalIDataSet.getSOPClassUID()));
				}
				if(objIDataElement.getTag().equals(EnumConfidentialityProfileAttributes.SERIES_INSTANCE_UID.getValue()))
				{
					if(objPairedCollection.get(EnumConfidentialityProfileAttributes.SERIES_INSTANCE_UID.getValue()) == null)
						objPairedCollection.put(EnumConfidentialityProfileAttributes.SERIES_INSTANCE_UID.getValue(), generateSOPInstance(objOriginalIDataSet.getSOPClassUID()));
				}
				if(objIDataElement.getTag().equals(EnumConfidentialityProfileAttributes.FRAME_OF_REFERENCE_UID.getValue()))
				{
					if(objPairedCollection.get(EnumConfidentialityProfileAttributes.FRAME_OF_REFERENCE_UID.getValue()) == null)
						objPairedCollection.put(EnumConfidentialityProfileAttributes.FRAME_OF_REFERENCE_UID.getValue(), generateSOPInstance(objOriginalIDataSet.getSOPClassUID()));
				}
				objBasicAppAgent.addDataElementToConfidentialList(objIDataElement);
				IDataElement objTempDE = objBasicAppAgent.getDataElement(objIDataElement, objPairedCollection.get(objIDataElement.getTag()));
				objEncryptDataSet.addDataElement(objTempDE);
			}
			else
			{
				IDataElement objTempDE = objBasicAppAgent.getDataElement(objIDataElement);
				objEncryptDataSet.addDataElement(objTempDE);
			}
		}

		//add the Encrypted attribute sequence to the Data Set. It will contain the confidential attributes present in the data set.
		objBasicAppAgent.addEncryptedAttributeSequence(objEncryptDataSet);
		return objEncryptDataSet;
	}

	/**
	 * This method decryts the encrypted data elements present in the given Data Set.
	 * @param objIDataSet instance of data set containing encrypted Data Elements.
	 * @param objIV - salt parameters.
	 * @return {@link InvalidDataSetException} - instance of data set containing all attributes with the confidential attributes.
	 * @throws NoSuchAlgorithmException Signal is Encryption of the Data Set Fails.
	 * @throws NoSuchPaddingException Signal is Encryption of the Data Set Fails.
	 * @throws InvalidKeyException Signal is Encryption of the Data Set Fails.
	 * @throws IllegalBlockSizeException Signal is Encryption of the Data Set Fails.
	 * @throws BadPaddingException Signal is Encryption of the Data Set Fails.
	 * @throws IOException Signal is Encryption of the Data Set Fails.
	 */
	public IDataSet decryptDataSet(IDataSet objIDataSet, EnumProfileAlgorithm enumProfileAlgorithm, byte[] objKey, byte[] objIV) throws  NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, IOException,InvalidAlgorithmParameterException
	{
		
		if(objIDataSet == null)
		{
			if(DicomConfig.createInstance().isLoggingOn())
			{
				DicomLogger objDicomLogger = DicomLogger.getLogger();
				objDicomLogger.log(ResourceBundleWrapper.getValue("warning","DataSetNull",new Locale(DicomConfig.createInstance().getLocale())));
			}
			return null;
		}
		
		// if the Encryption Algorithm or the Key is not specified then exception should be thrown as the data set cannot be encrypted then.
		if(enumProfileAlgorithm == null || objKey == null)
		{
			String strValue = null;
			if(enumProfileAlgorithm == null)
			{
				strValue = ResourceBundleWrapper.getValue("exceptions","BasicAppConfProfile.NoAlgorithmSpecified",new Locale(DicomConfig.createInstance().getLocale()));
			}
			else
			{
				strValue = ResourceBundleWrapper.getValue("exceptions","BasicAppConfProfile.KeyNotSpecified",new Locale(DicomConfig.createInstance().getLocale()));
			}

			if(DicomConfig.createInstance().isLoggingOn())
			{
				DicomLogger objDicomLogger = DicomLogger.getLogger();
				objDicomLogger.log(strValue);

			}
			throw new ConfidentialityAttributeException(strValue);
		}

		//instance of Basic Application Agent.
		BasicAppAgent objBasicAppAgent = new BasicAppAgent();
		objBasicAppAgent.setProfileAlgorithm(enumProfileAlgorithm);
		objBasicAppAgent.setSecretKey(objKey);
		objBasicAppAgent.setSaltParameter(objIV);

		//Get the Original Data Set.
		IDataSet objRSPDataSet = objBasicAppAgent.decryptDataSet(objIDataSet);

		return objRSPDataSet;
	}


	//this method generated dummy Instance UIDs.
	private String generateSOPInstance(String strSOPClassUID)
	{
		String strSOPInstanceUID = strSOPClassUID + "." + ((new Date()).getTime());
		return strSOPInstanceUID;
	}
}
