{
********************************************************************************

YaPeTaVi - Yet another Periodic Table Viewer
Copyright (C) 2000-2012 Geoffray Levasseur <geoffray.levasseurbrandin@numericable.fr>.
All rights reserved.
http://www.geoffray-levasseur.org/

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 3 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; if not, write to the Free Software Foundation, Inc., 59 Temple
Place - Suite 330, Boston, MA  02111-1307, USA.

********************************************************************************

Description:
   Console debuging facilities

}
unit uDebug;

{$mode objfpc}{$H+}

interface

uses
  Classes;

type
  TVerboseLevel = (vlFull, vlHigh, vlLow, vlNone);

var
  DebugLevel: Integer;
  VerboseLevel: TVerboseLevel;
  LogError: Boolean;
  sLogFileName: string;
  FLogFile: Text;

// init logging facilities
function InitDbg(const FileName: string): Boolean;

// close logging file
procedure TerminateDbg;

// print and log messages
procedure PrintDbg(Mes: string; Verbosity: TVerboseLevel);
procedure PrintDbg(Mes: string);
procedure PrintLnDbg(Mes: string; Verbosity: TVerboseLevel);
procedure PrintLnDbg(Mes: TStringList; Verbosity: TVerboseLevel);
procedure PrintLnDbg(Verbosity: TVerboseLevel);
procedure PrintLnDbg(Mes: string);
procedure PrintDbgStat(Stat: Boolean; Verbosity: TVerboseLevel);
procedure PrintDbgResult(Res: Boolean; Verbosity: TVerboseLevel);
procedure PrintDbgResult(Res: string; Verbosity: TVerboseLevel);

// put full informations about heap inside a string
function GetHeapStatus(aHS: THeapStatus): string;

implementation

uses
  Crt, SysUtils;

const
  dbgStatOk = '-';
  dbgStatWarn = '!';
  dbgOk = 'Ok';
  dbgFail = 'Fail';
  sTimeJump = '           '; //number of space = time length with braces

resourcestring
  rsErrorBase = '*** ERROR: ';
  rsWarningBase = '! WARNING: ';
  rsInfoBase = 'Info: ';
  rsDebugBase = '[Debug]: ';

  rsErrorLogrotate = 'E logrotate failed on file %s';
  rsErrorButLogrotate = 'E logrotate worked but the log file %s still exists';
  rsErrorCreatingLogfile = 'E could not create log file %s';
  rsErrorWritingLogFile = 'E unable to write in logfile %s';
  rsWarningNoLogfile = 'W no log file will be created, ' +
    'logging on stdout (console) only';
  rsInfoLoggingStarted = 'Logging started: %s';
  rsInfoLoggingTerminated = 'Closing log file %s: application terminated';

procedure LoggingError(Msg: string);
begin
  LogError := True;
  PrintLnDbg(Msg, vlLow);
  PrintLnDbg(rsWarningNoLogfile, vlLow);
end;

//create logfile and rotate old ones (10 max)
function InitDbg(const FileName: string): Boolean;
var
  Count: Byte;
  I: Integer;
  F: File;
  Orig, Dest: string;
begin
  {$I-}
  //init some vars
  Count := 1;
  Result := True;
  //first step: do the logrotate (10 logfile max fixed)
  if FileExists(Trim(FileName)) then
    while FileExists(Trim(FileName) + '.' + IntToStr(Count)) do
      Inc(Count)
  else
    Count := 0;
  if Count > 0 then
    for I := Count downto 1 do
    begin
      Assign(F, Trim(FileName) + '.' + IntToStr(I - 1));
      if I = 1 then
        Orig := Trim(FileName)
      else
        Orig := Trim(FileName) + '.' + IntToStr(I - 1);
      Dest := Trim(FileName) + '.' + IntToStr(I);
      if I >= 10 then
        if not FileIsReadOnly(Orig) then
          Erase(F)
        else
        begin
          LoggingError(Format(rsErrorLogrotate, [Trim(FileName)]));
          Result := False;
          Exit;
        end
      else
        if (not FileIsReadOnly(Orig)) and (not FileExists(Dest)) then
          RenameFile(Orig, Dest)
        else
        begin
          LoggingError(Format(rsErrorLogrotate, [Trim(FileName)]));
          Result := False;
          Exit;
        end;
    end;
  {$I+}
  //second step: check if file can be safely created and create it
  if FileExists(Trim(FileName)) then
  begin
    LoggingError(Format(rsErrorButLogrotate, [Trim(FileName)]));
    Result := False;
    Exit;
  end;
  Assign(FLogFile, Trim(FileName));
  {$I-}
  Rewrite(FLogFile);
  {$I+}
  if IOResult <> 0 then
  begin
    LoggingError(Format(rsErrorCreatingLogfile, [Trim(FileName)]));
    Result := False;
    Exit;
  end;
  //third step: write header and assign vars
  LogError := False;
  sLogFileName := Trim(FileName);
  PrintLnDbg(Format(rsInfoLoggingStarted, [sLogFileName]), vlLow);
end;

procedure TerminateDbg;
begin
  PrintLnDbg(Format(rsInfoLoggingTerminated, [sLogFileName]), vlLow);
  if (not LogError) and (Trim(sLogFileName) <> '') then
    CloseFile(FLogFile);
end;

function VerbosityToInt(V: TVerboseLevel): integer;
begin
  Result := 4; //default is all the messages to be shown
  case V of
    vlFull: Result := 4;
    vlHigh: Result := 2;
    vlLow: Result := 1;
    vlNone: Result := 0; //nothing is displayed
  end;
end;

procedure PrintDbg(Mes: string; Verbosity: TVerboseLevel);
var
  I: integer;
  Err: Boolean;
  C: Char;
  LineBegining: Boolean;
begin
  //showing debugging message only if Debug is defined
  {$IFNDEF Debug}
  if LineBegining and (Length(Mes) > 2) and (Mes[2] = ' ') and
    (Mes[1] = 'D') then
    Exit;
  {$ENDIF}
  //if it's not meant to be displayed get out of here
  if VerbosityToInt(Verbosity) > VerbosityToInt(VerboseLevel) then
    Exit;
  //init error state
  {$I-}
  Err := False;
  //display time only if we are on a new line
  try
    LineBegining := WhereX <= 1;
    if LineBegining or ((Length(Mes) > Length(sTimeJump)) and
      (Copy(Mes, 1, Length(sTimeJump)) = sTimeJump)) then
    begin
      Write('[' + TimeToStr(Time) + '] ');
      if (not LogError) and (Trim(sLogFileName) <> '') then
      begin
        Write(FLogFile, '[' + TimeToStr(Time) + '] ');
        Err := (IOResult <> 0) or Err;
      end;
      //this is used in case where indentation is needed in log
      if DebugLevel > 0 then
        for I := 0 to DebugLevel do
        begin
          Write('   '); //this is 3 spaces
          if (not LogError) and (Trim(sLogFileName) <> '') then
          begin
            Write(FLogFile, '   '); //this is 3 spaces
            Err := (IOResult <> 0) or Err;
          end;
        end;
    end;
    // we use the first char to know if it's error warning or info
    if LineBegining and (Length(Mes) > 2) and (Mes[2] = ' ') and
      (Mes[1] in ['E', 'W', 'i', 'D']) then
    begin
      C := Mes[1];
      Mes := Copy(Mes, 3, Length(Mes) - 2);
      case C of
        'E': Mes := rsErrorBase + Mes;
        'W': Mes := rsWarningBase + Mes;
        'i': Mes := rsInfoBase + Mes;
        'D': Mes := rsDebugBase + Mes;
      end;
    end;
    //write the message
    Write(Mes);
    if (not LogError) and (Trim(sLogFileName) <> '') then
    begin
      Write(FLogFile, Mes);
      Err := (IOResult <> 0) or Err;
    end;
  except
    Err := True;
  end;
  {$I+}
  //force truely writing the file
  if not LogError then
    Flush(FLogFile);
  //if error occurs somewhere, trigger an error
  if Err then
    LoggingError(Format(rsErrorWritingLogFile, [sLogFileName]));
end;

procedure PrintDbg(Mes: string);
begin
  PrintDbg(Mes, vlFull);
end;

procedure PrintLnDbg(Mes: TStringList; Verbosity: TVerboseLevel);
var
  I: Integer;
begin
  if VerbosityToInt(Verbosity) > VerbosityToInt(VerboseLevel) then
    Exit;
  for I := 0 to Mes.Count - 1 do
    if I = 0 then
      PrintLnDbg(Mes[I], Verbosity)
    else
      PrintDbg(sTimeJump + Mes[I] + sLineBreak, Verbosity);
end;

procedure PrintLnDbg(Mes: string);
begin
  PrintLnDbg(Mes, vlFull);
end;

procedure PrintLnDbg(Mes: string; Verbosity: TVerboseLevel);
begin
  if VerbosityToInt(Verbosity) > VerbosityToInt(VerboseLevel) then
    Exit;
  PrintDbg(Mes + sLineBreak, Verbosity);
end;

procedure PrintLnDbg(Verbosity: TVerboseLevel);
begin
  if VerbosityToInt(Verbosity) > VerbosityToInt(VerboseLevel) then
    Exit;
  Writeln;
  if (not LogError) and (Trim(sLogFileName) <> '') then
    Writeln(FLogFile);
end;

procedure PrintDbgStat(Stat: Boolean; Verbosity: TVerboseLevel);
begin
  if VerbosityToInt(Verbosity) > VerbosityToInt(VerboseLevel) then
    Exit;
  if Stat then
    PrintDbg(dbgStatOk, Verbosity)
  else
    PrintDbg(dbgStatWarn, Verbosity);
end;

procedure PrintDbgResult(Res: Boolean; Verbosity: TVerboseLevel);
begin
  if VerbosityToInt(Verbosity) > VerbosityToInt(VerboseLevel) then
    Exit;
  if Res then
    PrintLnDbg(dbgOk, Verbosity)
  else
    PrintLnDbg(dbgFail, Verbosity);
end;

procedure PrintDbgResult(Res: string; Verbosity: TVerboseLevel);
begin
  if VerbosityToInt(Verbosity) > VerbosityToInt(VerboseLevel) then
    Exit;
  PrintLnDbg(Res, Verbosity);
end;

function GetHeapStatus(aHS: THeapStatus): string;
begin
  Result := Format(
    'Heap Status:   ' + LineEnding + LineEnding +
    'Addr Space:    ' + #9 + ' %d ' + LineEnding +
    'Uncommitted:   ' + #9 + ' %d ' + LineEnding +
    'Committed:     ' + #9 + ' %d ' + LineEnding +
    'Allocated:     ' + #9 + ' %d ' + LineEnding +
    'Free:          ' + #9 + ' %d ' + LineEnding +
    'Free Small:    ' + #9 + ' %d ' + LineEnding +
    'Free Big:      ' + #9 + ' %d ' + LineEnding +
    'Unused:        ' + #9 + ' %d ' + LineEnding +
    'Overhead:      ' + #9 + ' %d ' + LineEnding +
    'Heap Errorcode:' + #9 + ' %d ',
    [aHS.TotalAddrSpace, aHS.TotalUncommitted, aHS.TotalCommitted,
    aHS.TotalAllocated, aHS.TotalFree, aHS.FreeSmall, aHS.FreeBig,
    aHS.Unused, aHS.Overhead, aHS.HeapErrorCode]);
end;

initialization
  {$IFDEF Debug}
    VerboseLevel := vlFull;
  {$ELSE}
    VerboseLevel := vlLow;
  {$ENDIF}
  DebugLevel := 0;
  LogError := True; //means that we can't write in the log file as it's not opened
end.

