unit View.Main;

interface

uses
  System.SysUtils, System.Classes, JS, Web, WEBLib.Graphics, WEBLib.Controls,
  WEBLib.Forms, WEBLib.Dialogs, WEBLib.ExtCtrls, Vcl.Controls, Vcl.StdCtrls,
  WEBLib.StdCtrls, Data.DB, XData.Web.JsonDataset, XData.Web.Dataset,
  App.Types, ConnectionModule, XData.Web.Client, View.Map, View.Units, View.Complaints,
  Module.Websocket;

type
  TFViewMain = class(TWebForm)
    lblUsername: TWebLabel;
    pnlMain: TWebPanel;
    WebMessageDlg1: TWebMessageDlg;
    lblAppTitle: TWebLabel;
    WebMemo1: TWebMemo;
    xdwcBadgeCounts: TXDataWebClient;
    lblCallsList: TWebLinkLabel;
    lblUsers: TWebLinkLabel;
    btnMap: TWebButton;
    btnComplaints: TWebButton;
    btnUnits: TWebButton;
    tmrBadgeCounts: TWebTimer;
    lblMainTitle: TWebLabel;
    pnlMap: TWebPanel;
    pnlUnits: TWebPanel;
    pnlComplaints: TWebPanel;
    tmrGlobalRefresh: TWebTimer;
    pnlDetails: TWebPanel;
    btnDetailsModalClose: TWebButton;
    pnlArchive: TWebPanel;
    btnArchiveModalClose: TWebButton;
    btnLogout: TWebButton;
    btnDevices: TWebButton;
    procedure WebFormCreate(Sender: TObject);
    procedure mnuLogoutClick(Sender: TObject);
    procedure lblLogoutClick(Sender: TObject);
    procedure btnUnitsClick(Sender: TObject);
    procedure btnComplaintsClick(Sender: TObject);
    procedure btnMapClick(Sender: TObject);
    procedure tmrBadgeCountsTimer(Sender: TObject);
    procedure tmrGlobalRefreshTimer(Sender: TObject);
    procedure btnDetailsModalCloseClick(Sender: TObject);
    procedure btnArchiveModalCloseClick(Sender: TObject);
    procedure btnLogoutClick(Sender: TObject);
    procedure btnDevicesClick(Sender: TObject);
  private
    { Private declarations }
    FUserInfo: string;
    FChildForm: TWebForm;
    FMapForm: TFViewMap;
    FUnitsForm: TFViewUnits;
    FComplaintsForm: TFViewComplaints;
    FDetailsForm: TWebForm;
    FArchiveForm: TWebForm;
    FLogoutProc: TLogoutProc;
    [async] procedure RefreshBadgesAsync;
    procedure ShowUnitDetails(UnitId: string);
    procedure SetHeaderTitle(const title: string);
    procedure HideDetailsModal;
    procedure ShowDetailsModal(const titleText: string);
    procedure HideArchiveModal;
    procedure ShowArchiveModal(const titleText: string);

    // WebSocket push handlers — called by Module.Websocket when the server broadcasts
    procedure HandleWsBadgeCounts(aData: TJSObject);
    procedure HandleWsUnitMap(aData: TJSObject);
    procedure HandleWsComplaintMap(aData: TJSObject);
    procedure HandleWsUnitList(aData: TJSObject);
    procedure HandleWsComplaintList(aData: TJSObject);

    type TActivePanel = (apNone, apMap, apUnits, apComplaints);
    var
      FActivePanel: TActivePanel;
      FGlobalRefreshTick: Integer;
      FMapRefreshTick: Integer;
      FUnitsRefreshTick: Integer;
      FComplaintsRefreshTick: Integer;

    procedure SetActivePanel(panel: TActivePanel);
    procedure MarkPanelCreatedUpToDate(panel: TActivePanel);
    procedure RefreshPanelIfNeeded(panel: TActivePanel);
    procedure RefreshActivePanelFromTimer;

  public
    { Public declarations }
    class procedure Display(LogoutProc: TLogoutProc);
    procedure ShowForm( AFormClass: TWebFormClass );
    procedure ShowComplaintDetails(ComplaintId: string);
    procedure ShowComplaintArchive(ComplaintId: string);
    procedure SetActiveNavButton(const BtnId: string);
    procedure ShowMapFocusUnit(const unitId: string);
    procedure ShowMapFocusComplaint(const complaintId: string);
    procedure HidePanel(aPanel: TWebPanel);
    procedure ShowPanel(aPanel: TWebPanel);
  end;

var
  FViewMain: TFViewMain;

implementation

uses
  Auth.Service,
  View.Login,
  View.UserProfile,
  View.ComplaintDetails,
  View.Admin,
  View.Users,
  View.EditUser,
  View.UnitDetails,
  View.ComplaintArchive,
  View.DeviceManager,
  Utils;

{$R *.dfm}

const
  DETAILS_HOST_ID = 'pnl_details_host';
  ARCHIVE_HOST_ID = 'pnl_archive_host';

procedure TFViewMain.WebFormCreate(Sender: TObject);
var
  userName: string;
begin
  userName := JS.toString(AuthService.TokenPayload.Properties['user_name']);
  lblUsername.Caption := ' ' + userName.ToLower + ' ';
  FChildForm := nil;
  FDetailsForm := nil;
  FArchiveForm := nil;

  FActivePanel := apNone;
  FGlobalRefreshTick := 0;
  FMapRefreshTick := 0;
  FUnitsRefreshTick := 0;
  FComplaintsRefreshTick := 0;

  if JS.toBoolean(AuthService.TokenPayload.Properties['user_admin']) then
  begin
    // Show admin controls
    btnDevices.Visible := True;
    asm
      var el = document.getElementById('btn_devices');
      if (el) el.classList.remove('d-none');
    end;
  end
  else
    lblUsers.Visible := False;

  Utils.HideSpinner('spinner');

  HidePanel(pnlMain);
  ShowPanel(pnlMap);
  HidePanel(pnlUnits);
  HidePanel(pnlComplaints);
  HidePanel(pnlDetails);
  HidePanel(pnlArchive);

  if not Assigned(FMapForm) then
  begin
    Application.CreateForm(TFViewMap, pnlMap.ElementID, TWebForm(FMapForm));
    MarkPanelCreatedUpToDate(apMap);
  end;

  SetHeaderTitle('Map');
  SetActiveNavButton('view.main.btnmap');
  SetActivePanel(apMap);

  // Initial badge counts still loaded via HTTP so the UI is populated immediately.
  RefreshBadgesAsync;

  // Polling timers are replaced by WebSocket server-push broadcasts.
  tmrBadgeCounts.Enabled := False;
  tmrGlobalRefresh.Enabled := False;

  dmWebsocket := TdmWebsocket.Create(Self);
  dmWebsocket.OnBadgeCounts   := HandleWsBadgeCounts;
  dmWebsocket.OnUnitMap       := HandleWsUnitMap;
  dmWebsocket.OnComplaintMap  := HandleWsComplaintMap;
  dmWebsocket.OnUnitList      := HandleWsUnitList;
  dmWebsocket.OnComplaintList := HandleWsComplaintList;
  dmWebsocket.Connect(DMConnection.WsUrl);
end;

procedure TFViewMain.SetActivePanel(panel: TActivePanel);
begin
  FActivePanel := panel;
end;

procedure TFViewMain.MarkPanelCreatedUpToDate(panel: TActivePanel);
begin
  case panel of
    apMap: FMapRefreshTick := FGlobalRefreshTick;
    apUnits: FUnitsRefreshTick := FGlobalRefreshTick;
    apComplaints: FComplaintsRefreshTick := FGlobalRefreshTick;
  end;
end;

procedure TFViewMain.RefreshPanelIfNeeded(panel: TActivePanel);
var
  needsRefresh: Boolean;
begin
  if FGlobalRefreshTick <= 0 then
    Exit;

  needsRefresh := False;

  case panel of
    apMap: needsRefresh := (FMapRefreshTick <> FGlobalRefreshTick);
    apUnits: needsRefresh := (FUnitsRefreshTick <> FGlobalRefreshTick);
    apComplaints: needsRefresh := (FComplaintsRefreshTick <> FGlobalRefreshTick);
  end;

  if not needsRefresh then
    Exit;

  Console.Log('GlobalRefresh: refreshing panel=' + IntToStr(Ord(panel)) + ' at tick=' + IntToStr(FGlobalRefreshTick));

  case panel of
    apMap:
      if Assigned(FMapForm) then
        FMapForm.RefreshData;

    apUnits:
      if Assigned(FUnitsForm) then
        FUnitsForm.RefreshData;

    apComplaints:
      if Assigned(FComplaintsForm) then
        FComplaintsForm.RefreshData;
  end;

  case panel of
    apMap: FMapRefreshTick := FGlobalRefreshTick;
    apUnits: FUnitsRefreshTick := FGlobalRefreshTick;
    apComplaints: FComplaintsRefreshTick := FGlobalRefreshTick;
  end;
end;

procedure TFViewMain.RefreshActivePanelFromTimer;
begin
  RefreshPanelIfNeeded(FActivePanel);
end;


procedure TFViewMain.SetHeaderTitle(const title: string);
var el: TJSElement;
begin
  el := Document.getElementById('lbl_main_title');
  if el <> nil then
    el.innerHTML := title
  else
    console.log('SetHeaderTitle: lbl_main_title not found');
end;


procedure TFViewMain.mnuLogoutClick(Sender: TObject);
begin
  FLogoutProc;
end;


procedure TFViewMain.lblLogoutClick(Sender: TObject);
begin
  FLogoutProc;
end;


procedure TFViewMain.btnArchiveModalCloseClick(Sender: TObject);
begin
  HideArchiveModal;
end;

procedure TFViewMain.btnComplaintsClick(Sender: TObject);
begin
  ShowForm(TFViewComplaints);
end;


procedure TFViewMain.btnDetailsModalCloseClick(Sender: TObject);
begin
  HideArchiveModal;
  HideDetailsModal;
end;

procedure TFViewMain.btnLogoutClick(Sender: TObject);
begin
  FLogoutProc;
end;

procedure TFViewMain.btnDevicesClick(Sender: TObject);
begin
  ShowForm(TFViewDeviceManager);
end;

procedure TFViewMain.btnMapClick(Sender: TObject);
begin
  ShowForm(TFViewMap);
end;


procedure TFViewMain.btnUnitsClick(Sender: TObject);
begin
  ShowForm(TFViewUnits);
end;


class procedure TFViewMain.Display(LogoutProc: TLogoutProc);
begin
  if Assigned(FViewMain) then
    FViewMain.Free;
  FViewMain := TFViewMain.CreateNew;
  FViewMain.FLogoutProc := LogoutProc;
end;


procedure TFViewMain.ShowForm(AFormClass: TWebFormClass);
begin
  HideArchiveModal;
  HideDetailsModal;

  if AFormClass = TFViewMap then
  begin
    ShowPanel(pnlMap);
    HidePanel(pnlUnits);
    HidePanel(pnlComplaints);
    HidePanel(pnlMain);

    SetHeaderTitle('Map');
    SetActiveNavButton('view.main.btnmap');
    SetActivePanel(apMap);

    if not Assigned(FMapForm) then
    begin
      Application.CreateForm(TFViewMap, pnlMap.ElementID, TWebForm(FMapForm));
      MarkPanelCreatedUpToDate(apMap);
    end
    else
      RefreshPanelIfNeeded(apMap);

    Exit;
  end;

  if AFormClass = TFViewUnits then
  begin
    HidePanel(pnlMap);
    ShowPanel(pnlUnits);
    HidePanel(pnlComplaints);
    HidePanel(pnlMain);

    SetHeaderTitle('Units');
    SetActiveNavButton('view.main.btnunits');
    SetActivePanel(apUnits);

    if not Assigned(FUnitsForm) then
    begin
      Application.CreateForm(TFViewUnits, pnlUnits.ElementID, TWebForm(FUnitsForm));
      MarkPanelCreatedUpToDate(apUnits);
    end
    else
      RefreshPanelIfNeeded(apUnits);

    Exit;
  end;

  if AFormClass = TFViewComplaints then
  begin
    HidePanel(pnlMap);
    HidePanel(pnlUnits);
    ShowPanel(pnlComplaints);
    HidePanel(pnlMain);

    SetHeaderTitle('Complaints');
    SetActiveNavButton('view.main.btncomplaints');
    SetActivePanel(apComplaints);

    if not Assigned(FComplaintsForm) then
    begin
      Application.CreateForm(TFViewComplaints, pnlComplaints.ElementID, TWebForm(FComplaintsForm));
      MarkPanelCreatedUpToDate(apComplaints);
    end
    else
      RefreshPanelIfNeeded(apComplaints);

    Exit;
  end;

  // everything else (admin/users/etc)
  SetActivePanel(apNone);

  HidePanel(pnlMap);
  HidePanel(pnlUnits);
  HidePanel(pnlComplaints);
  ShowPanel(pnlMain);

  if Assigned(FChildForm) and (FChildForm <> FMapForm) and (FChildForm <> FUnitsForm) and (FChildForm <> FComplaintsForm) then
    FChildForm.Free;

  Application.CreateForm(AFormClass, pnlMain.ElementID, FChildForm);
end;

procedure TFViewMain.HidePanel(aPanel: TWebPanel);
var
  el: TJSHTMLElement;
begin
  if (aPanel = nil) or (aPanel.ElementHandle = nil) then
    Exit;

  el := TJSHTMLElement(aPanel.ElementHandle);
  el.classList.add('d-none');
end;

procedure TFViewMain.ShowPanel(aPanel: TWebPanel);
var
  el: TJSHTMLElement;
begin
  if (aPanel = nil) or (aPanel.ElementHandle = nil) then
    Exit;

  el := TJSHTMLElement(aPanel.ElementHandle);
  el.classList.remove('d-none');
end;

procedure TFViewMain.ShowComplaintDetails(ComplaintId: string);
begin
  ShowDetailsModal('Complaint Details');

  if Assigned(FDetailsForm) then
    FDetailsForm.Free;

  FDetailsForm := TFViewComplaintDetails.CreateForm(DETAILS_HOST_ID, ComplaintId);
end;

procedure TFViewMain.ShowComplaintArchive(ComplaintId: string);
begin
  ShowArchiveModal('Archived Complaint Details');

  if Assigned(FArchiveForm) then
    FArchiveForm.Free;

  FArchiveForm := TFViewComplaintArchive.CreateForm(ARCHIVE_HOST_ID, ComplaintId);
end;

procedure TFViewMain.ShowUnitDetails(UnitId: string);
begin
  ShowDetailsModal('Unit Details');

  if Assigned(FDetailsForm) then
    FDetailsForm.Free;

  FDetailsForm := TFViewUnitDetails.CreateForm(DETAILS_HOST_ID, UnitId);
end;


procedure TFViewMain.SetActiveNavButton(const btnId: string);
var
  m, c, u: TJSHTMLElement;
begin
  m := TJSHTMLElement(btnMap.ElementHandle);
  c := TJSHTMLElement(btnComplaints.ElementHandle);
  u := TJSHTMLElement(btnUnits.ElementHandle);

  m.classList.remove('active');
  c.classList.remove('active');
  u.classList.remove('active');

  if btnId = 'view.main.btnmap' then
    m.classList.add('active')
  else if btnId = 'view.main.btncomplaints' then
    c.classList.add('active')
  else if btnId = 'view.main.btnunits' then
    u.classList.add('active');
end;


// ---------------------------------------------------------------------------
// WebSocket push handlers
// ---------------------------------------------------------------------------

procedure TFViewMain.HandleWsBadgeCounts(aData: TJSObject);
var
  el: TJSElement;
begin
  el := Document.getElementById('view.main.badgecomplaints');
  if Assigned(el) then
    TJSHtmlElement(el).innerText := string(aData['BadgeComplaints']);

  el := Document.getElementById('view.main.badgeunits');
  if Assigned(el) then
    TJSHtmlElement(el).innerText := string(aData['BadgeUnits']);
end;

procedure TFViewMain.HandleWsUnitMap(aData: TJSObject);
begin
  if Assigned(FMapForm) then
    FMapForm.ApplyWsUnitMapData(TJSArray(aData['data']));
end;

procedure TFViewMain.HandleWsComplaintMap(aData: TJSObject);
begin
  if Assigned(FMapForm) then
    FMapForm.ApplyWsComplaintMapData(TJSArray(aData['data']));
end;

procedure TFViewMain.HandleWsUnitList(aData: TJSObject);
begin
  if Assigned(FUnitsForm) then
    FUnitsForm.ApplyWsData(aData);
end;

procedure TFViewMain.HandleWsComplaintList(aData: TJSObject);
begin
  if Assigned(FComplaintsForm) then
    FComplaintsForm.ApplyWsData(aData);
end;

// ---------------------------------------------------------------------------

procedure TFViewMain.tmrBadgeCountsTimer(Sender: TObject);
begin
  console.log('Badges Refreshed');
  RefreshBadgesAsync;
end;



procedure TFViewMain.tmrGlobalRefreshTimer(Sender: TObject);
begin
  Inc(FGlobalRefreshTick);
  Console.Log('GlobalRefresh tick=' + IntToStr(FGlobalRefreshTick) + ' activePanel=' + IntToStr(Ord(FActivePanel)));
  RefreshActivePanelFromTimer;
end;

[async] procedure TFViewMain.RefreshBadgesAsync;
var
  resp: TXDataClientResponse;
  badgeObj: TJSObject;
  el: TJSElement;
begin
  try
    resp := await(xdwcBadgeCounts.RawInvokeAsync('IApiService.GetBadgeCounts', []));
    badgeObj := TJSObject(resp.Result);

    el := Document.getElementById('view.main.badgecomplaints');
    if Assigned(el) then
      TJSHtmlElement(el).innerText := string(badgeObj['BadgeComplaints']);

    el := Document.getElementById('view.main.badgeunits');
    if Assigned(el) then
      TJSHtmlElement(el).innerText := string(badgeObj['BadgeUnits']);
  except
    on E: Exception do
    begin
      el := Document.getElementById('view.main.badgecomplaints');
      if Assigned(el) then TJSHtmlElement(el).innerText := '�';
      el := Document.getElementById('view.main.badgeunits');
      if Assigned(el) then TJSHtmlElement(el).innerText := '�';
      Console.Log('Badge refresh error: ' + E.Message);
    end;
  end;
end;


procedure TFViewMain.ShowMapFocusUnit(const unitId: string);
var
  pendingUnitId: string;
begin
  ShowForm(TFViewMap);

  pendingUnitId := unitId;

  window.setTimeout(
    procedure
    begin
      if Assigned(FMapForm) then
        FMapForm.FocusUnit(pendingUnitId);
    end, 50);
end;

procedure TFViewMain.ShowMapFocusComplaint(const complaintId: string);
var
  pendingComplaintId: string;
begin
  ShowForm(TFViewMap);

  pendingComplaintId := complaintId;

  window.setTimeout(
    procedure
    begin
      if Assigned(FMapForm) then
        FMapForm.FocusComplaint(pendingComplaintId);
    end, 50);
end;


//------------------------------------------------------------------------------

procedure TFViewMain.HideDetailsModal;
begin
  HidePanel(pnlDetails);

  if Assigned(FDetailsForm) then
  begin
    FDetailsForm.Free;
    FDetailsForm := nil;
  end;
end;


procedure TFViewMain.ShowDetailsModal(const titleText: string);
var
  el: TJSElement;
begin
  ShowPanel(pnlDetails);

  el := Document.getElementById('lbl_details_title');
  if el <> nil then
    el.innerHTML := titleText;
end;


procedure TFViewMain.HideArchiveModal;
begin
  HidePanel(pnlArchive);

  if Assigned(FArchiveForm) then
  begin
    FArchiveForm.Free;
    FArchiveForm := nil;
  end;
end;

procedure TFViewMain.ShowArchiveModal(const titleText: string);
var
  el: TJSElement;
begin
  ShowPanel(pnlArchive);

  el := Document.getElementById('lbl_archive_title');
  if el <> nil then
    el.innerHTML := titleText;
end;


end.
