unit datlocalcorrect;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, LResources, Forms, Controls, Graphics, Dialogs,
  StdCtrls, ExtCtrls, Buttons, ComCtrls, Spin
  , upascaltz //Required for timezone corrections
  ;

type

  { Tdatlocalcorrectform }

  Tdatlocalcorrectform = class(TForm)
    DirectorySelectButton: TButton;
    CorrectButton: TBitBtn;
    FileSelectButton: TButton;
    CustomGroupBox: TGroupBox;
    CustomOffsetEdit: TFloatSpinEdit;
    CustomOffsetLabel: TLabel;
    InputDirectoryDisplay: TEdit;
    InputFileDisplay: TEdit;
    Label11: TLabel;
    Label6: TLabel;
    OutputDir: TLabeledEdit;
    Memo1: TMemo;
    SelectDirectoryDialog1: TSelectDirectoryDialog;
    TZMethodRadioGroup: TRadioGroup;
    StandardGroupBox: TGroupBox;
    SettingsGroupBox: TGroupBox;
    InGroupBox: TGroupBox;
    OpenDialog1: TOpenDialog;
    OutGroupBox: TGroupBox;
    OutputFile: TLabeledEdit;
    StatusBar1: TStatusBar;
    TZLocationBox: TComboBox;
    TZRegionBox: TComboBox;
    procedure CorrectButtonClick(Sender: TObject);
    procedure DirectorySelectButtonClick(Sender: TObject);
    procedure FileSelectButtonClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure TZMethodRadioGroupClick(Sender: TObject);
    procedure TZLocationBoxChange(Sender: TObject);
    procedure TZRegionBoxChange(Sender: TObject);
  private
    procedure ReadINI();
    procedure FillTimezones();
    procedure CorrectFile(InfileString, OutFileString: string);
  public

  end;

var
  datlocalcorrectform: Tdatlocalcorrectform;
  InFileName: string;
  OutFileName: string;
  InDirName: string;
  OutDirName: string;
  Timediff: int64;
  LocalRecINIsection: string;
  AZones: TStringList;
  ptz: TPascalTZ;
  subfix: ansistring; //Used for time zone conversions
  LocalRecTZRegion, LocalRecTZLocation: string; //Only used for local timeone correction

  { Indicates that programmatic changes are taking place to the Time Zone }
  TZChanging: boolean = False;

  TimezoneMethod: integer;
  FileMode: boolean = False; {False = single file. True = directory}
  HoursDiff: double = 0.0;

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
  , dlheader //For Timezone conversions.
  ;

{ Tdattimecorrectform }

{ Populate form from INI file }
procedure Tdatlocalcorrectform.ReadINI();
var
  pieces: TStringList;

begin

  pieces := TStringList.Create;
  pieces.Delimiter := ',';
  pieces.StrictDelimiter := False; //Parse spaces also

  LocalRecINIsection := 'LocalReconstruct:';

  { Pull Timezone information from INI file if it exists.}
  LocalRecTZRegion := vConfigurations.ReadString(LocalRecINIsection, 'Local region');
  TZRegionBox.Text := LocalRecTZRegion;
  FillTimezones();

  { Read the previously recorded entries. }
  LocalRecTZLocation := vConfigurations.ReadString(LocalRecINIsection, 'Local time zone');
  TZLocationBox.Text := LocalRecTZLocation;

  if Assigned(pieces) then FreeAndNil(pieces);

end;

//Select file for correction
procedure Tdatlocalcorrectform.FileSelectButtonClick(Sender: TObject);
begin

  { Set mode and clear displays}
  FileMode := False; {Single file selection}
  InputFileDisplay.Text := '';
  InputDirectoryDisplay.Text := '';
  InputFileDisplay.Visible := True;
  InputDirectoryDisplay.Visible := False;
  OutputFile.Visible := True;
  OutputDir.Visible := False;

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

  OpenDialog1.Filter := 'data log files|*.dat|All files|*.*';
  OpenDialog1.InitialDir := appsettings.LogsDirectory;

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

    { Create output file name }
    OutFileName := ExtractFilePath(InFileName) + LazFileUtils.ExtractFileNameOnly(InFileName) + '_LocalCorr' + ExtractFileExt(InFileName);
    OutputFile.Text := OutFileName;
  end;
end;

procedure Tdatlocalcorrectform.DirectorySelectButtonClick(Sender: TObject);
begin

  { Set mode and clear displays}
  FileMode := True;
  InputFileDisplay.Text := '';
  InputDirectoryDisplay.Text := '';
  InputFileDisplay.Visible := False;
  InputDirectoryDisplay.Visible := True;
  OutputFile.Visible := False;
  OutputDir.Visible := True;

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

  SelectDirectoryDialog1.InitialDir := appsettings.LogsDirectory;

  { Get Input directory from user }
  if (SelectDirectoryDialog1.Execute) then
  begin
    InDirName := SelectDirectoryDialog1.FileName;
    InputDirectoryDisplay.Text := InDirName + DirectorySeparator + '*.dat';

    { Show desired output directory }
    OutDirName := InDirName + DirectorySeparator + 'LocalCorr';
    OutputDir.Text := OutDirName + DirectorySeparator + '*_LocalCorr.dat';
  end;

end;

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

  { get previous timezone settings }
  { Initialize required variables. }
  AZones := TStringList.Create;
  ptz := TPascalTZ.Create();

  ReadINI();

  StandardGroupBox.Enabled := True;
  CustomGroupBox.Enabled := False;
  CustomOffsetEdit.Value := HoursDiff;

end;

procedure Tdatlocalcorrectform.FormDestroy(Sender: TObject);
begin
  if Assigned(AZones) then FreeAndNil(AZones);
  if Assigned(ptz) then FreeAndNil(ptz);
end;

procedure Tdatlocalcorrectform.TZMethodRadioGroupClick(Sender: TObject);
begin

  TimezoneMethod := TZMethodRadioGroup.ItemIndex;
  case TimezoneMethod of
    0: begin
      StandardGroupBox.Enabled := True;
      CustomGroupBox.Enabled := False;
    end;
    1: begin
      StandardGroupBox.Enabled := False;
      CustomGroupBox.Enabled := True;
    end;
  end;
end;

procedure Tdatlocalcorrectform.TZLocationBoxChange(Sender: TObject);
begin
  if not TZChanging then
  begin
    TZChanging := True;
    { Save the TZ selection. }
    LocalRecTZLocation := TZLocationBox.Text;
    Application.ProcessMessages;
    vConfigurations.WriteString(LocalRecINIsection, 'Local time zone',
      LocalRecTZLocation);
    TZChanging := False;
  end;
end;

procedure Tdatlocalcorrectform.TZRegionBoxChange(Sender: TObject);
begin
  if not TZChanging then
  begin
    TZChanging := True;

    { Get and save region }
    LocalRecTZRegion := TZRegionBox.Text;
    Application.ProcessMessages; //Wait for GUI to put screen text into variable.
    vConfigurations.WriteString(LocalRecINIsection, 'Local region', LocalRecTZRegion);
    //Save TZ Region

    { Fill up timezone location names }
    FillTimezones();

    { Clear out selected time zone location because time zone region was just changed. }
    LocalRecTZLocation := '';
    TZLocationBox.Text := LocalRecTZLocation;
    vConfigurations.WriteString(LocalRecINIsection, 'Local time zone',
      LocalRecTZLocation);

    TZChanging := False;
  end;

end;

{ Correct one file }
procedure Tdatlocalcorrectform.CorrectFile(InfileString, OutfileString: string);
var
  InFile, OutFile: TextFile;
  Str: string;
  pieces: TStringList;

  index: integer;

  UTCRecord: TDateTime;
  LocalRecord: TDateTime;
  ComposeString: string;

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

begin

  StatusBar1.Panels.Items[0].Text := 'Correcting input file: ' + InfileString;
  Application.ProcessMessages;

  pieces := TStringList.Create;

  AssignFile(InFile, InFileString);
  AssignFile(OutFile, OutfileString);


  { Start reading file. }
  if FileExists(OutfileString) then
  begin
    if (MessageDlg('Overwrite existing file?', 'Do you want to overwrite the existing file ' + OutfileString +
      ' ?', 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
        { Read one line at a time from the file. }
        Readln(InFile, Str);

        //StatusBar1.Panels.Items[0].Text := 'Processing : ' + Str;
        //Application.ProcessMessages;

        { Ignore most comment lines which have # as first character. }
        if (AnsiStartsStr('#', Str)) then
        begin
          { Touch up time zone location line }
          if AnsiStartsStr('# Local timezone:', Str) then
          begin
            case TimezoneMethod of
              0: WriteLn(OutFile, format('# Local timezone: %s', [LocalRecTZLocation]));
              1: WriteLn(OutFile, Str);
            end;
          end
          else
            { Write untouched header line }
            WriteLn(OutFile, Str);
        end
        else
        begin
          { Separate the fields of the record. }
          pieces.Delimiter := ';';
          pieces.DelimitedText := Str;

          { Parse the UTC timestamp}
          UTCRecord := ScanDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz', pieces.Strings[0]);

          case TimezoneMethod of
            0: begin { Standard correction method uses selected region and timezone. }
              LocalRecord := ptz.GMTToLocalTime(UTCRecord, LocalRecTZLocation, subfix);
            end;
            1: begin { Custom correction method uses offset hours value. }
              LocalRecord := UTCRecord + (HoursDiff / 24.0);
            end;
          end;

          { Compose the string for timestamp replacement. }
          ComposeString := FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz;', UTCRecord) + FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz', LocalRecord);

          { Compose remainderof string }
          for index := 2 to pieces.Count - 1 do
          begin
            ComposeString := ComposeString + ';' + pieces.Strings[index];
          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';
      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;

{ Correct file(s) }
procedure Tdatlocalcorrectform.CorrectButtonClick(Sender: TObject);
var
  sr: TSearchRec;
begin
  { Clear status bar. }
  StatusBar1.Panels.Items[0].Text := 'Processing file(s).';
  Application.ProcessMessages;

  HoursDiff:=CustomOffsetEdit.Value;

  if not FileMode then
  begin
    { Just correct one file. }
    CorrectFile(InFileName, OutFileName);

  end
  else
  begin {All files in a directory.}

    { Make directory to store files. }
    if (not (DirectoryExists(OutDirName))) then
      MkDir(OutDirName);

    { Work on all files in the directory. }
    if FindFirstUTF8(InDirName + DirectorySeparator + '*.dat', faAnyFile, sr) = 0 then
      repeat
        CorrectFile(InDirName + DirectorySeparator + sr.Name, OutDirName + DirectorySeparator +
          LazFileUtils.ExtractFileNameOnly(sr.Name) + '_LocalCorr.dat');
      until FindNextUTF8(sr) <> 0;
    FindCloseUTF8(sr);
  end;

end;

{ Fill timezone location dropdown entries. }
procedure Tdatlocalcorrectform.FillTimezones();
begin
  if (FileExists(appsettings.TZDirectory + LocalRecTZRegion) and (length(LocalRecTZRegion) > 0)) then
  begin
    ptz.Destroy;
    ptz := TPascalTZ.Create();
    try
      ptz.ParseDatabaseFromFile(appsettings.TZDirectory + LocalRecTZRegion);
    except
      ShowMessage(Format('Failed getting zones from %s', [LocalRecTZRegion]));
    end;
    ptz.GetTimeZoneNames(AZones, True);
    { Only geo name = true, does not show short names. }
    TZLocationBox.Items.Clear;
    TZLocationBox.Items.AddStrings(AZones);
  end;
end;

initialization
  {$I datlocalcorrect.lrs}

end.
