unit table_OnOff;
  (* damping done in peripheral *)

  interface
    procedure ProcessCurrentLine_OnOff;

  implementation
    uses
      BoardManager       (* fmBoardManager *),
      Controls           (* tTime *),
      Dashboard          (* fmDashboard *),
      DateUtils          (* DateOf, HourSpan, IncDay, IncSecond, SecondsBetween,
                              StartOfTheDay *),
      Dialogs            (* ShowMessageFmt *),
      GraphManager2      (* fmGraphManager *),
      HC3DM              (* dmHC3, Global *),
      HC3Utilities       (* BoolSpell, FDT, LastSecondOfTheDay, LastUIDOf *),
      IndoorInstruments  (* fmIndoorSensors *),
      RRF_StringAndTimeUtils (* LocalToUTC *),
      ObservedAndWM      (* GetObservedName, obLRRelay, tObserved, obTVRelay *),
      OutdoorInstruments (* fmOutdoorInstruments *),
      SensorDevice       (* tSensorDevice *),
      SysUtils           (* Format *),
      TableManager       (* tuOnOff *);

    const
      HoursPerSecond = 1.0 / 3600.0;
      MaxDevices = 10;

    type
      tOnOffDevice = class(tSensorDevice)
        private
          OnAtEdge: boolean;
          AnchorLocal, WhenAtEdgeLocal: tDateTime;
          UIDAtEdge: integer;
          HoursOnAtEdge: real;
        public
          constructor Create(const ObservedUID: tObserved);
          procedure   GenerateNewPair(const StartTimeLocal: tDateTime;
                                      const IsOn: boolean;
                                      const StartHoursOn: real);
          procedure   NewData(const IsNowOn: boolean);
        end (* tOnOffDevice *);

    var
      NDevices: integer;
      OnOffDevice: array[1 .. MaxDevices] of tOnOffDevice;

    procedure ProcessCurrentLine_OnOff;
      (* just pass on/off to the device *)

      var
        DeviceIndex: integer;

      procedure GetDeviceIndex(const ObservedUID: tObserved);
        (* this may entail creating the device, if this ObservedUID &
         station tag are new *)
      
        function FindDeviceIndex: integer;
          (* if not found, return 0 *)

          var
            I: integer;

          begin (* FindDeviceIndex *)
            Result := 0;
            for I := 1 to NDevices do
              with OnOffDevice[I] do
                if TheObservedUID = ObservedUID then
                  begin (* found it *)
                    Result := I;
                    break
                  end   (* found it *)
          end   (* FindDeviceIndex *);

        begin (* GetDeviceIndex *)
          DeviceIndex := FindDeviceIndex;
          if DeviceIndex = 0 then
            begin (* create new device *)
              inc(NDevices);
              OnOffDevice[NDevices] := tOnOffDevice.Create(ObservedUID);
              DeviceIndex := NDevices
            end   (* create new device *)
        end   (* GetDeviceIndex *);

      begin (* ProcessCurrentLine_OnOff *)
        with Global.LatestData do
          if NArgs >= 1 then
            if WMArg[1] in [wm2IsNowOn, wm2OnOffIntent] then
              begin (* pass arg to the sensor device *)
                GetDeviceIndex(ObservedUID);
                OnOffDevice[DeviceIndex].NewData(BoolArg[1])
              end   (* pass arg to the sensor device *);
      end   (* ProcessCurrentLine_OnOff *);

{ tOnOffDevice }
  (*   The table has two records for each run of unchanged state, the Anchor and
    the Edge.  They start out separated by one second, but later the Edge
    record's timestamps are updated if the state persists.
  *)

    constructor tOnOffDevice.Create(const ObservedUID: tObserved);
      (* constructor.  Must retrieve old data *)

      var
        WasOn: boolean;
        EdgeUID: integer;
        LocalAnchor, LocalEdge: tDateTime;
        RecentOnHours: real;

      begin (* tOnOffDevice.Create *)
        // ShowMessageFmt('creating onoff device for ObservedUID = %d', [ord(ObservedUID)]);
        inherited Create(tuOnOff, ObservedUID, Global.LatestData.UTCStamp, stACDetect);
        WasOn         := false;
        RecentOnHours := 0;
        with fmDashboard do
          ExtendChat('on_117 created on-off device for ' + GetObservedName(ObservedUID),
                     llblCreated);
        with qGetRecentData do
          begin (* qGetRecentData *)
            Open;
            if RecordCount = 0 then
              GenerateNewPair(StartOfTheDay(Global.LatestData.LocalStamp),
                                              WasOn,
                                              RecentOnHours)

            else
              begin (* extend table for this Observed *)
                EdgeUID       := FieldByName('UID'            ).AsInteger;
                LocalEdge     := FieldByName('WhenRecorded'   ).AsDateTime;
                RecentOnHours := FieldByName('HoursOn'        ).AsFloat;
                WasOn         := FieldByName('OnOnOn'         ).AsBoolean;
                Next;
                LocalAnchor   := FieldByName('WhenRecorded').AsDateTime;

                AnchorLocal     := LocalAnchor;
                HoursOnAtEdge   := RecentOnHours;
                OnAtEdge        := WasOn;
                TheObservedUID  := ObservedUID;
                UIDAtEdge       := EdgeUID;
                WhenAtEdgeLocal := LocalEdge
              end   (* extend table for this Observed *);

            Close
          end   (* qGetRecentData *)
      end   (* tOnOffDevice.Create *);

      procedure tOnOffDevice.GenerateNewPair(const StartTimeLocal: tDateTime;
                                             const IsOn: boolean;
                                             const StartHoursOn: real);
        (* query does all the work *)

        var
          FinishWhenLocal, FinishWhenUTC, StartWhenUTC: tDateTime;
          StartUID: integer;
          NewHoursOn: real;

        begin (* tOnOffDevice.GenerateNewPair *)
          StartUID := LastUIDOf('OnOff') + 1;
          if IsOn then
            NewHoursOn := HoursPerSecond
          else
            NewHoursOn := 0;
          HoursOnAtEdge := StartHoursOn + NewHoursOn;

          StartWhenUTC    := LocalToUTC(StartTimeLocal);
          FinishWhenLocal := IncSecond(StartTimeLocal);
          FinishWhenUTC   := IncSecond(StartWhenUTC);

          // with fmDashboard do
          //   ExtendChat(Format('on_171  %s NewPair(%s, %s, %.2f): '    +
          //                      'FW: %s, FWUTC: %s, FHO: %.2f, ' +
          //                      'SUID: %d, SWUTC: %s',
          //                       [ObservedInfo[TheObservedUID].Name,
          //                        FDT(StartTimeLocal), BoolSpell(IsOn), StartHoursOn,
          //                        FDT(FinishWhenLocal), FDT(FinishWhenUTC), HoursOnAtEdge,
          //                        StartUID, FDT(StartWhenUTC)]));
          with dmHC3.qNewOnOffPair do
            begin (* dmHC3.qNewOnOffPair *)
              ParamByName('FinishWhen'   ).AsDateTime := FinishWhenLocal;
              ParamByName('FinishWhenUTC').AsDateTime := FinishWhenUTC;
              ParamByName('FinalHoursOn' ).AsFloat    := HoursOnAtEdge;
              ParamByName('ObservedUID'  ).AsInteger  := ord(TheObservedUID);
              ParamByName('OnOnOn'       ).AsBoolean  := IsOn;
              ParamByName('StartHoursOn' ).AsFloat    := StartHoursOn;
              ParamByName('StartUID'     ).AsInteger  := StartUID;
              ParamByName('StartWhen'    ).AsDateTime := StartTimeLocal;
              ParamByName('StartWhenUTC' ).AsDateTime := StartWhenUTC;
              ExecSQL
            end   (* dmHC3.qNewOnOffPair *);
          AnchorLocal := StartTimeLocal;
          UIDAtEdge := StartUID + 1;
          OnAtEdge := IsOn;
          WhenAtEdgeLocal := FinishWhenLocal
        end   (* tOnOffDevice.GenerateNewPair *);

    procedure tOnOffDevice.NewData(const IsNowOn: boolean);
      (* may have to create zeroes, move entries *)

      var
        HoursPassed: real;

      procedure AdvanceEdge(const NewEdgeTimeLocal: tDateTime);
        (* new times, new HoursOn *)

        var
          NewEdgeTimeUTC: tDateTime;
          NewHoursOn: real;

        begin (* AdvanceEdge *)
          if OnAtEdge then
            NewHoursOn := HoursOnAtEdge +
              HoursPerSecond * SecondsBetween(NewEdgeTimeLocal, WhenAtEdgeLocal)
          else
            NewHoursOn := HoursOnAtEdge;
          NewEdgeTimeUTC := LocalToUTC(NewEdgeTimeLocal);

          // with fmDashboard do
          //   ExtendChat(Format('on_219  %s AdvanceEdge(%s): NHO: %.2f, NETUTC: %s',
          //                      [ObservedInfo[TheObservedUID].Name, FDT(NewEdgeTimeLocal),
          //                       NewHoursOn, FDT(NewEdgeTimeUTC)]));

          with dmHC3.qAdvanceOnOffEdge do
            begin (* dmHC3.qAdvanceOnOffEdge *)
              ParamByName('FinalHoursOn').AsFloat    := NewHoursOn;
              ParamByName('NewTimeLocal').AsDateTime := NewEdgeTimeLocal;
              ParamByName('NewTimeUTC'  ).AsDateTime := NewEdgeTimeUTC;
              ParamByName('UID'         ).AsInteger  := UIDAtEdge;
              ExecSQL
            end   (* dmHC3.qAdvanceOnOffEdge *);

          WhenAtEdgeLocal := NewEdgeTimeLocal;
          HoursOnAtEdge := NewHoursOn
        end   (* AdvanceEdge *);

      procedure AdvanceOneDay;
        (* move edge to midnight, then start new pair just past midnight.
          EndOfTheDay caused crash in DB, perhaps because of milliseconds *)

        const
          OnAtStartOfDay = false;
          HoursOnAtStartOfDay = 0;

        begin (* AdvanceOneDay *)
          with fmDashboard do
            ExtendChat(Format('on_246  entering table_OnOff.AdvanceOneDay(%s)',
                              [GetObservedName(TheObservedUID)]));

          AdvanceEdge(LastSecondOfTheDay(WhenAtEdgeLocal));
          GenerateNewPair(StartOfTheDay(IncDay(WhenAtEdgeLocal)),
                          OnAtStartOfDay, HoursOnAtStartOfDay);
          with fmDashboard do
            ExtendChat('on_252  leaving table_OnOff.AdvanceOneDay');
        end   (* AdvanceOneDay *);

      begin (* tOnOffDevice.NewData *)
        with Global.LatestData do
          begin (* Global.LatestData *)
            while DateOf(WhenAtEdgeLocal) < DateOf(LocalStamp) do
              AdvanceOneDay;
            if IsNowOn = OnAtEdge then
              AdvanceEdge(LocalStamp)
            else
              begin (* move old edge, start new pair *)
                AdvanceEdge(IncSecond(LocalStamp, -1));
                GenerateNewPair(LocalStamp, IsNowOn, HoursOnAtEdge)
              end   (* move old edge, start new pair *);

            HoursPassed := HourSpan(WhenAtEdgeLocal, StartOfTheDay(WhenAtEdgeLocal));
            
            if TheObservedUID = obTVRelay then
              with fmOutdoorInstruments do
                NewDataWaterfall(            AnchorLocal, OnAtEdge, HoursPassed, HoursOnAtEdge)
            else
              with fmIndoorSensors do
                NewDataOnOff(TheObservedUID, AnchorLocal, OnAtEdge, HoursPassed, HoursOnAtEdge);

            if Global.Status <> gsReceivingDump then
              fmGraphManager.UpdateGraphs(TheSensorUID)
          end   (* Global.LatestData *)
      end   (* tOnOffDevice.NewData *);

  initialization
    NDevices := 0;

end.
