/*******************************************************************************
 *  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.IOException;
import java.util.Locale;

import cdac.medinfo.sdk.dcm30_04.commoninterface.IDataElement;
import cdac.medinfo.sdk.dcm30_04.commoninterface.IDataTypeSQ;
import cdac.medinfo.sdk.dcm30_04.commoninterface.IDicomStream;
import cdac.medinfo.sdk.dcm30_04.commoninterface.IDictionaryReader;
import cdac.medinfo.sdk.dcm30_04.commoninterface.IItem;
import cdac.medinfo.sdk.dcm30_04.commoninterface.ITransferSyntax;
import cdac.medinfo.sdk.dcm30_04.exceptions.InvalidDataTypeException;
import cdac.medinfo.sdk.dcm30_04.exceptions.InvalidItemException;
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;

import cdac.medinfo.sdk.dcm30_04.utils.ICollectionIterator;
import cdac.medinfo.sdk.dcm30_04.utils.ISingleCollection;
import cdac.medinfo.sdk.dcm30_04.utils.ResourceBundleWrapper;
import cdac.medinfo.sdk.dcm30_04.utils.SingleCollection;

/**
 * SQ is the value that shows  a sequence of zero or more Items.
 * Each Item contains a DataSet i.e. a series of DataElements which may also contain sequences again.
 * So SQ DataType is a represents a structure which can represent nesting of DataSets.
 */
public class SQ extends DicomValue implements IDataTypeSQ  
{
	private boolean isUndefinedLength;

	
	/**
	 * serialVersionUID
	 */
	private static final long serialVersionUID = 1L;

	ISingleCollection<IItem> objSingleCollection = null;

	boolean bSequenceDelimitationExpected = false;
	int iGroupNumber;
	int iElementNumber;
	ITransferSyntax objITransferSyntax = null;

	//This DictionaryReader object would be used to hold 
	//the Dictionary Reference.This reference will be used by Item
	//Class to pass to Implicit DataElements in it.
	IDictionaryReader objIDictionaryReader = null;


	/**
	 * This constructor creates SQ with default TransferSyntax 
	 */
	public SQ()
	{
		this(new TransferSyntax());
	}

	/**
	 * This constructor creates SQ with specified TransferSyntax
	 * @param objTransferSyntax TransferSyntax of the outer DataSet which is necessary to be the same as
	 * 							the inner DataSet in the sequence
	 */
	public SQ(ITransferSyntax objTransferSyntax)
	{
		iVRType = DicomConstants.DCM_DT_SQ;
		objITransferSyntax = objTransferSyntax;
		objSingleCollection = new SingleCollection<IItem>();
		setValue(objSingleCollection);
	}

	/*
	 * This constructor internally calls other constructor which actually does the task
	 * This constructor should throw exception if dictionaryreader is tried to be set as null
	 */
	/**
	 * This constructor creates SQ with default TransferSyntax and specified DictionaryReader
	 * @param objDictionaryReader DictionaryReader used in the outer DataSet which is necessary to be the same as
	 * 							  the inner DataSet in the sequence
	 */
	public SQ(IDictionaryReader objDictionaryReader)
	{
		this(new TransferSyntax(),objDictionaryReader);
	}

	/*
	 * This constructor should throw exception if TS or dictionaryreader is tried to be set as null 
	 */
	/**
	 * This constructor creates SQ with specified transferSyntax and specified DictionaryReader
	 * @param objTransferSyntax TransferSyntax of the outer DataSet which is necessary to be the same as
	 * 							  the inner DataSet in the sequence
	 * @param objDictionaryReader DictionaryReader used in the outer DataSet which is necessary to be the same as
	 * 							  the inner DataSet in the sequence
	 */
	public SQ(ITransferSyntax objTransferSyntax,IDictionaryReader objDictionaryReader)
	{
		iVRType = DicomConstants.DCM_DT_SQ;
		objITransferSyntax = objTransferSyntax;
		objIDictionaryReader = objDictionaryReader;
		objSingleCollection = new SingleCollection<IItem>();
		setValue(objSingleCollection);
	}

	/**
	 * This method retrieves the length of value.
	 * @return int - integer value of length.
	 */
	@SuppressWarnings("unchecked")
	public int getValueLength()
	{
		if(isUndefinedLength)
			return DicomConstants.DCM_UNDEFINED_LENGTH;

		int iValueLength =0;
		if(value != null) // This allows Empty Sequence.
		{
			objSingleCollection = (ISingleCollection<IItem>)value;
			for(int index=0 ; index < objSingleCollection.size() ; index++)
			{ 
				if(objSingleCollection.get(index) != null)
				{
					IItem objIItem = objSingleCollection.get(index);
					iValueLength+=objIItem.getItemLength();
				}
			}
		}
		return iValueLength;
	}

	/**
	 * This method Validates the sequence according to the rules specified by DICOM Satndard
	 * @param objValue object representing custom collection of Items
	 * @return boolean - returns status indicating validation success or failure
	 * @throws ValidationException If validation of Item fails.
	 */
	public boolean validate(Object objValue) throws ValidationException
	{
		return true;
	}

	/**
	 * This method Validates the all Sequence Items according to the rules specified by DICOM Satndard
	 * @return boolean - returns status indicating validation success or failure
	 * @throws ValidationException If validation of Item fails.
	 */
	public boolean validate() throws ValidationException
	{
		try
		{
			ICollectionIterator<IItem> objIterator = objSingleCollection.iterator();
			while(objIterator.hasNext())
			{
				IItem objIItem = objIterator.next();
				objIItem.validate();
			}
		}
		catch( InvalidItemException ie)
		{
			if(DicomConfig.createInstance().isLoggingOn())
			{
				DicomLogger.getLogger().log(ie.getMessage());
			}
				throw new ValidationException(ResourceBundleWrapper.getValue("exceptions", "InvalidSequence", new Locale(DicomConfig.createInstance().getLocale()))+ Integer.toHexString(iGroupNumber) + "," + Integer.toHexString(iElementNumber), ie);
		}
		return true;
	}

	/**
	 * This method Validates the all Sequence Items according to the rules specified by DICOM Standard
	 * @return boolean - returns status indicating validation success or failure
	 * @throws ValidationException If validation of Item fails.
	 */
	public boolean validateForModule() throws ValidationException
	{
		for(IItem objIItem : objSingleCollection)
		{
			try
			{
				objIItem.validateForModule();
			}
			catch( InvalidItemException ie)
			{
				if(DicomConfig.createInstance().isLoggingOn())
				{
					DicomLogger.getLogger().log(ie.getMessage());
				}

				throw new ValidationException(ResourceBundleWrapper.getValue("exceptions", "InvalidSequence", new Locale(DicomConfig.createInstance().getLocale()))+ Integer.toHexString(iGroupNumber) + "," + Integer.toHexString(iElementNumber), ie);
			}
		}
		return true;
	}

	/**
	 * This method sets the TransferSyntax of Sequence Item.
	 * @param objITransferSyntax instance of TransferSyntax
	 */
	public void setTransferSyntax(ITransferSyntax objITransferSyntax)
	{
		this.objITransferSyntax = objITransferSyntax;

	}

	//Sets the Dictionary Reader
	/**
	 * This method sets the DictionaryReader.
	 * @param objIDictionaryReader instance of DictionaryReader
	 */
	public void setDictionaryReader(IDictionaryReader objIDictionaryReader)
	{
		this.objIDictionaryReader =  objIDictionaryReader;
	}

	//Gets the DictionaryReader
	/**
	 * Retrieves the DictionaryReader.
	 * @return IDictionaryReader - instance of DictionaryReader
	 */
	public IDictionaryReader getDictionaryReader()
	{
		return this.objIDictionaryReader;
	}

	/**
	 * Reads the SL DataType structure from underlying stream
	 * @param objIDicomStream stream from which the DataType is to be written
	 * @param iSQLength length of the DicomValue to be read from the stream
	 * @return boolean - returns status success/failure
	 * @throws InvalidDataTypeException If data type validation fails.
	 * @throws IOException If Input/Output operation fails.
	 */
	public boolean read(IDicomStream objIDicomStream,int iSQLength) throws InvalidDataTypeException, IOException
	{
		try
		{
			if(iSQLength == 0)
			{
				return true;
			}

			if(DicomConstants.DCM_UNDEFINED_LENGTH == iSQLength)
			{
				bSequenceDelimitationExpected = true;
				isUndefinedLength = true;
			}

			/*
			 * Actually the length provided by DataElement should not be set in the DicomValue since there may
			 * be padding issues involved. For detailed description see in IDicomValue interface
			 */
			//		/*
			//		 * This line is commented because SQLength is to be set in iValueLength field of parent DicomValue
			//		 */
			////		this.iSQLength=iSQLength;
			//		
			//		iValueLength = iSQLength;
			int iBeforeSequenceRead = objIDicomStream.getReadBytes();
			int iAfterSequenceRead = 0;

			int iTempGroupNumber = 0;
			int iTempElementNumber = 0;

			if(bSequenceDelimitationExpected == true)
			{
				while(true)
				{
					iTempGroupNumber = objIDicomStream.readShort();
					iTempElementNumber = objIDicomStream.readShort();

					if(iTempGroupNumber < 0)
					{
						iTempGroupNumber+= 2;
						iTempGroupNumber = (65534 + iTempGroupNumber);
					}
					if(iTempElementNumber < 0)
					{
						iTempElementNumber+= 2;
						iTempElementNumber = (65534 + iTempElementNumber);
					}

					if(iTempGroupNumber == DicomConstants.DCM_SQ_DELIMITATION_DATA_MAJOR_TAG 
							&& iTempElementNumber == DicomConstants.DCM_SQ_DELIMITATION_DATA_MINOR_TAG)
					{
						objIDicomStream.readInt();
						break;
					}

					//Since the GroupNumber was getting modified even if it
					//is Greater than 0.
					if(iTempGroupNumber == DicomConstants.DCM_NESTED_ITEM_DATA_MAJOR_TAG 
							&& iTempElementNumber == DicomConstants.DCM_NESTED_ITEM_DATA_MINOR_TAG)
					{
						int iItemLength = objIDicomStream.readInt();

						if(iItemLength != 0)
						{
							IItem objIItem = createItem();
							objIItem.setTransferSyntax(objITransferSyntax);
							objIItem.setDictionaryReader(objIDictionaryReader);
							objIItem.read(objIDicomStream,iItemLength);
							addItem(objIItem);
						}
					}
				}
			}
			else
			{
				while(iSQLength != (iAfterSequenceRead - iBeforeSequenceRead))
				{
					iTempGroupNumber = objIDicomStream.readShort();
					iTempElementNumber = objIDicomStream.readShort();

					if(iTempGroupNumber < 0 )
					{
						iTempGroupNumber+= 2;
						iTempGroupNumber = (65534 + iTempGroupNumber);
					}
					//Since the GroupNumber was getting modified even if it
					//is Greater than 0.				
					if(iTempElementNumber < 0)
					{
						iTempElementNumber+= 2;
						iTempElementNumber = (65534 + iTempElementNumber);
					}			
					if(iTempGroupNumber == DicomConstants.DCM_NESTED_ITEM_DATA_MAJOR_TAG 
							&& iTempElementNumber == DicomConstants.DCM_NESTED_ITEM_DATA_MINOR_TAG)
					{
						int iItemLength = objIDicomStream.readInt();

						if(iItemLength != 0)
						{
							IItem objIItem = createItem();
							objIItem.setTransferSyntax(objITransferSyntax);
							objIItem.setDictionaryReader(objIDictionaryReader);
							objIItem.read(objIDicomStream,iItemLength);
							addItem(objIItem);
						}
					}
					iAfterSequenceRead = objIDicomStream.getReadBytes();
				}
			}
			setValue(objSingleCollection);
		}
		catch( InvalidItemException ie)
		{
			if(DicomConfig.createInstance().isLoggingOn())
			{
				DicomLogger.getLogger().log(ie.getMessage());
			}
			throw new InvalidDataTypeException(ResourceBundleWrapper.getValue("exceptions", "InvalidSequence", new Locale(DicomConfig.createInstance().getLocale())), ie);
		}
		return true;
	}

	//get each item and call write on it.
	//When all the items are over, write the Sequence Delemitation Item
	//and the 4 bytes zero length if length is UNDEFINED
	/**
	 * This method writes the series of byte buffers present in the SQ datatype.
	 * For this purpose it writes them in the structure specified in the Dicom standard.
	 * @param objIDicomStream stream on which to write the DataElements
	 * @return int - status indicating success/failure
	 * @throws IOException If Input/Output operation fails.
	 */
	@SuppressWarnings("unchecked")
	public int write(IDicomStream objIDicomStream) throws IOException
	{
		objSingleCollection = (ISingleCollection<IItem>)getValue();
		if(objSingleCollection == null)
			return 1;

		/*
		 * 
		 */
		if(getValueLength() == DicomConstants.DCM_UNDEFINED_LENGTH)
			bSequenceDelimitationExpected = true;
		IItem objIItem= null;
		/*
		 * We have to use the iterator provided in SingleCollection to obtain the items from the sequence
		 */
		ICollectionIterator<IItem> objIterator = objSingleCollection.iterator();
		while(objIterator.hasNext())
		{
			objIItem = objIterator.next();
			if(objIItem != null)
				objIItem.write(objIDicomStream);
		}

		//Here the condition is to be changed to check if the UNDEFINED length is specified by user
		//Then the condition would be if(iValueLength==DicomConstants.DCM_UNDEFINED_LENGTH)
		if(bSequenceDelimitationExpected == true)
		{
			IDataElement objIDataElement = new ImplicitDE(getDictionaryReader());
			objIDataElement.setTag(DicomConstants.DCM_SQ_DELIMITATION_DATA_MAJOR_TAG, DicomConstants.DCM_SQ_DELIMITATION_DATA_MINOR_TAG);
			//Write the Sequence Delemitation Tag and 4 byte length as int
			objIDataElement.serialize(objIDicomStream);
		}
		return 1;
	}

	/**
	 * Retrieves the maximum length of Sequence Item.
	 * @return int - status indicating success/failure
	 */
	public long getMaxLength()
	{
		return 0;
	}

	/**
	 * Sets the maximum length of Sequence Item
	 * @param lMaxLength integer value of length.
	 */
	public void setMaxLength(long lMaxLength)
	{

	}

	/**
	 * Retrieves the TransferSyntax of Sequence Item.
	 * @return ITransferSyntax - instance of TransferSyntax
	 */
	public ITransferSyntax getTransferSyntax()
	{
		return objITransferSyntax;
	}

	/**
	 * Retrieves the value of item from collection.
	 * @return Object - instance of base class Object.
	 */
	@SuppressWarnings("unchecked")
	public Object getValue()
	{
		objSingleCollection = (ISingleCollection<IItem>)value;
		return objSingleCollection;
	}

	/*
	 * The return type is modified to indicate error condition
	 * For error condition, see description in IDataTypeSQ interface
	 */
	/**
	 * This method adds a item in the collection.
	 * @param objIItem instance of IItem.
	 * @return int - status indicating success/failure
	 * @throws InvalidItemException If Item validation fails.
	 */
	public int addItem(IItem objIItem) throws InvalidItemException
	{
		if(isValidItem(objIItem))
		{
			objSingleCollection.add(objIItem);
			return 1;
		}
		else
			return 0; 
	}

	/*
	 * (non-Javadoc) This method is needed to be overriden by SQ because SQ has its own 
	 * container as collection of items
	 * @see cdac.medinfo.sdk.dcm30_04.baselibs.dicomv3.DicomValue#setValue(java.lang.Object)
	 */
	/**
	 * Sets the value of Data Type according to the Dicom Standard.
	 * @param objValue instance of base class Object.
	 * @throws InvalidDataTypeException If DataType validation fails.
	 */
	@SuppressWarnings("unchecked")
	public void setValue(Object objValue) throws InvalidDataTypeException
	{
		try
		{
			validate(objValue);
			value=objValue;

			/*
			 * When we create a new SQ and pass vector of items into it through setValue() method of DicomValue,
			 * this method actually sets the vector of items to the internal vector of items in SQ by taking it
			 * from the value field of DicomValue 
			 */
			objSingleCollection = (ISingleCollection<IItem>)value;
		}
		catch( ValidationException ve)
		{
			if(DicomConfig.createInstance().isLoggingOn())
			{
				DicomLogger.getLogger().log(ve.getMessage());
			}
			throw new InvalidDataTypeException(" Sequence ", ve);
		}
	}	

	/*
	 * This method is intended to be used by specific sequences to allow only 
	 * sequence specific items. If the specific sequence related class is not
	 * available then this method will allow any Item addition in sequence
	 * This method is intended to be overriden by specific sequences to check for addition of
	 * valid items according to the specific sequences
	 */
	/**
	 * This method checks the validity of Item according to Dicom Rules.
	 * @param objIItem instance of Item.
	 * @return boolean - returns status success/failure
	 */
	protected boolean isValidItem(IItem objIItem)
	{
		return true;
	}

	/*
	 * This method will give item of the desired item class depending on runtine sequence object.
	 * This method is intended to be overriden in sub-classes. Otherwise will always give object of parent Item
	 */
	/**
	 * This method creates the Item.
	 * @return IItem - instance of Item.
	 */
	protected IItem createItem()
	{
		return new Item();
	}

	/**
	 * This method returns an iterator over the collection of sequences
	 * @return ICollectionIterator<IItem> - iterator over the collection of IItems
	 */
	public ICollectionIterator<IItem> iterator()
	{
		return objSingleCollection.iterator();
	}

	/**
	 * This method returns an iterator over the collection of sequences
	 * @return ICollectionIterator<IItem> - iterator over the collection of IItems
	 */
	public int noOfItems()
	{
		return objSingleCollection.size();
	}

	/**
	 *  Dumps the value of Items of a Sequence.
	 *  @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
	{
		int index = 1;
		for(IItem objIItem : objSingleCollection)
		{
			objIDicomStream.writeString("\n");
			objIDicomStream.writeString("\tItem# " + index++ + "\n");
			objIItem.dumpDICOMInfo(objIDicomStream);
		}
	}

	/**
	 * Implementation of this method returns the Item of this Sequence.
	 * @return - IItem of this Sequence.
	 */ 
	public IItem getNewItem()
	{
		return this.createItem();
	}

	/**
	 * This function sets the Value length to -1.
	 * If this function is called then then length of value is undefined length(-1)
	 */
	public void changeToUndefined()
	{
		this.isUndefinedLength = true;
	}
}
