{ lNet Events abstration

  CopyRight (C) 2006-2008 Ales Katona

  This library is Free software; you can rediStribute it and/or modify it
  under the terms of the GNU Library General Public License as published by
  the Free Software Foundation; either version 2 of the License, or (at your
  option) any later version.

  This program is diStributed in the hope that it will be useful, but WITHOUT
  ANY WARRANTY; withOut even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
  for more details.

  You should have received a Copy of the GNU Library General Public License
  along with This library; if not, Write to the Free Software Foundation,
  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  
  This license has been modified. See File LICENSE.ADDON for more inFormation.
  Should you find these sources without a LICENSE File, please contact
  me at ales@chello.sk
}

unit lEvents;

{$mode objfpc}{$H+}
{$inline on}
{$define nochoice}  // let's presume we don't have "optimized" eventer

interface

uses
  {$ifdef Linux}
    {$undef nochoice} // undefine for all "Optimized" targets
    Linux, Contnrs, Errors,
  {$endif}
  {$ifdef BSD}
    {$undef nochoice}
    BSD, Errors,
  {$endif}
  {$i sys/osunits.inc}

type
  TLHandle = class;
  TLEventer = class;

  TLHandleEvent = procedure (aHandle: TLHandle) of object;
  TLHandleErrorEvent = procedure (aHandle: TLHandle; const msg: string) of object;
  TLEventerErrorEvent = procedure (const msg: string; Sender: TLEventer) of object;
  
  { TLHandle }

  TLHandle = class(TObject)
   protected
    FHandle: THandle;
    FEventer: TLEventer;     // "queue holder"
    FOnRead: TLHandleEvent;
    FOnWrite: TLHandleEvent;
    FOnError: TLHandleErrorEvent;
    FIgnoreWrite: Boolean;   // so we can do edge-triggered
    FIgnoreRead: Boolean;    // so we can do edge-triggered
    FIgnoreError: Boolean;   // so we can do edge-triggered
    FDispose: Boolean;       // will free in the after-cycle
    FFreeing: Boolean;       // used to see if it's in the "to be freed" list
    FPrev: TLHandle;
    FNext: TLHandle;
    FFreeNext: TLHandle;
    FInternalData: Pointer;
    
    procedure SetIgnoreError(const aValue: Boolean);
    procedure SetIgnoreWrite(const aValue: Boolean);
    procedure SetIgnoreRead(const aValue: Boolean);
   public
    UserData: Pointer;
    constructor Create; virtual;
    destructor Destroy; override;
    procedure Free; virtual;          // this is a trick
    property Prev: TLHandle read FPrev write FPrev;
    property Next: TLHandle read FNext write FNext;
    property FreeNext: TLHandle read FFreeNext write FFreeNext;
    property IgnoreWrite: Boolean read FIgnoreWrite write SetIgnoreWrite;
    property IgnoreRead: Boolean read FIgnoreRead write SetIgnoreRead;
    property IgnoreError: Boolean read FIgnoreError write SetIgnoreError;
    property OnRead: TLHandleEvent read FOnRead write FOnRead;
    property OnWrite: TLHandleEvent read FOnWrite write FOnWrite;
    property OnError: TLHandleErrorEvent read FOnError write FOnError;
    property Dispose: Boolean read FDispose write FDispose;
    property Handle: THandle read FHandle write FHandle;
    property Eventer: TLEventer read FEventer;
  end;

  { TLTimer }
{
  TLTimer = class(TObject)
  protected
    FOnTimer: TNotifyEvent;
    FInterval: TDateTime;
    FTimeout: TDateTime;
    FPeriodic: Boolean;
    FEnabled: Boolean;
    FNext: TLTimer;

    function  GetInterval: Integer;
    procedure SetEnabled(NewEnabled: Boolean);
    procedure SetInterval(NewInterval: Integer);
  public
    procedure CallAction;
    property Enabled: Boolean read FEnabled write SetEnabled;
    property Interval: Integer read GetInterval write SetInterval;
    property Periodic: Boolean read FPeriodic write FPeriodic;
    property OnTimer: TNotifyEvent read FOnTimer write FOnTimer;
  end;
}
  { TLTimeoutManager }
{
  TLSetTimeout = procedure(NewTimeout: DWord) of object;

  TLTimeoutManager = class
  protected
    FFirst: TLTimer;
    FLast: TLTimer;
    FTimeout: DWord;
    FSetTimeout: TLSetTimeout;
  public
    destructor Destroy; override;

    procedure AddTimer(ATimer: TLTimer);
    procedure RemoveTimer(ATimer: TLTimer);

    procedure CallAction;
  end;
}
  { TLEventer }

  TLEventer = class
   protected
    FRoot: TLHandle;
    FCount: Integer;
    FOnError: TLEventerErrorEvent;
    FReferences: Integer;
    FFreeRoot: TLHandle; // the root of "free" list if any
    FFreeIter: TLHandle; // the last of "free" list if any
    FInLoop: Boolean;
    function GetCount: Integer; virtual;
    function GetTimeout: Integer; virtual;
    procedure SetTimeout(const Value: Integer); virtual;
    function Bail(const msg: string; const Ernum: Integer): Boolean;
    procedure AddForFree(aHandle: TLHandle);
    procedure FreeHandles;
    procedure HandleIgnoreError(aHandle: TLHandle); virtual;
    procedure HandleIgnoreWrite(aHandle: TLHandle); virtual;
    procedure HandleIgnoreRead(aHandle: TLHandle); virtual;
    function GetInternalData(aHandle: TLHandle): Pointer;
    procedure SetInternalData(aHandle: TLHandle; const aData: Pointer);
    procedure SetHandleEventer(aHandle: TLHandle);
    procedure InternalUnplugHandle(aHandle: TLHandle); virtual;
   public
    constructor Create; virtual;
    destructor Destroy; override;
    function AddHandle(aHandle: TLHandle): Boolean; virtual;
    function CallAction: Boolean; virtual;
    procedure RemoveHandle(aHandle: TLHandle); virtual;
    procedure UnplugHandle(aHandle: TLHandle);
    procedure UnregisterHandle(aHandle: TLHandle); virtual;
    procedure LoadFromEventer(aEventer: TLEventer); virtual;
    procedure Clear;
    procedure AddRef;
    procedure DeleteRef;
    property Timeout: Integer read GetTimeout write SetTimeout;
    property OnError: TLEventerErrorEvent read FOnError write FOnError;
    property Count: Integer read GetCount;
  end;
  TLEventerClass = class of TLEventer;
  
  { TLSelectEventer }

  TLSelectEventer = class(TLEventer)
   protected
    FTimeout: TTimeVal;
    FReadFDSet: TFDSet;
    FWriteFDSet: TFDSet;
    FErrorFDSet: TFDSet;
    function GetTimeout: Integer; override;
    procedure SetTimeout(const Value: Integer); override;
    procedure ClearSets;
   public
    constructor Create; override;
    function CallAction: Boolean; override;
  end;
  
{$i sys/lkqueueeventerh.inc}
{$i sys/lepolleventerh.inc}

  function BestEventerClass: TLEventerClass;
  
implementation

uses
  syncobjs,
  lCommon;
  
var
  CS: TCriticalSection;
  
{ TLHandle }

procedure TLHandle.SetIgnoreError(const aValue: Boolean);
begin
  if FIgnoreError <> aValue then begin
    FIgnoreError := aValue;
    if Assigned(FEventer) then
      FEventer.HandleIgnoreError(Self);
  end;
end;

procedure TLHandle.SetIgnoreWrite(const aValue: Boolean);
begin
  if FIgnoreWrite <> aValue then begin
    FIgnoreWrite := aValue;
    if Assigned(FEventer) then
      FEventer.HandleIgnoreWrite(Self);
  end;
end;

procedure TLHandle.SetIgnoreRead(const aValue: Boolean);
begin
  if FIgnoreRead <> aValue then begin
    FIgnoreRead := aValue;
    if Assigned(FEventer) then
      FEventer.HandleIgnoreRead(Self);
  end;
end;

constructor TLHandle.Create;
begin
  FOnRead := nil;
  FOnWrite := nil;
  FOnError := nil;
  UserData := nil;
  FEventer := nil;
  FPrev := nil;
  FNext := nil;
  FFreeNext := nil;
  FFreeing := False;
  FDispose := False;
  FIgnoreWrite := False;
  FIgnoreRead := False;
  FIgnoreError := False;
end;

destructor TLHandle.Destroy;
begin
  if Assigned(FEventer) then
    FEventer.InternalUnplugHandle(Self);
end;

procedure TLHandle.Free;
begin
  CS.Enter;

  if Assigned(FEventer) and FEventer.FInLoop then
    FEventer.AddForFree(Self)
  else
    inherited Free;

  CS.Leave;
end;

{ TLTimer }
{
function TLTimer.GetInterval: Integer;
begin
  Result := Round(FInterval * MSecsPerDay);
end;

procedure TLTimer.SetEnabled(NewEnabled: integer);
begin
  FTimeout := Now + Interval;
  FEnabled := true;
end;

procedure TLTimer.SetInterval(const aValue: Integer);
begin
  FInterval := AValue / MSecsPerDay;
end;

procedure TLTimer.CallAction;
begin
  if FEnabled and Assigned(FOnTimer) and (Now - FStarted >= FInterval) then 
  begin
    FOnTimer(Self);
    if not FOneShot then
      FStarted := Now
    else
      FEnabled := false;
  end;
end;
}
{ TLEventer }

constructor TLEventer.Create;
begin
  FRoot := nil;
  FFreeRoot := nil;
  FFreeIter := nil;
  FInLoop := False;
  FCount := 0;
  FReferences := 1;
end;

destructor TLEventer.Destroy;
begin
  Clear;
end;

function TLEventer.GetCount: Integer;
begin
  Result := FCount;
end;

function TLEventer.GetTimeout: Integer;
begin
  Result := 0;
end;

procedure TLEventer.SetTimeout(const Value: Integer);
begin
end;

function TLEventer.Bail(const msg: string; const Ernum: Integer): Boolean;
begin
  Result := False; // always false, substitute for caller's result
  if Assigned(FOnError) then
    FOnError(msg + LStrError(Ernum), Self);
end;

procedure TLEventer.AddForFree(aHandle: TLHandle);
begin
  if not aHandle.FFreeing then begin
    aHandle.FFreeing := True;
    if not Assigned(FFreeIter) then begin
      FFreeIter := aHandle;
      FFreeRoot := aHandle;
    end else begin
      FFreeIter.FreeNext := aHandle;
      FFreeIter := aHandle;
    end;
  end;
end;

procedure TLEventer.FreeHandles;
var
  Temp, Temp2: TLHandle;
begin
  Temp := FFreeRoot;
  while Assigned(Temp) do begin
    Temp2 := Temp.FreeNext;
    Temp.Free;
    Temp := Temp2;
  end;
  FFreeRoot := nil;
  FFreeIter := nil;
end;

procedure TLEventer.HandleIgnoreError(aHandle: TLHandle);
begin

end;

procedure TLEventer.HandleIgnoreWrite(aHandle: TLHandle);
begin

end;

procedure TLEventer.HandleIgnoreRead(aHandle: TLHandle);
begin

end;

function TLEventer.GetInternalData(aHandle: TLHandle): Pointer;
begin
  Result := aHandle.FInternalData;
end;

procedure TLEventer.SetInternalData(aHandle: TLHandle; const aData: Pointer);
begin
  aHandle.FInternalData := aData;
end;

procedure TLEventer.SetHandleEventer(aHandle: TLHandle);
begin
  aHandle.FEventer := Self;
end;

procedure TLEventer.InternalUnplugHandle(aHandle: TLHandle);
begin
  if aHandle.FEventer = Self then begin
    if aHandle.FEventer.FInLoop then begin
      aHandle.FEventer.AddForFree(aHandle);
      Exit;
    end;

    aHandle.FEventer := nil; // avoid recursive AV
    if Assigned(aHandle.FPrev) then begin
      aHandle.FPrev.FNext := aHandle.FNext;
      if Assigned(aHandle.FNext) then
        aHandle.FNext.FPrev := aHandle.FPrev;
    end else if Assigned(aHandle.FNext) then begin
      aHandle.FNext.FPrev := aHandle.FPrev;
      if aHandle = FRoot then
        FRoot := aHandle.FNext;
    end else FRoot := nil;
    if FCount > 0 then
      Dec(FCount);
  end;
end;

function TLEventer.AddHandle(aHandle: TLHandle): Boolean;
begin
  Result := False;
  if not Assigned(aHandle.FEventer) then begin
    if not Assigned(FRoot) then begin
      FRoot := aHandle;
    end else begin
      if Assigned(FRoot.FNext) then begin
        FRoot.FNext.FPrev := aHandle;
        aHandle.FNext := FRoot.FNext;
      end;
      FRoot.FNext := aHandle;
      aHandle.FPrev := FRoot;
    end;
    aHandle.FEventer := Self;
    Inc(FCount);
    Result := True;
  end;
end;

function TLEventer.CallAction: Boolean;
begin
  Result := True;
  // override in ancestor
end;

procedure TLEventer.RemoveHandle(aHandle: TLHandle);
begin
  aHandle.Free;
end;

procedure TLEventer.UnplugHandle(aHandle: TLHandle);
begin
  CS.Enter;

  InternalUnplugHandle(aHandle);

  CS.Leave;
end;

procedure TLEventer.UnregisterHandle(aHandle: TLHandle);
begin
  // do nothing, specific to win32 LCLEventer crap (windows is shit)
end;

procedure TLEventer.LoadFromEventer(aEventer: TLEventer);
begin
  Clear;
  FRoot := aEventer.FRoot;
  FOnError := aEventer.FOnError;
end;

procedure TLEventer.Clear;
var
  Temp1, Temp2: TLHandle;
begin
  Temp1 := FRoot;
  Temp2 := FRoot;
  while Assigned(Temp2) do begin
    Temp1 := Temp2;
    Temp2 := Temp1.FNext;
    Temp1.Free;
  end;
  FRoot := nil;
end;

procedure TLEventer.AddRef;
begin
  Inc(FReferences);
end;

procedure TLEventer.DeleteRef;
begin
  if FReferences > 0 then
    Dec(FReferences);
  if FReferences = 0 then
    Free;
end;

{ TLSelectEventer }

constructor TLSelectEventer.Create;
begin
  inherited Create;
  FTimeout.tv_sec := 0;
  FTimeout.tv_usec := 0;
end;

function TLSelectEventer.GetTimeout: Integer;
begin
  if FTimeout.tv_sec < 0 then
    Result := -1
  else
    Result := (FTimeout.tv_sec * 1000) + FTimeout.tv_usec;
end;

procedure TLSelectEventer.SetTimeout(const Value: Integer);
begin
  if Value >= 0 then begin
    FTimeout.tv_sec := Value div 1000;
    FTimeout.tv_usec := Value mod 1000;
  end else begin
    FTimeout.tv_sec := -1;
    FTimeout.tv_usec := 0;
  end;
end;

procedure TLSelectEventer.ClearSets;
begin
  fpFD_ZERO(FReadFDSet);
  fpFD_ZERO(FWriteFDSet);
  fpFD_ZERO(FErrorFDSet);
end;

function TLSelectEventer.CallAction: Boolean;
var
  Temp, Temp2: TLHandle;
  n: Integer;
  MaxHandle: THandle;
  TempTime: TTimeVal;
begin
  if FInLoop then
    Exit;

  if not Assigned(FRoot) then begin
    Sleep(FTimeout.tv_sec * 1000 + FTimeout.tv_usec div 1000);
    Exit;
  end;

  FInLoop := True;
  Temp := FRoot;
  MaxHandle := 0;
  ClearSets;
  while Assigned(Temp) do begin
    if  (not Temp.FDispose       )  // handle still valid
    and (   (not Temp.IgnoreWrite)  // check write or
         or (not Temp.IgnoreRead )  // check read or
         or (not Temp.IgnoreError)) // check for errors
    then begin
      if not Temp.IgnoreWrite then
        fpFD_SET(Temp.FHandle, FWriteFDSet);
      if not Temp.IgnoreRead then
        fpFD_SET(Temp.FHandle, FReadFDSet);
      if not Temp.IgnoreError then
        fpFD_SET(Temp.FHandle, FErrorFDSet);
      if Temp.FHandle > MaxHandle then
        MaxHandle := Temp.FHandle;
    end;
    Temp2 := Temp;
    Temp := Temp.FNext;
    if Temp2.FDispose then
      Temp2.Free;
  end;

  TempTime := FTimeout;
  if FTimeout.tv_sec >= 0 then
    n := fpSelect(MaxHandle + 1, @FReadFDSet, @FWriteFDSet, @FErrorFDSet, @TempTime)
  else
    n := fpSelect(MaxHandle + 1, @FReadFDSet, @FWriteFDSet, @FErrorFDSet, nil);
  
  if n < 0 then
    Bail('Error on select', LSocketError);
  Result := n > 0;
  
  if Result then begin
    Temp := FRoot;
    while Assigned(Temp) do begin
      if not Temp.FDispose and (fpFD_ISSET(Temp.FHandle, FWriteFDSet) <> 0) then
        if Assigned(Temp.FOnWrite) and not Temp.IgnoreWrite then
          Temp.FOnWrite(Temp);
      if not Temp.FDispose and (fpFD_ISSET(Temp.FHandle, FReadFDSet) <> 0) then
        if Assigned(Temp.FOnRead) and not Temp.IgnoreRead then
          Temp.FOnRead(Temp);
      if not Temp.FDispose and (fpFD_ISSET(Temp.FHandle, FErrorFDSet) <> 0) then
        if Assigned(Temp.FOnError) and not Temp.IgnoreError then
          Temp.FOnError(Temp, 'Handle error' + LStrError(LSocketError));
      Temp2 := Temp;
      Temp := Temp.FNext;
      if Temp2.FDispose then
        AddForFree(Temp2);
    end;
  end;
  FInLoop := False;
  if Assigned(FFreeRoot) then
    FreeHandles;
end;

{$i sys/lkqueueeventer.inc}
{$i sys/lepolleventer.inc}

{$ifdef nochoice}

function BestEventerClass: TLEventerClass;
begin
  Result := TLSelectEventer;
end;

{$endif}

initialization
  CS := TCriticalSection.Create;

finalization
  CS.Free;

end.
