unit PickDateRange;
  (* DomainEnd is now a pure date.  Some things may break *)

  interface
    uses
      AdsTable         (* tAdsQuery *),
      Buttons          (* tBitBtn *),
      Classes,
      ComCtrls         (*  *),
      Controls         (* tControl, tDate, mrCancel, mrOK *),
      DBChart          (* tDBChart *),
      ExtCtrls         (* tLabeledEdit, tRadioGroup *),
      Forms            (* tForm *),
      LabeledSpinEdits (* tLabeledSpinInteger *),
      MyConsts         (* nullstring, UsualFormColort *),
      ObservedAndWM    (* tTableUsed *),
      RRFSpinBase      (* used by LabeledSpinEdits *),
      StdCtrls         (* tCheckBox, tLabel; also used by ComCtrls *),
      Windows;

    type
      tButtonClickMethod = procedure(Sender: tObject) of object;
      tWhichTables = (wtAll, wtNew, wtOld, wtOldest, wtOldNew, wtOldestOld);
      tDateRangeType = (drAll, drChoose, drLastNDays);

    type
      tDateRange = class(tObject)
          DomainBegin: tDate;
          DomainEnd:   tDate;
          ChartAffected: tDBChart;
          constructor Create(const TheLegend: string;
                             TheChart: tDBChart = nil;
                             TheButtonClick: tButtonClickMethod = nil);
        private
          TheMethod: tButtonClickMethod;
          Legend: string;
        public
          function  DateRangeString: string;
          function  NowActive: boolean;
          procedure PickRange;
          procedure SchemaToSQL(const TU: tTableUsed;
                                const OldSchema: string;
                                TheQuery: tAdsQuery;
                                OrderBy: string = nullstring);
          procedure SetQueryDates(TheQuery: tAdsQuery);
          procedure SetRange(const RangeBegin, RangeEnd: tDate);
          procedure SetToAllDates;
          procedure SetToLastNDays(const N: integer);
          procedure SetToToday;
          function  WhichTables(const TU: tTableUsed): tWhichTables;
        end (* tDateRange *);

    type
      tfmPickDateRange = class(tForm)
          btnCancel: tBitBtn;
          btnOK:     tBitBtn;
          btnLastNDays: tButton;
          ckbxAllPossibleDates: tCheckBox;
          ckbxDebug:            tCheckBox;
          dtpBegin: tDateTimePicker;
          dtpEnd:   tDateTimePicker;
          lbBegin: tLabel;
          lbEnd:   tLabel;
          lbedGraphLegend: tLabeledEdit;
          spinDays: tLabeledSpinInteger;
          procedure btnCancelClick           (Sender: tObject);
          procedure btnLastNDaysClick        (Sender: tObject);
          procedure btnOKClick               (Sender: tObject);
          procedure ckbxAllPossibleDatesClick(Sender: tObject);
          procedure lbedGraphLegendChange    (Sender: tObject);
          procedure spinDaysChange           (Sender: tObject);
          procedure Validate                 (Sender: tObject);
        private
          AffectedRange: tDateRange;
          procedure UnifyEnabling;
        public
          procedure PickDateRange(TheAffectedRange: tDateRange);
        end (* tfmPickDateRange *);

    var
      fmPickDateRange: tfmPickDateRange;

  implementation
    uses
      DateUtils     (* DateOf, DayOf, IncDay, MonthOf, YearOf *),
      GraphManager2 (* fmGraphManager *),
      HC3DM         (* Global *),
      SysUtils      (* Date, Format, FormatDateTime *),
      TableManager  (* TableInfo *);
      {$R *.dfm}

    procedure tfmPickDateRange.btnCancelClick(Sender: tObject);
      (* just quit *)

      begin (* tfmPickDateRange.btnCancelClick *)
        ModalResult := mrCancel
      end   (* tfmPickDateRange.btnCancelClick *);

    procedure tfmPickDateRange.btnLastNDaysClick(Sender: tObject);
      (* don't touch the end bracket *)

      begin (* tfmPickDateRange.btnLastNDaysClick *)
        dtpBegin.Date := IncDay(DateOf(Date), -spinDays.Value);
        Validate(nil)
      end   (* tfmPickDateRange.btnLastNDaysClick *);

    procedure tfmPickDateRange.btnOKClick(Sender: tObject);
      (* pass results back to caller *)

      begin (* tfmPickDateRange.btnOKClick *)
        with AffectedRange do
          begin (* AffectedRange *)
            DomainBegin := DateOf(dtpBegin.Date);
            DomainEnd   := DateOf(dtpEnd.Date);

            if Assigned(AffectedRange.ChartAffected) then
              ChartAffected.Title.Text.Text :=
                Format('%s, %s', [Legend, DateRangeString]);

            if Assigned(TheMethod) then
              TheMethod(nil)
          end   (* ASffectedRange *);

        ModalResult := mrOK
      end   (* tfmPickDateRange.btnOKClick *);

    procedure tfmPickDateRange.ckbxAllPossibleDatesClick(Sender: tObject);
      (* set extreme values *)

      begin (* tfmPickDateRange.ckbxAllPossibleDatesClick *)
        if ckbxAllPossibleDates.Checked then
          begin (* expand range *)
            dtpBegin.Date := Global.EarliestPossibleDate;
            dtpEnd.Date   := DateOf(Date);
            btnOK.Enabled := true
          end   (* expand range *);
        lbBegin.Enabled := not ckbxAllPossibleDates.Checked;
        UnifyEnabling
      end   (* tfmPickDateRange.ckbxAllPossibleDatesClick *);

    procedure tfmPickDateRange.lbedGraphLegendChange(Sender: tObject);
      (* allow graph to show new location of sensors *)
    
      begin (* tfmPickDateRange.lbedGraphLegendChange *)
        AffectedRange.Legend := lbedGraphLegend.Text;
        Caption := 'set date range for ' + lbedGraphLegend.Text
      end   (* tfmPickDateRange.lbedGraphLegendChange *);

    procedure tfmPickDateRange.PickDateRange(TheAffectedRange: tDateRange);
      (* Store a link to range owned elsewhere *)

      begin (* tfmPickDateRange.PickDateRange *)
        self.Color := UsualFormColor;
        AffectedRange := TheAffectedRange;

        with AffectedRange do
          begin (* initialize from target range *)
            Caption := 'set date range for ' + Legend;
            dtpBegin.Date := DomainBegin;
            dtpEnd.Date := DomainEnd;
            lbedGraphLegend.Text := Legend
          end   (* initialize from target range *);

        spinDaysChange(nil);
        Validate(nil);
        ShowModal
      end   (* tfmPickDateRange.PickDateRange *);

    procedure tfmPickDateRange.spinDaysChange(Sender: tObject);
      (* button does the work *)

      begin (* tfmPickDateRange.spinDaysChange *)
        btnLastNDays.Caption := Format('start %d days ago', [spinDays.Value])
      end   (* tfmPickDateRange.spinDaysChange *);

    procedure tfmPickDateRange.UnifyEnabling;
      (* Enable/disable group *)

      begin (* tfmPickDateRange.UnifyEnabling *)
        dtpBegin.Enabled     := lbBegin.Enabled;
        lbEnd.Enabled        := lbBegin.Enabled;
        dtpEnd.Enabled       := lbBegin.Enabled;
        spinDays.Enabled     := lbBegin.Enabled;
        btnLastNDays.Enabled := lbBegin.Enabled
      end   (* tfmPickDateRange.UnifyEnabling *);

    procedure tfmPickDateRange.Validate(Sender: tObject);
      (* Don't allow bogus range *)

      begin (* tfmPickDateRange.Validate *)
        ckbxAllPossibleDates.Checked :=
               (DateOf(dtpBegin.Date) = Global.EarliestPossibleDate) and (dtpEnd.Date > Date);

        lbBegin.Enabled := not ckbxAllPossibleDates.Checked;
        UnifyEnabling;
        btnOK.Enabled := (dtpBegin.Date <= dtpEnd.Date)
      end   (* tfmPickDateRange.Validate *);

{ tDateRange }

    constructor tDateRange.Create(const TheLegend: string;
                                  TheChart: tDBChart;
                                  TheButtonClick: tButtonClickMethod);
      (* Initialize, preparing to
            set Items[1] of the radiogroup to the date range string
            set the caption of the control to dates: <date range string>
            run the method *)

      begin (* tDateRange.Create *)
        inherited Create();

        ChartAffected := TheChart;
        Legend := TheLegend;
        TheMethod := TheButtonClick;
        SetToToday
      end   (* tDateRange.Create *);

    function tDateRange.DateRangeString: string;
      (* describe the date range *)
    
      var
        FullBegin, FullEnd: string;

      begin (* tDateRange.DateRangeString *)
        FullBegin := FormatDateTime('yyyy-mm-dd', DomainBegin);
        FullEnd   := FormatDateTime('yyyy-mm-dd', DomainEnd);

        if DomainBegin <= Global.EarliestPossibleDate then
          if DomainEnd > Date then
            Result := 'all data'
          else
            Result := 'through ' + FullEnd

        else if DomainBegin = DomainEnd then
          Result := FullBegin

        else if YearOf(DomainBegin) <> YearOf(Date) then
          Result := Format('%s through %s', [FullBegin, FullEnd])

        else if MonthOf(DomainBegin) = MonthOf(DomainEnd) then
          Result := Format('%d-%d %s',
                           [DayOf(DomainBegin),
                            DayOf(DomainEnd),
                            FormatDateTime('mmmm', DomainBegin)])
        else
          Result := Format('%s through %s',
                           [FormatDateTime('d mmmm', DomainBegin),
                            FormatDateTime('d mmmm', DomainEnd)])
      end   (* tDateRange.DateRangeString *);

    function tDateRange.NowActive: boolean;
      (* tell the user if the range includes the current moment.  Code is
      ugly because SysUtils.Date is not guaranteed to be a pure Date, with
      no time *)

      begin (* tDateRange.NowActive *)
        Result := (Date >= DomainBegin) and (DateOf(Date) <= DomainEnd)
      end   (* tDateRange.NowActive *);

    procedure tDateRange.PickRange;
      (* Update the object by running the form *)

      begin (* tDateRange.PickRange *)
        fmPickDateRange.PickDateRange(self)
      end   (* tDateRange.PickRange *);

    procedure tDateRange.SchemaToSQL(const TU: tTableUsed;
                                     const OldSchema: string;
                                     TheQuery: tAdsQuery;
                                     OrderBy: string);
      (* fill in the SQL, but don't open *)
    
      var
        NewSQL, OldestSQL, TheSQL: string;

      begin (* tDateRange.SchemaToSQL *)
        NewSQL    := StringReplace(OldSchema, '_old', nullstring, [rfIgnoreCase]);
        OldestSQL := StringReplace(OldSchema, '_old', '_oldest' , [rfIgnoreCase]);

        TheSQL := NewSQL;
        
        if TableInfo[TU].Table_oldExists then
          case WhichTables(TU) of
                  wtAll: TheSQL := Format('%s union %s union %s',
                                          [OldestSQL, OldSchema, NewSQL]);
                  wtOld: TheSQL := OldSchema;
               wtOldest: TheSQL := OldestSQL;
               wtOldNew: TheSQL := Format('%s union %s', [OldSchema, NewSQL]);
            wtOldestOld: TheSQL := Format('%s union %s', [OldestSQL, OldSchema])
            end (* case *);

        with TheQuery do
          begin (* TheQuery *)
            Close;
            SQL.Text := TheSQL + OrderBy;

            if fmPickDateRange.ckbxDebug.Checked then
              begin (* debugging *)
                fmGraphManager.DevelopmentNote('dates: ' + DateRangeString);
                fmGraphManager.DevelopmentNote(SQL.Text)
              end   (* debugging *)
          end   (* TheQuery *)
      end   (* tDateRange.SchemaToSQL *);

    procedure tDateRange.SetQueryDates(TheQuery: tAdsQuery);
      (* Let the query use the specified date range *)

      begin (* tDateRange.SetQueryDates *)
        TheQuery.ParamByName('DomainBegin').AsDate := DomainBegin;
        TheQuery.ParamByName('DomainEnd'  ).AsDate := DomainEnd
      end   (* tDateRange.SetQueryDates *);

    procedure tDateRange.SetRange(const RangeBegin, RangeEnd: tDate);
      (* service routine for GenericGraph *)
    
      begin (* tDateRange.SetRange *)
        DomainBegin := RangeBegin;
        DomainEnd   := RangeEnd;
        fmPickDateRange.dtpBegin.Date := DomainBegin;
        fmPickDateRange.dtpEnd.Date   := DomainEnd
      end   (* tDateRange.SetRange *);

    procedure tDateRange.SetToAllDates;
      (* different reset *)

      begin (* tDateRange.SetToAllDates *)
        DomainBegin := Global.EarliestPossibleDate;
        DomainEnd   := DateOf(Date);
        fmPickDateRange.dtpBegin.Date := DomainBegin;
        fmPickDateRange.dtpEnd.Date := DomainEnd;
        if Assigned(ChartAffected) then
          ChartAffected.Title.Text.Text := Legend + ', all data'
      end   (* tDateRange.SetToAllDates *);

    procedure tDateRange.SetToLastNDays(const N: integer);
      (* for now, using only N = 7 *)
    
      begin (* tDateRange.SetToLastNDays *)
        DomainBegin := IncDay(DateOf(Date), -N+1);
        DomainEnd   := DateOf(Date);
        fmPickDateRange.dtpBegin.Date := DomainBegin;
        fmPickDateRange.dtpEnd.Date   := DomainEnd
      end   (* tDateRange.SetToLastNDays *);

    procedure tDateRange.SetToToday;
      (* Initialization *)

      begin (* tDateRange.SetToToday *)
        DomainBegin := DateOf(Date);
        DomainEnd   := DomainBegin;

        if Assigned(ChartAffected) then
          ChartAffected.Title.Text.Text := Legend + ' today'
      end   (* tDateRange.SetToToday *);

    function tDateRange.WhichTables(const TU: tTableUsed): tWhichTables;
      (* see if the date range is all new, all old, or a bridge *)

      var
        EarliestNew: tDate;

      begin (* tDateRange.WhichTables *)
        EarliestNew := TableInfo[TU].TheEarliestNew;

        with Global do
          if DomainBegin >= EarliestNew then                Result := wtNew
          else if YearOf(DomainEnd) <= OldestThrough then   Result := wtOldest
          else if YearOf(DomainBegin) <= OldestThrough then
            if DomainEnd < EarliestNew then                 Result := wtOldestOld
            else                                            Result := wtAll
          else if DomainEnd < EarliestNew then              Result := wtOld
          else                                              Result := wtOldNew
      end   (* tDateRange.WhichTables *);

end.

