/*
* Copyright (c) 1997 - 2016
* Actelion Pharmaceuticals Ltd.
* Gewerbestrasse 16
* CH-4123 Allschwil, Switzerland
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
*    list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
*    this list of conditions and the following disclaimer in the documentation
*    and/or other materials provided with the distribution.
* 3. Neither the name of the the copyright holder nor the
*    names of its contributors may be used to endorse or promote products
*    derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/

package com.actelion.research.chem.io;

import com.actelion.research.chem.MolfileParser;
import com.actelion.research.chem.StereoMolecule;
import com.actelion.research.chem.UniqueStringList;
import com.actelion.research.io.BOMSkipper;

import java.io.*;
import java.nio.charset.StandardCharsets;

public class SDFileParser extends CompoundFileParser {
    private static final int DEFAULT_RECORDS_TO_INSPECT = 10240;
    private static final String[] cIDFieldNames = { "Idorsia No", "Actelion No", "ID", "IDNUMBER", "COMPOUND_ID", "NAME", "COMPND" };
	public static final String cNewLineString = "\n";

	private StringBuilder		mMolfileBuffer,mDataBuffer;
	private StereoMolecule		mMol;
	private String[]			mFieldName;
	private String[]			mFieldData;
	private int					mNoOfRecords,mIDFieldIndex;
	private boolean				mAssumeChiralTrue;

	public SDFileParser(String fileName) {
		this(fileName, null);
		}

	public SDFileParser(String fileName, String[] fieldName) {
	    mNoOfRecords = -1;
	    
		mFieldName = fieldName;
		
		try {
			mReader = new BufferedReader(new InputStreamReader(new FileInputStream(fileName), StandardCharsets.UTF_8));
			BOMSkipper.skip(mReader);
		} catch (IOException e) {
			mReader = null;
		}

		init();
		}


	public SDFileParser(File file) {
		this(file, null);
		}


	public SDFileParser(File file, String[] fieldName) {
        mNoOfRecords = -1;
		mFieldName = fieldName;
		try {
    		mReader = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8));
			BOMSkipper.skip(mReader);
		} catch (IOException e) {
			mReader = null;
		}
		
		init();
	}


	public SDFileParser(Reader reader) {
		this(reader, null);
	}


	public SDFileParser(Reader reader, String[] fieldName) {
        mNoOfRecords = -1;
		mFieldName = fieldName;
		mReader = (reader instanceof BufferedReader) ? (BufferedReader)reader : new BufferedReader(reader);
		
		init();
		}


	/**
	 * Some software exports mol/sd-files with an unset chiral flag despite
	 * the original molecule is a pure enantiomer. This method allows the
	 * MolfileParser to override the molfile's chiral flag for such cases.
	 * @param b
	 */
	public void setAssumeChiralTrue(boolean b) {
		mAssumeChiralTrue = b;
		}


	private void init() {
		mMolfileBuffer = new StringBuilder(10240);
		mDataBuffer = new StringBuilder(10240);
		}
	
	private void extractAllFieldNames(int recordsToInspect) {
	    int records = 0;
//		TreeSet<String> fieldNameList = new TreeSet<String>(); Changed to keep the original order of field names. TLS 6Jan16
		UniqueStringList fieldNameList = new UniqueStringList();

		while (records < recordsToInspect) {
			String line;
			try {
				line = mReader.readLine();
				}
			catch (IOException e) {
				if (records < recordsToInspect)
					mNoOfRecords = records;
				break;
				}

			if (line == null) {
				if (records < recordsToInspect)
					mNoOfRecords = records;
				break;
				}

			if (line.startsWith("$$$$"))
				records++;

			if (line.startsWith(">")) {
				String fieldName = extractFieldName(line);
				if (fieldName != null)
					fieldNameList.addString(fieldName);
				}
			}

		try {
			mReader.close();
		    }
		catch (IOException e) {}

		mFieldName = fieldNameList.toArray();
		}


	/**
	 * Only accurate if getFieldNames() or getFieldNames(int) was called earlier
	 * and if the number of records of the SD-file is smaller than the number
	 * of records that were examined within the getFieldNames() method.
	 * If not all records of the file were seen, then -1 is returned.
	 * For getRowCount() to reliably return the record count call getFieldNames(Integer.MAX_VALUE) first.
	 * @return number of rows or -1
	 */
	public int getRowCount() {
		return mNoOfRecords;
		}


	protected boolean advanceToNext() {
		if (mReader == null)
			return false;

// removed 13.8.2012 TLS; no need to read molfile in order to advance to the next record
//		if (mMolfileBuffer.length() != 0)
//			return true;

		mMolfileBuffer.setLength(0);
		mDataBuffer.setLength(0);
		
    	mMol = null;

		boolean molfileComplete = false;
		int fieldIndex = -1;
		String fieldName = null;
		String line;
		mFieldData = (mFieldName == null) ? null : new String[mFieldName.length];
		mIDFieldIndex = -1;

		do {
			try {
				line = mReader.readLine();
				if (line == null) {
	    			mMolfileBuffer.setLength(0);
		    		mReader.close();
			    	return false;
				    }
				}
			catch (IOException e) {
				mMolfileBuffer.setLength(0);
				return false;
				}

			if (!molfileComplete) {
				if (line.startsWith(">")) {	// to handle sd-record with molfiles without 'M  END'
					molfileComplete = true;
					mMolfileBuffer.append("M  END");
		    		mMolfileBuffer.append('\n');
		    		mDataBuffer.append(line);
		    		mDataBuffer.append('\n');
					}
				else {
					mMolfileBuffer.append(line);
		    		mMolfileBuffer.append('\n');
			    	if (line.startsWith("M  END"))
						molfileComplete = true;
					continue;
					}
				}
			else {
	    		mDataBuffer.append(line);
	    		mDataBuffer.append('\n');
				}

			if (mFieldName != null) {
				if (line.length() == 0) {
					fieldIndex = -1;
					}
				else if (fieldIndex == -1) {
					fieldName = extractFieldName(line);
					if (fieldName != null) {
					    // find fieldIndex to given fieldName
						fieldIndex = -1;
						for (int field=0; field<mFieldName.length; field++) {
							if (fieldName.equals(mFieldName[field])) {
								fieldIndex = field;
								break;
								}
							}

						// check whether field qualifies as compound identifier
						if (mIDFieldIndex == -1) {
                            for (String idName:cIDFieldNames) {
                                if (fieldName.equals(idName)) {
                                    mIDFieldIndex = fieldIndex;
                                    break;
                                    }
                                }
                            
                            }
						}
					}
				else {
					if (mFieldData[fieldIndex] == null) {
						mFieldData[fieldIndex] = line;
						}
					else {
						mFieldData[fieldIndex] = mFieldData[fieldIndex].concat(cNewLineString).concat(line);
						}
					}
				}
			} while (!line.startsWith("$$$$"));

		return true;
		}


	/**
	 * @return the molecule of the current record (null in case of parsing error)
	 */
	public StereoMolecule getMolecule() {
	    if (mMol != null)
	        return mMol;

		MolfileParser molfileParser = new MolfileParser();
		if (mAssumeChiralTrue)
			molfileParser.setAssumeChiralTrue(true);

	    mMol = molfileParser.getCompactMolecule(getNextMolFile());
	    if (mMol != null && (mMol.getName() == null || mMol.getName().length() == 0))
	        mMol.setName(getMoleculeName());
	    return mMol;
	    }


    public String getMoleculeName() {
        return (mIDFieldIndex != -1 && mFieldData != null) ?
                mFieldData[mIDFieldIndex] : (mMol != null) ? mMol.getName() : null;
        }


	/**
	 * Returns the molfile of the current record
	 * as one big String as it was read from the input file.
	 * Line endings are '\n'.
	 * @return 
	 */
    public String getNextMolFile() {
		String molfile = mMolfileBuffer.toString();
		return molfile;
		}


	/**
	 * Returns the field data of the current record
	 * as one big String as it was read from the input file.
	 * Line endings are '\n'.
	 * @return 
	 */
    public String getNextFieldData() {
		String fieldData = mDataBuffer.toString();
		return fieldData;
		}


	/**
	 * Returns a list of field names. If the field names were not passed
	 * to the constructor of SDFileParser, this method parses the file/reader
	 * to extract all field names and uses up this SDFileParser. In this case
	 * one needs to instantiate a new SDFileParser to sequentially iterate
	 * through the file/reader's records and supply the field name array to
	 * the constructor.
	 * @return array of field names
	 */
	public String[] getFieldNames() {
        if (mFieldName == null)
            extractAllFieldNames(DEFAULT_RECORDS_TO_INSPECT);

        return mFieldName;
	    }

    /**
     * Returns a list of field names. If the field names were not passed
     * to the constructor of SDFileParser, this method parses the file/reader
     * <recordsToInspect> records to extract all field names and uses up this
     * SDFileParser. In this case one needs to instantiate a new SDFileParser
     * to sequentially iterate through the file/reader's records and supply
     * the field name array to the constructor.
     * @return array of field names
     */
	public String[] getFieldNames(int recordsToInspect) {
        if (mFieldName == null)
            extractAllFieldNames(recordsToInspect);

        return mFieldName;
	    }

	/*	public boolean moreRecordsAvailable() {
		if (mFieldDataList == null || mFieldDataList.size() == 0)
			return false;

		mCurrentFieldData = mFieldDataList.get(0);
		mFieldDataList.remove(0);
		return true;
		}
*/

	public String getFieldData(int index) {
		if (mFieldData == null)
			return null;

		return mFieldData[index];
		}


	protected String extractFieldName(String line) {
		if (line.length() == 0
		 || line.charAt(0) != '>')
			return null;

		int index = 1;
		int openBracket = 0;
		int closeBracket = 0;
		while (index < line.length()) {
			if (line.charAt(index) == '<') {
				if (openBracket != 0)
					return null;
				openBracket = index;
				}
			else if (line.charAt(index) == '>') {
				if (closeBracket != 0)
					return null;
				closeBracket = index;
				}
			index++;
			}

		if (openBracket != 0 && openBracket < closeBracket)
			return line.substring(openBracket+1, closeBracket);

		// allow for MACCS-II field numbers, which have format DTn
		index = line.indexOf("DT", 1);
		if (index == -1)
			return null;

		int i = index+2;
		while (line.length()>i && Character.isDigit(line.charAt(i)))
			i++;
		
		return (i == index+2) ? null : line.substring(index, i);
		}
	}
