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

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.util.Locale;
import cdac.medinfo.sdk.dcm30_04.dicomstreams.DicomBufferStream;
import cdac.medinfo.sdk.dcm30_04.enums.EnumPDUType;
import cdac.medinfo.sdk.dcm30_04.services.dicomnetcommons.IConnectionListener;
import cdac.medinfo.sdk.dcm30_04.services.dicomnetcommons.INetworkAgent;
import cdac.medinfo.sdk.dcm30_04.services.dicomnetcommons.InvalidPDUTypeException;
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.ResourceBundleWrapper;
import cdac.medinfo.sdk.dcm30_04.utils.StandardMethods;
import cdac.medinfo.sdk.dcm30_04.services.dicomnetcommons.ConnectionClosedException;
import cdac.medinfo.sdk.dcm30_04.services.dicomnetcommons.PDULengthException;
import cdac.medinfo.sdk.dcm30_04.services.dicomnetcommons.EnumErrorCode;
import cdac.medinfo.sdk.dcm30_04.services.dicomnetcommons.PDUException;


/**
 * The Class PDUReader.
 */
public class PDUReader extends Thread  
{
	/** The boolean variable representing whether PDU reader is running or stopped */
	boolean stop;

	/** The network agent. */
	private INetworkAgent objINetworkAgent;

	/** The associate pdu receiver. */
	private IAssociatePDUReceiver objIAssociatePDUReceiver;

	/** The service pdu receiver. */
	private IServicePDUReceiver objIServicePDUReceiver;

	/**The Connection listener*/
	private IConnectionListener objIConnectionListener;

	/** The max Length that can be read from network*/
	int iReadMaxLength;

	PDUWriter pduWriter;

	/**
	 * Instantiates a new PDU reader.
	 * @param objINetworkAgent object of Network Agent.
	 */
	public PDUReader(INetworkAgent objINetworkAgent)
	{
		super("PDUReader");
		this.objINetworkAgent = objINetworkAgent;
	}


	/**
	 * Sets PDUWriter on this instance.
	 * @param pduWriter the pduWriter 
	 */
	public void setPduWriter(PDUWriter pduWriter) 
	{
		this.pduWriter = pduWriter;
	}


	/**
	 * Adds the associate pdu receiver.
	 * @param receiver The Associate PDU receiver that will register itself for Association PDU.
	 * If the Association PDU is received it is notified to IAssociate PDU Receiver.
	 */
	public void addAssociatePDUReceiver(IAssociatePDUReceiver receiver)
	{
		objIAssociatePDUReceiver = receiver;
	}

	/**
	 * Adds the service pdu receiver.
	 * @param receiver The Service PDU receiver that will register itself for Service PDU.
	 *  If the Service PDU is received it is notified to IService PDU Receiver.
	 */
	public void addServicePDUReceiver(IServicePDUReceiver receiver)
	{
		objIServicePDUReceiver = receiver;
	}

	
	/**
	 * Adds the connection listener.
	 * The entity that registers itself as an Connection listener is notified 
	 * if connection with the remote entity is disconnected or an exception occurs during read.
	 * @param objIConnectionListener object of Connection listener.
	 */
	public void addConnectionListener(IConnectionListener objIConnectionListener)
	{
		this.objIConnectionListener = objIConnectionListener;
	}
	
	/**
	 * Sets the read max length.
	 * @param maxLength The maximum length of data that can be read.
	 */
	public void setReadMaxLength(int maxLength) 
	{
		iReadMaxLength = maxLength;
	}

	/**
	 * Gets the read max length.
	 * @return int - the read max length
	 */
	public int getReadMaxLength() 
	{
		return iReadMaxLength;
	}

	/**
	 * This method starts the PDU reader.
	 */
	public void run()
	{
		while(!stop)
		{
			try{

				readPDU();
			}
			catch(SocketTimeoutException ste)
			{
				if (DicomConfig.createInstance().isLoggingOn())
				{
					DicomLogger objDicomLogger = DicomLogger.getLogger();					
					if(null != objINetworkAgent && null != objINetworkAgent.getSocketAddress())
						objDicomLogger.log(ResourceBundleWrapper.getValue("warning", "SocketReadTimeout", new Locale(DicomConfig.createInstance().getLocale()))
								+"["+
								ResourceBundleWrapper.getValue("status", "IPAddress",new Locale(DicomConfig.createInstance().getLocale()))+"= "+ objINetworkAgent.getSocketAddress().getAddress().getHostAddress()+" , "+
								ResourceBundleWrapper.getValue("status", "Port",new Locale(DicomConfig.createInstance().getLocale())) + "= "+objINetworkAgent.getSocketAddress().getPort()+"]");

					objDicomLogger.log(ResourceBundleWrapper.getValue("warning", "PDUReaderClosed", new Locale(DicomConfig.createInstance().getLocale())));					
				}
				stop = true;
				objIConnectionListener.closeConnection();
//				objIConnectionListener.onException(new ConnectionClosedException(ste),EnumErrorCode.SOCKET_TIMEOUT);
			}
			catch(ConnectionClosedException cce)
			{
				if (DicomConfig.createInstance().isLoggingOn())
				{
					DicomLogger objDicomLogger = DicomLogger.getLogger();					
					if(null != objINetworkAgent && null != objINetworkAgent.getSocketAddress())
						objDicomLogger.log(ResourceBundleWrapper.getValue("warning", "SCPConnectionClosed", new Locale(DicomConfig.createInstance().getLocale()))
								+"["+
								ResourceBundleWrapper.getValue("status", "IPAddress",new Locale(DicomConfig.createInstance().getLocale()))+"= "+ objINetworkAgent.getSocketAddress().getAddress().getHostAddress()+" , "+
								ResourceBundleWrapper.getValue("status", "Port",new Locale(DicomConfig.createInstance().getLocale())) + "= "+objINetworkAgent.getSocketAddress().getPort()+"]");

					objDicomLogger.log(ResourceBundleWrapper.getValue("warning", "PDUReaderClosed", new Locale(DicomConfig.createInstance().getLocale())));					
				}
				stop = true;
				objIConnectionListener.closeConnection();
			}
			catch(SocketException e)
			{				
				if (DicomConfig.createInstance().isLoggingOn())
				{					
					DicomLogger objDicomLogger = DicomLogger.getLogger();
					objDicomLogger.log(ResourceBundleWrapper.getValue("warning", "SCPConnectionClosed", new Locale(DicomConfig.createInstance().getLocale())));
				}
				stop = true;
				objIConnectionListener.closeConnection();
			}
			catch(IOException e)
			{				
				if (DicomConfig.createInstance().isLoggingOn())
				{					
					DicomLogger objDicomLogger = DicomLogger.getLogger();
					objDicomLogger.log(ResourceBundleWrapper.getValue("warning", "PDUReaderClosed", new Locale(DicomConfig.createInstance().getLocale())));
				}
				stop = true;
				objIConnectionListener.closeConnection();
			}
		}		
	}

	/**
	 * This method reads the PDU from the network.
	 * @throws IOException Signals that IOException has occurred.
	 */
	private void readPDU() throws IOException
	{
		try
		{
		//System.out.println("PDUReader reading the pduBuffer...");
		EnumPDUType enumPDUType = null;
		ByteBuffer pduHeader = null, payload = null;
		int iPDULength =0;
		//		Read PDU Header
		if(objINetworkAgent != null && !stop)
		{
			byte[] buffer = objINetworkAgent.read(6);
			if(buffer != null)
				pduHeader = ByteBuffer.wrap(buffer);
		}
		if(pduHeader != null && pduHeader.capacity() > 0 && !stop)
		{
			//			pduHeader.flip();
			//			ByteArrayInputStream in = new ByteArrayInputStream(pduHeader.array());
			//			DicomBufferStream bStream = new DicomBufferStream();
			//			bStream.setEndianNess(DicomConstants.DCM_ENDIANNESS_LITTLE_ENDIAN);
			//			bStream.setInputStream(in);
			//			try{
			//			enumPDUType =	StandardMethods.getEnumPDUType(bStream.readByte());
			//			bStream.readByte();
			//			iPDULength = bStream.readInt();
			//			}catch(Exception e)
			//			{
			//			e.printStackTrace();
			//			}

			//			Get PDU Type
			enumPDUType = StandardMethods.getEnumPDUType(pduHeader.get());

			// When invalid PDu is received, PDU type is unrecognized also the PDU length is 
			// garbage value Hence aborting session.
			if(enumPDUType.equals(EnumPDUType.NULL))
				throw new InvalidPDUTypeException();

			//			Reserved Byte
			pduHeader.get();
			//			Get PDU Length
			iPDULength = pduHeader.getInt();
			//			if(iPDULength >0)
			//			System.out.println("PDU Length " + iPDULength);
			pduHeader.position(0);
		}
		//		Read PDU of iPDULength from NetworkAgent
		if(iPDULength > 0 && objINetworkAgent != null && !stop)
		{
			byte[] buffer = objINetworkAgent.read(iPDULength);
			if(buffer != null)
				payload = ByteBuffer.wrap(buffer);
		}
		if(payload != null && pduHeader != null)
		{
			payload.position(0);
			ByteBuffer pduBuffer = ByteBuffer.allocate(pduHeader.capacity() + payload.capacity());
			pduBuffer.put(pduHeader);
			pduBuffer.put(payload);
			pduBuffer.position(0);
			parseAndNotify(enumPDUType, pduBuffer, iPDULength);
		}
		}
		catch(InvalidPDUTypeException ipte)
		{
			if(DicomConfig.createInstance().isLoggingOn())
			{
				DicomLogger objDicomLogger = DicomLogger.getLogger();
				objDicomLogger.log(ipte.getMessage());
			}
			objIConnectionListener.onException(ipte,EnumErrorCode.INVALID_PDUTYPE);
		}
		catch(PDULengthException ple)
		{
			if(DicomConfig.createInstance().isLoggingOn())
			{
				DicomLogger objDicomLogger = DicomLogger.getLogger();
				objDicomLogger.log(ple.getMessage());
			}
			objIServicePDUReceiver.onException(new PDUException(ple),EnumErrorCode.AGRRED_PDU_LENGTH_VIOLATED);
		}
	}

	/**
	 * This function parse the PDU and notify service agent or Association agent 
	 * depending on whether service or association PDU is received.
	 * @param enumPDUType the enum value containing pdu type
	 * @param pduBuffer the pdu buffer
	 */
	private void parseAndNotify(EnumPDUType enumPDUType, ByteBuffer pduBuffer, int iPDUlength) throws IOException
	{
		//		Create Stream and write PDU Buffer onto it.Parse the PDU from stream according
		//		to its type
		DicomBufferStream objDicomBufferStream = new DicomBufferStream(); 
		ByteArrayInputStream objByteArrayInputStream = new ByteArrayInputStream(pduBuffer.array());
		objDicomBufferStream.setInputStream(objByteArrayInputStream);
		objDicomBufferStream.setEndianNess(DicomConstants.DCM_ENDIANNESS_BIG_ENDIAN);
		PDU objPDU = null;
		switch(enumPDUType)
		{
		case ASSOCIATE_RQ:
			objPDU = new AssociateRQPDU();
			break;
		case ASSOCIATE_AC:
			objPDU = new AssociateACPDU();
			break;
		case ASSOCIATE_RJ:
			objPDU = new AssociateRJPDU();
			break;
		case ABORT:
			objPDU = new AAbortPDU();
			break;
		case RELEASE_RQ:
			objPDU = new AReleaseRQorRPPDU(EnumPDUType.RELEASE_RQ);
			break;
		case RELEASE_RP:
			objPDU = new AReleaseRQorRPPDU(EnumPDUType.RELEASE_RP);
			break;
		case P_DATA_TF:
		{
			/*
			 *When the PDU length exceeds the negotiated length during agreed association
			 *we send ABORT to client. 
			 */
			if(iPDUlength > iReadMaxLength)
				throw new PDULengthException();
			objPDU = new P_DATA_TF_PDU();
			break;
		}

		}

		//		Parse the PDU
		if(objPDU != null)
		{	
			objPDU.parse(objDicomBufferStream);

			//		Fire Event containing the parsed PDU
			if(objPDU.getPDUType() == EnumPDUType.P_DATA_TF)
			{
				//System.out.println("P-DATA-TF PDU received by PDUHandler... " + objPDU.getLength());
				//			objPDataTF.display();
				ServiceEvent serviceEvent = new ServiceEvent(this);
				serviceEvent.setPDU(objPDU);
				objIServicePDUReceiver.servicePDUReceive(serviceEvent);
			}
			else
			{
				//System.out.println("Association PDU received by PDUHandler... " + objPDU.getLength());
				{
					// System.out.println("Type of Association PDU received by PDUHandler... " + objPDU.getPDUType().getValue());
				}
				AssociateEvent associateEvent = new AssociateEvent(this);
				associateEvent.setPDU(objPDU);
				objIAssociatePDUReceiver.associatePDUReceive(associateEvent);
			}
		}
	}

	/**
	 * This function closes the PDU Reader.
	 */
	public  void close()
	{
		stop = true;
		//objIConnectionListener = null;
		objINetworkAgent = null;
		objIServicePDUReceiver = null;
		objIAssociatePDUReceiver = null;
		pduWriter = null;
	}


}
