// ------------------------------------------------------------------------------
// DPF.iOS.HTTP Component
//
// Dadeh Pardazane Faragir ( DPF ) Co.
//
// Web: http://www.dpfaragir.com
//
// Developed By: Babak Yaghoobi
//
// Email #1: yaghoobi@dpfaragir.com
// Email #2: b_yaghobi@yahoo.com
//
// ------------------------------------------------------------------------------
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// ------------------------------------------------------------------------------
unit DPF.iOS.HTTP;

interface

{$I DPF.iOS.Defs.inc}

uses
  System.Classes,
  System.SysUtils,
  System.TypInfo,
  DPF.iOS.BaseControl,
  DPF.iOS.NSOperationQueue,
{$IFDEF IOS}
  Macapi.ObjCRuntime,
  DPF.iOS.Common,

  iOSapi.UIKit,
  Macapi.ObjectiveC,
  iOSapi.CocoaTypes,
  iOSapi.CoreGraphics,
  iOSapi.Foundation,
{$ENDIF}
  FMX.Forms,
  FMX.Dialogs;

type
  TDPFHttp = class;
{$IFDEF IOS}
  // ----------------------------------------------------------------------------

  NSURLAuthenticationChallengeSenderClass = interface( NSObjectClass )
    ['{09D6CF3B-864E-4C62-A8DF-C4426CDEE8FB}']
  end;

  NSURLAuthenticationChallengeSender = interface( NSObject )
    ['{4503CADC-8784-4F4F-9B3F-1A615F53E3A8}']
    procedure cancelAuthenticationChallenge( challenge: NSURLAuthenticationChallenge ); cdecl;
    procedure continueWithoutCredentialForAuthenticationChallenge( challenge: NSURLAuthenticationChallenge ); cdecl;
    procedure performDefaultHandlingForAuthenticationChallenge( challenge: NSURLAuthenticationChallenge ); cdecl;
    procedure rejectProtectionSpaceAndContinueWithChallenge( challenge: NSURLAuthenticationChallenge ); cdecl;
    procedure useCredential( credential: NSURLCredential; forAuthenticationChallenge: NSURLAuthenticationChallenge ); cdecl;
  end;

  TNSURLAuthenticationChallengeSender = class( TOCGenericImport<NSURLAuthenticationChallengeSenderClass, NSURLAuthenticationChallengeSender> )
  end;

  NSURLConnectionDataDelegate = interface( IObjectiveC )
    ['{EAC573C7-A086-4411-954F-847383C18924}']
    procedure connectionDidFinishLoading( connection: NSURLConnection ); cdecl;
    procedure connection( connection: NSURLConnection; didReceiveData: NSData ); cdecl; overload;
    procedure connection( connection: NSURLConnection; didReceiveResponse: NSURLResponse ); cdecl; overload;
    procedure connection( connection: NSURLConnection; didFailWithError: NSError ); cdecl; overload; // Sometime Raise Error on Deploying Device

    procedure connection( connection: NSURLConnection; willSendRequestForAuthenticationChallenge: NSURLAuthenticationChallenge ); cdecl; overload;
    // procedure connection( connection: NSURLConnection; didReceiveAuthenticationChallenge: NSURLAuthenticationChallenge ); cdecl; overload;
  end;

  TNSURLConnectionDataDelegate = class( TOCLocal, NSURLConnectionDataDelegate )
  private
    FDPFHttp: TDPFHttp;
    Buffer  : NSMutableData;
  public
    constructor Create( AOwner: TDPFHttp );
    destructor Destroy; override;

    // -------------------------------------------------------------------------
    procedure connection( connection: NSURLConnection; didFailWithError: NSError ); overload; cdecl;
    procedure connection( connection: NSURLConnection; didReceiveData: NSData ); overload; cdecl;
    procedure connection( connection: NSURLConnection; didReceiveResponse: NSURLResponse ); overload; cdecl;
    procedure connectionDidFinishLoading( connection: NSURLConnection ); cdecl;

    procedure connection( connection: NSURLConnection; willSendRequestForAuthenticationChallenge: NSURLAuthenticationChallenge ); overload; cdecl;
    // procedure connection( connection: NSURLConnection; didReceiveAuthenticationChallenge: NSURLAuthenticationChallenge ); overload; cdecl;
    // -------------------------------------------------------------------------
  end;

  // ----------------------------------------------------------------------------
{$ENDIF}

  // ------------------------------------------------------------------------------
  THTTPHeader = record
    Field: string;
    Value: string;
  end;

  TOnReceiveDataStream = procedure( Sender: TObject; Data: TStream; var isFree: Boolean ) of object;
  TOnReceiveData       = procedure( Sender: TObject; Data: string; var isFree: Boolean ) of object;
  TOnReceiveError      = procedure( Sender: TObject; Error: string; var isFree: Boolean ) of object;
  TDPFHttpOnProgress   = procedure( Sender: TObject; const DownloadSize: Int64; const DownloadedSize: Int64 ) of object;

{$IFDEF IOS}
  TOnReceiveDataBin = procedure( Sender: TObject; Data: NSData; var isFree: Boolean ) of object;
{$ENDIF}
  TDPFNSURLRequestCache = ( rcUseProtocolCachePolicy = 0, rcReloadIgnoringLocalCacheData = 1, rcReturnCacheDataElseLoad = 2, rcReturnCacheDataDontLoad = 3, rcReloadIgnoringLocalAndRemoteCacheData = 4, rcReloadRevalidatingCacheData = 5 );

  [ComponentPlatformsAttribute( PidWin32 or PidWin64 or pidLinux32 or pidAndroid or PidOSX32 or PidiOSDevice or PidiOSSimulator )]
  TDPFHttp = class( TComponent )
  private
    FOnReceiveError     : TOnReceiveError;
    FOnReceiveData      : TOnReceiveData;
    FConnectionTimeout  : Integer;
    FBusy               : Boolean;
    FWaitForFree        : Boolean;
    FLastURL            : string;
    FLastStatusCode     : Integer;
    FOperationQueue     : TDPFNSOperationQueue;
    FUserPassword       : string;
    FUserName           : string;
    FAuthFailureCount   : Integer;
    FOnProgress         : TDPFHttpOnProgress;
    FURLRequestCache    : TDPFNSURLRequestCache;
    FOnReceiveDataStream: TOnReceiveDataStream;
{$IFDEF IOS}
    FDownloadSize          : Int64;
    FDownloadedSize        : Int64;
    FOnReceiveDataBin      : TOnReceiveDataBin;
    FConnectionDataDelegate: TNSURLConnectionDataDelegate;
    // FUIIMage               : UIImageView;
    FConnection: NSURLConnection;
{$ENDIF}
  protected
  public
{$IFDEF IOS}
    property OnReceiveDataBin: TOnReceiveDataBin read FOnReceiveDataBin write FOnReceiveDataBin;
    function GetUrlContentData( const HTTPURL: string; const HTTPHeader: array of THTTPHeader; ASync: Boolean = false; HTTPMethod: string = 'GET'; FHttpBody: string = '' ): NSData; overload;
    function GetUrlContentString( const HTTPURL: string; const HTTPHeader: array of THTTPHeader; ASync: Boolean = false; HTTPMethod: string = 'GET'; FHttpBody: string = '' ): string; overload;
    function GetUrlContentImage( const HTTPURL: string; const HTTPHeader: array of THTTPHeader; ASync: Boolean = false; HTTPMethod: string = 'GET' ): UIImage; overload;
    procedure ConnectionCancel;
{$ENDIF}
    constructor Create( AOwner: TComponent ); override;
    destructor Destroy; override;
  published
    property ConnectionTimeout  : Integer read FConnectionTimeout write FConnectionTimeout default 20; // 20 Second
    property OnReceiveData      : TOnReceiveData read FOnReceiveData write FOnReceiveData;
    property OnReceiveDataStream: TOnReceiveDataStream read FOnReceiveDataStream write FOnReceiveDataStream;
    property OnReceiveError     : TOnReceiveError read FOnReceiveError write FOnReceiveError;
    property OnProgress         : TDPFHttpOnProgress read FOnProgress write FOnProgress;

    property Busy            : Boolean read FBusy;
    property LastURL         : string read FLastURL;
    property OperationQueue  : TDPFNSOperationQueue read FOperationQueue write FOperationQueue;
    property UserName        : string read FUserName write FUserName;
    property UserPassword    : string read FUserPassword write FUserPassword;
    property AuthFailureCount: Integer read FAuthFailureCount write FAuthFailureCount default 1;

    property URLRequestCache: TDPFNSURLRequestCache read FURLRequestCache write FURLRequestCache default rcUseProtocolCachePolicy;
  end;
  // ----------------------------------------------------------------------------

function BuildHeaderRecord( Field: string; Value: string ): THTTPHeader;

implementation

// ------------------------------------------------------------------------------
function BuildHeaderRecord( Field: string; Value: string ): THTTPHeader;
begin
  Result.Field := Field;
  Result.Value := Value;
end;

// ----------------------------------------------------------------------------
constructor TDPFHttp.Create( AOwner: TComponent );
begin
{$IFDEF IOS}
  DPFNSLog( 'TDPFHttp.Create: ' + name );
{$ENDIF}
  inherited Create( AOwner );
  FConnectionTimeout := 20;
  FWaitForFree       := False;
  FLastStatusCode    := 0;
  FAuthFailureCount  := 1;
  FURLRequestCache   := rcUseProtocolCachePolicy;
{$IFDEF IOS}
{$ENDIF}
end;

// ----------------------------------------------------------------------------
destructor TDPFHttp.Destroy;
begin
{$IFDEF IOS}
  DPFNSLog( 'TDPFHttp.Destroy' );
  try
    ConnectionCancel;
  except
    DPFNSLog( 'TDPFHttp.Destroy except' );
    FWaitForFree := True;
    exit;
  end;
  if { not FBusy } true then
  begin
    if Assigned( FConnectionDataDelegate ) then
    begin
      FConnectionDataDelegate.DisposeOf;
      FConnectionDataDelegate := nil;
    end;
    inherited;
  end
  else
    FWaitForFree := True;
{$ELSE}
  inherited;
{$ENDIF}
end;

{$IFDEF IOS}

// ----------------------------------------------------------------------------
procedure TDPFHttp.ConnectionCancel;
begin
  if Assigned( FConnection ) then
  begin
    FConnection.cancel;
    FConnection := nil;
    FBusy       := False;
  end;
  Application.ProcessMessages;
  if Assigned( FConnectionDataDelegate ) then
  begin
    FConnectionDataDelegate.DisposeOf;
    FConnectionDataDelegate := nil;
  end;
  if FWaitForFree then
    Destroy;
end;

// ----------------------------------------------------------------------------
// HTTPURL: String ; //Sample:  http://www.dpfaragir.com
// HTTPHeader
function TDPFHttp.GetUrlContentData( const HTTPURL: string; const HTTPHeader: array of THTTPHeader; ASync: Boolean = false; HTTPMethod: string = 'GET'; FHttpBody: string = '' ): NSData;
var
  Error      : {$IFDEF VER260}PPointer{$ELSE}NSError{$ENDIF};
  I          : Integer;
  FURLRequest: NSMutableURLRequest;
  FResponse  : NSURLResponse;
  FURL       : NSURL;
  isFree     : Boolean;

begin
  isFree := false;
  Result := nil;
  if FBusy or FWaitForFree then
    exit;
  DPFNSLog( 'TDPFHttp.GetUrlContentData' );

  FLastURL := HTTPURL;

  FBusy := True;
  FURL  := TNSURL.Wrap( TNSURL.OCClass.URLWithString( NSStr( HTTPURL ).stringByAddingPercentEscapesUsingEncoding( NSUTF8StringEncoding ) ) );

  FURLRequest := TNSMutableURLRequest.Wrap( TNSMutableURLRequest.OCClass.requestWithURL( FURL, Integer( FURLRequestCache ), FConnectionTimeout ) );

  FURLRequest.setHTTPMethod( NSStr( HTTPMethod ) );
  for I := low( HTTPHeader ) to high( HTTPHeader ) do
  begin
    FURLRequest.setValue( NSStr( HTTPHeader[I].Value ), NSStr( HTTPHeader[I].Field ) );
  end;

  FURLRequest.setHTTPBody( NSSTR( FHttpBody ).dataUsingEncoding( NSUTF8StringEncoding ) );

  if ASync then
  begin
    FConnectionDataDelegate := TNSURLConnectionDataDelegate.Create( Self );
    FConnection             := TNSURLConnection.Wrap( TNSURLConnection.OCClass.connectionWithRequest( FURLRequest, ( FConnectionDataDelegate as ILocalObject ).GetObjectID ) );
    if Assigned( FConnection ) then
    begin
      if Assigned( OperationQueue ) then
        FConnection.setDelegateQueue( OperationQueue.FNSOperationQueue );
      // FConnection.scheduleInRunLoop( TNSRunLoop.Wrap( TNSRunLoop.OCClass.mainRunLoop), NSDefaultRunLoopMode );
      FConnection.start;
    end;
  end
  else
  begin
    Error  := nil;
    Result := TNSURLConnection.OCClass.sendSynchronousRequest( FURLRequest, FResponse, Error );
    if ( Result = nil ) and ( Error <> nil ) and Assigned( FOnReceiveError ) then
      FOnReceiveError( Self, UTF8ToString( {$IFDEF VER260}TNSError.Wrap( Error ){$ELSE}Error{$ENDIF}.localizedDescription.UTF8String ), isFree );
    FBusy := false;
  end;
  if isFree then
    exit;
end;

// ----------------------------------------------------------------------------
// HTTPURL: String ; //Sample:  http://www.dpfaragir.com
// HTTPHeader
function TDPFHttp.GetUrlContentString( const HTTPURL: string; const HTTPHeader: array of THTTPHeader; ASync: Boolean = false; HTTPMethod: string = 'GET'; FHttpBody: string = '' ): string;
var
  NSD  : NSData;
  NSStr: NSString;
begin
  Result := '';
  NSD    := GetUrlContentData( HTTPURL, HTTPHeader, ASync, HTTPMethod, FHttpBody );
  if NSD <> nil then
  begin
    NSStr  := TNSString.Wrap( TNSString.Alloc.initWithData( NSD, NSUTF8StringEncoding ) );
    Result := UTF8ToString( NSStr.UTF8String );
    NSStr.release;
  end;
end;

// ----------------------------------------------------------------------------
// HTTPURL: String ; //Sample:  http://www.dpfaragir.com
// HTTPHeader
function TDPFHttp.GetUrlContentImage( const HTTPURL: string; const HTTPHeader: array of THTTPHeader; ASync: Boolean = false; HTTPMethod: string = 'GET' ): UIImage;
var
  NSD: NSData;
begin
  NSD    := GetUrlContentData( HTTPURL, HTTPHeader, ASync, HTTPMethod );
  Result := nil;
  if NSD <> nil then
    Result := TUIImage.Wrap( TUIImage.OCClass.imageWithData( NSD ) );
end;

// ----------------------------------------------------------------------------
{ TNSURLConnectionDataDelegate }

// ----------------------------------------------------------------------------
procedure TNSURLConnectionDataDelegate.connection( connection: NSURLConnection; didReceiveResponse: NSURLResponse );
var
  FNSHTTPURLResponse: NSHTTPURLResponse;
begin
  FDPFHttp.FBusy           := True;
  FDPFHttp.FDownloadSize   := didReceiveResponse.expectedContentLength;
  FDPFHttp.FDownloadedSize := 0;
  if didReceiveResponse.isKindOfClass( objc_getClass( 'NSHTTPURLResponse' ) ) then
  begin
    FNSHTTPURLResponse       := TNSHTTPURLResponse.Wrap( ( didReceiveResponse as ILocalObject ).GetObjectID );
    FDPFHttp.FLastStatusCode := FNSHTTPURLResponse.statusCode;
    if FDPFHttp.FDownloadSize = -1 then
      FDPFHttp.FDownloadSize := TNSNumber.Wrap( FNSHTTPURLResponse.allHeaderFields.objectForKey( ( NSStr( 'Content-Length' ) as ILocalObject ).GetObjectID ) ).longLongValue;
  end;
  Buffer.setLength( 0 );
end;

// ----------------------------------------------------------------------------
procedure TNSURLConnectionDataDelegate.connection( connection: NSURLConnection; didReceiveData: NSData );
begin
  FDPFHttp.FBusy := True;
  Buffer.appendData( didReceiveData );
  FDPFHttp.FDownloadedSize := FDPFHttp.FDownloadedSize + didReceiveData.length;
  if Assigned( FDPFHttp.FOnProgress ) then
    FDPFHttp.FOnProgress( FDPFHttp, FDPFHttp.FDownloadSize, FDPFHttp.FDownloadedSize );
end;

// ----------------------------------------------------------------------------
procedure TNSURLConnectionDataDelegate.connection( connection: NSURLConnection; didFailWithError: NSError );
var
  isFree: Boolean;
begin
  isFree := false;

  FDPFHttp.FConnection := nil;
  DPFNSLog( 'TNSURLConnectionDataDelegate.connection' );
  if FDPFHttp.FWaitForFree then
  begin
    FDPFHttp.FBusy := false;
    FDPFHttp.Destroy;
    exit;
  end;

  if Assigned( FDPFHttp.FOnReceiveError ) then
  begin
    FDPFHttp.FOnReceiveError( FDPFHttp, UTF8ToString( didFailWithError.localizedDescription.UTF8String ), isFree );
    if isFree then
      exit;
  end;
  FDPFHttp.FBusy := false;
end;

// ----------------------------------------------------------------------------
procedure TNSURLConnectionDataDelegate.connection( connection: NSURLConnection; willSendRequestForAuthenticationChallenge: NSURLAuthenticationChallenge );
var
  credential     : NSURLCredential;
  ChallengeSender: NSURLAuthenticationChallengeSender;
begin
  if willSendRequestForAuthenticationChallenge.previousFailureCount > FDPFHttp.FAuthFailureCount then
    connection.cancel
  else
  begin
    credential      := TNSURLCredential.Wrap( TNSURLCredential.OCClass.credentialWithUser( NSStr( FDPFHttp.UserName ), NSStr( FDPFHttp.UserPassword ), NSURLCredentialPersistenceForSession ) );
    ChallengeSender := TNSURLAuthenticationChallengeSender.Wrap( willSendRequestForAuthenticationChallenge.sender );
    ChallengeSender.useCredential( credential, willSendRequestForAuthenticationChallenge );
  end;
end;

// ----------------------------------------------------------------------------
{ procedure TNSURLConnectionDataDelegate.connection( connection: NSURLConnection; didReceiveAuthenticationChallenge: NSURLAuthenticationChallenge );
  begin
  end; }

// ----------------------------------------------------------------------------
procedure TNSURLConnectionDataDelegate.connectionDidFinishLoading( connection: NSURLConnection );
var
  NSResult : NSString;
  StrResult: string;
  isFree   : Boolean;
  MS       : TMemoryStream;
begin
  isFree := false;

  FDPFHttp.FConnection := nil;

  DPFNSLog( 'Start: TNSURLConnectionDataDelegate.connectionDidFinishLoading' );
  FDPFHttp.FBusy := false;
  StrResult      := '';
  if FDPFHttp.FWaitForFree then
  begin
    DPFNSLog( 'FDPFHttp.FWaitForFree' );
    FDPFHttp.FBusy := false;
    FDPFHttp.Destroy;
    exit;
  end;

  if Assigned( FDPFHttp.FOnReceiveDataBin ) then
  begin
    FDPFHttp.FOnReceiveDataBin( FDPFHttp, Buffer, isFree );
    if isFree then
      exit;
  end;

  if Assigned( FDPFHttp.FOnReceiveDataStream ) then
  begin
    MS := TMemoryStream.Create;
    try
      MS.WriteBuffer( Buffer.bytes, Buffer.length );
      MS.Position := 0;
      FDPFHttp.FOnReceiveDataStream( FDPFHttp, MS, isFree );
    finally
      MS.Free;
    end;
    if isFree then
      exit;
  end;

  if Assigned( FDPFHttp.FOnReceiveData ) then
  begin
    NSResult  := TNSString.Wrap( TNSString.Alloc.initWithData( Buffer, NSUTF8StringEncoding ) );
    StrResult := UTF8ToString( NSResult.UTF8String );
    FDPFHttp.FOnReceiveData( FDPFHttp, StrResult, isFree );
    NSResult.release;
    if isFree then
      exit;
  end;
  DPFNSLog( 'End: TNSURLConnectionDataDelegate.connectionDidFinishLoading' );
end;

// ----------------------------------------------------------------------------
constructor TNSURLConnectionDataDelegate.Create( AOwner: TDPFHttp );
begin
  inherited Create;
  FDPFHttp := AOwner;
  Buffer   := TNSMutableData.Create;
end;

// ----------------------------------------------------------------------------
destructor TNSURLConnectionDataDelegate.Destroy;
begin
  if Buffer.retainCount > 0 then
  begin
    Buffer.setLength( 0 );
    Buffer.release;
  end;
  inherited Destroy;
end;

{$ENDIF}

// ----------------------------------------------------------------------------
end.
