unit Module.Websocket;

interface

uses
  System.SysUtils, System.Classes, WEBLib.WebSocketClient, Web, WEBLib.Controls, WEBLib.Modules,
  Auth.Service, JS;

type
  // Handler signature: receives the fully parsed JSON object for one push message.
  TWsDataHandler = procedure(aData: TJSObject) of object;

  TdmWebsocket = class(TWebDataModule)

  private
    procedure EMiMobileWebSocketClientConnect(Sender: TObject);
    procedure EMiMobileWebSocketClientDataReceived(Sender: TObject;
      Origin: string; SocketData: TJSObjectRecord);
    procedure EMiMobileWebSocketClientDisconnect(Sender: TObject);
    procedure EMiMobileWebSocketClientMessageReceived(Sender: TObject;
      AMessage: string);
    procedure EMiMobileWebSocketClientBinaryDataReceived(Sender: TObject;
      AData: TBytes);

    procedure DispatchMessage(const AMessage: string);

    FOnBadgeCounts:   TWsDataHandler;
    FOnUnitMap:       TWsDataHandler;
    FOnComplaintMap:  TWsDataHandler;
    FOnUnitList:      TWsDataHandler;
    FOnComplaintList: TWsDataHandler;

  public
    EMiMobileWebSocketClient: TWebSocketClient;
    procedure DataModuleCreate(Sender: TObject);
    procedure Connect(const AWsUrl: string);

    // Assign these before calling Connect so that pushes are routed immediately.
    property OnBadgeCounts:   TWsDataHandler read FOnBadgeCounts   write FOnBadgeCounts;
    property OnUnitMap:       TWsDataHandler read FOnUnitMap       write FOnUnitMap;
    property OnComplaintMap:  TWsDataHandler read FOnComplaintMap  write FOnComplaintMap;
    property OnUnitList:      TWsDataHandler read FOnUnitList      write FOnUnitList;
    property OnComplaintList: TWsDataHandler read FOnComplaintList write FOnComplaintList;

  end;

var
  dmWebsocket: TdmWebsocket;

implementation

{%CLASSGROUP 'Vcl.Controls.TControl'}

{$R *.dfm}

procedure TdmWebsocket.DataModuleCreate(Sender: TObject);
begin

end;

procedure TdmWebsocket.Connect(const AWsUrl: string);
var
  Rest, HostPort, Scheme, Path, Token: string;
  ColonSlashSlash, SlashPos, ColonPos: Integer;
begin
  if AWsUrl = '' then
    Exit;

  // Parse  ws://host:port/path  or  wss://host:port/path
  ColonSlashSlash := Pos('://', AWsUrl);
  if ColonSlashSlash = 0 then
    Exit;

  Scheme := LowerCase(Copy(AWsUrl, 1, ColonSlashSlash - 1));
  Rest   := Copy(AWsUrl, ColonSlashSlash + 3, MaxInt);

  SlashPos := Pos('/', Rest);
  if SlashPos > 0 then
  begin
    HostPort := Copy(Rest, 1, SlashPos - 1);
    Path := Copy(Rest, SlashPos, MaxInt);
  end
  else
  begin
    HostPort := Rest;
    Path := '/';
  end;

  // Append JWT token as query param — browsers can't set Authorization headers on WebSocket.
  Token := AuthService.GetToken;
  if Token <> '' then
  begin
    if Pos('?', Path) > 0 then
      Path := Path + '&token=' + Token
    else
      Path := Path + '?token=' + Token;
  end;

  EMiMobileWebSocketClient.PathName := Path;

  ColonPos := Pos(':', HostPort);
  if ColonPos > 0 then
  begin
    EMiMobileWebSocketClient.HostName := Copy(HostPort, 1, ColonPos - 1);
    if Scheme = 'wss' then
      EMiMobileWebSocketClient.Port := StrToIntDef(Copy(HostPort, ColonPos + 1, MaxInt), 443)
    else
      EMiMobileWebSocketClient.Port := StrToIntDef(Copy(HostPort, ColonPos + 1, MaxInt), 80);
  end
  else
  begin
    EMiMobileWebSocketClient.HostName := HostPort;
    if Scheme = 'wss' then
      EMiMobileWebSocketClient.Port := 443
    else
      EMiMobileWebSocketClient.Port := 80;
  end;

  EMiMobileWebSocketClient.UseSSL := (Scheme = 'wss');
  EMiMobileWebSocketClient.Active := True;
end;

procedure TdmWebsocket.DispatchMessage(const AMessage: string);
var
  obj: TJSObject;
  requestId: string;
begin
  if AMessage = '' then
    Exit;

  asm
    try {
      obj = JSON.parse(AMessage);
    } catch(e) {
      obj = null;
    }
  end;

  if obj = nil then
  begin
    console.log('WS: could not parse message');
    Exit;
  end;

  requestId := string(obj['RequestId']);

  if SameText(requestId, 'BADGE_COUNTS') then
  begin
    if Assigned(FOnBadgeCounts) then FOnBadgeCounts(obj);
  end
  else if SameText(requestId, 'UNIT_MAP') then
  begin
    if Assigned(FOnUnitMap) then FOnUnitMap(obj);
  end
  else if SameText(requestId, 'COMPLAINT_MAP') then
  begin
    if Assigned(FOnComplaintMap) then FOnComplaintMap(obj);
  end
  else if SameText(requestId, 'UNIT_LIST') then
  begin
    if Assigned(FOnUnitList) then FOnUnitList(obj);
  end
  else if SameText(requestId, 'COMPLAINT_LIST') then
  begin
    if Assigned(FOnComplaintList) then FOnComplaintList(obj);
  end
  else
    console.log('WS: unknown RequestId: ' + requestId);
end;

procedure TdmWebsocket.EMiMobileWebSocketClientBinaryDataReceived(
  Sender: TObject; AData: TBytes);
begin
  console.log('WS: binary data received (ignored)');
end;

procedure TdmWebsocket.EMiMobileWebSocketClientConnect(Sender: TObject);
begin
  console.log('WS: connected');
end;

procedure TdmWebsocket.EMiMobileWebSocketClientDataReceived(Sender: TObject;
  Origin: string; SocketData: TJSObjectRecord);
begin
  // Text messages arrive via EMiMobileWebSocketClientMessageReceived.
end;

procedure TdmWebsocket.EMiMobileWebSocketClientDisconnect(Sender: TObject);
begin
  console.log('WS: disconnected');
end;

procedure TdmWebsocket.EMiMobileWebSocketClientMessageReceived(Sender: TObject;
  AMessage: string);
begin
  DispatchMessage(AMessage);
end;

end.
