program OWLChess;

uses Winprocs, WinTypes, Objects, OWindows, ODialogs, BWCC, AppUtils,
     Chessdll, Board, MoveList, ChessDlg, ChConst, FileDlgs, Strings,
     CommDlg, WinDos, CTimers;

{$R OWLChess}

const

  ChessSignature : array [0..33] of Char = 'Borland Pascal Chess saved game'#26#0;
  ChessFileFilter = 'Chess games'#0'*.chs'#0#0;

type

  TGameMode = (SinglePlayer, TwoPlayer, AutoPlay);

  PChessApp = ^TChessApp;
  TChessApp = object(TXtendedApp)
    procedure InitMainWindow; virtual;
    function  IdleAction: Boolean; virtual;
  end;

  PChessWindow = ^TChessWindow;
  TChessWindow = object(TWindow)
    Game: HChess;
    Board: PChessBoard;
    ThinkState: TSearchStatus;
    Mode : TGameMode;
    Player: TColor;
    MoveHistory: PMoveList;
    InfoPane: PChessInfoWindow;
    ThinkMenu: HMenu;
    GameFileName: array [0..fsPathName] of Char;
    Timer: array [cWhite..cBlack] of PChessTimer;
    ActiveTimer: PChessTimer;
    constructor Init(AParent: PWindowsObject; ATitle: PChar);
    destructor  Done; virtual;
    procedure SetupWindow; virtual;         { first place HWindow is valid }
    procedure WMDestroy(var Msg: TMessage); { last place HWindow is valid }
      virtual wm_First + wm_Destroy;
    function  GetClassName: PChar; virtual;
    procedure GetWindowClass(var WC: TWndClass); virtual;
    procedure RestartGame;
    function  CanClose: Boolean; virtual;
    function  IdleAction: Boolean;
    function  ShowMsg(const Ctx, MsgCode: Integer): Integer;
    procedure ReportGameState;
    procedure GameOver;
    function  SaveGame(FileName: PChar): Boolean;
    function  LoadGame(FileName: PChar): Boolean;
    procedure RecordMove(const Move: TMove);
    procedure StartComputerMove;
    procedure AcceptComputerMove;
    procedure AMSubmitMove(var Msg: TMessage);
      virtual am_SubmitMove;
    procedure CMNewGame(var Msg: TMessage);
      virtual cm_First + cm_NewGame;
    procedure CMLoadGame(var Msg: TMessage);
      virtual cm_First + cm_LoadGame;
    procedure CMSaveGame(var Msg: TMessage);
      virtual cm_First + cm_SaveGame;
    procedure CMSaveAs(var Msg: TMessage);
      virtual cm_First + cm_SaveAs;
{    procedure CMAutoPlay(var Msg: TMessage);
      virtual cm_First + cm_AutoPlay;
}    procedure CMPauseGame(var Msg: TMessage);
      virtual cm_First + cm_PauseGame;
    procedure CMUndoMove(var Msg: TMessage);
      virtual cm_First + cm_UndoMove;
    procedure CMRedoMove(var Msg: TMessage);
      virtual cm_First + cm_RedoMove;
    procedure CMSettings(var Msg: TMessage);
      virtual cm_First + cm_Settings;
    procedure CMStopThinking(var Msg: TMessage);
      virtual cm_First + cm_StopThinking;
    procedure WMTimer(var Msg: TMessage);
      virtual wm_First + wm_Timer;
    procedure WMSetCursor(var Msg: TMessage);
      virtual wm_First + wm_SetCursor;
  end;

constructor TChessWindow.Init(AParent: PWindowsObject; ATitle: PChar);
begin
  inherited Init(AParent, ATitle);
  Attr.X := 10;
  Attr.Y := 50;
  Attr.W := 350;
  Attr.H := 350;
  Attr.Style := ws_OverlappedWindow;
  Attr.Menu := LoadMenu(HInstance, PChar(idMainMenu));
  ThinkMenu := LoadMenu(HInstance, PChar(idThinkMenu));
  LoadINISettings;
  GameFileName[0] := #0;
  if ChessSettings.OnePlayer then
    Mode := SinglePlayer
  else
    Mode := TwoPlayer;
  Player := cWhite;
  Status := Context(cxChessError, Ord(NewGame(Game)));
  if Status <> 0 then Exit;
  ThinkState := GetSearchStatus(Game);
  Timer[cWhite] := New(PChessTimer, Init);
  Timer[cBlack] := New(PChessTimer, Init);
  ActiveTimer := nil;
  MoveHistory := New(PMoveList, Init(20, 10));
  Board := New(PChessBoard, Init(@Self, Game));
  InfoPane := New(PChessInfoWindow, Init(@Self, PChar(dlgInfoPane)));
end;

destructor  TChessWindow.Done;
begin
  Dispose(MoveHistory, Done);
  Dispose(Timer[cWhite], Done);
  Dispose(Timer[cBlack], Done);
  SaveINISettings;
  DisposeGame(Game);
  DestroyMenu(ThinkMenu);
  inherited Done;
end;

procedure TChessWindow.SetupWindow;
var
  W, WX, WY, H: Word;
  WR, CR, IR: TRect;
begin
  inherited SetupWindow;
  W := Board^.IdealWidth;
  H := W;
  GetWindowRect(HWindow, WR);
  GetClientRect(HWindow, CR);
  GetClientRect(InfoPane^.HWindow, IR);
  WX := (WR.Right - WR.Left) - CR.Right;
  WY := (WR.Bottom - WR.Top) - CR.Bottom;
  if H < IR.Bottom then
    H := IR.Bottom;
  SetWindowPos(HWindow, 0, 0, 0, W + 75 + IR.Right + WX,
                              H + 50 + WY, swp_NoZOrder or swp_NoMove);
  SetWindowPos(Board^.HWindow, 0, 25, 25, W, W, swp_NoZOrder);
  SetWindowPos(InfoPane^.HWindow, 0, W + 50, 25, 0, 0,
                               swp_NoZOrder or swp_NoSize);
  ShowWindow(Board^.HWindow, sw_ShowNormal);
  SetTimer(HWindow, 1, ChessSettings.RefreshRate, nil);
end;

procedure TChessWindow.WMDestroy(var Msg: TMessage);
begin
  KillTimer(HWindow, 1);
  inherited WMDestroy(Msg);
end;

function  TChessWindow.GetClassName: PChar;
begin
  GetClassName := 'TPWOWLChess';
end;

procedure TChessWindow.GetWindowClass(var WC: TWndClass);
var
  LB: TLogBrush;
begin
  inherited GetWindowClass(WC);
  WC.Style := cs_ByteAlignWindow;
  WC.hCursor := 0;
  { Duplicate the BWCCPattern brush.  hbrBackground brush will be destroyed
    when our window is closed.  If we didn't duplicate this brush, but just
    used BWCCGetPattern's result directly, BWCC could be left without
    a valid background brush when our window closes.  }
  GetObject(BWCCGetPattern, SizeOf(LB), @LB);
  WC.hbrBackground := CreateBrushIndirect(LB);
  { !! add icon here }
end;

procedure TChessWindow.RestartGame;
var
  Cursor : HCursor;
begin
  UpdateWindow(HWindow);  { Clean up after the dialog that just closed }
  Cursor := SetCursor(LoadCursor(0, idc_Wait));
  if ActiveTimer <> nil then
    ActiveTimer^.Stop;
  Timer[cWhite]^.Clear;
  Timer[cBlack]^.Clear;
  MoveHistory^.FreeAll;
  EnableMenuItem(Attr.Menu, cm_UndoMove, mf_ByCommand or
                                         mf_Disabled or mf_Grayed);
  EnableMenuItem(Attr.Menu, cm_RedoMove, mf_ByCommand or
                                         mf_Disabled or mf_Grayed);
  DisposeGame(Game);
  ShowMsg(cxChessError, Ord(NewGame(Game)));
  ThinkState := GetSearchStatus(Game);
  Board^.ResetBoard(Game);
  SetCursor(Cursor);
end;

function TChessWindow.CanClose: Boolean;
begin
  CanClose := inherited CanClose and
              ((ThinkState = ssGameOver) or
               (MoveHistory^.Count = 0) or
               (MessageBox(HWindow, PChar(strCancelGame),
                 PChar(strLeaveGame), mb_YesNo) = id_Yes));
  UpdateWindow(HWindow);    { Clean up after the message box asap }
end;

function  TChessWindow.IdleAction: Boolean;
var
  OldState: TSearchStatus;
  Value: Integer;
  Line: array [0..15] of TMove;
begin
  OldState := ThinkState;
  if (OldState = ssMoveSearch) and (ActiveTimer <> nil) then
    ActiveTimer^.Start;
  Think(Game, ChessSettings.ThinkTime.Position, ThinkState);
    { Return True if we want to continue to get IdleAction calls ASAP,
      Return False if we don't need more IdleAction immediately }
  if (OldState = ssMoveSearch) and (ActiveTimer <> nil) then
    ActiveTimer^.Stop;
  IdleAction := False;
  if (ThinkState = ssComplete) and (OldState <> ssComplete) then
    AcceptComputerMove
  else
  if (ThinkState = ssGameOver) and (Oldstate <> ssGameOver) then
    GameOver
  else
    IdleAction := True;  { continue calling IdleAction - still thinking }
end;

function  TChessWindow.ShowMsg(const Ctx, MsgCode: Integer): Integer;
var
  S: array [0..100] of Char;
begin
  S[0] := #0;
  if MsgCode <> 0 then
    LoadString(HInstance, Ctx + MsgCode, S, SizeOf(S));
  InfoPane^.Msg^.SetText(S);
  ShowMsg := MsgCode;
end;

procedure TChessWindow.ReportGameState;
var State: TChessStatus;
    Count: Integer;
begin
  State := GetChessStatus(Game, Count);
  ShowMsg(cxChessState, ord(State));
  InfoPane^.Update(Game, Timer[cWhite]^.GetCurrentTicks,
                         Timer[cBlack]^.GetCurrentTicks);
end;

procedure TChessWindow.RecordMove(const Move: TMove);
begin
  if MoveHistory^.Count = 0 then  { Enable the menu on first move }
    EnableMenuItem(Attr.Menu, cm_UndoMove, mf_ByCommand or mf_Enabled);
  if MoveHistory^.RedoAvail then  { not any more...}
    EnableMenuItem(Attr.Menu, cm_RedoMove, mf_ByCommand or mf_Disabled
                                                        or mf_Grayed);
  MoveHistory^.AddMove(Move);
end;

procedure TChessWindow.StartComputerMove;
var
  TimeLimit : Longint;
begin
  with ChessSettings do
  begin
    if NoLimit then
      TimeLimit := MaxLongint
    else {if LimitTurn then}  {!! do matchuser and limitgame}
      TimeLimit := TurnTime * 18;
  end;
  ComputerMove(Game, TimeLimit);
end;


procedure TChessWindow.AcceptComputerMove;
var
  Move: TMove;
begin
  GetLastMove(Game, Move);
  RecordMove(Move);
  Board^.ExecuteMove(Move);
  ReportGameState;
  if Mode = AutoPlay then
    StartComputerMove
  else
  begin
    Board^.Enable;
    SetMenu(HWindow, Attr.Menu);
    Board^.ResetValidMoves;
    ActiveTimer := Timer[GetPlayer(Game)];
    ActiveTimer^.Start;
    EnableMenuItem(Attr.Menu, cm_PauseGame, mf_ByCommand or mf_Enabled);
  end;
end;

procedure TChessWindow.GameOver;
begin
  if GetPlayer(Game) = cWhite then
    MessageBox(HWindow, PChar(strWhiteWins), PChar(strGameOver), mb_OK)
  else
    MessageBox(HWindow, PChar(strBlackWins), PChar(strGameOver), mb_OK);
end;

function TChessWindow.LoadGame(FileName: PChar): Boolean;
var
  S: PBufStream;
  Test: array [0..SizeOf(ChessSignature)] of Char;
  NewMoveList : PMoveList;
  X: Integer;

  function ReplayMoves(P: PMove): Boolean; far;
  begin
    SubmitMove(Game, P^.Change);
    Board^.ExecuteMove(P^);
    ReplayMoves := (X >= MoveHistory^.UndoPos);
    Inc(X);
  end;

begin
  LoadGame := False;
  S := New(PBufStream, Init(FileName, stOpenRead, 1024));
  S^.Read(Test, SizeOf(ChessSignature));
  if S^.Status <> stOK then
    MessageBox(HWindow, PChar(Context(cxStreamError, S^.Status)),
                        PChar(strLoadError), mb_Ok)
  else
  if StrLComp(ChessSignature, Test, SizeOf(ChessSignature)) <> 0 then
    MessageBox(HWindow, PChar(strNotAChessFile),
                        PChar(strInvalidFile), mb_Ok)
  else
  begin
    NewMoveList := PMoveList(S^.Get);
    if S^.Status <> stOK then
      MessageBox(HWindow, PChar(Context(cxStreamError, S^.Status)),
                          PChar(strLoadError), mb_Ok)
    else
    begin
      RestartGame;
      Dispose(MoveHistory, Done);
      MoveHistory := NewMoveList;
      X := 0;
      MoveHistory^.FirstThat(@ReplayMoves);
      if MoveHistory^.UndoAvail then
        EnableMenuItem(Attr.Menu, cm_UndoMove, mf_ByCommand or mf_Enabled);
      if MoveHistory^.RedoAvail then
        EnableMenuItem(Attr.Menu, cm_RedoMove, mf_ByCommand or mf_Enabled);
      Board^.ResetValidMoves;
      ReportGameState;
      LoadGame := True;
    end;
  end;
  Dispose(S, Done);
end;

function TChessWindow.SaveGame(FileName: PChar): Boolean;
var
  S: PBufStream;
begin
  S := New(PBufStream, Init(FileName, stCreate, 1024));
  S^.Write(ChessSignature, SizeOf(ChessSignature));
  S^.Put(MoveHistory);
  if S^.Status <> stOK then
    MessageBox(HWindow, PChar(Context(cxStreamError, S^.Status)),
                        PChar(strSaveError), mb_Ok);
  SaveGame := S^.Status = stOK;
  Dispose(S, Done);
end;

procedure TChessWindow.AMSubmitMove(var Msg: TMessage);
var
  Move: TMove;
begin
  Msg.Result := ShowMsg(cxChessError, Ord(SubmitMove(Game, PChange(Msg.LParam)^)));
    { Result = True if SubmitMove returns zero, else Result = False }
  LongBool(Msg.Result) := not LongBool(Msg.Result);
  if LongBool(Msg.Result) then
  begin
    if ActiveTimer <> nil then
      ActiveTimer^.Stop;
    ActiveTimer := Timer[GetPlayer(Game)];
    GetLastMove(Game, Move);       { Retrieve the full move from the engine }
    RecordMove(Move);              { Enter in history list, enable Redo menu}
    Board^.ExecuteMove(Move);      { Adjust the board }
    ReportGameState;
(*    if ChessSettings.TwoPlayer then
    begin
      Player := GetPlayer(Game);
      Board^.ResetValidMoves;       { Refresh the valid move tables }
      ActiveTimer^.Start;
    end
    else
*)    begin
      StartComputerMove;
      ThinkState := GetSearchStatus(Game);
      SetMenu(HWindow, ThinkMenu);
      Board^.Disable;              { Prevent mouse dragging of pieces }
      EnableMenuItem(Attr.Menu, cm_PauseGame, mf_ByCommand or mf_Disabled or mf_Grayed);
    end;
  end;
end;

procedure TChessWindow.CMNewGame(var Msg: TMessage);
begin
  if (ThinkState = ssGameOver) or
     (MoveHistory^.Count = 0) or
     (MessageBox(HWindow, PChar(strCancelGame),
        PChar(strStartNewGame), mb_YesNo) = id_Yes) then
  begin
    RestartGame;
  end;
end;

procedure TChessWindow.CMLoadGame(var Msg: TMessage);
var
  Temp : array [0..fsPathName] of Char;
begin
  StrCopy(Temp, GameFileName);
  if ((ThinkState = ssGameOver) or
     (MoveHistory^.Count = 0) or
     (MessageBox(HWindow, PChar(strCancelGame),
        PChar(strLoadSavedGame), mb_YesNo) = id_Yes))
    and
     (XApp^.ExecDialog(New(PCDFileOpen, Init(@Self,
        ofn_FileMustExist, Temp, SizeOf(Temp),
        ChessFileFilter))) = idOk) then
    if LoadGame(Temp) then
      StrCopy(GameFileName, Temp);
end;

procedure TChessWindow.CMSaveGame(var Msg: TMessage);
begin
  if GameFileName[0] = #0 then
    CMSaveAs(Msg)
  else
    SaveGame(GameFileName);
end;

procedure TChessWindow.CMSaveAs(var Msg: TMessage);
var
  Temp : array [0..fsPathName] of Char;
begin
  StrCopy(Temp, GameFileName);
  if XApp^.ExecDialog(New(PCDFileSaveAs, Init(@Self,
      ofn_PathMustExist, Temp, SizeOf(Temp), ChessFileFilter))) = idOk then
    if SaveGame(Temp) then
      StrCopy(GameFileName, Temp);
end;

procedure TChessWindow.CMPauseGame(var Msg: TMessage);
var
  P: PChar;
begin
  if ActiveTimer = Timer[Player] then
  begin
    ActiveTimer^.Stop;
    ActiveTimer := nil;
    ModifyMenu(Attr.Menu, cm_PauseGame, mf_ByCommand or mf_String,
                           cm_PauseGame, StrNewRes(P, PChar(strResumeMenu)));
    StrDispose(P);
    Board^.Disable;
  end
  else
    if ActiveTimer = nil then
    begin
      ActiveTimer := Timer[GetPlayer(Game)];
      ActiveTimer^.Start;
      ModifyMenu(Attr.Menu, cm_PauseGame, mf_ByCommand or mf_String,
                             cm_PauseGame, StrNewRes(P, PChar(strPauseMenu)));
      StrDispose(P);
      Board^.Enable;
    end;
end;


procedure TChessWindow.CMUndoMove(var Msg: TMessage);
var
  M : TMove;
  RedoBefore, UndoBefore: Boolean;
begin
  { No error checking is performed here - it is assumed that the
    menu enable/disable code will only allow the user
    to select this menu item when there is a valid undo available. }

  UndoBefore := MoveHistory^.UndoAvail;
  RedoBefore := MoveHistory^.RedoAvail;

  MoveHistory^.Undo(M);
  RetractMove(Game, M);
  Board^.RetractMove(M);

  if Mode = SinglePlayer then  { Undo both player's and computer's move }
  begin
    MoveHistory^.Undo(M);
    RetractMove(Game, M);
    Board^.RetractMove(M);
  end;
  if MoveHistory^.RedoAvail and not RedoBefore then
    EnableMenuItem(Attr.Menu, cm_RedoMove, mf_ByCommand or mf_Enabled);
  if (not MoveHistory^.UndoAvail) and UndoBefore then
   EnableMenuItem(Attr.Menu, cm_UndoMove, mf_ByCommand or
                                          mf_Disabled or mf_Grayed);
  Board^.ResetValidMoves;
end;

procedure TChessWindow.CMRedoMove(var Msg: TMessage);
var
  M : TMove;
  RedoBefore, UndoBefore: Boolean;
begin
  { No error checking is performed here - it is assumed that the
    menu enable/disable code will only allow the user
    to select this menu item when there is a valid redo available. }
  UndoBefore := MoveHistory^.UndoAvail;
  RedoBefore := MoveHistory^.RedoAvail;

  MoveHistory^.Redo(M);
  SubmitMove(Game, M.Change);
  Board^.ExecuteMove(M);

  if Mode = SinglePlayer then
  begin
    MoveHistory^.Redo(M);  { Redo both player's and computer's moves }
    SubmitMove(Game, M.Change);
    Board^.ExecuteMove(M);
  end;

  { Update the menus, but only when the undo/redo state changes
    (to avoid menubar flicker caused by unnecessary menu changes) }
  if (not MoveHistory^.RedoAvail) and RedoBefore then
    EnableMenuItem(Attr.Menu, cm_RedoMove, mf_ByCommand or
                                           mf_Disabled or mf_Grayed);
  if MoveHistory^.UndoAvail and not UndoBefore then
    EnableMenuItem(Attr.Menu, cm_UndoMove, mf_ByCommand or mf_Enabled);
  Board^.ResetValidMoves;
end;

procedure TChessWindow.CMSettings(var Msg: TMessage);
begin
  XApp^.ExecDialog(new(PSettingsDlg, Init(@Self, PChar(dlgSettings),
                                                 ChessSettings)));
end;

procedure TChessWindow.CMStopThinking(var Msg: TMessage);
begin
  if ThinkState = ssMoveSearch then
    ForceMove(Game);     { Move search will terminate at next Think call }
end;

procedure TChessWindow.WMTimer(var Msg: TMessage);
begin
  InfoPane^.Update(Game, Timer[cWhite]^.GetCurrentTicks,
                         Timer[cBlack]^.GetCurrentTicks);
end;

procedure TChessWindow.WMSetCursor(var Msg: TMessage);
begin
  DefWndProc(Msg);
  Msg.Result := 1;  { Cancel any pending WMSetCursor in children }
  if (ThinkState = ssMoveSearch) and (Msg.LParamLo = HTClient) then
    SetCursor(LoadCursor(0, PChar(idc_Wait)))
  else
  begin
    if Msg.WParam = Board^.HWindow then
      Msg.Result := 0   { Allow Board to use its own cursor }
    else
      SetCursor(LoadCursor(0, PChar(idc_Arrow)));
  end;
end;


procedure TChessApp.InitMainWindow;
begin
  MainWindow := new(PChessWindow, Init(nil, 'OWL Chess'));
end;

function TChessApp.IdleAction: Boolean;
begin
  IdleAction := PChessWindow(MainWindow)^.IdleAction;
end;

var
  App: TChessApp;

begin
  RegisterType(RMoveList);
  App.Init('OWL Chess');
  App.Run;
  App.Done;
end.
