unit dattimecorrect;

{$mode objfpc}

interface

uses
  Classes, SysUtils, FileUtil, LResources, Forms, Controls, Graphics, Dialogs,
  StdCtrls, ExtCtrls, Buttons, ComCtrls,
  upascaltz;

type

  { Tdattimecorrectform }

  Tdattimecorrectform = class(TForm)
    FixedMethodRadio: TRadioButton;
    LatitudeEdit: TLabeledEdit;
    LongitudeEdit: TLabeledEdit;
    TimeSpanEdit: TLabeledEdit;
    LinearMethodRadio: TRadioButton;
    SunriseDifferenceRadioButton: TRadioButton;
    TimeZoneExistsText: TStaticText;
    TimeZoneEdit: TLabeledEdit;
    CorrectButton: TBitBtn;
    FileSelectButton: TButton;
    CorrectionGroupBox: TGroupBox;
    InputFile: TLabeledEdit;
    Memo1: TMemo;
    StartDateTimeEdit: TLabeledEdit;
    EndDateTimeEdit: TLabeledEdit;
    OutGroupBox: TGroupBox;
    InGroupBox: TGroupBox;
    OutputFile: TLabeledEdit;
    OpenDialog1: TOpenDialog;
    StatusBar1: TStatusBar;
    TimeDifference: TLabeledEdit;
    procedure CorrectButtonClick(Sender: TObject);
    procedure FileSelectButtonClick(Sender: TObject);
    procedure FixedMethodRadioClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure LinearMethodRadioClick(Sender: TObject);
    procedure SunriseDifferenceRadioButtonClick(Sender: TObject);
  private
    { private declarations }
    procedure GetTimeDifference();
    procedure UpdateExplanation();
  public
    { public declarations }
  end;

var
  dattimecorrectform: Tdattimecorrectform;
  InFileName, OutputFilename: string;
  TimeDiffS: int64 = 0;
  FixedMethod: boolean = True;
  LinearMethod: boolean = False;
  SunriseDifferenceMethod: boolean = False;
  ptzCorr: TPascalTZ; //Time zone region for Correction.
  LinearCorrectionFactor: double = 0.0;
  RangeTimeMS: int64; // type Int64 = - 9223372036854775808..9223372036854775807;
  UTCStartRecordTime, UTCEndRecordTime: TDateTime;
  datLocalTZ: string; //Timezone information stored in the file.

  {Field that contains the MSAS variable, -1 = undefined.}
  MSASField: integer = -1;

  {Field that contains the Sunrise difference variable, -1 = undefined.}
  SunriseDiffField: integer = -1;

  MyLatitude: extended = 0.0; //Latitude
  MyLongitude: extended = 0.0; //Longitude
  MyElevation: extended = 0.0; //Elevation

implementation

uses
  appsettings //Required to read application settings (like locations).
  , dateutils //Required to convert logged UTC string to TDateTime
  , strutils //Required for checking lines in conversion file.
  , LazFileUtils //required for ExtractFileNameOnly
  , moon //required for Moon calculations
  , Unit1 //required for FPointSeparator
  ;

  { Tdattimecorrectform }

//Select file for correction
procedure Tdattimecorrectform.FileSelectButtonClick(Sender: TObject);
begin
  { Clear input filename in preparation for new selected filename}
  InFileName := '';
  InputFile.Text := InFileName;

  datLocalTZ := '';
  TimeZoneEdit.Text := datLocalTZ;

  UTCStartRecordTime := 0;
  StartDateTimeEdit.Text := '';

  UTCEndRecordTime := 0;
  EndDateTimeEdit.Text := '';

  TimeSpanEdit.Text := '';

  RangeTimeMS := 0;

  TimeDiffS := 0;
  TimeDifference.Text := '';

  LatitudeEdit.Text := '';
  LongitudeEdit.Text := '';

  {Indicate status}
  StatusBar1.Panels.Items[0].Text := 'Select a file for processing.';

  OpenDialog1.Filter := 'data log files|*.dat|All files|*.*';
  OpenDialog1.InitialDir := vConfigurations.ReadString('DatTimeCorrect',
    'InputDirectory', appsettings.LogsDirectory);

  { Get Input filename from user }
  if (OpenDialog1.Execute) then
  begin
    InFileName := OpenDialog1.FileName;
    InputFile.Text := InFileName;

    ptzCorr := TPascalTZ.Create();
    {Initialize the Timezone database path}
    ptzCorr.DatabasePath := appsettings.TZDirectory;

    GetTimeDifference();

    { Save "Select file" directory}
    vConfigurations.WriteString('DatTimeCorrect', 'InputDirectory',
      OpenDialog1.InitialDir);

  end;

  StatusBar1.Panels.Items[0].Text := 'File selected, updating details.';

  UpdateExplanation();

  StatusBar1.Panels.Items[0].Text :=
    'Select "Correction method", then press "Correct .dat file" button.';

end;

procedure Tdattimecorrectform.FixedMethodRadioClick(Sender: TObject);
begin
  FixedMethod := True;
  LinearMethod := False;
  SunriseDifferenceMethod := False;
  UpdateExplanation();
end;

procedure Tdattimecorrectform.LinearMethodRadioClick(Sender: TObject);
begin
  FixedMethod := False;
  LinearMethod := True;
  SunriseDifferenceMethod := False;
  UpdateExplanation();
end;

procedure Tdattimecorrectform.SunriseDifferenceRadioButtonClick(Sender: TObject);
begin
  FixedMethod := False;
  LinearMethod := False;
  SunriseDifferenceMethod := True;
  UpdateExplanation();
end;

procedure Tdattimecorrectform.FormCreate(Sender: TObject);
begin
  { Clear status bar }
  StatusBar1.Panels.Items[0].Text := '';

  { Restore "Select file" directory. }
end;

procedure Tdattimecorrectform.FormDestroy(Sender: TObject);
begin
  if Assigned(ptzCorr) then FreeAndNil(ptzCorr);
end;

{ Correct file }
procedure Tdattimecorrectform.CorrectButtonClick(Sender: TObject);
var
  InFile, OutFile: TextFile;
  Str: string;
  pieces: TStringList;

  index: integer;
  i: integer = 0;

  UTCRecord: TDateTime;
  CorrUTCRecordTime, CorrLocalRecordTime: TDateTime; {Corrected UTC record}
  ComposeString: string;

  CurrentDifferenceMS: int64 = 0;
  WriteAllowable: boolean = True; //Allow output file to be written or not.

  {Sunrise comparison variables}
  SunriseCurrentRdg: double = 0;
  SunriseLastRdg: double = 0;
  SunriseTimeStamp: TDateTime;
  SunriseDifferenceMS: int64;
  {A change from Dawn to Sunrise}
  DawnThreshold: double = 13.0; {Approximate reading at Dawn or darker.}
  SunriseThreshold: double = 10.0; {Approximate reading at sunrise or brighter.}
  Daytime: boolean = False; {Indicates if readings are during daytime.}
begin
  pieces := TStringList.Create;

  StatusBar1.Panels.Items[0].Text := ''; { Clear status bar }
  Application.ProcessMessages;

  { Start reading file. }
  AssignFile(InFile, InFileName);
  AssignFile(OutFile, OutputFile.Text);
  if FileExists(OutputFile.Text) then
  begin
    if (MessageDlg('Overwrite existing file?',
      'Do you want to overwrite the existing file?', mtConfirmation,
      [mbOK, mbCancel], 0) = mrOk) then
      WriteAllowable := True
    else
      WriteAllowable := False;
  end;
  if WriteAllowable then
  begin
    {$I+}
    try
      Reset(InFile);
      Rewrite(OutFile); {Open file for writing}
      StatusBar1.Panels.Items[0].Text := 'Reading Input file';
      Application.ProcessMessages;

      repeat
        Readln(InFile, Str); {Read one line at a time from the file.}
        StatusBar1.Panels.Items[0].Text := 'Processing : ' + Str; {Update statusbar}
        Application.ProcessMessages;

        if (AnsiStartsStr('#', Str)) then
        begin {Ignore most comment lines which have # as first character.}
          if (AnsiStartsStr('# DL time difference (seconds)', Str) and
            (FixedMethod or LinearMethod)) then  { Touch up time difference line }
            WriteLn(OutFile, '# DL time difference (seconds): 0')


          { Check if file contains SunriseDiff field. }
          else if (AnsiStartsStr('# UTC Date & Time, Local Date', str)) then
          begin
            pieces.Delimiter := ',';
            pieces.StrictDelimiter := True; //Do not parse spaces also
            pieces.DelimitedText := Str;

            { Get the field locations. }
            for i := 0 to pieces.Count - 1 do
            begin
              case Trim(pieces.Strings[i]) of
                'SunriseDiff': SunriseDiffFIeld := i;
              end;
            end;

            {Remove Sunrise field description}
            if (FixedMethod or LinearMethod) then
              WriteLn(OutFile, AnsiReplaceStr(Str, ', SunriseDiff', ''));

            { Append Sunrise field to header field descriptions}
            if (SunriseDifferenceMethod) then
              WriteLn(OutFile, Str + ', SunriseDiff');

          end

          {Remove Sunrise field units}
          else if (AnsiStartsStr('# YYYY-MM-DDTHH:mm:ss.fff;', Str)) then
          begin
            if (FixedMethod or LinearMethod) then
              WriteLn(OutFile, AnsiReplaceStr(Str, ';s', ''));

            { Append Sunrise units}
            if (SunriseDifferenceMethod) then
              WriteLn(OutFile, Str + ';s');

          end


          else { Write untouched header line }
            WriteLn(OutFile, Str);

        end
        else
        begin {Process records}
          pieces.Delimiter := ';';
          pieces.DelimitedText := Str;{Separate the fields of the record.}

          //parse the fields, and convert as necessary.
          //Convert UTC string 'YYYY-MM-DDTHH:mm:ss.fff' into TDateTime
          UTCRecord := ScanDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz', pieces.Strings[0]);

          { Correct UTC and Local times }
          if FixedMethod then
          begin
            CorrUTCRecordTime := IncSecond(UTCRecord, -1 * TimeDiffS);
          end;

          if LinearMethod then
          begin
            //get difference from start to current record
            CurrentDifferenceMS := MilliSecondsBetween(UTCStartRecordTime, UTCRecord);
            //determine factor for correction on current record
            CorrUTCRecordTime :=
              IncMilliSecond(UTCRecord, round(double(CurrentDifferenceMS) *
              LinearCorrectionFactor));
          end;

          {Sunrise difference method; there is no time offset applied}
          if SunriseDifferenceMethod then
          begin
            CorrUTCRecordTime := UTCRecord;
          end;

          CorrLocalRecordTime := ptzCorr.Convert(CorrUTCRecordTime, 'UTC', datLocalTZ);
          ComposeString :=
            FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz;', CorrUTCRecordTime) +
            FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz', CorrLocalRecordTime);

          {Compose remainder of string.}
          for index := 2 to pieces.Count - 1 do
          begin
            {Remove Sunrise difference already there.}
            if not (index = SunriseDiffField) then
              ComposeString := ComposeString + ';' + pieces.Strings[index];
          end;


          if SunriseDifferenceMethod then
          begin
            pieces.Delimiter := ';';
            pieces.DelimitedText := Str;{Separate the fields of the record.}

            {Get current reading}
            SunriseCurrentRdg := StrToFloatDef(pieces.Strings[MSASField], 0.0);

            {Switch from Dawn to Sunrise}
            if ((not Daytime) and (SunriseCurrentRdg < SunriseThreshold)) then
            begin
              {Get Sunrise time for the current day}
              SunriseTimeStamp := Sun_Rise(UTCRecord, MyLatitude, -1.0 * MyLongitude);

              {Compare current timestamp with Sunrise timestamp}
              SunriseDifferenceMS := MilliSecondsBetween(UTCRecord, SunriseTimeStamp);

              {Indicate daytime readings}
              Daytime := True;
            end;

            {Switch from Sunrise to Dawn}
            if ((Daytime) and (SunriseCurrentRdg > DawnThreshold)) then
            begin
              Daytime := False;
            end;

            {Write out difference time}
            ComposeString := ComposeString + ';' + Format(
              '%0.3f', [SunriseDifferenceMS / 1000.0]);

            {Roll current reading over to last reading}
            SunriseLastRdg := SunriseCurrentRdg;

            {Time unchanged}
            CorrUTCRecordTime := UTCRecord;

          end;

          WriteLn(OutFile, ComposeString);

        end;
      until (EOF(InFile));
      // EOF(End Of File) The the program will keep reading new lines until there is none.
      CloseFile(InFile);
      StatusBar1.Panels.Items[0].Text := 'Finished correcting.';
      Application.ProcessMessages;

    except
      on E: EInOutError do
      begin
        MessageDlg('Error', 'File handling error occurred. Details: ' +
          E.ClassName + '/' + E.Message, mtError, [mbOK], 0);
      end;
    end;
    Flush(OutFile);
    CloseFile(OutFile);

  end;//End of WriteAllowable check.

end;

{ Get time difference listed in the input file.}
procedure Tdattimecorrectform.GetTimeDifference();
var
  InFile: TextFile;
  Str: string;
  ReadMode: integer = 0;
  {0= waiting for END OF HEADER, 1= waiting for first record, 2 waiting for last record}
  pieces: TStringList;
  i: integer = 0; //General purpose counter
begin
  pieces := TStringList.Create;

  StatusBar1.Panels.Items[0].Text := 'File selected. Getting details, please wait ...';
  Application.ProcessMessages;

  //Start reading file.
  AssignFile(InFile, InputFile.Text);
  {$I+}
  try
    Reset(InFile);

    repeat
      { Read one line at a time from the file.}
      Readln(InFile, Str);

      { Get Time difference data from header.}
      if AnsiStartsStr('# DL time difference (seconds)', Str) then
      begin
        TimeDiffS := StrToIntDef(AnsiRightStr(Str, length(Str) - RPos(':', Str)), 0);
      end;

      {Get the position coordinates}
      if AnsiStartsStr('# Position (lat, lon, elev(m)):', Str) then
      begin
        pieces.Delimiter := ',';
        pieces.StrictDelimiter := True; //Do not parse spaces also
        pieces.DelimitedText := AnsiRightStr(Str, length(Str) - RPos(':', Str));
        {Check if European commas were used instead of decimal points}
        if pieces.Count >= 5 then
        begin
          MyLatitude := StrToFloatDef(pieces.Strings[0] + '.' +
            pieces.Strings[1], 0, FPointSeparator);
          MyLongitude := StrToFloatDef(pieces.Strings[2] + '.' +
            pieces.Strings[3], 0, FPointSeparator);
        end
        else
        if pieces.Count >= 2 then
        begin
          MyLatitude := StrToFloatDef(pieces.Strings[0], 0, FPointSeparator);
          MyLongitude := StrToFloatDef(pieces.Strings[1], 0, FPointSeparator);
        end;
        LatitudeEdit.Text := format('%0.6f', [MyLatitude]);
        LongitudeEdit.Text := format('%0.6f', [MyLongitude]);
      end;


      {Get field number for MSAS}
      if AnsiStartsStr('# UTC Date & Time, Local Date', Str) then
      begin
        pieces.Delimiter := ',';
        pieces.StrictDelimiter := True; //Do not parse spaces also
        pieces.DelimitedText := Str;
        { Get the field locations. }
        for i := 0 to pieces.Count - 1 do
        begin
          case Trim(pieces.Strings[i]) of
            'MSAS': MSASField := i;
          end;
        end;
      end;


      { Get the .dat timezone information if it exists}
      if (AnsiStartsStr('# Local timezone:', str)) then
      begin
        pieces.Delimiter := ':';
        pieces.StrictDelimiter := True; //Do not parse spaces also
        pieces.DelimitedText := Str;
        if (pieces.Count > 1) then
          if Trim(pieces.Strings[1]) <> '' then
          begin
            datLocalTZ := Trim(pieces.Strings[1]);
            TimeZoneEdit.Text := datLocalTZ;
            {Determine if time zone is valid.}
            if ptzCorr.TimeZoneExists(datLocalTZ) then
              TimeZoneExistsText.Caption := 'Valid'
            else
              TimeZoneExistsText.Caption := '*** Invalid ***';
          end;
      end;

      {Read the datestamp from the first and last record}
      if ReadMode = 2 then
      begin {Waiting for last record}
        { Separate the fields of the record.}
        pieces.Delimiter := ';';
        pieces.DelimitedText := Str;
        UTCEndRecordTime := ScanDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz', pieces.Strings[0]);
        {Pull out UTC record time}
        EndDateTimeEdit.Text :=
          FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz', UTCEndRecordTime);
        {Display date to user}
        TimeSpanEdit.Text := Format(
          '%dd', [DaysBetween(UTCEndRecordTime, UTCStartRecordTime)]);
      end;
      if ReadMode = 1 then
      begin {waiting for first record}
        { Separate the fields of the record.}
        pieces.Delimiter := ';';
        pieces.DelimitedText := Str;
        UTCStartRecordTime :=
          ScanDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz', pieces.Strings[0]);
        {Pull out UTC record time}
        StartDateTimeEdit.Text :=
          FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz', UTCStartRecordTime);
        {Display date to user}
        ReadMode := 2; {Indicate that Waiting for last record to be read}
      end;
      if AnsiStartsStr('# END OF HEADER', Str) then
      begin
        ReadMode := 1; //Indicate that Waiting for first record to be read
      end;

    until (EOF(InFile));
    // EOF(End Of File) The the program will keep reading new lines until there are none.
    ReadMode := 3; {Indicate that No more records to be read}
    CloseFile(InFile);
  except
    on E: EInOutError do
    begin
      MessageDlg('Error', 'File handling error occurred. Details: ' +
        E.ClassName + '/' + E.Message, mtError, [mbOK], 0);
    end;
  end;


  {Display logged time difference}
  TimeDifference.Text := Format('%ds', [TimeDiffS]);

  if Assigned(pieces) then FreeAndNil(pieces);

  StatusBar1.Panels.Items[0].Text := 'Details retrieved.';
  Application.ProcessMessages;

end;

procedure Tdattimecorrectform.UpdateExplanation();
var
  OutFileSuffix: string = '';
begin

  Memo1.Clear;
  if FixedMethod then
  begin
    Memo1.Append(Format('Offset on every record: %ds', [TimeDiffS]));
    OutFileSuffix := 'Fixed';
  end;
  if LinearMethod then
  begin
    {Compute the date range in seconds , possibly show d/h/m/s for convenience}
    RangeTimeMS := MilliSecondsBetween(UTCEndRecordTime, UTCStartRecordTime);
    Memo1.Append(Format('Calculated recording range: %0.3fs', [RangeTimeMS / 1000.0]));
    {Display date to user}
    LinearCorrectionFactor := -1 * TimeDiffS / (RangeTimeMS / 1000.0);
    Memo1.Append(Format('Correction factor: %ds/%0.3fs = %0.8f s/s',
      [TimeDiffS, (RangeTimeMS / 1000.0), LinearCorrectionFactor]));
    {Display date to user}
    OutFileSuffix := OutFileSuffix + 'Linear';
  end;
  if SunriseDifferenceMethod then
  begin
    Memo1.Append(
      'A file will be created noting the difference between sunrise time and reading going to 0 MPSAS.');
    OutFileSuffix := 'Sunrise';
  end;

  if (Length(InFileName) > 0) then
  begin
    {Update output filename}
    OutputFilename := ExtractFilePath(InFileName) +
      LazFileUtils.ExtractFileNameOnly(InFileName) + '_' + OutFileSuffix +
      'TimeCorr' + ExtractFileExt(InFileName);

    OutputFile.Text := OutputFilename;
    CorrectButton.Enabled := True;
  end
  else
  begin
    OutputFilename := '';
    OutputFile.Text := '';
    CorrectButton.Enabled := False;
  end;

end;

initialization
  {$I dattimecorrect.lrs}

end.
