{* UltraStar Deluxe - Karaoke Game
 *
 * UltraStar Deluxe is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING. If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 * $URL$
 * $Id$
 *}

unit UHookableEvent;

interface

{$IFDEF FPC}
  {$MODE Delphi}
{$ENDIF}

{$I switches.inc}

uses ULua;

type
  { Record holding information about a hook of an event }
  PHook = ^THook;
  THook = record
    Handle: Integer; //< Handle to identify the hook, e.g. for unhooking by plugin
    Parent: Integer; //< Lua Core Handle this hook belongs to

    Func: String;   //< Name of the global that holds the function

    Next: PHook; //< Next Hook in list (nil for the first)
  end;

  { procedure is called before each call to the hooking lua functions, to push values on stack
    returns the number of pushed arguments}
  PrepareStackProc = Function(L: PLua_State): Integer;

  { class representing a hookable event }
  THookableEvent = class
    private
      iHandle: Integer; //< used to unregister at lua core
      LastHook: PHook;  //< last hook in hook list, first to be called
      NextHookHandle: Integer;  //< handle to identify next hook

      sName: String;    //< the events name

      PrepareStack: PrepareStackProc; //< prepare stack procedure passed to constructor
      CallinProcess: boolean; //< true if a chain call is in process, to prepare unhooking during calls
      HooksToRemove: array of PHook; // hooks to delete after chaincall

      procedure RemoveWaitingHooks;
    public
      constructor Create(Name: String; const Proc: PrepareStackProc = nil);

      property Name: String read sName;             //< returns the events name
      property Handle: Integer read iHandle;        //< returns the events name

      procedure Hook(L: Plua_State; Parent: Integer; Func: String); //< pushes hook object/table to the lua stack
      procedure UnHook(L: Plua_State; hHook: Integer);              //< unhook by plugin. push true or error string to lua stack

      procedure UnHookByParent(Parent: Integer);     //< deletes all hooks by a specified parent (unhook by core)

      function CallHookChain(Breakable: Boolean): PLua_State;   //< calls the events hookchain. if breakable, plugin can breake the chain by returning a value != 0 or false or nil

      destructor Destroy; override;
  end;

{ the default function for THookableEvent.PrepareStack it don't pass any arguments }
function PrepareStack_Dummy(L: PLua_State): Integer;

{ function in resulting hook table. it calls the unhook command of the event on plugins demand }
function LuaHook_UnHook(L: Plua_State): Integer; cdecl;

implementation
uses ULuaCore;

constructor THookableEvent.Create(Name: String; const Proc: PrepareStackProc);
begin
  inherited Create;
  
  Self.sName := Name;

  if (@Proc = nil) then
    Self.PrepareStack := @PrepareStack_Dummy
  else
    Self.PrepareStack := Proc;

  //init LastHook pointer w/ nil
  LastHook := nil;
  NextHookHandle := 1;

  iHandle := LuaCore.RegisterEvent(Self);
end;

destructor THookableEvent.Destroy;
var
  Prev: PHook;
  Cur:  PHook;
begin
  //delete all hooks
  Cur := LastHook;
  While (Cur <> nil) do
  begin
    Prev := Cur;
    Cur := Prev.Next;

    Dispose(Prev);
  end;

  //remove from luacores list
  LuaCore.UnRegisterEvent(iHandle);

  inherited;
end;

{ adds hook to events list and pushes hook object/table to the lua stack }
procedure  THookableEvent.Hook(L: PLua_State; Parent: Integer; Func: String);
  var
    Item: PHook;
    P: TLuaPlugin;
begin
  P := LuaCore.GetPluginById(Parent);
  if (P <> nil) then
  begin
    // get mem and fill it w/ data
    New(Item);
    Item.Handle := NextHookHandle;
    Inc(NextHookHandle);

    Item.Parent := Parent;
    Item.Func := Func;

    // add at front of the hook chain
    Item.Next := LastHook;
    LastHook := Item;

    //we need 2 free stack slots
    lua_checkstack(L, 2);

    //create the hook table, we need 2 elements (event name and unhook function)
    lua_createtable(L, 0, 2);

    //push events name
    lua_pushstring(L, PAnsiChar(Name));

    //add the name to the table
    lua_setfield(L, -2, 'Event');

    //push hook id to the stack
    lua_pushinteger(L, Item.Handle);

    //create a c closure, append one value from stack(the id)
    //this will pop both, the function and the id
    lua_pushcclosure(L, LuaHook_UnHook, 1);

    //add the function to our table
    lua_setfield(L, -2, 'Unhook');

    //the table is left on the stack, it is our result
  end;
end;

{ removes hooks in HookstoRemove array from chain }
procedure THookableEvent.RemoveWaitingHooks;
  function IsInArray(Cur: PHook): boolean;
    var I: Integer;
  begin
    Result := false;
    for I := 0 to high(HooksToRemove) do
      if (HooksToRemove[I] = Cur) then
      begin
        Result := true;
        Break;
      end;
  end;

  var
    Cur, Prev: PHook;
begin
  Prev := nil;
  Cur := LastHook;

  while (Cur <> nil) do
  begin
    if (IsInArray(Cur)) then
    begin //we found the hook
      if (prev <> nil) then
        Prev.Next := Cur.Next
      else //last hook found
        LastHook := Cur.Next;

      //free hooks memory
      Dispose(Cur);

      if (prev <> nil) then
        Cur := Prev.Next
      else
        Cur := LastHook;
    end
    else
    begin
      Prev := Cur;
      Cur := Prev.Next;
    end;
  end;

  SetLength(HooksToRemove, 0);
end;

{ unhook by plugin. push true or error string to lua stack }
procedure  THookableEvent.UnHook(L: Plua_State; hHook: Integer);
  var
    Cur, Prev: PHook;
    Len: integer;
begin
  if (hHook < NextHookHandle) and (hHook > 0) then
  begin
    //Search for the Hook
    Prev := nil;
    Cur := LastHook;

    while (Cur <> nil) do
    begin
      if (Cur.Handle = hHook) then
      begin //we found the hook
        if not CallinProcess then
        begin // => remove it
          if (prev <> nil) then
            Prev.Next := Cur.Next
          else //last hook found
            LastHook := Cur.Next;

          //free hooks memory
          Dispose(Cur);
        end
        else
        begin // add to list of hooks to remove
          Len := Length(HooksToRemove);
          SetLength(HooksToRemove, Len + 1);
          HooksToRemove[Len] := Cur;
        end;
        
        //indicate success
        lua_pushboolean(L, True);
        exit; //break the chain and exit the function
      end;
      Prev := Cur;
      Cur := Prev.Next;
    end;

    lua_pushstring(L, PAnsiChar('handle already unhooked')); //the error description
  end
  else
    lua_pushstring(L, PAnsiChar('undefined hook handle')); //the error description
end;

{ deletes all hooks by a specified parent (unhook by core) }
procedure  THookableEvent.UnHookByParent(Parent: Integer);
  var
    Cur, Prev: PHook;
begin
  Prev := nil;
  Cur := LastHook;

  While (Cur <> nil) do
  begin
    if (Cur.Parent = Parent) then
    begin //found a hook from parent => remove it
      if (Prev <> nil) then
        Prev.Next := Cur.Next
      Else
        LastHook := Cur.Next;

      Dispose(Cur);

      if (Prev <> nil) then
        Cur := Prev.Next
      else
        Cur := LastHook;
    end
    else //move through the chain
    begin
      Prev := Cur;
      Cur := Prev.Next;
    end;
  end;
end;

{ calls the events hookchain. if breakable, plugin can breake the chain
  by returning a value
  breakable is pushed as the first parameter to the hooking functions
  if chain is broken the LuaStack is returned, with all results left
  you may call lua_clearstack }
function  THookableEvent.CallHookChain(Breakable: Boolean): Plua_State;
  var
    Cur: PHook;
    P: TLuaPlugin;
begin
  Result := nil;

  CallinProcess := true;
  
  Cur := LastHook;
  While (Cur <> nil) do
  begin
    P := LuaCore.GetPluginById(Cur.Parent);
    lua_pushboolean(P.LuaState, Breakable);

    if  (P.CallFunctionByName(Cur.Func, 1 + PrepareStack(P.LuaState), LUA_MULTRET))
    and Breakable
    and (lua_gettop(P.LuaState) > 0) then
    begin //Chain Broken
      Result := P.LuaState;
      Break;
    end;

    Cur := Cur.Next;
  end;

  RemoveWaitingHooks;
  CallinProcess := false;
end;

{ the default function for THookableEvent.PrepareStack it don't pass any arguments }
function PrepareStack_Dummy(L: PLua_State): Integer;
begin
  Result := 0;
end;

{ function in resulting hook table. it calls the unhook command of the event on plugins demand }
function LuaHook_UnHook(L: Plua_State): Integer; cdecl;
  var
    Name: string;
    Event: THookableEvent;
    hHook: integer;
begin
  Result := 0;

  if not lua_isTable(L, 1) then
    LuaL_Error(L, 'Can''t find hook table in LuaHook_Unhook. Please call Unhook with method separator (colon) instead of a point.');

  // get event name
  Lua_GetField(L, 1, 'Event');
  if not lua_isString(L, -1) then
    LuaL_Error(L, 'Can''t get event name in LuaHook_Unhook');

  Name := Lua_ToString(L, -1);

  // get event by name
  Event := LuaCore.GetEventbyName(Name);

  // free stack slots
  Lua_pop(L, Lua_GetTop(L));

  if (Event = nil) then
    LuaL_Error(L, PAnsiChar('event ' + Name + ' does not exist (anymore?) in LuaHook_Unhook'));

  // get the hookid
  hHook := lua_ToInteger(L, lua_upvalueindex(1));

  Event.UnHook(L, hHook);
end;

end.