/*******************************************************************************
 *  Copyright 2017 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.hl7282.baselibs.parserserializer;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Locale;

import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

import cdac.medinfo.sdk.hl7282.baselibs.messages.LocalExtensionAgent;
import cdac.medinfo.sdk.hl7282.baselibs.messages.MessageFactory;
import cdac.medinfo.sdk.hl7282.baselibs.segments.BHS;
import cdac.medinfo.sdk.hl7282.baselibs.segments.FHS;
import cdac.medinfo.sdk.hl7282.baselibs.segments.MSH;
import cdac.medinfo.sdk.hl7282.baselibs.segments.SegmentFactory;
import cdac.medinfo.sdk.hl7282.baselibs.segments.SegmentSet;
import cdac.medinfo.sdk.hl7282.commoninterface.IDelimiter;
import cdac.medinfo.sdk.hl7282.commoninterface.IHL7Stream;
import cdac.medinfo.sdk.hl7282.commoninterface.ISegment;
import cdac.medinfo.sdk.hl7282.commoninterface.ISegmentSet;
import cdac.medinfo.sdk.hl7282.exceptions.InvalidSegmentException;
import cdac.medinfo.sdk.hl7282.exceptions.InvalidSegmentSetException;
import cdac.medinfo.sdk.hl7282.exceptions.ParseException;
import cdac.medinfo.sdk.hl7282.exceptions.SegmentNotSupportedException;
import cdac.medinfo.sdk.hl7282.hl7streams.HL7FileStream;
import cdac.medinfo.sdk.hl7282.unrecognized.UnRecognizedSegment;
import cdac.medinfo.sdk.hl7282.utils.EnumSegments;
import cdac.medinfo.sdk.hl7282.utils.HL7Config;
import cdac.medinfo.sdk.hl7282.utils.HL7Constants;
import cdac.medinfo.sdk.hl7282.utils.HL7Logger;
import cdac.medinfo.sdk.hl7282.utils.ResourceBundleWrapper;
import cdac.medinfo.sdk.hl7282.utils.StandardMethods;

/**
 * The HL7Parser parses the HL7Message and provides a SegmentSet.
 * SegmentSet contains all segments of a HL7 Message in the same order as they appeared.
 */
public class HL7Parser 
{
	

	private LocalExtensionAgent objLocalExtensionAgent = null;
	
	public HL7Parser()
	{
		objLocalExtensionAgent = MessageFactory.createInstance().getLocalExtensionAgent();
	}

	/**
	 * Sets LocalExtensionAgent to process locally extended segments.
	 * @param objAgent instance of LocalExtensionAgent
	 */
	public void setLocalExtensionAgent(LocalExtensionAgent objAgent)
	{
		this.objLocalExtensionAgent = objAgent;
	}

	/**
	 * Retrieves instance of LocalExtensionAgent.
	 * @return LocalExtensionAgent - instance of LocalExtensionAgent
	 */
	public LocalExtensionAgent getLocalExtensionAgent()
	{
		return this.objLocalExtensionAgent;
	}

	/**
	 * Parses the HL7Stream which contains a HL7 Message and populates the SegmentSet accordingly.
	 * @param objHL7Stream instance of HL7Stream
	 * @return ISegmentSet instance of SegmentSet
	 * @throws IOException if Input/Output operation fails
	 * @throws ParseException if parsing operation fails
	 * @throws SegmentNotSupportedException if Segment is not supported or not registered with LocalExtensionAgent
	 */
	public ISegmentSet parse(IHL7Stream objHL7Stream) throws IOException, ParseException,SegmentNotSupportedException
	{
		String strMsgHeader = EnumSegments.HL7_SEG_MSH.getValue();
		String strBatchHeader = EnumSegments.HL7_SEG_BHS.getValue();
		String strBatchFileHeader = EnumSegments.HL7_SEG_FHS.getValue();
		
		ISegmentSet objSegmentSet = new SegmentSet();
		IDelimiter objIDelimiter = null;
		ISegment objSegment;
		StringBuffer strSegmentName = new StringBuffer();	
		int iRetVal = 0;
		String strSegment="";
		try
		{
			StandardMethods.clearStringBuffer(strSegmentName);
			iRetVal = objHL7Stream.readString(strSegmentName,3);
			if(strSegmentName.toString().equals(strMsgHeader) || strSegmentName.toString().equals(strBatchHeader) || strSegmentName.toString().equals(strBatchFileHeader))
			{
				while(iRetVal != HL7Constants.END_OF_STREAM)
				{
					/*
					 * Each segment will be first checked for registration as a locally extended segment.  
					 */
					if(objLocalExtensionAgent != null && objLocalExtensionAgent.isSegmentLocallyRegistered(strSegmentName.toString()))
					{
						objSegment = objLocalExtensionAgent.createSegment(strSegmentName.toString());
					}
					else
					{
						objSegment = SegmentFactory.createSegment(strSegmentName.toString());
					}
					
					if(objSegment == null)
					{ 
						char cReadChar ;
					    strSegment=strSegmentName.toString();
						do
						{
							cReadChar = objHL7Stream.readChar();
							strSegment=strSegment+cReadChar;	
							if(objHL7Stream.available() == 0 && cReadChar != '\r')
							{
								
								try {
									throw new ParseException(ResourceBundleWrapper.getValue("exceptions", "MissingCarrigeReturn", new Locale(HL7Config.createInstance().getLocale())));
								} catch (ParseException e) {
									if(HL7Config.createInstance().isLoggingOn())
									{
										HL7Logger objHL7Logger = HL7Logger.getLogger();						
										objHL7Logger.log(e.getMessage());
									}
									break;
								}
							}
						}while(cReadChar != '\r');
						UnRecognizedSegment objUnRecognizedSegment = new UnRecognizedSegment();
						objUnRecognizedSegment.setName(strSegmentName.toString());
						objUnRecognizedSegment.setUnRecognizedData(strSegment);
						objSegmentSet.addUnRecognizedSegment(objUnRecognizedSegment);
					}
					else
					{
						char cReadChar ;
					  	strSegment="";
						do
						{
							cReadChar = objHL7Stream.readChar();
							strSegment=strSegment+cReadChar;
							if(objHL7Stream.available() == 0 && cReadChar != '\r')
							{
								
								try {
									throw new ParseException(ResourceBundleWrapper.getValue("exceptions", "MissingCarrigeReturn", new Locale(HL7Config.createInstance().getLocale())));
								} catch (ParseException e) {
									if(HL7Config.createInstance().isLoggingOn())
									{
										HL7Logger objHL7Logger = HL7Logger.getLogger();						
										objHL7Logger.log(e.getMessage());
									}
									break;
								}
							}
						}while(cReadChar != '\r');						
					int retValue =objSegment.parse(strSegment, objIDelimiter);
					if(retValue == HL7Constants.HL7_INVALID_SEGMENT_TERMINATOR)
					{
						UnRecognizedSegment objUnRecognizedSegment = new UnRecognizedSegment();
						objUnRecognizedSegment.setName(strSegmentName.toString());
						objUnRecognizedSegment.setUnRecognizedData(strSegment);
						objSegmentSet.addUnRecognizedSegment(objUnRecognizedSegment);
					}
					}
					
					if(objSegment!=null)
					{
						if(objSegment.getName() == EnumSegments.HL7_SEG_MSH)
							objIDelimiter = ((MSH)objSegment).getDelimiter();
						else if(objSegment.getName() == EnumSegments.HL7_SEG_BHS)
							objIDelimiter = ((BHS)objSegment).getDelimiter();
						else if(objSegment.getName() == EnumSegments.HL7_SEG_FHS)
							objIDelimiter = ((FHS)objSegment).getDelimiter();

						objSegmentSet.addSegment(objSegment);
					}
					StandardMethods.clearStringBuffer(strSegmentName);
					iRetVal = objHL7Stream.readString(strSegmentName,3);
				}
			}
			else
			{
				throw new InvalidSegmentSetException(ResourceBundleWrapper.getValue("exceptions", "MissingHeader", new Locale(HL7Config.createInstance().getLocale())));
			}
		}
		catch(InvalidSegmentSetException ex)
		{
			throw new ParseException(this.getClass().getSimpleName(),ex);
		}
		catch(InvalidSegmentException ex)
		{
			throw new ParseException(this.getClass().getSimpleName(),ex);
		}
		return objSegmentSet;
	}
	
	/**
	 * Parses the HL7 Message from given file and populates the SegmentSet accordingly.
	 * @param strFilePath path to file
	 * @return ISegmentSet instance of SegmentSet
	 * @throws IOException if Input/Output operation fails
	 * @throws ParseException if parsing operation fails
	 */
	public ISegmentSet parse(String strFilePath)throws IOException,ParseException
	{
		FileInputStream objFileInputStream = new FileInputStream(strFilePath);
		HL7FileStream objHL7FileStream = new HL7FileStream();
		objHL7FileStream.setInputStream(objFileInputStream);
		
		ISegmentSet objSegmentSet = parse(objHL7FileStream);
		

		objHL7FileStream.close();

		return objSegmentSet;
	}
	
	/**
	 * Parses the HL7 specified XML File to create the Segment string and populates the SegmentSet accordingly.
	 * @param strXMLFilePath path of XMl file to be read 
	 * @return ISegmentSet instance of SegmentSet
	 * @throws IOException if Input/Output operation fails
	 * @throws ParseException if parsing operation fails
	 * @throws SegmentNotSupportedException if Segment is not supported or not registered with LocalExtensionAgent
	 * @throws XMLStreamException if unexpected processing errors occur in XML file
	 */
	public ISegmentSet readXML(String strXMLFilePath)throws IOException,ParseException, SegmentNotSupportedException, XMLStreamException
	{
		Reader objReader= new FileReader(strXMLFilePath);
		ISegmentSet objSegmentSet = readXML(objReader);
		
		objReader.close();

		return objSegmentSet;
	}
	

	/**
	 * Parses the HL7 specified XML to create the Segment string and populates the SegmentSet accordingly.
	 * @param xmlFile to be read 
	 * @return ISegmentSet instance of SegmentSet
	 * @throws IOException if Input/Output operation fails
	 * @throws ParseException if parsing operation fails
	 * @throws SegmentNotSupportedException if Segment is not supported or not registered with LocalExtensionAgent
	 * @throws XMLStreamException if unexpected processing errors occur in XML file
	 */
	public ISegmentSet readXML(Reader objXMLReader) throws IOException, ParseException,SegmentNotSupportedException, XMLStreamException
	{
		String strMsgHeader = EnumSegments.HL7_SEG_MSH.getValue();
		String strBatchHeader = EnumSegments.HL7_SEG_BHS.getValue();
		String strBatchFileHeader = EnumSegments.HL7_SEG_FHS.getValue();
		
		ISegmentSet objSegmentSet = new SegmentSet();
		IDelimiter objIDelimiter = null;
		ISegment objSegment;
		String segmentName=null;
		String strSegment="";
		try
		{
			XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
			
			/*
			 * if & is encountered then stax considers it as a separate event. To get whole data of characters as one event set the isCoalescing property to true. By default it is false
			 **/			
			xmlInputFactory.setProperty("javax.xml.stream.isCoalescing", true);	
			
	            XMLEventReader xmlEventReader = xmlInputFactory.createXMLEventReader(objXMLReader);
	           /*count=-1 means that the element is message structure. Eg<ADT_A01>
	             count=0 to check the first segment
	             count=1 means element is a segment. Eg <MSH>,<EVN> etc
	             count=2 is to check if Group is encountered
	            */	            
	            int count=-1;
	            while(xmlEventReader.hasNext()){
	                XMLEvent xmlEvent = xmlEventReader.nextEvent();
	               if (xmlEvent.isStartElement()){
	            	   if(count==-1){
		            		count=0;
		            		continue;
		            	}	                   
	              StartElement startElement = xmlEvent.asStartElement();
	                	  if(count==0 && startElement.getName().getLocalPart().equals(strMsgHeader)||startElement.getName().getLocalPart().equals(strBatchHeader)||startElement.getName().getLocalPart().equals(strBatchFileHeader)){	                      
	                		  segmentName =startElement.getName().getLocalPart();
	                		  
	                		  count=1;
	                	  }else if(count==2){
	                		  segmentName =startElement.getName().getLocalPart();
	                		  if(segmentName.contains("."))
	                		  {
	                			  continue;
	                		  }
	                		 
	                		  if(segmentName.length()==3)
	                		  {
	                			  count=1;
	                		  }
	                		 
	                	  }
	                	  else
			       			{
			       				throw new InvalidSegmentSetException(ResourceBundleWrapper.getValue("exceptions", "MissingHeader", new Locale(HL7Config.createInstance().getLocale())));
			       			}	                	  
	               }
	                	if(count==1){
	                	if(segmentName==null){
	                		 strSegment= parseSegment(xmlEventReader,xmlEvent, strSegment);
		                	 if(strSegment==null){
		                		 try {
										throw new ParseException(ResourceBundleWrapper.getValue("exceptions", "MissingEndElement", new Locale(HL7Config.createInstance().getLocale())));
									} catch (ParseException e) {
										if(HL7Config.createInstance().isLoggingOn())
										{
											HL7Logger objHL7Logger = HL7Logger.getLogger();						
											objHL7Logger.log(e.getMessage());
										}
										break;
									} 
		                	 }
	                		UnRecognizedSegment objUnRecognizedSegment = new UnRecognizedSegment();
							objUnRecognizedSegment.setName(segmentName);
							objUnRecognizedSegment.setUnRecognizedData(strSegment);
							objSegmentSet.addUnRecognizedSegment(objUnRecognizedSegment);
	                	}
	                		else{
	                			  	
			                	 strSegment= parseSegment(xmlEventReader,xmlEvent,strSegment);
			                	 if(strSegment==null){
			                		 try {
											throw new ParseException(ResourceBundleWrapper.getValue("exceptions", "MissingEndElement", new Locale(HL7Config.createInstance().getLocale())));
										} catch (ParseException e) {
											if(HL7Config.createInstance().isLoggingOn())
											{
												HL7Logger objHL7Logger = HL7Logger.getLogger();						
												objHL7Logger.log(e.getMessage());
											}
											break;
										} 
			                	 }
			                	 if(objLocalExtensionAgent != null && objLocalExtensionAgent.isSegmentLocallyRegistered(segmentName))
			      					{
			      						objSegment = objLocalExtensionAgent.createSegment(segmentName);
			      					}
		                		  else{
		                			  objSegment = SegmentFactory.createSegment(segmentName);
		                		  }
			                	 strSegment=strSegment+'\r';

			                        	int retValue =objSegment.parse(strSegment, objIDelimiter);
			                        	if(retValue == HL7Constants.HL7_INVALID_SEGMENT_TERMINATOR)
			        					{
			        						UnRecognizedSegment objUnRecognizedSegment = new UnRecognizedSegment();
			        						objUnRecognizedSegment.setName(segmentName);
			        						objUnRecognizedSegment.setUnRecognizedData(strSegment);
			        						objSegmentSet.addUnRecognizedSegment(objUnRecognizedSegment);
			        					}
			                        	strSegment="";
				                	  if(objSegment!=null)
					    				{
					    					if(objSegment.getName() == EnumSegments.HL7_SEG_MSH)
					    						objIDelimiter = ((MSH)objSegment).getDelimiter();
					    					else if(objSegment.getName() == EnumSegments.HL7_SEG_BHS)
					    						objIDelimiter = ((BHS)objSegment).getDelimiter();
					    					else if(objSegment.getName() == EnumSegments.HL7_SEG_FHS)
					    						objIDelimiter = ((FHS)objSegment).getDelimiter();

					    					objSegmentSet.addSegment(objSegment);					    					
					    				}
		                	}
	                		count=2;
	                	}	           
	            }
		}catch (FileNotFoundException e) {
	            e.printStackTrace();
	        }
		catch(InvalidSegmentSetException ex)
		{
			throw new ParseException(this.getClass().getSimpleName(),ex);
		}
		catch(InvalidSegmentException ex)
		{
			throw new ParseException(this.getClass().getSimpleName(),ex);
		}
		return objSegmentSet;
	}

	/**
	 * Parses the Segment for field, component, subcomponent and repetition elements
	 * @param xmlEventReader to parse through the entire Segment
	 * @param xmlFirstEvent to get the current Segment 
	 * @param strSegment stores the parsed segment
	 * @return String containing the parsed Segment
	 * @throws XMLStreamException 
	 */
	private String parseSegment(XMLEventReader xmlEventReader,XMLEvent xmlFirstEvent,String strSegment) throws XMLStreamException{
		StartElement currentSegment=xmlFirstEvent.asStartElement();
		String currentSegmentName=currentSegment.getName().getLocalPart();
		
		/*elementDepth checks if xml element is a field, component or subcomponent*/
		int elementDepth=0;
		
		/*fieldCounter maintains the order in which fields appear*/
		int fieldCounter=0;
		
		/*componentCounter maintains the order in which components appear*/
		int componentCounter=0;
		
		/*subcomponentCounter maintains the order in which subcomponents appear*/
		int subcomponentCounter=0;
		
		while(xmlEventReader.hasNext()){  	
            XMLEvent xmlEvent;
				xmlEvent = xmlEventReader.nextEvent();
			
           if (xmlEvent.isStartElement()){ 
        	   elementDepth++;
          StartElement startElement = xmlEvent.asStartElement();
          
         /*elementDepth is 1 if element is field */
		if(elementDepth==1){
			int fieldSeq=Integer.parseInt((startElement.getName().getLocalPart()).split("\\.")[1]);
			fieldCounter++;
			
			/*if fields repeat then the fieldCounter will be greater than the fieldSeq Eg<PID.3>...</PID.3><PID.3>...</PID.3>
			 * then add repetition separator
			 * */
			if(fieldCounter>fieldSeq){
				fieldCounter--;
				strSegment=strSegment+"~";
			}		
			else{
				if(currentSegmentName=="MSH" || currentSegmentName=="BHS" || currentSegmentName=="FHS"){
					
					/*when segment is MSH then field separator is added only after second field because MSH segment's
					 *  first field's value itself is field separator therefore we do not separately add
					 *  */
					if(fieldSeq>2){
						strSegment=strSegment+"|";
						if(fieldSeq!=fieldCounter){
							
							/*when fields are missing then adds field separator*/
							while(fieldSeq!=fieldCounter){
								strSegment=strSegment+"|";
								fieldCounter++;
							}
						}
					}
				}
				else{
					/*for all segments other than MSH add field separator from first field itself*/
						strSegment=strSegment+"|";
						
						/*when fields are missing then adds field separator*/
						if(fieldSeq!=fieldCounter){
							while(fieldSeq!=fieldCounter){
								strSegment=strSegment+"|";
								fieldCounter++;
							}
						}
			}
			}	
		XMLEvent event=xmlEventReader.nextEvent();
		int eventType=event.getEventType();
		
		if( eventType==XMLStreamConstants.CHARACTERS){
         Characters data = event.asCharacters(); 
         if(data.getData().contains("\n")||(data.getData().contains("\t")))
        	 continue;
         else
        	 strSegment=strSegment+data.getData();
		}
		}	
		/*elementDepth is 1 if element is component*/
		if(elementDepth==2){
			int eventType=xmlEvent.getEventType();
			if(eventType==XMLStreamConstants.START_ELEMENT){
		   	   int componentSeq=Integer.parseInt((startElement.getName().getLocalPart()).split("\\.")[1]);
	       	   componentCounter++;
	       	   
	       	/*if components repeat then the componentCounter will be greater than the componentSeq then adds repetition separator*/
	       	   if(componentCounter>componentSeq){
	       		   componentCounter--;
	       		strSegment=strSegment+"~";
	       	   }else{
	       		if(componentCounter>1)
	    	       	strSegment=strSegment+"^";
	       		
	       		/*when components are missing then adds component separator*/
	    	       	if(componentSeq!=componentCounter){
	    				while(componentSeq!=componentCounter){
	    					strSegment=strSegment+"^";
	    					componentCounter++;
	    				}
	    	       	}
	       	   }
		}
			XMLEvent event=xmlEventReader.nextEvent();
			eventType=event.getEventType();
			if(eventType==XMLStreamConstants.CHARACTERS){
	         Characters data = event.asCharacters(); 
	         if(data.getData().contains("\n")||(data.getData().contains("\t")))
	        	 continue;
	         else
	        	 strSegment=strSegment+data.getData();
			}else
				continue;
		}
		/*elementDepth is 3 if element is subcomponent*/
		if(elementDepth==3){
			int eventType=xmlEvent.getEventType();
			if(eventType==XMLStreamConstants.START_ELEMENT){
		   	   int subcomponentSeq=Integer.parseInt((startElement.getName().getLocalPart()).split("\\.")[1]);
		   	subcomponentCounter++;
		   	
		   	/*if subcomponents repeat then the subcomponentCounter will be greater than the subcomponentSeq*/
		   	if(subcomponentCounter>subcomponentSeq){
		   		subcomponentCounter--;
		   		strSegment=strSegment+"~";
		   	}
		   	else{
		   		if(subcomponentCounter>1)
			       	strSegment=strSegment+"&";
		   		
		   		/*when subcomponents are missing then adds subcomponent separator*/
			       	if(subcomponentSeq!=subcomponentCounter){
						while(subcomponentSeq!=subcomponentCounter){
							strSegment=strSegment+"&";
							subcomponentCounter++;
						}
			       	}
		   	}
		}
			XMLEvent event=xmlEventReader.nextEvent();
			eventType=event.getEventType();
			if(eventType==XMLStreamConstants.CHARACTERS){
	         Characters data = event.asCharacters(); 
	         if(data.getData().contains("\n")||(data.getData().contains("\t")))
	        	 continue;
	         else
	        	 strSegment=strSegment+data.getData();
			}else
				continue;
           }
         }
		
           if(xmlEvent.isEndElement()){
        	elementDepth--;
        	if(elementDepth==0 && componentCounter!=0)
        		componentCounter=0;
        	if(elementDepth==1 && subcomponentCounter!=0)
        		subcomponentCounter=0;
               EndElement endElement = xmlEvent.asEndElement();
               if(endElement.getName().getLocalPart().equals(currentSegmentName)){
               return strSegment;
               }
           }
			
		}
		return null;
	}
	
}
