/*******************************************************************************
 *  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.baselibs;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import cdac.medinfo.sdk.dcm30_04.commoninterface.IDataElement;
import cdac.medinfo.sdk.dcm30_04.commoninterface.IDicomValue;
import cdac.medinfo.sdk.dcm30_04.commoninterface.IDicomStream;
import cdac.medinfo.sdk.dcm30_04.commoninterface.IDictionaryReader;
import cdac.medinfo.sdk.dcm30_04.commoninterface.ITag;
import cdac.medinfo.sdk.dcm30_04.commoninterface.ITransferSyntax;
import cdac.medinfo.sdk.dcm30_04.dicomstreams.DicomBufferStream;
import cdac.medinfo.sdk.dcm30_04.dictionaryreaderwriters.DictionaryReader;
import cdac.medinfo.sdk.dcm30_04.enums.EnumLoggingLevel;
import cdac.medinfo.sdk.dcm30_04.exceptions.InvalidDataElementException;
import cdac.medinfo.sdk.dcm30_04.exceptions.InvalidDictionaryException;
import cdac.medinfo.sdk.dcm30_04.exceptions.PaddingRequiredException;
import cdac.medinfo.sdk.dcm30_04.exceptions.ValidationException;
import cdac.medinfo.sdk.dcm30_04.utils.DicomConfig;
import cdac.medinfo.sdk.dcm30_04.utils.DicomConstants;
import cdac.medinfo.sdk.dcm30_04.utils.DicomLogger;


/**
 * The class is used  for entering data, user/stream has to create DataElement object and specify details.
 *  Once the DataSet object is filled, then user will call  fillModel() method which will populate the model.
 */
public abstract class DataElement implements IDataElement  
{
	ITag objITag;
	int iVM;
	IDicomValue objIDicomValue;
	ITransferSyntax objITransferSyntax;
	IDictionaryReader objIDictionaryReader;
	protected int TAG_LENGTH = 4;
	protected int VR_LENGTH = 2;
	protected int VALUE_INT_LENGTH = 4;//for OB,OW,UT,UN,SQ length of value is 4.
	protected int VALUE_SHORT_LENGTH = 2;//for others data types length of value is 2.
	protected int RESERVED_BYTES_LENGTH = 2;

	/**
	 * Creates Data Element with Default Dictionary Reader & Little Endian Explicit Transfer syntax.
	 */
	//Created with default Explicit Transfer Syntax and dictionary reader
	DataElement()
	{
		this(null,null);
	}

	/**
	 * Creates empty Data Element with specified Transfer Syntax.
	 * @param objITransferSyntax value of Transfer Sytntax.
	 */
	//Created with Default dictionary reader
	DataElement(ITransferSyntax objITransferSyntax)
	{
		this(objITransferSyntax,null);
	}

	/**
	 * Creates empty Data Element with specified dictionary reader and Little Endian Explicit Transfer syntax.
	 * @param objIDictReader Dictionary Reader instance.
	 */
	//Created with Default Explicit Transfer Syntax
	DataElement(IDictionaryReader objIDictionaryReader)
	{
		this(new TransferSyntax(DicomConstants.DCM_VR_EXPLICIT,DicomConstants.DCM_ENDIANNESS_LITTLE_ENDIAN),objIDictionaryReader);
	}

	/**
	 * Creates empty Data Element with specified dictionary reader and Transfer syntax.
	 * If Dictionary Reader is empty then default dictionary reader is created.
	 * @param objITransferSyntax value of Transfer Syntax.
	 * @param objIDictReader Dictionary Reader instance.
	 */
	DataElement(ITransferSyntax objITransferSyntax,IDictionaryReader objIDictReader) throws InvalidDataElementException
	{
		try
		{
			iVM = 0;
			//Since Transfer Syntax creation is responsibility of specialized classes.Hence whatever value is set in the
			//specialized classes, we set it here.
			this.objITransferSyntax = objITransferSyntax;

			//If Dictionary Reader is not set then the default dictionary will be used.
			if(objIDictionaryReader == null)
			{
				objIDictionaryReader = DictionaryReader.createInstance();
				if(!objIDictionaryReader.isDictionaryPopulated())
					objIDictionaryReader.populate();
			}
			else
				objIDictionaryReader = objIDictReader;
		}
		catch(InvalidDictionaryException ide)
		{
			if(DicomConfig.createInstance().isLoggingOn())
			{
				DicomLogger.getLogger().log(ide.getMessage());
			}
			throw new InvalidDataElementException(ide.getMessage(),ide);
		}
	}


	/**
	 * Retrieves value length of data element
	 * @return int - value length of data element
	 */
	public int getValueLength()
	{
		int iValueLength = 0;
		if(objIDicomValue != null)
		{
			int iDicomValueLength = objIDicomValue.getValueLength();
			int iVRType = objIDicomValue.getIVRType();
			if((iDicomValueLength >0) && ((iDicomValueLength % 2) != 0) && ((iVRType != DicomConstants.DCM_DT_OB)
					|| (iVRType != DicomConstants.DCM_DT_OW) || (iVRType != DicomConstants.DCM_DT_OF)
					|| (iVRType != DicomConstants.DCM_DT_UN) || (iVRType != DicomConstants.DCM_DT_SQ)))
				iValueLength = iDicomValueLength + 1;
			else
				iValueLength = iDicomValueLength;
		}
		return iValueLength;
	}

	/**
	 * Retrieves the value multiplicity of the value.
	 * @return integer - returns the value multiplicity of the value in DataElement 
	 */
	public int getValueMultiplicity()
	{
		return iVM;
	}

	/**
	 * Sets the value multiplicity of the value in tha DataElement
	 * @param iVM integer value multiplicity of the value
	 */
	public void setValueMultiplicity(int iVM)
	{
		this.iVM = iVM;
	}

	/**
	 * Sets the Tag in Data Element.
	 * @param iGroupNumber Integer Value of Group Number. 
	 * @param iElementNumber Integer Value of Element Number. 
	 */
	//note that these iGroupNumber and iElementNumber should be Short....
	public void setTag(int iGroupNumber,int iElementNumber)
	{
		objITag = new Tag(iGroupNumber,iElementNumber);
	}

	/**
	 * Returns the Tag part of the Data Element.
	 * @return ITag - Tag containing Group Number and Element Number.
	 */
	public ITag getTag()
	{
		return objITag;
	}

	/**
	 * Returns the value part of the Data Element.
	 * @return IDicomValue - instance containing the value of the Data Element.
	 */
	public IDicomValue getDicomValue()
	{
		return objIDicomValue;
	}

	/**
	 * sets the DicomValue as a value in the DataElement
	 * @param objDicomValue object of DicomValue representing the value of DataElement
	 */
	public void setDicomValue(IDicomValue objDicomValue)
	{
		int iValMultiplicity = objDicomValue.getValueMultiplicity();
		setValueMultiplicity(iValMultiplicity);
		objIDicomValue = objDicomValue;
	}

	/**
	 * Sets the Transfer Syntax.
	 * @param objITransferSyntax value of Transfer Syntax.
	 */
	public void setTransferSyntax(ITransferSyntax objITransferSyntax)
	{
		this.objITransferSyntax = objITransferSyntax;
	}

	/**
	 * Sets the Dictionary Reader. Dictionary Reader is used to verify the Value Representation of
	 * a Data Element while creating a Data Element.
	 * @param objIDictionaryReader DictionaryReader to read from the Dictionary.
	 */
	public void setDictionaryReader(IDictionaryReader objIDictionaryReader)
	{
		if(!objIDictionaryReader.isDictionaryPopulated())
			objIDictionaryReader.populate();
		this.objIDictionaryReader = objIDictionaryReader;
	}

	/**
	 * This method returns the dictionaryreader set in the Implicit DataElement
	 * @return objIDictionaryReader - object of dictionary reader set in the Implicit DataElement
	 */
	public IDictionaryReader getDictionaryReader()
	{
		return objIDictionaryReader;
	}

	/**
	 * This method returns the TransferSyntax set in the DataElement
	 * @return objITransferSyntax - TransferSyntax set in the DataElement
	 */
	public ITransferSyntax getTransferSyntax()
	{
		return objITransferSyntax;
	}

	/**
	 * The implementation of this method parses the Data Element from the given Stream.
	 * @param  objIDicomStream Stream on which the DICOM data is available.
	 * @return int - shows status as success or failure.
	 * @throws InvalidDataElementException If Data Element fails to satisfy Validation Criteria. 
	 * @throws IOException If Input/Output operation fails.
	 */
	public abstract int parse(IDicomStream objIDicomStream) throws InvalidDataElementException, IOException;

	/**
	 * The implementation of this method serializes the Data Element from the given Stream
	 * @param objIDicomStream Stream on which the DICOM data is available
	 * @return int - shows status as success or failure.
	 * @throws InvalidDataElementException If Data Element fails to satisfy Validation Criteria. 
	 * @throws IOException If Input/Output operation fails.
	 */
	public abstract int serialize(IDicomStream objIDicomStream) throws InvalidDataElementException, IOException;

	/**
	 * The implementation of this method retrieves the length of DataElement.
	 * @return int - Integer value of length.
	 */
	public abstract int getDataElementLength();

	/*
	 * This method validates the DataElement's length to be even and calls validate of DicomValue
	 */
	/**
	 * Validates the DataElement and throws exception on validation Failure.
	 * @return boolean - Result Status, True : Success, False : Failure
	 * @throws InvalidDataElementException If Data Element fails to satisfy Validation Criteria. 
	 */
	public boolean validate() throws InvalidDataElementException
	{
		/*
		 * Check the length of the value for odd length. If odd length value found throw
		 * PaddingRequiredException which is the main cause for InvalidDataElementException  
		 */
		try{
			if(getValueLength() != -1 && (getValueLength() % 2) != 0)
				throw new PaddingRequiredException("Odd Length Value found.Needs padding.");
			if(objIDicomValue != null)
				objIDicomValue.validate();
		}
		catch( PaddingRequiredException objPadEx)
		{
			if(DicomConfig.createInstance().isLoggingOn())
			{
				DicomLogger.getLogger().log(objPadEx.getMessage());
			}
			// If Validation mode is STRICT or HIGH, the exception is thrown. Otherwise validate method returns false.
			
				throw new InvalidDataElementException(Integer.toHexString(objITag.getGroupNumber())+","+ Integer.toHexString(objITag.getElementNumber()),objPadEx);
		}
		catch( ValidationException ve)
		{
			if(DicomConfig.createInstance().isLoggingOn())
			{
				DicomLogger.getLogger().log(ve.getMessage());
			}
				throw new InvalidDataElementException(Integer.toHexString(objITag.getGroupNumber())+","+ Integer.toHexString(objITag.getElementNumber()),ve);
		}
		return true;
	}
	/**
	 * This method checks for equality of two data elements.
	 * @param objTempObject Data Element whose equality has to be checked with instance data element.
	 * @return boolean - true if the data element passed as parameter and instance data element  are equal and false if data elements passed as parameter and instance data element are not equal.
	 */
	public boolean equals(Object objTempObject)
	{
		//checks for the equality of data element passed as parameter with instance data element.
		if(objTempObject == null || !(objTempObject instanceof IDataElement))
			return false;

		IDataElement objTempIDataElement = (IDataElement)objTempObject;
		ITag objTempITag = objTempIDataElement.getTag();
		if(objTempITag == null)
			return false;
		if(objITag.equals(objTempITag))
		{
			if(getValueLength() == objTempIDataElement.getValueLength())
			{
				if(getDicomValue() != null && getDicomValue().getValue() != null && objTempIDataElement.getDicomValue() != null && objTempIDataElement.getDicomValue().getValue() != null)
				{	
					if(getDicomValue().getValue().toString().equalsIgnoreCase(objTempIDataElement.getDicomValue().getValue().toString()))
					{
						return true;
					}
				}
			}
		}
		return false;
	}

	/**
	 * This function will retrieve the value of dicom data of the Data Element.
	 * @return Object - value of dicom data of the Data Element.
	 */
	public Object getValue()
	{
		Object object = null;
		if(objIDicomValue != null)
		{
			object = objIDicomValue.getValue();
		}
		return object;
	}

	/**
	 * Create similar object of this class.
	 * This does the deep copy and create a new instance of this class nad return it.
	 * @return a clone of this instance.
	 */
	public Object clone()
	{
		try
		{			
			ByteArrayOutputStream objByteArrayOutputStream = new ByteArrayOutputStream();     
			ObjectOutputStream objObjectOutputStream = new ObjectOutputStream(objByteArrayOutputStream);         

			objObjectOutputStream.writeObject(this);
			objObjectOutputStream.flush();
			objObjectOutputStream.close(); 

			ObjectInputStream objObjectInputStream = new ObjectInputStream(new ByteArrayInputStream(objByteArrayOutputStream.toByteArray()));
			DataElement objDataElement = (DataElement)objObjectInputStream.readObject();

			return objDataElement;
		}
		catch (IOException ioe) 
		{
			if(DicomConfig.createInstance().isLoggingOn() && DicomConfig.createInstance().getLoggingLevel() == EnumLoggingLevel.SEVERE)
			{
				DicomLogger objDicomLogger = DicomLogger.getLogger();
				objDicomLogger.log(ioe.getMessage());
			}
			else if (DicomConfig.createInstance().isLoggingOn() && DicomConfig.createInstance().getLoggingLevel() == EnumLoggingLevel.INFO) 
			{
				DicomLogger objDicomLogger = DicomLogger.getLogger();
				DicomBufferStream objDicomBufferStream = new DicomBufferStream();
				ByteArrayOutputStream objByteArrayOutputStream = new ByteArrayOutputStream();
				objDicomBufferStream.setOutputStream(objByteArrayOutputStream);
				try 
				{
					this.serialize(objDicomBufferStream);
				}
				catch (InvalidDataElementException e) {}
				catch (IOException e) {}
				String strLogMessage = objDicomBufferStream.toString();
				objDicomLogger.log(strLogMessage);
				objDicomLogger.log(ioe.getMessage());
			}
		} 
		catch (ClassNotFoundException e) 
		{
			if(DicomConfig.createInstance().isLoggingOn() && DicomConfig.createInstance().getLoggingLevel() == EnumLoggingLevel.SEVERE)
			{
				DicomLogger objDicomLogger = DicomLogger.getLogger();
				objDicomLogger.log(e.getMessage());
			}
			else if (DicomConfig.createInstance().isLoggingOn() && DicomConfig.createInstance().getLoggingLevel() == EnumLoggingLevel.INFO) 
			{
				DicomLogger objDicomLogger = DicomLogger.getLogger();
				DicomBufferStream objDicomBufferStream = new DicomBufferStream();
				ByteArrayOutputStream objByteArrayOutputStream = new ByteArrayOutputStream();
				objDicomBufferStream.setOutputStream(objByteArrayOutputStream);
				try 
				{
					this.serialize(objDicomBufferStream);
				} 
				catch (InvalidDataElementException e1) {} 
				catch (IOException e1) {}
				String strLogMessage = objDicomBufferStream.toString();
				objDicomLogger.log(strLogMessage);
				objDicomLogger.log(e.getMessage());
			}
		}
		return null;
	}


	/**
	 * Dumps the contents of Data Element in readable form to the specified stream.
	 * Contents such as VR, Value Length, and Value are dumped.
	 * @param objIDicomStream - the Stream on which the Data is to be written.
	 * @throws IOException - Exception is thrown indicating runtime failure of writing operation.
	 */
	public void dumpDICOMInfo(IDicomStream objIDicomStream) throws IOException
	{
		objITag.dumpDICOMInfo(objIDicomStream);
		objIDicomStream.writeString("VR :" + objIDictionaryReader.getVR(objITag.getGroupNumber(), objITag.getElementNumber()) + " ");
		objIDicomStream.writeString("Length : " + getValueLength());
		objIDicomValue.dumpDICOMInfo(objIDicomStream);
		objIDicomStream.writeString("\n");
	}
}