context: /* Copyright (C) 2005-2008   Nina Jeliazkova <nina@acad.bg>
   *                    2009   Egon Willighagen <egonw@users.sf.net>
   *
   * Contact: cdk-devel@lists.sourceforge.net
   *
   * This program is free software; you can redistribute it and/or
   * modify it under the terms of the GNU Lesser General Public License
   * as published by the Free Software Foundation; either version 2.1
   * of the License, or (at your option) any later version.
   * All we ask is that proper credit is given for our work, which includes
   * - but is not limited to - adding the above copyright notice to the beginning
   * of your source code files, and to any copyright notice that you may distribute
   * with programs based on this work.
   *
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT Any WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU Lesser General Public License for more details.
   *
   * You should have received a copy of the GNU Lesser General Public License
   * along with this program; if not, write to the Free Software
   * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
   */
  
  using NCDK.Common.Collections;
  using NCDK.IO.Formats;
  using NCDK.IO.Listener;
  using System;
  using System.Collections.Generic;
  using System.Diagnostics;
  using System.Globalization;
  using System.IO;
  using System.Linq;
  using System.Text;
  
  namespace NCDK.IO.RandomAccess
  {
      /// <summary>
      /// Random access of SDF file. Doesn't load molecules in memory, uses prebuilt
      /// index and seeks to find the correct record offset.
      /// </summary>
      /// <remarks>
      /// Random access to text files of compounds.
      /// Reads the file as a text and builds an index file, if the index file doesn't already exist.
      /// The index stores offset, length and a third field reserved for future use.
      /// Subsequent access for a record N uses this index to seek the record and return the molecule.
      /// Useful for very big files.
      /// </remarks>
      // @author     Nina Jeliazkova <nina@acad.bg>
      // @cdk.module io
      public class RandomAccessSDFReader
          : RandomAccessChemObjectReader<IAtomContainer>
      {
          private IChemObjectBuilder Builder { get; set; }
  
          private Stream raFile;
          private readonly object lockRaFile = new object();
  
          private readonly string filename;
          private int indexVersion = 1;
  
          private readonly object lockIndexing = new object();
          private long[][] index = null;
          private int numberOfRecords;
          private byte[] b;
          private bool IsIndexCreated { get; set; } = false;
  
          public RandomAccessSDFReader(string file)
              : this(file, null, null)
          {
          }
  
          /// <summary>
          /// Reads the file and builds an index file, if the index file doesn't already exist.
          /// </summary>
          /// <param name="file">the file object containing the molecules to be indexed</param>
          /// <param name="builder">a chem object builder</param>
          /// <exception cref="System.IO.IOException">if there is an error during reading</exception>
          public RandomAccessSDFReader(string file, IChemObjectBuilder builder)
              : this(file, builder, null)
          {
          }
  
          /// <summary>
          /// Reads the file and builds an index file, if the index file doesn't already exist.
          /// </summary>
          /// <param name="file">file the file object containing the molecules to be indexed</param>
          /// <param name="builder">builder a chem object builder</param>
          /// <param name="listener">listen for read event</param>
          /// <exception cref="System.IO.IOException">if there is an error during reading</exception>
          public RandomAccessSDFReader(string file, IChemObjectBuilder builder, IReaderListener listener)
              : base()
          {
              this.filename = Path.GetFullPath(file);
              this.Builder = builder ?? CDK.Builder;
              if (listener != null)
                  Listeners.Add(listener);
              raFile = new FileStream(filename, FileMode.Open, FileAccess.Read);
              numberOfRecords = -1;
              IndexTheFile();
          }
  
          internal static long[][] Resize(long[][] index, int newLength)
          {
              long[][] newIndex = Arrays.CreateJagged<long>(newLength, 3);
              for (int i = 0; i < index.Length; i++)
              {
                  newIndex[i][0] = index[i][0];
                  newIndex[i][1] = index[i][1];
                  newIndex[i][2] = index[i][2];
              }
              return newIndex;
          }
  
          private void SaveIndex(string file)
          {
              if (numberOfRecords == 0)
              {
                  File.Delete(file);
                  return;
              }
              using (var o = new StreamWriter(file))
              {
                  o.Write(indexVersion.ToString(NumberFormatInfo.InvariantInfo));
                  o.Write('\n');
                  o.Write(filename);
                  o.Write('\n');
                  o.Write(raFile.Length.ToString(NumberFormatInfo.InvariantInfo));
                  o.Write('\n');
                  o.Write(numberOfRecords.ToString(NumberFormatInfo.InvariantInfo));
                  o.Write('\n');
                  for (int i = 0; i < numberOfRecords; i++)
                  {
                      o.Write(index[i][0].ToString(NumberFormatInfo.InvariantInfo));
                      o.Write("\t");
                      o.Write(index[i][1].ToString(NumberFormatInfo.InvariantInfo));
                      o.Write("\t");
                      o.Write(index[i][2].ToString(NumberFormatInfo.InvariantInfo));
                      o.Write("\t");
                  }
                  o.Write(numberOfRecords.ToString(NumberFormatInfo.InvariantInfo));
                  o.Write('\n');
                  o.Write(filename);
                  o.Write('\n');
              }
          }
  
          private void LoadIndex(string file)
          {
              using (var ins = new StreamReader(file))
              {
                  string version = ins.ReadLine();
                  if (!int.TryParse(version, out int iv))
                      throw new Exception($"Invalid index version {version}");
                  if (int.Parse(version, NumberFormatInfo.InvariantInfo) != indexVersion)
                      throw new Exception($"Expected index version {indexVersion} instead of {version}");
  
                  string fileIndexed = ins.ReadLine();
                  if (!string.Equals(filename, fileIndexed, StringComparison.Ordinal))
                      throw new Exception($"Index for {fileIndexed} found instead of {filename}. Creating new index.");
  
                  string line = ins.ReadLine();
                  int fileLength = int.Parse(line, NumberFormatInfo.InvariantInfo);
                  if (fileLength != raFile.Length)
                      throw new Exception($"Index for file of size {fileLength} found instead of {raFile.Length}");
  
                  line = ins.ReadLine();
                  int indexLength = int.Parse(line, NumberFormatInfo.InvariantInfo);
                  if (indexLength <= 0)
                      throw new Exception($"Index of zero length! {Path.GetFullPath(file)}");
                  index = Arrays.CreateJagged<long>(indexLength, 3);
                  numberOfRecords = 0;
                  int maxRecordLength = 0;
                  for (int i = 0; i < index.Length; i++)
                  {
                      line = ins.ReadLine();
                      string[] result = line.Split('\t');
                      for (int j = 0; j < 3; j++)
                      {
                          try
                          {
                              index[i][j] = long.Parse(result[j], NumberFormatInfo.InvariantInfo);
                          }
                          catch (Exception x)
                          {
                              throw new Exception($"Error reading index! {result[j]}", x);
                          }
                      }
  
                      if (maxRecordLength < index[numberOfRecords][1])
                          maxRecordLength = (int)index[numberOfRecords][1];
                      numberOfRecords++;
                  }
  
                  line = ins.ReadLine();
                  int indexLength2 = int.Parse(line, NumberFormatInfo.InvariantInfo);
                  if (indexLength2 <= 0)
                      throw new Exception("Index of zero length!");
                  if (indexLength2 != indexLength)
                      throw new Exception("Wrong index length!");
                  line = ins.ReadLine();
                  if (!string.Equals(line, filename, StringComparison.Ordinal))
                      throw new Exception($"Index for {line} found instead of {filename}");
                  b = new byte[maxRecordLength];
              }
          }
  
          static string ReadLine(Stream stream)
          {
              int c = stream.ReadByte();
  
gt:             if (c == -1)