unit table_Generic;

  interface
    uses
      ObservedAndWM (* GetObservedName, MeasurementNames, obGCFridge, tObserved,
                         obGuestRoom, obHobbyRoom, obImplicit, obKitchen,
                         obLeftPower, obLibrary, obLR, obMBR, obOffice,
                         obOutside, obPond.obRightPower, obShop, obTVRoom,
                         tTableUsed, tuCO2, tuSPS30, tuTRHP, tuWind,
                         tWhatMeasured2, wm2CO2ppm, wm2PartSize, wm2Resistance,
                         wm2RH, wm2TempF *);

    function ProcessCurrentLine_Generic(const TU: tTableUsed): boolean;

  implementation
    uses
      AdsTable             (* tAdsQuery *),
      BoardManager         (* stAnemometer, stWeathervane *),
      Dashboard            (* fmDashboard, llblParenData *),
      DateUtils            (* HourSpan *),
      Dialogs              (* ShowMessage *),
      GraphManager2        (* fmGraphManager *),
      GraphWind            (* fmGraphWind *),
      HC3DM                (* dmHC3, Global, SillyRealValue *),
      HC3Utilities         (* Degrees, INILoadFloat, INILoadInteger, Radians *),
      IndoorInstruments    (* fmIndoorSensors *),
      Math                 (* arctan2, Sign *),
      MyConsts             (* nullstring *),
      OutdoorInstruments   (* fmOutdoorInstruments *),
      SensorDevice         (* tSensorDevice *),
      SysUtils             (* Format *),
      table_generic_helper (* DewPointF, DewPointReady, HumidexF, Qualified,
                                TRHPSetup, TRHPWorthSaving *),
      TableManager         (* TableInfo *);

    const
      MaxDevices = 25;

    type
      tGenericDevice = class(tSensorDevice)
        private
          OldSPS30ParticleSize: real;               // for tuSPS30
          OldPressureInches, OldRH, OldTempF: real; // for tuTRHP
          OldCO2ppm: real;                          // for tuCO2
        public
          constructor Create(const TU: tTableUsed;
                             const ObservedUID: tObserved;
                             const Measurements: tMeasurements2);
          procedure   NewData;
        end (* tGenericDevice *);

    var
      CO2Threshhold, DeviceIndex, NDevices: integer;
      GenericDevice: array[1 .. MaxDevices] of tGenericDevice;
      MinCO2HourSpan, WindDirectionDamping, WindVectorX, WindVectorY: real;

    procedure Cleanup;
      (* reclaim storage *)

      var
        I: integer;

      begin (* Cleanup *)
        for I := 1 to NDevices do
          GenericDevice[I].Free
      end   (* Cleanup *);

    function GotDeviceIndex(const TU: tTableUsed;
                            const ObservedUID: tObserved;
                            const Measurements: tMeasurements2): boolean;
      (* create if no match *)

      var
        I: integer;

      begin (* GotDeviceIndex *)
        Result := false;
        for I := 1 to NDevices do
          with GenericDevice[I] do
            if TheObservedUID = ObservedUID then
              if TheMeasurements = Measurements then
                if TheTableUsed = TU then
                  begin (* found it *)
                    DeviceIndex := I;
                    Result := true;
                    break
                  end   (* found it *);

        if not Result then
          if NDevices < MaxDevices then
            begin (* create new device *)
              inc(NDevices);
                GenericDevice[NDevices] :=
                  tGenericDevice.Create(TU, ObservedUID, Measurements);
              DeviceIndex := NDevices;
              Result := true
            end   (* create new device *)
          else
            ShowMessage('too many devices')
      end   (* GotDeviceIndex *);

{ tGenericDevice }

    constructor tGenericDevice.Create(const TU: tTableUsed;
                                      const ObservedUID: tObserved;
                                      const Measurements: tMeasurements2);
      (* constructor *)

      var
        Description, ObsName: string;

      procedure GenericDescription;
        (* used here and there *)

        begin (* GenericDescription *)
          if TheSensorType in [stSCD30, stSCD40] then
            Description := Format('%s (%s)', [ObsName, TheTableName])
          else
            Description := Format('%s (%s)',
                                  [ObsName,
                                   MeasurementNames(Measurements)])
        end   (* GenericDescription *);

      procedure GetOldCO2;
        (* debulk tGenericDevice.Create *)

        begin (* GetOldCO2 *)
          with qGetRecentData do
            begin (* qGetRecentData *)
              Close;
              Open;
              OldCO2ppm := FieldByName('CO2ppm').AsFloat;
              Close
            end   (* qGetRecentData *);
          GenericDescription
        end   (* GetOldCO2 *);

      procedure GetOldTRHP;
        (* debulk tGenericDevice.Create *)

        procedure UseMeasurement(const WM: tWhatMeasured2;
                                 const FieldName: string;
                                 var OldThing: real);
          (* cliche *)

          begin (* UseMeasurement *)
            if WM in Measurements then
              with qGetRecentData.FieldByName(FieldName) do
                if not IsNull then
                  OldThing := AsFloat
          end   (* UseMeasurement *);

        begin (* GetOldTRHP *)
          with qGetRecentData do
            begin (* qGetRecentData *)
              Close;
              Open;
              UseMeasurement(wm2PressureInches, 'PressureInches', OldPressureInches);
              UseMeasurement(wm2RH,             'RH'            , OldRH            );
              UseMeasurement(wm2TempF,          'TempF'         , OldTempF         );
              Close
            end   (* qGetRecentData *);
          GenericDescription
        end   (* GetOldTRHP *);

      procedure GetOldWind;
        (* debulk tGenericDevice.Create *)

        var
          radOldWindDirection: real;

        begin (* GetOldWind *)
          if wm2Resistance in Measurements then
            begin (* direction *)
              Description := 'wind direction';
              with qGetRecentData do
                begin (* qRecentWind *)
                  Close;
                  Open;
                  radOldWindDirection := Radians(FieldByName('Direction').AsFloat);
                  Close;
                end   (* qRecentData *);
              WindVectorX := cos(radOldWindDirection);
              WindVectorY := sin(radOldWindDirection)
            end   (* direction *)

          else
            Description := 'wind speed';
        end   (* GetOldWind *);

      begin (* tGenericDevice.Create *)
        inherited Create(TU, ObservedUID, Global.LatestData.UTCStamp, Global.LatestData.SensorType);

        ObsName         := GetObservedName(ObservedUID);
        OldCO2ppm       := SillyRealValue;
        OldRH           := SillyRealValue;
        OldTempF        := SillyRealValue;
        TheMeasurements := Measurements;
        TheObservedUID  := ObservedUID;

        with Global.LatestData do
          case TU of
              tuCO2: GetOldCO2;
            tuSPS30: begin (* SPS30 *)
                       OldSPS30ParticleSize := 0;
                       Description := Format('%s (SPS30)', [ObsName])
                     end   (* SPS30 *);
             tuTRHP: GetOldTRHP;
             tuWind: GetOldWind
                else GenericDescription
            end (* case *);

        with fmDashboard do
          ExtendChat(Format('tg_184 created generic device #%d for %s',
                            [NDevices, Description]),
                     llblCreated)                                              
      end   (* tGenericDevice.Create *);

    procedure tGenericDevice.NewData;
      (* store it away, and notify UI *)

      var
        WorthSaving: boolean;

      function CO2WorthSaving: boolean;
        (* don't store boring values *)

        var
          GapLongEnough: boolean;
          I: integer;
          CO2ppm, HoursSinceLastEntry: real;

        begin (* CO2WorthSaving *)
          CO2ppm := SillyRealValue;
          with Global.LatestData do
            for I := 1 to NArgs do
              if WMArg[I] = wm2CO2ppm then
                CO2ppm := RealArg[I];

          HoursSinceLastEntry := HourSpan(Global.LatestData.LocalStamp, TheLocalEdge);
          GapLongEnough       := (HoursSinceLastEntry >= MinCO2HourSpan);

          Result := (CO2ppm <> SillyRealValue) and
                    (GapLongEnough or (abs(CO2ppm - OldCO2ppm) > CO2Threshhold))
        end   (* CO2WorthSaving *);

      procedure ReportToUI;
        (* debulk *)

        const
          obsetInside = [obGCFridge, obGuestRoom, obHobbyRoom, obKitchen,
                           obLeftPower, obLibrary, obLR, obMBR, obOffice,
                           obRightPower, obShop, obTVRoom];


        begin (* ReportToUI *)
          if TheObservedUID in [obImplicit, obOutside, obPond] then
            fmOutdoorInstruments.NewDataGeneric(TheTableUsed)

          else if TheObservedUID in obsetInside then
            fmIndoorSensors.NewDataGeneric(TheTableUsed);

          if (TheTableUsed = tuTRHP) and DewPointReady and (TheObservedUID = obOutside) then
            fmOutdoorInstruments.NewDataDewPointF(DewPointF, HumidexF);

          if Global.Status <> gsReceivingDump then
            begin (* update graph(s) *)
              if TheSensorType in [stAnemometer, stSPS30, stWeathervane] then
                fmGraphWind.UseNewData;
              if TheSensorType <> stWeathervane then
                fmGraphManager.UpdateGraphs(TheSensorUID)
            end   (* update graph(s) *)
        end   (* ReportToUI *);

      function SPS30WorthSaving: boolean;
        (* ignore silly size data *)

        var
          Tentative: real;

        begin (* SPS30WorthSaving *)
          Result := true;
          with Global.LatestData do
            begin (* Global.LatestData *)
              Tentative := RealValueOf(wm2PartSize);
              if Tentative < 0 then
                SetValueOf(wm2PartSize, OldSPS30ParticleSize)
              else
                OldSPS30ParticleSize := Tentative
            end   (* Global.LatestData *)
        end   (* SPS30WorthSaving *);

      procedure TRHPAction;
        (* really update the table *)

        begin (* TRHPAction *)
          if DewPointReady then
            with dmHC3.qUpdateTRHPWithDewpoint do
              begin (* qUpdateTRHPWithDewpoint *)
                ParamByName('DewPointF').AsFloat := DewPointF;
                ParamByName('HumidexF' ).AsFloat := HumidexF;
                ParamByName('UID'      ).AsInteger := Global.LatestData.RecentUID;
                ExecSQL
              end   (* qUpdateTRHPWithDewpoint *)
        end   (* TRHPAction *);

      procedure WindAction;
        (* really update the table *)

        var
          NewDirection: real;

        begin (* WindAction *)
          if abs(WindVectorY) < 0.01 then
             WindVectorY := 0.01;
          NewDirection := Degrees(pi/2 - arctan2(WindVectorX, WindVectorY));

          with dmHC3.qUpdateWindWithDirection do
            begin (* qUpdateWindWithDirection *)
              ParamByName('Direction').AsFloat   := NewDirection;
              ParamByName('UID'      ).AsInteger := Global.LatestData.RecentUID;
              ExecSQL
            end   (* qUpdateWindWithDirection *)
        end   (* WindAction *);

      function WindWorthSaving: boolean;
        (* if this is direction, just update direction.
           if this is velocity, store and anticipate update of direction *)

        var
          radThisDirection, ThisX, ThisY: real;

        begin (* WindWorthSaving *)
          Result := true;  // assume is velocity
          with Global.LatestData do
            if WMArg[1] = wm2Resistance then
              begin (* just a direction *)
                Result := false;
                radThisDirection := Radians(RealArg[1]);
                ThisX := cos(radThisDirection);
                ThisY := sin(radThisDirection);
                if WindVectorX < -99 then
                  begin (* first one *)
                    WindVectorX := ThisX;
                    WindVectorY := ThisY
                  end   (* first one *)
                else
                  begin (* combine with old data *)
                    WindVectorX :=     WindDirectionDamping     * WindVectorX +
                                   (1.0 - WindDirectionDamping) * ThisX;
                    WindVectorY :=     WindDirectionDamping     * WindVectorY +
                                   (1.0 - WindDirectionDamping) * ThisY;
                  end   (* combine with old data *)
              end   (* just a direction *)
        end   (* WindWorthSaving *);

      begin (* tGenericDevice.NewData *)
        case TheTableUsed of
            tuCO2: WorthSaving := CO2WorthSaving;
          tuSPS30: WorthSaving := SPS30WorthSaving;
           tuTRHP: WorthSaving := TRHPWorthSaving(OldPressureInches, OldRH, OldTempF,
                                     TheLocalEdge, TheMeasurements);
           tuWind: WorthSaving := WindWorthSaving
              else WorthSaving := true;
          end (* case *);

        if WorthSaving then
          begin (* save, possibly including DewPoint & Humidex *)
            RunStoreDataQuery;
            case TheTableUsed of
              tuTRHP: TRHPAction;
              tuWind: WindAction;
              end (* case *);

            TheLocalEdge := Global.LatestData.LocalStamp
          end   (* save, possibly including DewPoint *)
        else
          fmDashboard.LogBoring(TableInfo[TheTableUsed].TheBoringChar);

        ReportToUI
      end   (* tGenericDevice.NewData *);

    function ProcessCurrentLine_Generic(const TU: tTableUsed): boolean;
      (* device does all the work *)

      begin (* ProcessCurrentLine_Generic *)
        // if Global.LatestData.BoardTag = 'O' then
        //   ShowMessage('table_Generic.ProcessCurrentLine.Generic');

        with Global.LatestData do
          Result := GotDeviceIndex(TU, ObservedUID, Measurements);

        if Result then
          GenericDevice[DeviceIndex].NewData
      end   (* ProcessCurrentLine_Generic *);

 initialization
    CO2Threshhold        := INILoadInteger('CO2', 'CO2Threshhold');
    MinCO2HourSpan       := INILoadFloat  (       'MinCO2HourSpan');
    NDevices             := 0;
    WindDirectionDamping := INILoadFloat('Wind', 'DirectionDamping');
    WindVectorX          := SillyRealValue;
    WindVectorY          := SillyRealValue;
    TRHPSetup;

  finalization
    Cleanup;
end.
