unit sipclient;

interface

uses SysUtils, Classes, Messages, mmSystem, Windows, SyncObjs,
  wavfiles{$IFDEF VER130}, Forms{$ENDIF}, conference, recorder, speaker,
  microphone, account, call;

type
  TOnLog = procedure(Sender: TObject; const Text: String) of object;
  TOnCall = procedure(Sender: TObject; const ACall: ICall) of object;
  TOnAnswer = procedure(Sender: TObject; const ACall: ICall) of object;
  TOnDtmf = procedure(Sender: TObject; const ACall: ICall; const Dtmf: String) of object;
  TOnSilence = procedure(Sender: TObject; const ACall: ICall) of object;
  TOnPlayed = procedure(Sender: TObject; const ACall: ICall) of object;
  TOnBye = procedure(Sender: TObject; const ACall: ICall) of object;
  TOnRingback = procedure(Sender: TObject; const ACall: ICall) of object;
  TOnRegistration = procedure(Sender: TObject; const Registered: Boolean) of object;

type
  TDtmfMethod = (dtmfInBand, dtmfRFC2833, dtmfSipInfo);

{$IFDEF CPUX64}
  SipPointer = NativeInt;
{$ELSE}
  SipPointer = Integer;
{$ENDIF}

  TSipClientEventHandler = class(TInterfacedObject, ISipEventHandler)
  private
    FWindowHandle: THandle;
  protected
    procedure OnInvite(const Call: ICall);
    procedure OnRingBack(const Call: ICall);
    procedure OnSilence(const Call: ICall);
    procedure OnDtmf(const Call: ICall; const Code: AnsiChar);
    procedure OnConnect(const Call: ICall);
    procedure OnDisconnect(const Call: ICall);
    procedure OnStopPlaying(const Call: ICall);
    procedure OnAccountRegistered;
    procedure OnAccountUnregistered;
    procedure OnLog(const Text: String);
    procedure OnAudioOpen(const Call: ICall);
    procedure OnAudioClose(const Call: ICall);
  public
    constructor Create(const AWindowHandle: THandle);
  end;

type

  IWaveInDevice = interface
  ['{F9DE99B6-F2CA-46A3-A958-5D338D3FAAC9}']
    function GetName: String;
    procedure SetName(const Value: String);
    property Name: String read GetName write SetName;
  end;

  IWaveInDeviceList = interface
  ['{4D329C69-B5E3-4EFC-BB8B-C06F8A99953F}']
    function GetDevice(Index: Integer): IWaveInDevice;
    function GetCount: Integer;
    property Devices[Index: Integer]: IWaveInDevice read GetDevice; default;
    property Count: Integer read GetCount;
  end;

  IWaveOutDevice = interface
  ['{F9DE99B6-F2CA-46A3-A958-5D338D3FAAC9}']
    function GetName: String;
    procedure SetName(const Value: String);
    property Name: String read GetName write SetName;
  end;

  IWaveOutDeviceList = interface
  ['{4D329C69-B5E3-4EFC-BB8B-C06F8A99953F}']
    function GetDevice(Index: Integer): IWaveOutDevice;
    function GetCount: Integer;
    property Devices[Index: Integer]: IWaveOutDevice read GetDevice; default;
    property Count: Integer read GetCount;
  end;

  TSipClient = class(TComponent)
  private
    FAccount: TAccount;
    FOnCall: TOnCall;
    FOnAnswer: TOnAnswer;
    FOnDtmf: TOnDtmf;
    FOnPlayed: TOnPlayed;
    FOnSilence: TOnSilence;
    FOnBye: TOnBye;
    FOnRegistration: TOnRegistration;
    FActive: Boolean;
    FWindowHandle: THandle;
    FDtmfMethod: TDtmfMethod;
    FCS: TCriticalSection;
    FServer: String;
    FUser: String;
    FExtension: String;
    FPassword: String;
    FProxy: String;
    FStun: String;
    procedure WndProc(var Message: TMessage);
    procedure SetActive(const Value: Boolean);
    function LoadWavFromResource(const N: Integer): IWavFile;
  protected
    procedure OnSipLogHandler(var Msg: TMessage);
    procedure OnSipInviteHandler(var Msg: TMessage);
    procedure OnSipInviteOkHandler(var Msg: TMessage);
    procedure OnDtmfHandler(var Msg: TMessage);
    procedure OnWavStoppedHandler(var Msg: TMessage);
    procedure OnSilenceHandler(var Msg: TMessage);
    procedure OnSipByeHandler(var Msg: TMessage);
    procedure OnRegOkHandler(var Msg: TMessage);
    procedure OnRegByeHandler(var Msg: TMessage);
    procedure OnRingbackHandler(var Msg: TMessage);
    function GetRegistrationTimeout: Integer;
    procedure SetRegistrationTimeout(const Value: Integer);
    function GetRegistered: Boolean;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function CreateConference: IConference;
    function CreateRecorder: IRecorder;
    function CreateStereoRecorder: IRecorder;
    function GetWaveInDeviceList: IWaveInDeviceList;
    function GetWaveOutDeviceList: IWaveOutDeviceList;
    procedure SetCodecs(const Codecs: array of Integer);
    function Call(PhoneNumber: String; const ACallerID: String = ''; InDevice: Cardinal = WAVE_MAPPER; OutDevice: Cardinal = WAVE_MAPPER): ICall;
    procedure Hold(const Call: ICall);
    procedure UnHold(const Call: ICall);
    procedure Answer(const Call: ICall);
    procedure Decline(const Call: ICall);    
    procedure EndCall(const Call: ICall);
    procedure SendDtmf(const Call: ICall; const Digit: String);
    procedure PlayWavFile(const Call: ICall; const FileName: String); overload;
    procedure PlayWavFile(const Call: ICall; const WavFile: IWavFile); overload;
    procedure PlayText(const Call: ICall; const Text: String; const VoiceIndex: Integer = 0);
    procedure StopPlaying(const Call: ICall);
    procedure Register;
    procedure UnRegister;
    property Active: Boolean read FActive write SetActive;
    property DtmfMethod: TDtmfMethod read FDtmfMethod write FDtmfMethod;
    property RegistrationTimeout: Integer read GetRegistrationTimeout write SetRegistrationTimeout;
    property Registered: Boolean read GetRegistered;
    property Server: String read FServer write FServer;        
  published
    property OnCall: TOnCall read FOnCall write FOnCall;
    property OnAnswer: TOnAnswer read FOnAnswer write FOnAnswer;
    property OnDtmf: TOnDtmf read FOnDtmf write FOnDtmf;
    property OnPlayed: TOnPlayed read FOnPlayed write FOnPlayed;
    property OnBye: TOnBye read FOnBye write FOnBye;
    property OnRegistration: TOnRegistration read FOnRegistration write FOnRegistration;
    property OnSilence: TOnSilence read FOnSilence write FOnSilence;
    property Host: String read FServer write FServer;
    property User: String read FUser write FUser;
    property Extension: String read FExtension write FExtension;
    property Password: String read FPassword write FPassword;
    property Proxy: String read FProxy write FProxy;
    property Stun: String read FStun write FStun;
  end;

procedure Register;

implementation

{$R wav.res}

{$IFDEF VER130}
type
  IInterface = IUnknown;
{$ENDIF}

{$IFNDEF VER180}

{$R sipclient.dcr}

{$ENDIF}

procedure Register;
begin
  RegisterComponents('VoIP', [TSipClient]);
end;

type

  TWaveInDevice = class(TInterfacedObject, IWaveInDevice)
  private
    FName: String;
  protected
    function GetName: String;
    procedure SetName(const Value: String);
  public
    constructor Create;
  end;

  TWaveInDeviceList = class(TInterfacedObject, IWaveInDeviceList)
  private
    FList: TInterfaceList;
  protected
    function GetDevice(Index: Integer): IWaveInDevice;
    function GetCount: Integer;
  public
    constructor Create;
    destructor Destroy; override;
  end;

  TWaveOutDevice = class(TInterfacedObject, IWaveOutDevice)
  private
    FName: String;
  protected
    function GetName: String;
    procedure SetName(const Value: String);
  public
    constructor Create;
  end;

  TWaveOutDeviceList = class(TInterfacedObject, IWaveOutDeviceList)
  private
    FList: TInterfaceList;
  protected
    function GetDevice(Index: Integer): IWaveOutDevice;
    function GetCount: Integer;
  public
    constructor Create;
    destructor Destroy; override;
  end;

  ILogMessage = interface
  ['{0CC430D9-BB0F-493A-8508-E0C391108881}']
    function GetText: String;
    property Text: String read GetText;
  end;

  TLogMessage = class(TInterfacedObject, ILogMessage)
  private
    FText: String;
  protected
    function GetText: String;
  public
    constructor Create(const AText: String);
  end;

{ TLogMessage }

constructor TLogMessage.Create(const AText: String);
begin
  FText := AText;
end;

function TLogMessage.GetText: String;
begin
  Result := FText;
end;

const
  WM_SIP_LOG = WM_USER + 3124;
  WM_SIP_INVITE = WM_SIP_LOG + 1;
  WM_SIP_BYE = WM_SIP_INVITE + 1;
  WM_SIP_INVITE_OK = WM_SIP_BYE + 1;
  WM_REG_OK = WM_SIP_INVITE_OK + 1;
  WM_REG_BYE = WM_REG_OK + 1;
  WM_DTMF = WM_REG_BYE + 1;
  WM_STOP_PLAYING = WM_DTMF + 1;
  WM_RING_BACK = WM_STOP_PLAYING + 1;
  WM_SILENCE = WM_RING_BACK + 1;

{ TSipClient }

function TSipClient.Call(PhoneNumber: String; const ACallerID: String; InDevice: Cardinal; OutDevice: Cardinal): ICall;
begin
  Result := FAccount.MakeCall(PhoneNumber, InDevice, OutDevice, ACallerID);
end;

procedure TSipClient.Answer(const Call: ICall);
begin
  Call.Answer;
end;

constructor TSipClient.Create(AOwner: TComponent);
begin
  inherited;
  {$IFNDEF VER130}
  Randomize;
  FWindowHandle := Classes.AllocateHWnd(WndProc);
  {$ELSE}
  FWindowHandle := AllocateHWnd(WndProc);
  {$ENDIF}
  FActive := False;
  FCS := TCriticalSection.Create;
  FDtmfMethod := dtmfInBand;
end;

destructor TSipClient.Destroy;
begin
  FreeAndNil(FAccount);
  FCS.Free;
  {$IFNDEF VER130}
  Classes.DeallocateHWnd(FWindowHandle);
  {$ELSE}
  DeallocateHWnd(FWindowHandle);
  {$ENDIF}
  inherited;
end;

procedure TSipClient.EndCall(const Call: ICall);
begin
  Call.EndCall;
end;

procedure TSipClient.Hold(const Call: ICall);
begin
  Call.Hold;
end;

procedure TSipClient.OnDtmfHandler(var Msg: TMessage);
var
  C: ICall;
  s: String;
begin
  C := IInterface(Msg.lParam) as ICall;
  try
    s := Char(Msg.WParam);
    if Assigned(FOnDtmf) then
      FOnDtmf(Self, C, s);
  finally
    C._Release;
  end;
end;

procedure TSipClient.OnWavStoppedHandler(var Msg: TMessage);
var
  C: ICall;
  s: String;
begin
  C := IInterface(Msg.lParam) as ICall;
  try
    s := Char(Msg.WParam);
    if Assigned(FOnPlayed) then
      FOnPlayed(Self, C);
  finally
    C._Release;
  end;
end;

procedure TSipClient.OnSilenceHandler(var Msg: TMessage);
var
  C: ICall;
begin
  C := IInterface(Msg.lParam) as ICall;
  try
    if Assigned(FOnSilence) then
      FOnSilence(Self, C);
  finally
    C._Release;
  end;
end;

procedure TSipClient.OnRegByeHandler(var Msg: TMessage);
begin
  if Assigned(FOnRegistration) then
    FOnRegistration(Self, False);
end;

procedure TSipClient.OnRegOkHandler(var Msg: TMessage);
begin
  if Assigned(FOnRegistration) then
    FOnRegistration(Self, True);
end;

procedure TSipClient.OnSipByeHandler(var Msg: TMessage);
var
  C: ICall;
begin
  C := IInterface(Msg.lParam) as ICall;
  try
    C.Speaker := nil;
    C.Lock;
    try
      if Assigned(FOnBye) then
        FOnBye(Self, C);
    finally
      C.Unlock;
    end;
  finally
    C._Release;
  end;
end;

procedure TSipClient.OnSipInviteHandler(var Msg: TMessage);
var
  C: ICall;
  Event: Boolean;
begin
  Event := False;
  C := IInterface(Msg.lParam) as ICall;
  try
    C.Lock;
    try
      if C.State = csInactive then
      begin
        C.State := csRinging;
        Event := True;
      end;
    finally
      C.Unlock;
    end;  
  finally
    C._Release;
  end;
  if Assigned(FOnCall) and Event then
    FOnCall(Self, C);

end;

procedure TSipClient.OnSipInviteOkHandler(var Msg: TMessage);
var
  C: ICall;
begin
  C := IInterface(Msg.lParam) as ICall;
  try
    if Assigned(FOnAnswer) then
      FOnAnswer(Self, C);
  finally
    C._Release;
  end;
end;

procedure TSipClient.OnSipLogHandler(var Msg: TMessage);
var
  M: ILogMessage;
begin
  M := IInterface(Msg.lParam) as ILogMessage;
  try
  finally
    M._Release;
  end;
end;

procedure TSipClient.PlayWavFile(const Call: ICall;
  const FileName: String);
begin
  FAccount.PlayWavFile(Call, FileName);
end;

procedure TSipClient.PlayWavFile(const Call: ICall; const WavFile: IWavFile);
begin
  WavFile.Position := 0;
  Call.PlayWavFile(WavFile);
end;

procedure TSipClient.Register;
begin
  if not Active then
    Active := True;
  FAccount.SetAccount(FServer, FUser, FExtension, FPassword, FProxy, FStun);
  FAccount.Server := FServer;
  FAccount.User := FUser;
  FAccount.Extension := FExtension;
  FAccount.Password := FPassword;
  FAccount.Proxy := FProxy;
  FAccount.Register;
end;

procedure TSipClient.Decline(const Call: ICall);
begin
  Call.Decline;
end;

procedure TSipClient.SetActive(const Value: Boolean);
begin
  FActive := Value;
  if FActive then
  begin
    FAccount := TAccount.Create;
    FAccount.EventHandler := TSipClientEventHandler.Create(FWindowHandle);
    FAccount.Active := True;
    FAccount.SetAccount(FServer, FUser, FExtension, FPassword, FProxy, FStun);
  end
  else
  begin
    if Assigned(FAccount) then
      FAccount.Active := False;
    FreeAndNil(FAccount);
  end;
end;

procedure TSipClient.UnHold(const Call: ICall);
begin
  Call.UnHold;
end;

procedure TSipClient.UnRegister;
begin
  FAccount.Unregister;
end;

procedure TSipClient.WndProc(var Message: TMessage);
begin
  if Message.Msg = WM_SIP_LOG then
    OnSipLoghandler(Message)
  else if Message.Msg = WM_SIP_INVITE then
    OnSipInviteHandler(Message)
  else if Message.Msg = WM_SIP_INVITE_OK then
    OnSipInviteOkHandler(Message)
  else if Message.Msg = WM_DTMF then
    OnDtmfHandler(Message)
  else if Message.Msg = WM_RING_BACK then
    OnRingbackHandler(Message)
  else if Message.Msg = WM_SIP_BYE then
    OnSipByeHandler(Message)
  else if Message.Msg = WM_REG_OK then
    OnRegOkHandler(Message)
  else if Message.Msg = WM_REG_BYE then
    OnRegByeHandler(Message)
  else if Message.Msg = WM_STOP_PLAYING then
    OnWavStoppedHandler(Message)
  else if Message.Msg = WM_SILENCE then
    OnSilenceHandler(Message)
  else
    DefWindowProc(FWindowHandle, Message.Msg, Message.WParam, Message.LParam);
end;

procedure TSipClient.StopPlaying(const Call: ICall);
begin
  Call.PlayWavFile(nil);
end;

procedure TSipClient.PlayText(const Call: ICall; const Text: String; const VoiceIndex: Integer = 0);
begin
  FAccount.PlayText(Call, Text, VoiceIndex);
end;

procedure TSipClient.SendDtmf(const Call: ICall; const Digit: String);
var
  w: IWavFile;
  c: Char;
begin
  c := #0;
  if Length(Digit) > 0 then
    c := Digit[1];
  case FDtmfMethod of
  dtmfInBand:
  begin
    case c of
    '0': w := LoadWavFromResource(10);
    '1': w := LoadWavFromResource(1);
    '2': w := LoadWavFromResource(2);
    '3': w := LoadWavFromResource(3);
    '4': w := LoadWavFromResource(4);
    '5': w := LoadWavFromResource(5);
    '6': w := LoadWavFromResource(6);
    '7': w := LoadWavFromResource(7);
    '8': w := LoadWavFromResource(8);
    '9': w := LoadWavFromResource(9);
    '*': w := LoadWavFromResource(11);
    '#': w := LoadWavFromResource(12);
    end;
    PlayWavFile(Call, w);
  end;
  dtmfSipInfo: FAccount.SendDtmfInfo(Call, Digit);
  dtmfRFC2833: FAccount.SetDtmfCode(Call, Digit);
  end;
end;

function TSipClient.LoadWavFromResource(const N: Integer): IWavFile;
var
  rs: TResourceStream;
begin
  FCS.Enter;
  try
    rs := TResourceStream.CreateFromID(hInstance, N, RT_RCDATA);
    try
      Result := TWavStream.CreateFromStream(rs);
    finally
      rs.Free;
    end;
  finally
    FCS.Leave;
  end;
end;

function TSipClient.CreateConference: IConference;
begin
  Result := TConference.Create;
end;

function TSipClient.CreateRecorder: IRecorder;
begin
  Result := TRecorder.Create;
end;

function TSipClient.CreateStereoRecorder: IRecorder;
begin
  Result := TStereoRecorder.Create;
end;

procedure TSipClient.OnRingbackHandler(var Msg: TMessage);
var
  C: ICall;
begin
  C := IInterface(Msg.lParam) as ICall;
  try
    C.Lock;
    try
    finally
      C.Unlock;
    end;
  finally
    C._Release;
  end;
end;

procedure TSipClient.SetCodecs(const Codecs: array of Integer);
begin
  if not Active then
    raise Exception.Create('SIP engine is not active.');
  FAccount.SetCodecs(Codecs);
end;

function TSipClient.GetWaveInDeviceList: IWaveInDeviceList;
begin
  Result := TWaveInDeviceList.Create;
end;

function TSipClient.GetWaveOutDeviceList: IWaveOutDeviceList;
begin
  Result := TWaveOutDeviceList.Create;
end;

function TSipClient.GetRegistrationTimeout: Integer;
begin
  Result := FAccount.RegistrationTimeout;
end;

procedure TSipClient.SetRegistrationTimeout(const Value: Integer);
begin
  FAccount.RegistrationTimeout := Value;
end;

function TSipClient.GetRegistered: Boolean;
begin
  if not Active then
    Result := False
  else
    Result := FAccount.Registered;
end;

{ TSipClientEventHandler }

constructor TSipClientEventHandler.Create(const AWindowHandle: THandle);
begin
  inherited Create;
  FWindowHandle := AWindowHandle;
end;

procedure TSipClientEventHandler.OnConnect(const Call: ICall);
begin
  Call._AddRef;
  PostMessage(FWindowHandle, WM_SIP_INVITE_OK, 0, SipPointer(Pointer(Call)));
end;

procedure TSipClientEventHandler.OnDisconnect(const Call: ICall);
begin
  Call._AddRef;
  PostMessage(FWindowHandle, WM_SIP_BYE, 0, SipPointer(Pointer(Call)));
end;

procedure TSipClientEventHandler.OnDtmf(const Call: ICall;
  const Code: AnsiChar);
begin
  Call._AddRef;
  PostMessage(FWindowHandle, WM_DTMF, Byte(Code), SipPointer(Pointer(Call)));
end;

procedure TSipClientEventHandler.OnInvite(const Call: ICall);
begin
  Call._AddRef;
  PostMessage(FWindowHandle, WM_SIP_INVITE, 0, SipPointer(Pointer(Call)));
end;

procedure TSipClientEventHandler.OnRingBack(const Call: ICall);
begin
  Call._AddRef;
  if not Assigned(Call.Speaker) then
    Call.Speaker := CreateSpeaker(Call.AudioOutDeviceID);
  PostMessage(FWindowHandle, WM_RING_BACK, 0, SipPointer(Pointer(Call)));
end;

procedure TSipClientEventHandler.OnLog(const Text: String);
//var
//  M: ILogMessage;
begin
//  M := TLogMessage.Create(Text);
//  M._AddRef;
//  PostMessage(FWindowHandle, WM_SIP_LOG, 0, SipPointer(Pointer(M)));
end;

procedure TSipClientEventHandler.OnAccountRegistered;
begin
  PostMessage(FWindowHandle, WM_REG_OK, 0, 0)
end;

procedure TSipClientEventHandler.OnAccountUnregistered;
begin
  PostMessage(FWindowHandle, WM_REG_BYE, 0, 0)
end;

procedure TSipClientEventHandler.OnSilence(const Call: ICall);
begin
  Call._AddRef;
  PostMessage(FWindowHandle, WM_SILENCE, 0, SipPointer(Pointer(Call)));
end;

procedure TSipClientEventHandler.OnStopPlaying(const Call: ICall);
begin
  Call._AddRef;
  PostMessage(FWindowHandle, WM_STOP_PLAYING, 0, SipPointer(Pointer(Call)));
end;

procedure TSipClientEventHandler.OnAudioClose(const Call: ICall);
var
  Microphone: IMicrophone;
begin
  Microphone := Call.Microphone;
  if Assigned(Microphone) then
  begin
    Microphone.Call := nil;
    Call.Microphone := nil; 
  end;
  Call.Speaker := nil;
end;

procedure TSipClientEventHandler.OnAudioOpen(const Call: ICall);
begin
  if not Assigned(Call.Speaker) then
    Call.Speaker := CreateSpeaker(Call.AudioOutDeviceID);
  if not Assigned(Call.Microphone) then
  begin
    Call.Microphone := CreateMicrophone(Call.AudioInDeviceID);
    Call.Microphone.Call := Call;
  end;  
end;

{ TWaveInDevice }

constructor TWaveInDevice.Create;
begin
  inherited Create;

end;

function TWaveInDevice.GetName: String;
begin
  Result := FName;
end;

procedure TWaveInDevice.SetName(const Value: String);
begin
  FName := Value;
end;

{ TWaveInDeviceList }

constructor TWaveInDeviceList.Create;
var
  n, i: Integer;
  DevInCaps: TWaveInCaps;
  Device: IWaveInDevice;
begin
  inherited Create;
  FList := TInterfaceList.Create;
  n := waveInGetNumDevs;
  for i := 0 to n - 1 do
  begin
    Device := TWaveInDevice.Create;
    FList.Add(Device);
    waveInGetDevCaps(i, @DevInCaps, SizeOf(DevInCaps));
    Device.Name := PChar(@DevInCaps.szPname);
  end;
end;

destructor TWaveInDeviceList.Destroy;
begin
  FList.Free;
  inherited;
end;

function TWaveInDeviceList.GetCount: Integer;
begin
  Result := FList.Count;
end;

function TWaveInDeviceList.GetDevice(Index: Integer): IWaveInDevice;
begin
  Result := FList[Index] as IWaveInDevice;
end;

{ TWaveOutDevice }

constructor TWaveOutDevice.Create;
begin
  inherited Create;

end;

function TWaveOutDevice.GetName: String;
begin
  Result := FName;
end;

procedure TWaveOutDevice.SetName(const Value: String);
begin
  FName := Value;
end;

{ TWaveOutDeviceList }

constructor TWaveOutDeviceList.Create;
var
  n, i: Integer;
  DevOutCaps: TWaveOutCaps;
  Device: IWaveOutDevice;
begin
  inherited Create;
  FList := TInterfaceList.Create;

  n := WaveOutGetNumDevs;
  for i := 0 to n - 1 do
  begin
    Device := TWaveOutDevice.Create;
    FList.Add(Device);
    WaveOutGetDevCaps(i, @DevOutCaps, SizeOf(DevOutCaps));
    Device.Name := PChar(@DevOutCaps.szPname);
  end;
end;

destructor TWaveOutDeviceList.Destroy;
begin
  FList.Free;
  inherited;
end;

function TWaveOutDeviceList.GetCount: Integer;
begin
  Result := FList.Count;
end;

function TWaveOutDeviceList.GetDevice(Index: Integer): IWaveOutDevice;
begin
  Result := FList[Index] as IWaveOutDevice;
end;

end.
