////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2001-2026 Alexey Kuryakin daqgroup@mail.ru under MIT license //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// This file is part of the CRW-DAQ project by DaqGroup - component CRWLIB.   //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// Purpose:                                                                   //
// List mode data file format.                                                //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// History:                                                                   //
// 20110911 - Creation & test Ok.                                             //
// 20120530 - LMD_FILE_READER added.                                          //
// 20120531 - DaqScript in LMD_FILE_READER added.                             //
// 20230501 - Modified for FPC (A.K.)                                         //
////////////////////////////////////////////////////////////////////////////////

unit _crw_lmd; // List Mode Data format
{$I _crw_sysdef.inc}

{$I _crw_sysmode.inc}

{$WARN 5023 off : Unit "$1" not used in $2}
{$WARN 6058 off : Call to subroutine "$1" marked as inline is not inlined}

interface

uses
 //////////////////////////////////////////////////////
 {$I _crw_uses_first.inc} // NB: MUST BE FIRST USES !!!
 //////////////////////////////////////////////////////
 sysutils, classes, math, variants,
 _crw_alloc, _crw_str, _crw_fio, _crw_fifo, _crw_fpu, _crw_rtc,
 {_crw_intlcks,} _crw_polling, _crw_ee;

 //////////////////////////////////
 // General variables and constants
 //////////////////////////////////
const
 LMD_SignatureSec = '[List Mode Data File]'; // LMD file signature
 LMD_UserNotesSec = '[User Notes]';          // LMD User Notes section
 LMD_DaqScriptPre = '@DaqScript';            // LMD Reader DAQ Script prefix
 LMD_DateTimeHead = 1;                       // Use @DateTime in Header
 LMD_DateTimeData = 2;                       // Use @DateTime in DataStream
 LMD_DateTimeMode : Integer = 1;             // Use @DateTime or @TimeStamp

 {
 *******************************************************************************
 Purpose:
 ********
  List Mode Data File writer class.
  Uses to save multi-thread, multi-stream complex events, for example,
  in area of spectrometry,particle and nuclear physics measurements.
 Example:
 ********
  Lmd:=NewLmdWriter('C:\DEMO.LMD',     // Full path of LMD file to save data in
                    'Main,Spec',       // List of data streams, comma separated
                    'Demo comments',   // User comments text, EOL separated
                    60*1000,           // Flush data to file every 60 seconds
                    1000,              // Flush data to file if > 1000 events
                    tpLower );         // Writer thread priority
  ...
  idMain:=Lmd.StreamId['Main'];        // Get identifier for data stream Main
  idSpec:=Lmd.StreamId['Spec'];        // Get identifier for data stream Spec
  Lmd.StreamHead[idMain]:='EventNum,M1,M2,M3'; // Set names for Main fields
  Lmd.StreamHead[idSpec]:='EventNum,S1,S2,S3'; // Set names for Spec fields
  ...
  while HasData do begin                     // Measurement loop
   GetDataMain(m1,m2,m3);                    // Get data for Main data stream
   Lmd.PutEvent(idMain,[EventNum,m1,m2,m3]); // Write Main events
   ...
   GetDataSpec(s1,s2,s3);                    // Get data for Spec data stream
   Lmd.PutEvent(idSpec,[EventNum,s1,s2,s3]); // Write Spec events
   ...  
   EventNum:=EventNum+1;                     // Next event number
  end;
  ...
  Kill(Lmd);                           // Kill instance of LMD Writer
 Example of LMD file content:
 ****************************
  [List Mode Data File]
  TimeStamp=63451362564155
  DataStreamList=Main,Spec
  [User Notes]
  Demo Comments
  [DataStream Main]
  @Format=CSV
  @TimeStamp=63451362564155
  "EventNum","M1","M2","M3"
  1,0,7.432618,235
  2,60114,8.170371,2345
  3,54110,4.607718,234
  4,38857,12.02122,7654
  5,5628,14.67573,323
  ...
  [DataStream Spec]
  @Format=CSV
  @TimeStamp=63451362564296
  "EventNum","S1","S2","S3"
  1,44016,18.54163,24
  2,21490,21.43601,234
  3,9776,22.01738,964
  4,46405,20.82453,823
  5,57781,21.117,243
  ...
 *******************************************************************************
 }
type
 ELMDWriter = class(Exception);
 TLMDWriter = class(TLatch)
 private
  myCsvMode     : Integer;
  myFileName    : LongString;
  myUserNotes   : LongString;
  myWriteTime   : Double;
  mySavePeriod  : Integer;
  mySaveCounts  : Integer;
  myFloatDigits : Integer;
  myStreamList  : TStringList;
  myStreamHead  : TStringList;
  myWriter      : TThread;
  myLineCount   : SizeInt;
  myByteCount   : SizeInt;
  myErrorCount  : SizeInt;
  function  GetCsvMode:Integer;
  procedure SetCsvMode(aCsvMode:Integer);
  function  GetFileName:LongString;
  function  GetUserNotes:LongString;
  function  GetPriority:TThreadPriority;
  procedure SetPriority(aPriority:TThreadPriority);
  function  GetSuspended:Boolean;
  procedure SetSuspended(aSuspended:Boolean);
  function  GetFloatDigits:Integer;
  procedure SetFloatDigits(aFloatDigits:Integer);
  function  GetLineCount:SizeInt;
  function  GetByteCount:SizeInt;
  function  GetErrorCount:SizeInt;
  function  GetStreamCount:Integer;
  function  GetStreamId(aName:LongString):Integer;
  function  GetStreamName(aId:Integer):LongString;
  function  GetStreamHead(aId:Integer):LongString;
  procedure SetStreamHead(aId:Integer; aHead:LongString);
  function  GetHeader(var aHeader:LongString; aTimeStamp:Double):Integer;
  function  GetBuffer(aTimeStamp:Double; var aBuffer:LongString; aEnforce:Boolean):Integer;
 public
  constructor Create(const aFileName   : LongString;
                     const aStreamList : LongString = 'Main';
                     const aUserNotes  : LongString = '';
                           aSavePeriod : Integer = 1000*60*10;
                           aSaveCounts : Integer = 1024*16;
                           aPriority   : TThreadPriority = tpNormal;
                           aSuspended  : Boolean = True );
  destructor  Destroy; override;
 public
  property  CsvMode                   : Integer         read GetCsvMode write SetCsvMode;
  property  FileName                  : LongString      read GetFileName;
  property  UserNotes                 : LongString      read GetUserNotes;
  property  Priority                  : TThreadPriority read GetPriority    write SetPriority;
  property  Suspended                 : Boolean         read GetSuspended   write SetSuspended;
  property  FloatDigits               : Integer         read GetFloatDigits write SetFloatDigits;
  property  LineCount                 : SizeInt         read GetLineCount;
  property  ByteCount                 : SizeInt         read GetByteCount;
  property  ErrorCount                : SizeInt         read GetErrorCount;
  property  StreamCount               : Integer         read GetStreamCount;
  property  StreamId[Name:LongString] : Integer         read GetStreamId;
  property  StreamName[Id:Integer]    : LongString      read GetStreamName;
  property  StreamHead[Id:Integer]    : LongString      read GetStreamHead write SetStreamHead;
 public
  function  PutEvent(aStreamId:Integer; const aLine:LongString):Boolean; overload;
  function  PutEvent(aStreamId:Integer; const aData:array of const):Boolean; overload;
  function  ExchangeErrorCount(aCount:SizeInt):SizeInt;
  function  IncErrorCount(aCount:SizeInt=1):SizeInt;
 end;

const                   // CsvMode flags:
 csvForceHeadComma = 1; // Enforce comma if head empty
 csvForceTailComma = 2; // Enforce comma if tail empty
 csvForceBothComma = 3; // Enforce comma if both empty
 csvForceQuotedStr = 4; // Enforce double quoted strings

 //
 // Catenate comma separated values aHead,aTail.
 //  aMode: bit 0 to enforce comma when aHead=''
 //         bit 1 to enforce comma when aTail=''
 // Example:
 //  CsvCat('','',0)='';  CsvCat('a','',0)='a';  CsvCat('','b',0)='b';  CsvCat('a','b',0)='a,b';
 //  CsvCat('','',1)='';  CsvCat('a','',1)='a';  CsvCat('','b',1)=',b'; CsvCat('a','b',1)='a,b';
 //  CsvCat('','',2)='';  CsvCat('a','',2)='a,'; CsvCat('','b',2)='b';  CsvCat('a','b',2)='a,b';
 //  CsvCat('','',3)=','; CsvCat('a','',3)='a,'; CsvCat('','b',3)=',b'; CsvCat('a','b',3)='a,b';
 //
function CsvCat(const aHead:LongString; const aTail:LongString; aMode:Integer=0):LongString;

 //
 // Create comma separated list
 //  aMode: bit 0 to enforce comma when Head=''
 //         bit 1 to enforce comma when Tail=''
 //         bit 2 to enforce double quoted strings
 // Example:
 //  ArgsToCsvList(['a',1,pi],3)='"a",1,3.14';
 //
function ArgsToCsvList(const Args:array of const; aFloatDigits:Integer=0; aMode:Integer=csvForceQuotedStr):LongString;

function  NewLMDWriter(const aFileName   : LongString;
                       const aStreamList : LongString  = 'Main';
                       const aUserNotes  : LongString  = '';
                             aSavePeriod : Integer = 1000*60*10;
                             aSaveCounts : Integer = 1024*16;
                             aPriority   : TThreadPriority = tpNormal;
                             aSuspended  : Boolean = True ) : TLMDWriter;
procedure Kill(var TheObject:TLMDWriter); overload;

procedure LMD_Testing(aCycles:Integer=1000*1000);

 {
 ******************************************************************************
 LMD_FILE_READER - generic LMD file reader procedure, callback - based.
 LMD_FILE_READER arguments:
  LmdFile        - LMD file to read (full path)
  DqsFile        - DQS file contains DAQ Script program to run before
  EventHandler   - Callback function to call on each event to process
  EchoProc       - Callback procedure to print debug messages, or NIL
  UserDataPtr    - Points to custom user specified data (parameters)
  Debug          - Debug messages level, 0 to run silent
  TextBufferSize - Size of text file buffer to read fast
 EventHandler callback arguments:
  Event          - array of event data, 0..Length(Event)-1
  Params         - set of parameters for data processing
   LmdFile       - LMF file name
   DqsFile       - DAQ script file
   StreamId      - Name of current [Data Stream ..]
   FormatId      - Format of current data stream section, should be CSV
   ArgList       - List of argument names, use Event[Params.ArgList.IndexOf('Name')]
   UserNotes     - LMD file [User Notes] text
   DaqScript     - DAQ Script interpreter instance
   HeadTime      - LMD file time stamp from header @DateTime or @TimeStamp
   SectTime      - LMD data time stamp from data stream section
   LineNum       - LMD text file line number, 1-based
   EventNum      - Processing event number,   1-based
  UserDataPtr    - points to user specified parameters
  Return value   - True to continue, False to Break processing
 Notes:
 ******
  1) Use Event[i],i=0..Length(Event) to acess event data by index
  2) Use Event[Params.ArgList.IndexOf('Name')] to access by name
  3) Use Params.DaqScript.GetValie('Param',Param) to get parameters
  4) DaqScript parameters can be set in two ways:
     a) In LMD file, [User Notes] section, with prefix @DaqScript, like:
        [User Notes]
        ...
        @DaqScript Threshold1=1000
        @DaqScript Threshold2=2000
        ...
     b) In DQS file, as flat DAQ Script text.
  5) DaqScript executes the script before first run,
     first [User Notes] @DaqScript instructions, then DqsFile.
 Example:
 ********
  program Demo;
  type TUserData=record Count:Int64; BreakFlag:Boolean; end;
  // User defined callback event handler
  function TEST_READER(const Event:array of Variant; const Params:TLMDReaderParams;UserDataPtr:Pointer):Boolean;                         // True to continue, False to break
  var nArgs,EventId,Par1,Par2:Integer; t,Threshold:Double;
  begin
   Result:=False;
   try
    Inc(TUserData(UserDataPtr^).Count);           // Use custom user defined data
    nArgs:=Params.ArgList.Count;                  // Number of parameters=Length(Event)
    EventId:=Event[0];                            // Access event data by index
    Par1:=Event[Params.ArgList.IndexOf('Par1')];  // Assess event data by name
    Par2:=Event[Params.ArgList.IndexOf('Par2')];  // etc
    if Params.DaqScript.GetValue('Threshold',t)   // Get Threshold from DaqScript
    then Threshold:=t else Threshold:=0;          // or use some default value
    /// DATA PROCESSING ///                       // Put your code there ...
    if TUserData(UserDataPtr^).Count<10 then      // For example,
    Echo(Format('%d %d %d',[EventId,Par1,Par2])); // Print first 10 events
    Result:=not BreakFlag;                        // True to continue, False to break processing
   except
    on E:Exception do BugReport(E,'LMD_READER_HANDLER');
   end;
  end;
  var UserData : TUserData = (Count:0; BreakFlag:False);
  begin
   if LMD_FILE_READER('TEST.LMD','TEST.DQS',TEST_READER,SystemEchoProcedure,@UserData)
   then Echo('Success') else Echo('Failure');
  end.
 ******************************************************************************
 }
const                 // LMD Reader debug levels
 LMD_DEB_SILENT  = 0; // No print debug messages
 LMD_DEB_INFORM  = 1; // Print brief information
 LMD_DEB_MORINF  = 2; // Print more details 
 LMD_DEB_DETAIL  = 3; // Print full details

type
 ELMDReader = class(Exception);                     // LMD Reader exception class
 TLMDReaderEcho = procedure(const Msg:LongString);  // LMD Reader print procedure
 TLMDReaderNewEE = function :TExpressionEvaluator;  // LMD Reader NewExpressionEvaluator constructor
 TLMDReaderParams = packed record                   // LMD file parameters record
  LmdFile   : PureString;                           // LMD full file name
  DqsFile   : PureString;                           // DQS DAQ Script file name
  StreamId  : PureString;                           // Name of current data stream
  FormatId  : PureString;                           // Format of current section (CSV)
  ArgList   : TStringList;                          // List of argument names
  UserNotes : TStringList;                          // LMD file [User Notes] text
  DaqScript : TExpressionEvaluator;                 // @DaqScript expressions in [User Notes]
  HeadTime  : Double;                               // LMD file time stamp from header
  SectTime  : Double;                               // LMD file time stamp from section
  LineNum   : Int64;                                // LMD text file line number
  EventNum  : Int64;                                // Processed event number
 end;
 TLMDReaderHandler = function(                      // Callback function for each event
  const Event       : array of Variant;             // Array of event data to process
  const Params      : TLMDReaderParams;             // Current LMD file parameters
        UserDataPtr : Pointer                       // Custom user data
  ) : Boolean;                                      // True=Continue,False=Break

 //////////////////////////
 // Generic LMD file reader
 //////////////////////////
function LMD_FILE_READER(                  // Generic LMD file reader
 const LmdFile  : LongString;              // LMD full file name to read
 const DqsFile  : LongString = '';         // DQS DAQ Script file to set params
 EventHandler   : TLMDReaderHandler = nil; // Callback handler for each events
 EchoProc       : TLMDReaderEcho = nil;    // Callback debug print procedure
 NewEEProc      : TLMDReaderNewEE = nil;   // Callback to create DaqScript
 UserDataPtr    : Pointer = nil;           // Points to custom user data
 Debug          : Integer = 1;             // Debug messages level
 TextBufferSize : Integer = 1024*1024      // File buffer size
 ) : Boolean;                              // False if failed

implementation

procedure BugReport(const E:Exception; const Location:LongString='');
const ExeName:String[63]='';
begin
 if Length(ExeName)=0 then ExeName:=SysUtils.ExtractFileName(ParamStr(0));
 if Assigned(E) then
 if Length(Location)=0 
 then Echo(Format('@OnException=%s,%s (in %s)',[E.ClassName,E.Message,ExeName]))
 else Echo(Format('@OnException=%s,%s (in %s, %s)',[E.ClassName,E.Message,ExeName,Location]));
end;

type
 TLMDWriterThread = class(TThread)
 private
  myLmdWriter : TLmdWriter;
 public
  constructor Create(aLmdWriter:TLmdWriter);
  destructor  Destroy; override;
  procedure   Execute; override;
 end;

constructor TLmdWriterThread.Create;
begin
 inherited Create(True);
 myLmdWriter:=aLmdWriter;
end;

destructor TLmdWriterThread.Destroy;
begin
 inherited Destroy;
end;

procedure TLmdWriterThread.Execute;
 procedure Process(ms:Double; Enforce:Boolean);
 var F:Text; Data,Head:LongString; IoCode,BuffLines,BuffBytes,HeadLines,HeadBytes:Integer;
 begin
  try
   Data:=''; Head:='';
   BuffLines:=myLmdWriter.GetBuffer(ms,Data,Enforce);
   BuffBytes:=Length(Data);
   HeadLines:=0; HeadBytes:=0;
   if (BuffLines>0) or (BuffBytes>0) then  
   try
    System.Assign(F,myLmdWriter.FileName);
    if SysUtils.FileExists(myLmdWriter.FileName) then begin
     System.Append(F);
    end else begin
     System.Rewrite(F);
     HeadLines:=myLmdWriter.GetHeader(Head,ms);
     HeadBytes:=Length(Head);
     if (HeadLines>0) or (HeadBytes>0) then System.Write(F,Head);
    end;
    if Length(Data)>0 then System.Write(F,Data);
    IoCode:=System.IOResult;
    if IoCode<>0 then Raise ELMDWriter.CreateFmt('Error %d on saving file %s',
      [IoCode,AnsiQuotedStr(myLmdWriter.FileName,'"')]);
    LockedAdd(myLmdWriter.myLineCount,HeadLines+BuffLines);
    LockedAdd(myLmdWriter.myByteCount,HeadBytes+BuffBytes);
   finally
    System.Close(F);
    Data:='';
   end;
  except
   on E:Exception do begin
    BugReport(E,ClassName);
    myLmdWriter.IncErrorCount;
   end;
  end;
 end; 
begin
 try
  while not Terminated do begin
   Process(mSecNow,False);
   Sleep(TPolling.DefPollPeriod);
  end;
  Process(mSecNow,True);
 except
  on E:Exception do begin
   BugReport(E,ClassName);
   myLmdWriter.IncErrorCount;
  end;
 end;
end;


 ////////////////////////////
 // TLMDWriter implementation
 ////////////////////////////

constructor TLMDWriter.Create(const aFileName   : LongString;
                              const aStreamList : LongString;
                              const aUserNotes  : LongString;
                                    aSavePeriod : Integer;
                                    aSaveCounts : Integer;
                                    aPriority   : TThreadPriority;
                                    aSuspended  : Boolean );
var i,j,N:Integer;
begin
 inherited Create;
 myCsvMode:=csvForceBothComma+csvForceQuotedStr;
 myFileName:=UnifyFileAlias(aFileName);
 myUserNotes:=TrimRight(aUserNotes);
 myWriteTime:=0;
 mySavePeriod:=aSavePeriod;
 mySaveCounts:=aSaveCounts;
 myLineCount:=0;
 myByteCount:=0;
 myErrorCount:=0;
 myFloatDigits:=0;
 myStreamList:=TStringList.Create;
 myStreamList.Text:=aStreamList;
 N:=myStreamList.Count;
 for i:=0 to N-1 do for j:=1 to WordCount(myStreamList[i],ScanSpaces) do
 myStreamList.AddObject(ExtractWord(j,myStreamList[i],ScanSpaces),TStringList.Create);
 for i:=0 to N-1 do myStreamList.Delete(0);
 myStreamHead:=TStringList.Create;
 for i:=0 to myStreamList.Count-1 do myStreamHead.Add('');
 myWriter:=TLmdWriterThread.Create(Self);
 myWriter.Priority:=aPriority;
 myWriter.Suspended:=aSuspended;
end;

destructor TLMDWriter.Destroy;
var i:Integer;
begin
 Kill(myWriter);
 try
  Lock;
  try
   for i:=0 to myStreamList.Count-1 do myStreamList.Objects[i].Free;
   Kill(myStreamList);
   Kill(myStreamHead);
  finally
   Unlock;
  end;
 except
  on E:Exception do BugReport(E,ClassName);
 end;
 inherited Destroy;
end;

function TLMDWriter.GetCsvMode:Integer;
begin
 if Assigned(Self)
 then Result:=myCsvMode
 else Result:=0;
end;

procedure TLMDWriter.SetCsvMode(aCsvMode:Integer);
begin
 if Assigned(Self) then myCsvMode:=aCsvMode;
end;

function TLMDWriter.GetFileName:LongString;
begin
 if Assigned(Self)
 then Result:=myFileName
 else Result:='';
end;

function TLMDWriter.GetUserNotes:LongString;
begin
 if Assigned(Self)
 then Result:=myUserNotes
 else Result:='';
end;

function TLMDWriter.GetPriority:TThreadPriority;
begin
 if Assigned(Self)
 then Result:=myWriter.Priority
 else Result:=tpNormal;
end;

procedure TLMDWriter.SetPriority(aPriority:TThreadPriority);
begin
 if Assigned(Self) then myWriter.Priority:=aPriority;
end;

function TLMDWriter.GetSuspended:Boolean;
begin
 if Assigned(Self)
 then Result:=myWriter.Suspended
 else Result:=True;
end;

procedure TLMDWriter.SetSuspended(aSuspended:Boolean);
begin
 if Assigned(Self) then myWriter.Suspended:=aSuspended;
end;

function TLMDWriter.GetFloatDigits:Integer;
begin
 if Assigned(Self)
 then Result:=myFloatDigits
 else Result:=0;
end;

procedure TLMDWriter.SetFloatDigits(aFloatDigits:Integer);
begin
 if Assigned(Self) then myFloatDigits:=aFloatDigits;
end;

function TLMDWriter.GetLineCount:SizeInt;
begin
 if Assigned(Self)
 then Result:=LockedGet(myLineCount)
 else Result:=0;
end;

function TLMDWriter.GetByteCount:SizeInt;
begin
 if Assigned(Self)
 then Result:=LockedGet(myByteCount)
 else Result:=0;
end;

function TLMDWriter.GetErrorCount:SizeInt;
begin
 if Assigned(Self)
 then Result:=LockedGet(myErrorCount)
 else Result:=0;
end;

function TLMDWriter.ExchangeErrorCount(aCount:SizeInt):SizeInt;
begin
 if Assigned(Self)
 then Result:=LockedExchange(myErrorCount,aCount)
 else Result:=0;
end;

function TLMDWriter.IncErrorCount(aCount:SizeInt):SizeInt;
begin
 if Assigned(Self)
 then Result:=LockedAdd(myErrorCount,aCount)
 else Result:=0;
end;

function TLMDWriter.GetStreamCount:Integer;
begin
 Result:=0;
 if Assigned(Self) then
 try
  Lock;
  try
   Result:=myStreamList.Count;
  finally
   Unlock;
  end;
 except
  on E:Exception do begin BugReport(E,ClassName); IncErrorCount; end;
 end;
end;

function TLMDWriter.GetStreamId(aName:LongString):Integer;
begin
 Result:=0;
 if Assigned(Self) then
 try
  Lock;
  try
   Result:=myStreamList.IndexOf(aName)+1;
  finally
   Unlock;
  end;
 except
  on E:Exception do begin BugReport(E,ClassName); IncErrorCount; end;
 end;
end;

function TLMDWriter.GetStreamName(aId:Integer):LongString;
begin
 Result:='';
 if Assigned(Self) then
 try
  Lock;
  try
   if aId>=1 then
   if aId<=myStreamList.Count then
   Result:=myStreamList[aId-1];
  finally
   Unlock;
  end;
 except
  on E:Exception do begin BugReport(E,ClassName); IncErrorCount; end;
 end;
end;

function TLMDWriter.GetStreamHead(aId:Integer):LongString;
begin
 Result:='';
 if Assigned(Self) then
 try
  Lock;
  try
   if aId>=1 then
   if aId<=myStreamHead.Count then
   Result:=myStreamHead[aId-1];
  finally
   Unlock;
  end;
 except
  on E:Exception do begin BugReport(E,ClassName); IncErrorCount; end;
 end;
end;

procedure TLMDWriter.SetStreamHead(aId:Integer; aHead:LongString);
 function Parse(s:LongString):LongString;
 var i:Integer;
 begin
  Result:='';
  for i:=1 to WordCount(s,ScanSpaces) do
  if i=1 then Result:=AnsiQuotedStr(ExtractWord(i,s,ScanSpaces),'"')
  else Result:=Result+','+AnsiQuotedStr(ExtractWord(i,s,ScanSpaces),'"');
 end;
begin
 if Assigned(Self) then
 try
  Lock;
  try
   if aId>=1 then
   if aId<=myStreamHead.Count then
   myStreamHead[aId-1]:=Parse(aHead);
  finally
   Unlock;
  end;
 except
  on E:Exception do begin BugReport(E,ClassName); IncErrorCount; end;
 end;
end;

function TLmdWriter.GetHeader(var aHeader:LongString; aTimeStamp:Double):Integer;
const MaxLineLeng = 70;
var i:Integer; s:LongString; TheBuffer,TheNotes:TStringList;
begin
 Result:=0;
 aHeader:='';
 if Assigned(Self) then
 try
  TheBuffer:=TStringList.Create;
  TheNotes:=TStringList.Create;
  Lock;
  try
   TheBuffer.Add(LMD_SignatureSec);
   s:='';
   for i:=0 to myStreamList.Count-1 do begin
    if Length(s)>0 then s:=s+',';
    s:=s+myStreamList[i];
    if Length(s)>MaxLineLeng then begin
     TheBuffer.Add(Format('DataStreamList=%s',[s]));
     s:='';
    end;
   end;
   if Length(s)>0 then begin
    TheBuffer.Add(Format('DataStreamList=%s',[s]));
    s:='';
   end;
   if (aTimeStamp>0) and not IsNanOrInf(aTimeStamp) then
   if LMD_DateTimeMode and LMD_DateTimeHead = 0
   then TheBuffer.Add(Format('@TimeStamp=%1.0f',[aTimeStamp]))
   else TheBuffer.Add(Format('@DateTime=%s-%s',[GetDateStr(aTimeStamp,'.',True),GetTimeStr(aTimeStamp,':',True)]));
   if Length(myUserNotes)>0 then
   TheBuffer.Add(LMD_UserNotesSec);
   TheNotes.Text:=myUserNotes;
   for i:=0 to TheNotes.Count-1 do TheBuffer.Add(TheNotes[i]);
   aHeader:=TheBuffer.Text;
   Result:=TheBuffer.Count;
  finally
   Unlock;
   Kill(TheBuffer);
   Kill(TheNotes);
  end;
 except
  on E:Exception do begin BugReport(E,ClassName); IncErrorCount; end;
 end;
end;

function TLmdWriter.GetBuffer(aTimeStamp:Double; var aBuffer:LongString; aEnforce:Boolean):Integer;
var Trigger:Boolean; i,j:Integer; TheStream,TheBuffer:TStringList;
begin
 Result:=0;
 aBuffer:='';
 if Assigned(Self) then
 try
  Lock;
  TheBuffer:=nil;
  try
   Trigger:=aEnforce;
   if aTimeStamp>myWriteTime+mySavePeriod then Trigger:=True;
   for i:=0 to myStreamList.Count-1 do begin
    TheStream:=myStreamList.Objects[i] as TStringList;
    if TheStream.Count>=mySaveCounts then Trigger:=True;
   end;
   if Trigger then begin
    TheBuffer:=TStringList.Create;
    for i:=0 to myStreamList.Count-1 do begin
     TheStream:=myStreamList.Objects[i] as TStringList;
     if TheStream.Count>0 then begin
      TheBuffer.Add(Format('[DataStream %s]',[myStreamList[i]]));
      TheBuffer.Add('@Format=CSV');
      if LMD_DateTimeMode and LMD_DateTimeData = 0
      then TheBuffer.Add(Format('@TimeStamp=%1.0f',[aTimeStamp]))
      else TheBuffer.Add(Format('@DateTime=%s-%s',[GetDateStr(aTimeStamp,'.',True),GetTimeStr(aTimeStamp,':',True)]));
      if Length(myStreamHead[i])>0 then TheBuffer.Add(myStreamHead[i]);
      for j:=0 to TheStream.Count-1 do TheBuffer.Add(TheStream[j]);
      TheStream.Clear;
     end;
    end;
    if TheBuffer.Count>0 then aBuffer:=TheBuffer.Text;
    myWriteTime:=aTimeStamp;
    Result:=TheBuffer.Count;
   end;
  finally
   Kill(TheBuffer);
   Unlock;
  end;
 except
  on E:Exception do begin BugReport(E,ClassName); IncErrorCount; end;
 end;
end;

function TLMDWriter.PutEvent(aStreamId:Integer; const aLine:LongString):Boolean;
var TheStream:TStringList;
begin
 Result:=False;
 if Assigned(Self) then
 if Length(aLine)>0 then
 try
  Lock;
  try
   if aStreamId>=1 then
   if aStreamId<=myStreamList.Count then begin
    TheStream:=myStreamList.Objects[aStreamId-1] as TStringList;
    TheStream.Add(aLine);
    Result:=True;
   end;
  finally
   Unlock;
  end;
 except
  on E:Exception do begin BugReport(E,ClassName); IncErrorCount; end;
 end;
end;

function TLMDWriter.PutEvent(aStreamId:Integer; const aData:array of const):Boolean;
begin
 if Assigned(Self)
 then Result:=PutEvent(aStreamId,ArgsToCsvList(aData,myFloatDigits,myCsvMode))
 else Result:=False;
end;

function CsvCat(const aHead:LongString; const aTail:LongString; aMode:Integer):LongString;
begin
 if Length(aHead)>0 then aMode:=aMode or csvForceHeadComma;
 if Length(aTail)>0 then aMode:=aMode or csvForceTailComma;
 case aMode and csvForceBothComma of
  0                 :  Result:='';
  csvForceHeadComma :  Result:=aHead;
  csvForceTailComma :  Result:=aTail;
  csvForceBothComma :  Result:=aHead+','+aTail;
  else Result:='';
 end;
end;

function ArgsToCsvList(const Args: array of const; aFloatDigits:Integer; aMode:Integer):LongString;
const BoolChars : array[Boolean] of Char = ('0', '1');
var i,iMode : Integer;
begin
 Result:='';
 try
  for i:=0 to High(Args) do begin
   if i=High(Args) then iMode:=aMode and not csvForceTailComma else
   if i=0 then iMode:=aMode and not csvForceHeadComma else iMode:=aMode;
   with Args[i] do
   case VType of
    vtInteger:    Result:=CsvCat(Result,IntToStr(VInteger),iMode);
    vtBoolean:    Result:=CsvCat(Result,BoolChars[VBoolean],iMode);
    vtChar:       if iMode and csvForceQuotedStr <> 0
                  then Result:=CsvCat(Result,AnsiQuotedStr(VChar,'"'),iMode)
                  else Result:=CsvCat(Result,VChar,iMode);
    vtExtended:   if aFloatDigits=0
                  then Result:=CsvCat(Result,FloatToStr(VExtended^),iMode)
                  else Result:=CsvCat(Result,Format('%.*g',[aFloatDigits,VExtended^]),iMode);
    vtString:     if iMode and csvForceQuotedStr <> 0
                  then Result:=CsvCat(Result,AnsiQuotedStr(VString^,'"'),iMode)
                  else Result:=CsvCat(Result,VString^,iMode);
    vtPChar:      if iMode and csvForceQuotedStr <> 0
                  then Result:=CsvCat(Result,AnsiQuotedStr(VPChar,'"'),iMode)
                  else Result:=CsvCat(Result,VPChar,iMode);
    vtObject:     if iMode and csvForceQuotedStr <> 0
                  then Result:=CsvCat(Result,AnsiQuotedStr(VObject.ClassName,'"'),iMode)
                  else Result:=CsvCat(Result,VObject.ClassName,iMode);
    vtClass:      if iMode and csvForceQuotedStr <> 0
                  then Result:=CsvCat(Result,AnsiQuotedStr(VClass.ClassName,'"'),iMode)
                  else Result:=CsvCat(Result,VClass.ClassName,iMode);
    vtAnsiString: if iMode and csvForceQuotedStr <> 0
                  then Result:=CsvCat(Result,AnsiQuotedStr(LongString(VAnsiString),'"'),iMode)
                  else Result:=CsvCat(Result,LongString(VAnsiString),iMode);
    vtCurrency:   Result:=CsvCat(Result,CurrToStr(VCurrency^),iMode);
    vtVariant:    if iMode and csvForceQuotedStr <> 0
                  then Result:=CsvCat(Result,AnsiQuotedStr(String(VVariant^),'"'),iMode)
                  else Result:=CsvCat(Result,String(VVariant^),iMode);
    vtInt64:      Result:=CsvCat(Result,IntToStr(VInt64^),iMode);
   end;
  end;
 except
  on E:Exception do begin
   Result:=Format('@OnException=%s,%s',[E.ClassName,E.Message]);
   BugReport(E,'ArgsToCsvList');
  end;
 end;
end;

function  NewLMDWriter(const aFileName   : LongString;
                       const aStreamList : LongString;
                       const aUserNotes  : LongString;
                             aSavePeriod : Integer;
                             aSaveCounts : Integer;
                             aPriority   : TThreadPriority;
                             aSuspended  : Boolean ) : TLMDWriter;
begin
 Result:=nil;
 try
  Result:=TLMDWriter.Create(aFileName,aStreamList,aUserNotes,aSavePeriod,aSaveCounts,aPriority,aSuspended);
 except
  on E:Exception do BugReport(E,'NewLmdWriter');
 end;
end;

procedure Kill(var TheObject:TLMDWriter); overload;
begin
 try
  FreeAndNil(TheObject);
 except
  on E:Exception do BugReport(E,'KillLmdWriter');
 end;
end;

procedure LMD_Testing(aCycles:Integer);
const Lmd:TLmdWriter = nil;
var i,a1,a2,a3,a4:Integer; FName:LongString; N:Integer; mks:Double; b1,b2,b3,b4:Single;
begin
 Kill(Lmd);
 for i:=0 to 3 do Echo(Format('"%3s"  "%3s"  "%3s"  "%3s"',[CsvCat('','',i),CsvCat('a','',i),CsvCat('','b',i),CsvCat('a','b',i)]));
 Echo(ArgsToCsvList([1,1.23,'abc',False,'c',pi,'']));
 FName:='c:\Daq32\TEST.LMD';
 FileErase(FName);
 Lmd:=NewLmdWriter(FName,'Main,Addon1'+EOL+'Addon2,Addon3','Комментарий');
 Lmd.Priority:=tpIdle;
 Lmd.FloatDigits:=7;
 Lmd.Suspended:=False;
 Lmd.StreamHead[1]:='EventId,A1,B1';
 Lmd.StreamHead[2]:='EventId,A2,B2';
 Lmd.StreamHead[3]:='EventId,A3,B3';
 Lmd.StreamHead[4]:='EventId,A4,B4';
 for i:=1 to Lmd.StreamCount do Echo(Lmd.StreamName[i]+'  '+Lmd.StreamHead[i]);
 Echo(IntToStr(Lmd.StreamId['Main'])+'  '+Lmd.StreamHead[1]);
 Echo(IntToStr(Lmd.StreamId['Addon1'])+'  '+Lmd.StreamHead[2]);
 Echo(IntToStr(Lmd.StreamId['Addon2'])+'  '+Lmd.StreamHead[3]);
 Echo(IntToStr(Lmd.StreamId['Addon3'])+'  '+Lmd.StreamHead[4]);
 N:=Max(1,aCycles);
 mks:=mkSecNow;
 for i:=1 to N do begin
  a1:=random($FFFF); b1:=RandG(10,3);
  a2:=random($FFFF); b2:=RandG(20,3);
  a3:=random($FFFF); b3:=RandG(30,3);
  a4:=random($FFFF); b4:=RandG(40,3);
  Lmd.PutEvent(1,[i,a1,b1]);
  Lmd.PutEvent(2,[i,a2,b2]);
  Lmd.PutEvent(3,[i,a3,b3]);
  Lmd.PutEvent(4,[i,a4,b4]);
 end;
 Echo(Format('Queue Time = %.7g mks per 1 event',[(mkSecNow-mks)/N]));
 Kill(Lmd);
 Echo(Format('Whole time = %.7g sec',[(mkSecNow-mks)/1e6]));
end;

 //////////////////////////////////////////////////////////////
 // Performance tests.
 // File: PIXE_20120518_103925.LMD, 
 //       70.818.415 bytes, 2649370 lines, 2640959 events,
 // Zip:  12.234.385 bytes, 17%, 5.7 times, 20 sec (7.7 mks/ev)
 //       26.815 bytes/event, Asc/Bin = 27/18 = 1.49, 
 // Read: 680 ms, 0.257 mks/event (unix wc -l ...) 
 // Read: 1.2 sec 0.454 mks/event (readln(..))
 // Read: 2.6 sec 1     mks/event (all but handle)
 // Hand; 7.8 sec 2.946 mks/event (call handler)
 //////////////////////////////////////////////////////////////

 /////////////////////////////////
 // LMD_FILE_READER implementation
 /////////////////////////////////
function LMD_FILE_READER(                  // Generic LMD file reader
 const LmdFile  : LongString;              // LMD full file name to read
 const DqsFile  : LongString = '';         // DQS DAQ Script file to set params
 EventHandler   : TLMDReaderHandler = nil; // Callback handler for each events
 EchoProc       : TLMDReaderEcho = nil;    // Callback debug print procedure
 NewEEProc      : TLMDReaderNewEE = nil;   // Callback to create DaqScript
 UserDataPtr    : Pointer = nil;           // Points to custom user data
 Debug          : Integer = 1;             // Debug messages level
 TextBufferSize : Integer = 1024*1024      // File buffer size
 ) : Boolean;                              // False if failed
const
 sec_Empty       = 0;          // Identify empty section []
 sec_LMD_Sign    = 1;          // Identify LMD's section [List Mode Data File]
 sec_UserNotes   = 2;          // Identify LMD's section [User Notes]
 sec_DataStreams = 3;          // Identify 1-st  section [DataStream ...]
 DataStreamSign  = '[DataStream ';
var
 TextFile  : Text;             // Text file to read
 TextBuff  : Pointer;          // Text file buffer
 SectList  : TStringList;      // List of sections [..]
 Params    : TLMDReaderParams; // Current parameters
 Line      : LongString;       // Current input line buffer
 w1,w2     : LongString;       // Temporary, 1'st, 2'nd word
 SectInd   : Integer;          // Current section index
 i         : Integer;          // Temporary, for cycles
 Event     : array of Variant; // Current event data
 Tokens    : PChar;            // Points to parsing tokens
 procedure TrimSpacesAndComments(var s:LongString; com:Char);
 var i,p,q:Integer;
 begin
  p:=0; q:=0;
  for i:=1 to Length(s) do begin
   if s[i]='"' then Inc(q) else
   if s[i]=com then if not Odd(q) then p:=i;
   if p>0 then Break;
  end;
  if p=0 then s:=Trim(s) else s:=Trim(Copy(s,1,p-1));
 end;
 procedure Str2Var(var v:Variant; const s:LongString);
 var i,c,L:Integer; d:Double;
 begin
  Val(s,i,c); if c=0 then begin v:=i; exit; end;
  Val(s,d,c); if c=0 then begin v:=d; exit; end;
  i:=1; c:=Length(s); L:=c;
  if c>1 then if s[1]='"' then if s[L]='"' then begin Inc(i); Dec(c,2); end;
  if c=L then v:=s else v:=Copy(s,i,c);
 end;
 function CsvToken(var s:PChar):LongString;
 var p:PChar;
 begin
  p:=s;
  Result:='';
  while True do
  case s^ of
   #0:  begin SetString(Result,p,s-p); Break; end;
   ',': begin SetString(Result,p,s-p); Inc(s); Break; end;
   else Inc(s);
  end;
 end;
 procedure ClearEvent;
 var i:Integer;
 begin
  if Length(Event)>0 then begin
   for i:=0 to Length(Event)-1 do VarClear(Event[i]);
   SetLength(Event,0);
  end;
 end;
 procedure ClearStrings;
 begin
  Line:=''; w1:=''; w2:='';
  Params.FormatId:='';
  Params.StreamId:='';
 end;
 procedure Echo(const Msg:LongString);
 begin
  if Assigned(EchoProc) then
  try
   EchoProc(Msg+EOL);
  except
  end;
 end;
 procedure ProcessDaqScript;
 var i,code,n:Integer; Script:TStringList; s,w1,w2:LongString;
 begin
  Script:=TStringList.Create;
  try
   if IsNonEmptyStr(Params.DqsFile) then
   if FileExists(Params.DqsFile) then begin
    Script.LoadFromFile(Params.DqsFile);
   end;
   n:=0;
   for i:=0 to Params.UserNotes.Count-1 do begin
    s:=Params.UserNotes[i];
    TrimSpacesAndComments(s,';');
    if Length(s)=0 then Continue;
    if (s[1]<>'@') then Continue;
    w1:=ExtractWord(1,s,ScanSpaces);
    if IsSameText(w1,LMD_DaqScriptPre) then begin
     w2:=Trim(Copy(s,Pos(w1,s)+Length(w1)+1,MaxInt));
     if Length(w2)>0 then begin
      Script.Insert(n,w2);
      Inc(n);
     end;
    end;
   end;
   for i:=Script.Count-1 downto 0 do begin
    s:=Script[i];
    TrimSpacesAndComments(s,';');
    if Length(s)=0 then begin
     Script.Delete(i);
     Continue;
    end else Script[i]:=s;
   end;
   if Script.Count>0 then begin
    Params.DaqScript.Script:=Script.Text;
    code:=Params.DaqScript.RunScript;
    if Debug>=LMD_DEB_INFORM
    then Echo(Format('@Notify DaqScript result: %d, "%s".',[code,ee_ErrorMessage(code)]));
   end;
  finally
   Kill(Script);
  end;
 end;
begin
 Result:=False;
 try
  //
  // Clear dynamic variables
  //
  ClearEvent;
  ClearStrings;
  SectList:=nil;
  TextBuff:=nil;
  System.Assign(TextFile,'');
  try
   //
   // Clear all variables
   //
   SectInd:=sec_Empty;
   FillChar(Params,SizeOf(Params),0);
   Params.LmdFile:=UnifyFileAlias(LmdFile);
   Params.DqsFile:=UnifyFileAlias(DqsFile);
   if Assigned(NewEEProc)
   then Params.DaqScript:=NewEEProc
   else Params.DaqScript:=NewExpressionEvaluator;
   if not Assigned(EchoProc) then Debug:=LMD_DEB_SILENT;
   if Debug>=LMD_DEB_INFORM then Echo(Format('%s: Process file "%s".',[DateTimeToStr(Now),Params.LmdFile]));
   if not FileExists(LmdFile) then Raise ELMDReader.Create(Format('File not exist: "%s".',[Params.LmdFile]));
   //
   // Create section list with predefined sections
   //
   SectList:=TStringList.Create;
   SectList.AddObject('[]',TStringList.Create);
   SectList.AddObject(LMD_SignatureSec,TStringList.Create);
   SectList.AddObject(LMD_UserNotesSec,TStringList.Create);
   Params.UserNotes:=SectList.Objects[sec_UserNotes] as TStringList;
   //
   // Open LMD file with large buffer
   //
   {$IOCHECKS ON}
   GetMem(TextBuff,TextBufferSize);
   System.Assign(TextFile,Params.LmdFile);
   System.SetTextBuf(TextFile,TextBuff^,TextBufferSize);
   System.Reset(TextFile);
   //
   // Start file reading cycle
   //
   while not System.Eof(TextFile) do begin
    //
    // Read line from file, trim spaces & comments
    //
    System.Readln(TextFile,Line);
    Inc(Params.LineNum);
    TrimSpacesAndComments(Line,';');
    //
    // First line should be [List Mode Data File] signature
    //
    if(Params.LineNum=1) then begin
     if SectList.IndexOf(Line)<>sec_LMD_Sign // 1st line should be LMD signature
     then Raise ELMDReader.CreateFmt('Invalid LMD file signature: "%s".',[Line]);
     SectInd:=sec_LMD_Sign;
     Continue;
    end;
    //
    // Skip empty line...
    //
    if Length(Line)=0 then Continue;
    //
    // Switch section [..]
    //
    if Length(Line)>1 then
    if (Line[1]='[') and (Line[Length(Line)]=']') then begin
     SectInd:=Max(0,SectList.IndexOf(Line));
     Params.ArgList:=SectList.Objects[SectInd] as TStringList;
     if SectInd>=sec_DataStreams then Params.ArgList.Clear;
     if Debug>=LMD_DEB_MORINF then Echo(Format('@Section[%d] %s',[SectInd,SectList[SectInd]]));
     if IsSameText(Copy(Line,1,Length(DataStreamSign)),DataStreamSign)
     then Params.StreamId:=Copy(Line,Length(DataStreamSign)+1,Length(Line)-(Length(DataStreamSign)+1))
     else Params.StreamId:='';
     Params.FormatId:='';
     Continue;
    end;
    //
    // Handle sections
    //
    case SectInd of
     //
     // Skip empty or unknown sections
     //
     sec_Empty:
      begin
       if Debug>=LMD_DEB_DETAIL then Echo(Format('@SkipLine %s',[Line]));
       Continue;
      end;
     //
     // Process [List Mode Data File] section
     //
     sec_LMD_Sign:
      begin
       w1:=ExtractWord(1,Line,ScanSpaces);
       if Debug>=LMD_DEB_DETAIL then Echo(Format('@LMD-Header %s',[Line]));
       if IsSameText(w1,'DataStreamList') then begin
        for i:=2 to WordCount(Line,ScanSpaces) do begin
         w2:=ExtractWord(i,Line,ScanSpaces);
         w2:=Format('%s%s]',[DataStreamSign,w2]);
         if SectList.IndexOf(w2)<0 then begin
          if Debug>=LMD_DEB_MORINF then Echo(Format('@SectionAdd %s',[w2]));
          SectList.AddObject(w2,TStringList.Create);
         end;
        end;
       end else
       if IsSameText(w1,'@DateTime') then begin
        w2:=ExtractWord(2,Line,ScanSpaces);
        Params.HeadTime:=DateTimeToMsec(StrToIntDef(ExtractWord(1,w2,['.',':','-']),0),
                               StrToIntDef(ExtractWord(2,w2,['.',':','-']),0),
                               StrToIntDef(ExtractWord(3,w2,['.',':','-']),0),
                               StrToIntDef(ExtractWord(4,w2,['.',':','-']),0),
                               StrToIntDef(ExtractWord(5,w2,['.',':','-']),0),
                               StrToIntDef(ExtractWord(6,w2,['.',':','-']),0),
                               StrToIntDef(ExtractWord(7,w2,['.',':','-']),0));
        if Debug>=LMD_DEB_MORINF then Echo(Format('@TimeStamp %1.0f',[Params.HeadTime]));
       end else
       if IsSameText(w1,'@TimeStamp') then begin
        w2:=ExtractWord(2,Line,ScanSpaces);
        if not Str2Real(w2,Params.HeadTime) then Params.HeadTime:=0;
        if Debug>=LMD_DEB_MORINF then Echo(Format('@TimeStamp %1.0f',[Params.HeadTime]));
       end;
       Continue;
      end;
     //
     // Process [User Notes] section
     //
     sec_UserNotes:
      begin
       if Debug>=LMD_DEB_DETAIL then Echo(Format('@UserNotes %s',[Line]));
       Params.UserNotes.Add(Line);
       Continue;
      end;
     //
     // Process [Data Stream ..] sections
     //
     sec_DataStreams..MaxInt:
      if SectInd<SectList.Count then begin
       if Line[1] = '@' then begin
        w1:=ExtractWord(1,Line,ScanSpaces);
        if IsSameText(w1,'@Format') then begin
         w2:=ExtractWord(2,Line,ScanSpaces);
         if IsSameText(w2,'CSV') then Params.FormatId:='CSV';
        end else
        if IsSameText(w1,'@DateTime') then begin
         w2:=ExtractWord(2,Line,ScanSpaces);
         Params.SectTime:=DateTimeToMsec(StrToIntDef(ExtractWord(1,w2,['.',':','-']),0),
                                StrToIntDef(ExtractWord(2,w2,['.',':','-']),0),
                                StrToIntDef(ExtractWord(3,w2,['.',':','-']),0),
                                StrToIntDef(ExtractWord(4,w2,['.',':','-']),0),
                                StrToIntDef(ExtractWord(5,w2,['.',':','-']),0),
                                StrToIntDef(ExtractWord(6,w2,['.',':','-']),0),
                                StrToIntDef(ExtractWord(7,w2,['.',':','-']),0));
         if Debug>=LMD_DEB_MORINF then Echo(Format('@TimeStamp %1.0f',[Params.SectTime]));
        end else     
        if IsSameText(w1,'@TimeStamp') then begin
         w2:=ExtractWord(2,Line,ScanSpaces);
         if not Str2Real(w2,Params.SectTime) then Params.SectTime:=0;
         if Debug>=LMD_DEB_MORINF then Echo(Format('@TimeStamp %1.0f',[Params.SectTime]));
        end;
        Continue;
       end;
       if Line[1] = '"' then begin
        if Params.ArgList.Count=0 then begin
         tokens:=PChar(Line);
         while tokens^<>#0 do begin
          w2:=CsvToken(tokens);
          w2:=TrimChars(w2,[#0..' ','"'],[#0..' ','"']);
          Params.ArgList.Add(w2);
         end;
         if Debug>=LMD_DEB_MORINF then Echo(Format('@CSV-Head %s',[StringReplace(Trim(Params.ArgList.Text),EOL,',',[rfReplaceAll])]));
         Continue;
        end;
       end;
       if Params.FormatId='CSV' then begin
        if Debug>=LMD_DEB_DETAIL then Echo(Format('@Event[%d] %s',[Params.EventNum,Line]));
        if Assigned(EventHandler) then begin
         for i:=0 to Length(Event)-1 do VarClear(Event[i]);
         SetLength(Event,Params.ArgList.Count);
         i:=0; tokens:=PChar(Line);
         while tokens^<>#0 do begin
          if i>=Length(Event) then SetLength(Event,i+1);
          Str2Var(Event[i],CsvToken(tokens));
          Inc(i);
         end;
         if Length(Event)>i then SetLength(Event,i);
         Inc(Params.EventNum);
         if Params.EventNum=1 then ProcessDaqScript;
         if EventHandler(Event,Params,UserDataPtr) then begin
          if Debug>=LMD_DEB_DETAIL then Echo(Format('Line %d, Event %d processed.',[Params.LineNum,Params.EventNum]));
         end else begin
          if Debug>=LMD_DEB_INFORM then Echo(Format('Line %d, Event %d processing break.',[Params.LineNum,Params.EventNum]));
          Break;
         end;
        end;
       end;
      end else  Raise ELMDReader.Create(Format('Invalid section index %d',[SectInd]));
     else Raise ELMDReader.Create(Format('Invalid section index %d',[SectInd]));
    end; // case
   end; // while
   {$IOCHECKS OFF}
   Echo(Format('%s: %d line(s), %d event(s) processed.',[DateTimeToStr(Now),Params.LineNum,Params.EventNum]));
   Result:=True;
  finally
   ClearEvent;
   ClearStrings;
   System.Close(TextFile);
   Kill(Params.DaqScript);
   if Assigned(SectList) then begin
    for i:=0 to SectList.Count-1 do SectList.Objects[i].Free;
    Kill(SectList);
   end;
   FreeMem(TextBuff);
  end;
 except
  on E:Exception do begin
   BugReport(E,'LMD_READER');
   ExitCode:=1;
  end;
 end;
end;

///////////////////////////////////////
// Unit initialization and finalization
///////////////////////////////////////

procedure Init_crw_lmd;
begin
end;

procedure Free_crw_lmd;
begin
end;

initialization

 Init_crw_lmd;

finalization

 Free_crw_lmd;

end.

//////////////
// END OF FILE
//////////////

