unit LogManager;
  interface
    uses
      AdsData    (* tAdsStringField *),
      AdsFunc    (* used by AdsTable *),
      AdsTable   (* tAdsQuery *),
      AdvGrid    (* TMS *),
      AdvObj     (* TMS *),
      AdvUtil    (* TMS *),
      BaseGrid   (* TMS *),
      Classes    (**),
      ComCtrls   (* tPageControl, tTabSheet *),
      Controls   (**),
      DB         (* tDataSet, tDataSource, tIntegerField *),
      DBAdvGrid  (* tDBAdvGrid *),
      ExtCtrls   (* tRadioGroup, tShape *),
      Forms      (* Application, tForm *),
      Graphics   (* TMS *),
      Grids      (* TMS *),
      LineParser (* tLineParser, sepTab *),
      StdCtrls   (* tButton, tCheckBox, tEdit, tGroupBox, tLabel, tListbox *),
      Windows    (* tRect *);

    type
      tfmLogManager = class(tForm)
          pgctlLogManager: tPageControl;
            tabLog: tTabSheet;
              btnSaveLog:  tButton;
              ckbxAutoScroll:  tCheckBox;
              ckbxShowDecoded: tCheckBox;
              ckbxShowSimple:  tCheckBox;
              grpAnnotate: tGroupBox;
                btnAppend:  tButton;
                btnNewLine: tButton;
                edAnnotate: tEdit;
              lbxLog: tListBox;
              shapeClearAlert: tShape;
                btnClearLog: tButton;
            tabFilters: tTabSheet;
              btnExtract: tButton;
              qBoardSlots: tAdsQuery;
                dataBoardSlots: tDataSource;
                  dbadvgridBoardSlots: tDBAdvGrid;
              qObservedAndDontCare: tAdsQuery;
                dataObservedAndDontCare: tDataSource;
                  dbadvgridObserved: tDBAdvGrid;
              qSelectBoards: tAdsQuery;
                dataSelectBoards: tDataSource;
                  dbadvgridSelectBoards: tDBAdvGrid;
              qSelectSensorTypes: tAdsQuery;
                dataSelectSensorType: tDataSource;
                  dbadvgridSelectSensorType: tDBAdvGrid;
              grpSelectedLog: tGroupBox;
                btnClearSelected: tButton;
                btnSaveSelected:  tButton;
                lbxSelected: tListBox;
              lbDecode: tLabel;
              rgFilter: tRadioGroup;
            tabDecoding: tTabSheet;
              lbWhatMeasured: TLabel;
              lbxWhatMeasured: tListBox;

          procedure qObservedAndDontCareAfterScroll(DataSet: tDataSet);
          procedure qSelectBoardsAfterScroll       (DataSet: tDataSet);
          procedure qSelectSensorTypesAfterScroll  (DataSet: tDataSet);

          procedure btnAppendClick       (Sender: tObject);
          procedure btnClearLogClick     (Sender: tObject);
          procedure btnClearSelectedClick(Sender: tObject);
          procedure btnExtractClick      (Sender: tObject);
          procedure btnNewLineClick      (Sender: tObject);
          procedure btnSaveLogClick      (Sender: tObject);
          procedure btnSaveSelectedClick (Sender: tObject);
          procedure FormDblClick         (Sender: tObject);
          procedure FormShow             (Sender: tObject);

          procedure dbadvgridBoardSlotsCanSort      (Sender: tObject; ACol: integer; var DoSort: boolean);
          procedure dbadvgridObservedCanSort        (Sender: tObject; ACol: integer; var DoSort: boolean);
          procedure dbadvgridSelectSensorTypeCanSort(Sender: tObject; ACol: integer; var DoSort: boolean);

        private
          LinesLogged: integer;
        public
          procedure DeviceFailure;
          procedure EndDump;
          procedure ExtendLog(const S: string);
          procedure Initialize;
          procedure NewDataLine;
          procedure ParseFailure(const Reason: string);
          procedure SaveLogs;
        end (* tfmLogManager *);

    var
      fmLogManager: tfmLogManager;

  implementation
    uses
      Dashboard      (* fmDashboard *),
      DBAdvGridUtils (* SetUpDBAdvGridReadOnly *),
      Dialogs        (* tSaveDialog *),
      HC3DM          (* dmHC3, Global *),
      HC4Main        (* fmHC4Main *),
      HC3Utilities   (* SaveListBox *),
      Math           (* IntPower *),
      MyConsts       (* BLANK, nullstring, STAR, TAB, UsualFormColor *),
      ObservedAndWM  (* tBoardSlot, GetObservedName, MeasurementInfo, obImplicit,
                          tObserved, obZeroDoNotRemove, stDepthGauge, tSensorType,
                          tWhatMeasured, wm2DeviceFailure *),
      RelayManager   (* fmRelayManager *),
      RRF_StringAndTimeUtils (* MyStrToDateTime, UTCToLocal *),
      SysUtils       (* Format, FormatDateTime, rfReplaceAll, StringReplace,
                          StrToInt *);
      {$R *.dfm}

    var
      SelectedBoardTag: char;
      SelectedObservedUID: tObserved;
      SelectedSensorType: tSensorType;

{******** infrastructure ******************************************************}
    procedure tfmLogManager.FormDblClick(Sender: tObject);
      (* show master form *)

      begin (* tfmLogManager.FormDblClick *)
        fmHC4Main.Show
      end   (* tfmLogManager.FormDblClick *);

    procedure tfmLogManager.FormShow(Sender: tObject);
      (* initialize *)

      var
        WM: tWhatMeasured2;

      begin (* tfmLogManager.FormShow *)
        { global }
          self.Color := UsualFormColor;

        { log }

        { filters }
          lbxSelected.Clear;
          qSelectBoards.Open;
          qSelectBoards.First;
            SetUpDBAdvGridReadOnly(dbadvgridSelectBoards);

          qSelectSensorTypes.Open;
          qSelectSensorTypes.First;
            SetUpDBAdvGridReadOnly(dbadvgridSelectSensorType);
              SortOn('UID');
              SortOn('Name');

        { decode }
          qBoardSlots.Open;
            SetUpDBAdvGridReadOnly(dbadvgridBoardSlots);
              SortOn('UID');
              SortOn('Slot');

          with lbxWhatMeasured do
            begin (* lbxWhatMeasured *)
              Clear;
              for WM := low(tWhatMeasured2) to high(tWhatMeasured2) do
                Items.Add(Format('%2d: %s', [ord(WM), MeasurementInfo2[WM].Name]))
            end   (* lbxWhatMeasured *);

        pgctlLogManager.ActivePage := tabLog
      end   (* tfmLogManager.FormShow *);

    procedure tfmLogManager.Initialize;
      (* prepare for periodic notices *)

      begin (* tfmLogManager.Initialize *)
        LinesLogged := 0;
        qObservedAndDontCare.Open;
        SetUpDBAdvGridReadOnly(dbadvgridObserved);
          SortOn('UID');
          SortOn('Observed')
      end   (* tfmLogManager.Initialize *);

    procedure tfmLogManager.SaveLogs;
      (* save the logs *)
    
      begin (* tfmLogManager.SaveLogs *)
        btnSaveLogClick(nil);
        btnSaveSelectedClick(nil)
      end   (* tfmLogManager.SaveLogs *);

{******** end infrastructure **************************************************}

{******** logging *************************************************************}
    procedure tfmLogManager.btnSaveLogClick(Sender: tObject);
      (* usual dialog *)

      const
        DefaultFileName = 'HC2Log.txt';

      begin (* tfmLogManager.btnSaveLogClick *)
        SaveListBox(lbxLog, 'HC3Log.txt', 'save messages to file')
      end   (* tfmLogManager.btnSaveLogClick *);

    procedure tfmLogManager.btnClearLogClick(Sender: tObject);
      (* convenience *)

      begin (* tfmLogManager.btnClearLogClick *)
        lbxLog.Clear
      end   (* tfmLogManager.btnClearLogClick *);

    procedure tfmLogManager.DeviceFailure;
      (* log the line; LatestData has
        index     0          1             2       3        4

                 BTW  <UTCtimestamp>   <BadTag>  <Slot>  <reason>
       *)

      var
        Observed: string;

      begin (* tfmLogManager.DeviceFailure *)
        with Global.LatestData do
          begin (* Global.LatestData *)
            if ObservedUID <> obImplicit then
              Observed := GetObservedName(ObservedUID) + BLANK
            else
              Observed := nullstring;

            ExtendLog(Format('%s when observing %sfrom %s.%s with %s',
                             [Strings[4], Observed, BoardName, BoardSlotName, SensorTypeName]))
          end   (* Global.LatestData *)
      end   (* tfmLogManager.DeviceFailure *);

    procedure tfmLogManager.EndDump;
      (* conceal flags *)

      begin (* tfmLogManager.EndDump *)
        ExtendLog('dump ended');
        if ckbxShowSimple.Checked then
          ExtendLog('kind       timestamp        b   s  <-triple #1->')
      end   (* tfmLogManager.EndDump *);

    procedure tfmLogManager.ExtendLog(const S: string);
      (* common code *)
    
      var
        NewIndex: integer;

      begin (* tfmLogManager.ExtendLog *)
        with lbxLog do
          begin (* lbxLog *)
            NewIndex := Items.Add(S);
            if ckbxAutoScroll.Checked then
              ItemIndex := NewIndex
          end   (* lbxLog *)
      end   (* tfmLogManager.ExtendLog *);

    procedure tfmLogManager.NewDataLine;
      (* log it, possibly annotated
        count                                                         Count-2   Count-1
        index     0              1             2      3   (4      5       6)*    3*NArgs+1
          (btwDumpData or
             btwLiveData)  <UTCtimestamp>  <GoodTag> <Slot> (<wm> <man> <negExp>)* <checksum>
         case i of
                        1: Object[i] is NArgs
                        2: Object[i] is SensorUID
             3 .. Count-1: Object[i] is integer(Strings[I])
          1-BASED:
             WhatMeasured[j] is tWhatMeasured(Object[3*J+1])
             Value[j]        is integer(Object[3*J+2]/10^Object[3*j+6]
      *)

      const
        NormalIndent = '  ';
        TimeReminderInterval = 5;

      var
        Selected: boolean;
        Expanded: string;

      function ApplyFilters: boolean;
        (*  determine if current line qualifies. *)

        begin (* ApplyFilters *)
          with Global.LatestData do
            Result := ((SelectedBoardTag = STAR)
                          or (BoardTag = SelectedBoardTag))          and
                      ((SelectedObservedUID = obZeroDoNotRemove)
                          or (TheObservedUID = SelectedObservedUID)) and
                      ((SelectedSensorType = stZeroDoNotRemove)
                          or (SensorType = SelectedSensorType))
        end   (* ApplyFilters *);

      procedure ShowDecoded;
        (* board sensor observed
             wm value
             . . .
             wm value
        *)

        const
          BoardIndent = NormalIndent + '  ';
          ValueIndent = BoardIndent  + '  ';

        var
          I: integer;
          Observed: string;

        procedure ShowValue(const WM: tWhatMeasured2;
                            const Value: real);
          (* decode Ith triple *)

          begin (* ShowValue *)
            ExtendLog(Format('%s%s = %.3f', [ValueIndent,
                                             MeasurementInfo2[WM].Name,
                                             Value]))
          end   (* ShowValue *);

        begin (* ShowDecoded *)
          with Global.LatestData do
            begin (* Parser *)
              if Observed = nullstring then
                ExtendLog(Format('%s%s reporting from %s (%s)',
                                 [BoardIndent, SensorTypeName, BoardName, BoardSlotName]))
              else
                ExtendLog(Format('%s%s observing %s from %s (%s)',
                                [BoardIndent, SensorTypeName, Observed, BoardName, BoardSlotName]));

              if (SensorType = stDepthGauge) and (WMArg[1] = wm2DeviceFailure) then
                begin (* depth failure *)
                  ExtendLog(Format('%spin value  = %.0f', [ValueIndent, RealArg[2]]));
                  ExtendLog(Format('%svolts      = %.3f', [ValueIndent, RealArg[3]]));
                  ExtendLog(Format('%sresistance = %.0f', [ValueIndent, RealArg[4]]));
                  ExtendLog(Format('%sinches out = %.2f', [ValueIndent, RealArg[5]]));
                  ExtendLog(Format('%sraw depth  = %.2f', [ValueIndent, RealArg[6]]));
                end   (* depth failure *)

              else
                for I := 1 to NArgs do
                  ShowValue(WMArg[I], RealArg[I])
            end   (* Parser *)
        end   (* ShowDecoded *);

      procedure ShowTheLine(const WantedMain, WantedHere: boolean);
        (* show iff wanted *)
//        count                                                         Count-2   Count-1
//        index     0              1             2      3   (4      5       6)*    3*NArgs+1
//          (btwDumpData or
//             btwLiveData)  <UTCtimestamp>  <GoodTag> <Slot> (<wm> <man> <negExp>)* <checksum>
//         case i of

        begin (* ShowTheLine *)
          if WantedMain then
            begin (* not filtered out *)
              if ckbxShowSimple.Checked then
                ExtendLog(NormalIndent + Expanded);
              if ckbxShowDecoded.Checked then
                ShowDecoded;
              inc(LinesLogged);
              if LinesLogged mod TimeReminderInterval = 0 then
                with Global.LatestData do
                  ExtendLog(Format('(%s)  UTC  board  slot (WM val negexp)* cksum',
                                   [FormatDateTime('hh:nn:ss',
                                                   UTCToLocal(MyStrToDateTime(Strings[1])) )]))
            end   (* not filtered out *);

          if WantedHere then
             with lbxSelected do
               ItemIndex := Items.Add(Expanded);
        end   (* ShowTheLine *);

      begin (* tfmLogManager.NewDataLine *)
        Expanded := Global.LatestData.ExpandedLine;
        if rgFilter.ItemIndex = 0 then
          Selected := true
        else
          Selected := ApplyFilters;
        // with fmDashboard do
        //   ExtendChat(Format('LogManager.NewDataLine(%s)', [Expanded]));

        case rgFilter.ItemIndex of
          0: ShowTheLine(true,     false);
          1: ShowTheLine(true,     Selected);
          2: ShowTheLine(Selected, Selected)
          end (* case *)
      end   (* tfmLogManager.NewDataLine *);

    procedure tfmLogManager.ParseFailure(const Reason: string);
      (* draw attention *)

      begin (* tfmLogManager.ParseFailure *)
        ExtendLog(Format('%s in "%s"',
                         [Reason,
                          Global.LatestData.ExpandedLine]))
      end   (* tfmLogManager.ParseFailure *);

{******** end logging *********************************************************}

{******** selecting ***********************************************************}
    procedure tfmLogManager.btnClearSelectedClick(Sender: tObject);
      (* convenience *)
    
      begin (* tfmLogManager.btnClearSelectedClick *)
        lbxSelected.Clear
      end   (* tfmLogManager.btnClearSelectedClick *);

    procedure tfmLogManager.btnExtractClick(Sender: tObject);
      (* extract selected lines from log *)

      var
        I: integer;
        Parser: tLineParser;
        RetabbedLine, TheLine: string;

      function Wanted: boolean;
        (* process line of log; ignore if has no tabs
          good line will be like
                timestamp           tag slot
                        1111111111 2 2 2 2 2  2
               1234567890123456789 0 1 2 3 4  5
               2021-07-03 22:50:22-->S-->8-->31-->40000-->3-->40042
        that is
            Strings[0] is timestamp
            Strings[1] is board tag
            Strings[2] is slot number
        *)

        var
          BoardSlot: tBoardSlot;
          ObservedOK, SensorOK: boolean;
          BoardTag: char;
          ThisObservedUID: tObserved;
          ThisSensorType: tSensorType;

        begin (* Wanted *)
          Result := false;
          if length(RetabbedLine) > 22 then
            with Parser do
              begin (* Parser *)
                Parse(RetabbedLine);
                if Count >= 4 then
                  begin (* long enough *)
                    BoardTag := Strings[1][1];
                    BoardSlot := tBoardSlot(StrToInt(Strings[2]));
                    
                    if ((SelectedBoardTag = STAR) or (BoardTag = SelectedBoardTag)) then
                      begin (* board OK *)
                        SensorOK := (SelectedSensorType = stZeroDoNotRemove);
                        ObservedOK := (SelectedObservedUID = obZeroDoNotRemove);

                        if SensorOK and ObservedOK then
                          Result := true
                        else
                          with dmHC3.qSensorAndObserved do
                            begin (* qSensorAndObserved *)
                              Close;
                              ParamByName('BoardTag').AsString := BoardTag;
                              ParamByName('BoardSlot').AsInteger := ord(BoardSlot);
                              Open;
                              ThisSensorType  := tSensorType(FieldByName('SensorTypeUID').AsInteger);
                              ThisObservedUID :=   tObserved(FieldByName('ObservedUID'  ).AsInteger);
                              Close;
                              Result := (SensorOK or (ThisSensorType = SelectedSensorType)) and
                                        (ObservedOK or (ThisObservedUID = SelectedObservedUID))
                            end   (* qSensorAndObserved *);
                      end   (* board OK *);
                  end   (* long enough *)
              end   (* Parser *)
        end   (* Wanted *);

      begin (* tfmLogManager.btnExtractClick *)
        Parser := tLineParser.Create(sepTab);
        lbxSelected.Clear;
        with lbxLog do
          for I := 1 to Count do
            begin (* process one line *)
              TheLine := Items[I-1];
              RetabbedLine := StringReplace(TheLine, '-->', TAB, [rfReplaceAll]);
              if Wanted then
                with lbxSelected do
                  ItemIndex := Items.Add(TheLine)
            end   (* process one line *);

        Parser.Free
      end   (* tfmLogManager.btnExtractClick *);

    procedure tfmLogManager.btnSaveSelectedClick(Sender: tObject);
      (* usual dialog *)

      begin (* tfmLogManager.btnSaveSelectedClick *)
        SaveListBox(lbxSelected,
                    'selected HC3Log.txt',
                    'save selected messages to file')
      end   (* tfmLogManager.btnSaveSelectedClick *);

    procedure tfmLogManager.dbadvgridObservedCanSort(Sender: tObject;
                                                     ACol: integer;
                                                     var DoSort: boolean);
      (* sorting *)

      var
        Header, OrderBy: string;
    
      begin (* tfmLogManager.dbadvgridObservedCanSort *)
        DoSort := false;
        Header := dbadvgridObserved.Columns[ACol].Header;

        if      Header =   'UID'    then OrderBy := 'UID'
        else if Header = 'observed' then OrderBy := 'Observed';

        with qObservedAndDontCare do
          begin (* qObservedAndDontCare *)
            Close;
            SQL.Text := 'select                 ' +
                        '  Observed,            ' +
                        '  UID                  ' +
                        'from                   ' +
                        '  Observed             ' +
                        'union                  ' +
                        'select                 ' +
                        '  ''(don''''t care)'', ' +
                        '   -1                  ' +
                        'from                   ' +
                        '  Observed             ' +
                        'order by               ' +
                           OrderBy;
            ShowMessage(SQL.Text);
            Open
          end   (* qObservedAndDontCare *)
      end   (* tfmLogManager.dbadvgridObservedCanSort *);

    procedure tfmLogManager.dbadvgridSelectSensorTypeCanSort(Sender: tObject;
                                                             ACol: integer;
                                                             var DoSort: boolean);
      (* sorting *)

      var
        Header, OrderBy: string;

      begin (* tfmLogManager.dbadvgridSelectSensorTypeCanSort *)
        DoSort := false;
        Header := dbadvgridSelectSensorType.Columns[ACol].Header;

        if      Header =   'UID'       then OrderBy := 'UID'
        else if Header = 'sensor type' then OrderBy := 'Name';

        with qSelectSensorTypes do
          begin (* qSelectSensorTypes *)
            Close;
            SQL.Text := 'select                 ' +
                        '  Name,                ' +
                        '  UID                  ' +
                        'from                   ' +
                        '  SensorTypes3         ' +
                        'where                  ' +
                        '  Name <> ''bogus''    ' +
                        'union                  ' +
                        'select                 ' +
                        '  ''(don''''t care)'', ' +
                        '   -1                  ' +
                        'from                   ' +
                        '  SensorTypes3         ' +
                        'order by               ' +
                           OrderBy;
            Open
          end   (* qSelectSensorTypes *)
      end   (* tfmLogManager.dbadvgridSelectSensorTypeCanSort *);

    procedure tfmLogManager.dbadvgridBoardSlotsCanSort(Sender: tObject;
                                                       ACol: integer;
                                                       var DoSort: boolean);
      (* sorting *)

      var
        Header, OrderBy: string;
    
      begin (* tfmLogManager.dbadvgridBoardSlotsCanSort *)
        DoSort := false;
        Header := dbadvgridBoardSlots.Columns[ACol].Header;

        if      Header =  'UID' then OrderBy := 'UID'
        else if Header = 'slot' then OrderBy := 'Name';

        with qBoardSlots do
          begin (* qSelectSensorTypes *)
            Close;
            SQL.Text := 'select               ' +
                        '  Name as Slot,      ' +
                        '  UID                ' +
                        'from                 ' +
                        '  BoardSlots         ' +
                        'order by             ' +
                           OrderBy;
            Open
          end   (* qBoardSlots *)
      end   (* tfmLogManager.dbadvgridBoardSlotsCanSort *);

    procedure tfmLogManager.qObservedAndDontCareAfterScroll(DataSet: tDataSet);
      (* prepare for filtering *)

      var
        ObservedUID: integer;

      begin (* tfmLogManager.qObservedAndDontCareAfterScroll *)
        ObservedUID := DataSet.FieldByName('UID').AsInteger;
        if ObservedUID <= 0 then
          SelectedObservedUID := obZeroDoNotRemove
        else
          SelectedObservedUID := tObserved(ObservedUID)
      end   (* tfmLogManager.qObservedAndDontCareAfterScroll *);

    procedure tfmLogManager.qSelectBoardsAfterScroll(DataSet: tDataSet);
      (* prepare for filtering *)

      begin (* tfmLogManager.qSelectBoardsAfterScroll *)
        SelectedBoardTag := DataSet.FieldByName('Tag').AsString[1]
      end   (* tfmLogManager.qSelectBoardsAfterScroll *);

    procedure tfmLogManager.qSelectSensorTypesAfterScroll(DataSet: tDataSet);
      (* prepare for filtering *)
    
      begin (* tfmLogManager.qSelectSensorTypesAfterScroll *)
        SelectedSensorType:= tSensorType(DataSet.FieldByName('UID').AsInteger)
      end   (* tfmLogManager.qSelectSensorTypesAfterScroll *);

{******** end selecting *******************************************************}

{******** annotation-related **************************************************}
    procedure tfmLogManager.btnAppendClick(Sender: tObject);
      (* add note at end of selected line *)

      begin (* tfmLogManager.btnAppendClick *)
        with lbxLog do
          Items[ItemIndex] := Format('%s *** %s', [Items[ItemIndex],
                                                   edAnnotate.Text])
      end   (* tfmLogManager.btnAppendClick *);

    procedure tfmLogManager.btnNewLineClick(Sender: tObject);
      (* insert item just after current one *)

      begin (* tfmLogManager.btnNewLineClick *)
        with lbxLog do
          begin (* lbxLog *)
            Items.Insert(ItemIndex+1, edAnnotate.Text);
            ItemIndex := ItemIndex + 1
          end   (* lbxLog *)
      end   (* tfmLogManager.btnNewLineClick *);

{******** end annotation-related **********************************************}
end.
