{
 ***************************************************************************
 *                                                                         *
 *   This source 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 code 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.                              *
 *                                                                         *
 *   A copy of the GNU General Public License is available on the World    *
 *   Wide Web at <http://www.gnu.org/copyleft/gpl.html>. You can also      *
 *   obtain it by writing to the Free Software Foundation,                 *
 *   Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1335, USA.   *
 *                                                                         *
 ***************************************************************************

  Author: Mattias Gaertner

  Abstract:
    Defines TSourceLog which manage a source (= an ansistring) and all changes
    like inserting, deleting and moving parts of it.
}
unit SourceLog;

{$ifdef fpc}{$mode objfpc}{$endif}{$H+}

interface

{$I codetools.inc}

uses
  {$IFDEF MEM_CHECK}
  MemCheck,
  {$ENDIF}
  Classes, SysUtils,
  // LazUtils
  LazFileUtils, LazUTF8, LazDbgLog, LazStringUtils;

type
  TSourceLog = class;

  TSourceLogEntryOperation = (sleoInsert, sleoDelete, sleoMove);
  TOnSourceLogInsert = procedure(Sender: TSourceLog; Pos: integer;
                        const Txt: string) of object;
  TOnSourceLogDelete = procedure(Sender: TSourceLog; Pos, Len: integer)
                        of object;
  TOnSourceLogMove = procedure(Sender: TSourceLog; Pos, Len, MoveTo: integer)
                      of object;
  TOnSourceLogDecodeLoaded = procedure(Sender: TSourceLog; const Filename: string;
                        var Source, DiskEncoding, MemEncoding: string) of object;
  TOnSourceLogEncodeSaving = procedure(Sender: TSourceLog;
                          const Filename: string; var Source: string) of object;

  TSourceLogEntry = class
  private
  public
    Position: integer;
    Len: integer;
    MoveTo: integer;
    LineEnds: integer; // number of line ends in txt
    LengthOfLastLine: integer;
    Txt: string;
    Operation: TSourceLogEntryOperation;
    procedure AdjustPosition(var APosition: integer);
    constructor Create(APos, ALength, AMoveTo: integer; const ATxt: string;
      AnOperation: TSourceLogEntryOperation);
  end;
  
  TOnSourceChange = procedure(Sender: TSourceLog; Entry: TSourceLogEntry)
                         of object;

  { TSourceLogMarker }

  TSourceLogMarker = class
  private
  public
    Position: integer;
    NewPosition: integer;
    Deleted: boolean;
    Data: Pointer;
    Log: TSourceLog;
    destructor Destroy; override;
  end;

  TLineRange = packed record
    StartPos, EndPos: integer;
  end;
  PLineRange = ^TLineRange;

  { TSourceLog }

  TSourceLog = class
  private
    FDiskEncoding: string;
    FDiskLineEnding: string;
    FLineCount: integer;
    FLineRanges: PLineRange;
    FMemEncoding: string;
    FOnDecodeLoaded: TOnSourceLogDecodeLoaded;
    FOnEncodeSaving: TOnSourceLogEncodeSaving;
              // array of TLineRange
    FSrcLen: integer;
    FLog: TFPList; // list of TSourceLogEntry
    FMarkers: TFPList; // list of TSourceLogMarker;
    FModified: boolean;
    FOnInsert: TOnSourceLogInsert;
    FOnDelete: TOnSourceLogDelete;
    FOnMove: TOnSourceLogMove;
    FChangeHooks: {$ifdef fpc}^{$else}array of {$endif}TOnSourceChange;
    FChangeHookCount: integer;
    FChangeHookDelayed: boolean;
    FSource: string;
    FChangeStep: integer;
    FReadOnly: boolean;
    FWriteLock: integer;
    FChangeHookLock: integer;
    procedure SetSource(const NewSrc: string);
    function GetItems(Index: integer): TSourceLogEntry;
    procedure SetItems(Index: integer; AnItem: TSourceLogEntry);
    function GetMarkers(Index: integer): TSourceLogMarker;
    procedure BuildLineRanges;
    procedure SetReadOnly(const Value: boolean);
    function IndexOfChangeHook(AChangeHook: TOnSourceChange): integer;
  protected
    procedure IncreaseChangeStep; virtual; // any change
    procedure DoSourceChanged; virtual; // source change
    procedure DecodeLoaded(const AFilename: string;
                        var ASource, ADiskEncoding, AMemEncoding: string); virtual;
    procedure EncodeSaving(const AFilename: string; var ASource: string); virtual;
  public
    Data: Pointer;
    LastError: string;
    function LineCount: integer;
    function GetLine(Index: integer; WithLineEnd: boolean = true): string; // 0-based
    function GetLineLength(Index: integer): integer; // 0-based
    procedure GetLineRange(Index: integer; out LineRange: TLineRange); // 0-based
    function GetLineStart(Index: integer): integer; // 1-based
    property Items[Index: integer]: TSourceLogEntry
       read GetItems write SetItems; default;
    function Count: integer; // # Items
    property SourceLength: integer read fSrcLen;
    function ClearEntries: boolean;
    property ChangeStep: integer read FChangeStep;
    property Markers[Index: integer]: TSourceLogMarker read GetMarkers;
    function MarkerCount: integer;
    function AddMarker(Position: integer; SomeData: Pointer): TSourceLogMarker;
    function AddMarkerXY(Line, Column: integer; SomeData: Pointer): TSourceLogMarker;
    procedure AdjustPosition(var APosition: integer);
    procedure NotifyHooks(Entry: TSourceLogEntry);
    procedure IncreaseHookLock;
    procedure DecreaseHookLock;
    property Source: string read FSource write SetSource;
    property Modified: boolean read FModified write FModified;
    // Line and Column begin at 1
    procedure LineColToPosition(Line, Column: integer; out Position: integer);
    procedure AbsoluteToLineCol(Position: integer; out Line, Column: integer);
    function LineColIsOutside(Line, Column: integer): boolean;
    function LineColIsSpace(Line, Column: integer): boolean;
    function AbsoluteToLineColStr(Position: integer): string;
    procedure Insert(Pos: integer; const Txt: string);
    procedure Delete(Pos, Len: integer);
    procedure Replace(Pos, Len: integer; const Txt: string);
    procedure Move(Pos, Len, MoveTo: integer);
    function LoadFromFile(const Filename: string): boolean; virtual;
    function SaveToFile(const Filename: string): boolean; virtual;
    function GetLines(StartLine, EndLine: integer): string;
    function IsEqual(sl: TStrings): boolean;
    function OldIsEqual(sl: TStrings): boolean;
    procedure Assign(sl: TStrings);
    procedure AssignTo(sl: TStrings; UseAddStrings: Boolean);
    procedure LoadFromStream(aStream: TStream);
    procedure SaveToStream(aStream: TStream);
    property ReadOnly: boolean read FReadOnly write SetReadOnly;
    property DiskEncoding: string read FDiskEncoding write FDiskEncoding;
    property MemEncoding: string read FMemEncoding write FMemEncoding;
    property DiskLineEnding: string read FDiskLineEnding write FDiskLineEnding;
    property WriteLock: integer read FWriteLock;
    procedure IncWriteLock;
    procedure DecWriteLock;
    procedure Clear; virtual; // clear content, not Encoding, not LineEnding
    function ConsistencyCheck: integer;
    function CalcMemSize: PtrUInt; virtual;
    constructor Create(const ASource: string);
    destructor Destroy; override;
    
    procedure AddChangeHook(AnOnSourceChange: TOnSourceChange);
    procedure RemoveChangeHook(AnOnSourceChange: TOnSourceChange);
    property OnInsert: TOnSourceLogInsert read FOnInsert write FOnInsert;
    property OnDelete: TOnSourceLogDelete read FOnDelete write FOnDelete;
    property OnMove: TOnSourceLogMove read FOnMove write FOnMove;
    property OnDecodeLoaded: TOnSourceLogDecodeLoaded read FOnDecodeLoaded
                                                      write FOnDecodeLoaded;
    property OnEncodeSaving: TOnSourceLogEncodeSaving read FOnEncodeSaving
                                                      write FOnEncodeSaving;
  end;
  
implementation

{ TSourceLogEntry }

constructor TSourceLogEntry.Create(APos, ALength, AMoveTo: integer;
  const ATxt: string; AnOperation: TSourceLogEntryOperation);
begin
  Position:=APos;
  Len:=ALength;
  MoveTo:=AMoveTo;
  Operation:=AnOperation;
  LineEnds:=LineEndingCount(Txt, LengthOfLastLine);
  Txt:=ATxt;
end;

procedure TSourceLogEntry.AdjustPosition(var APosition: integer);
begin
  case Operation of
    sleoInsert:
      if APosition>=Position then inc(APosition,Len);
    sleoDelete:
      if (APosition>=Position) then begin
        if APosition>=Position+Len then
          dec(APosition,Len)
        else
          APosition:=Position;
      end;
    sleoMove:
      if Position<MoveTo then begin
        if APosition>=Position then begin
          if APosition<Position+Len then
            inc(APosition,MoveTo-Position)
          else if APosition<MoveTo then
            dec(APosition,Len);
        end;
      end else begin
        if APosition>=MoveTo then begin
          if APosition<Position then
            inc(APosition,Len)
          else if APosition<Position+Len then
            dec(APosition,Position-MoveTo);
        end;
      end;
  end;
end;


{ TSourceLogMarker }

{ TSourceLog }

constructor TSourceLog.Create(const ASource: string);
begin
  inherited Create;
  FModified:=false;
  FSource:=ASource;
  FSrcLen:=length(FSource);
  FLog:=TFPList.Create;
  FMarkers:=TFPList.Create;
  FLineRanges:=nil;
  FLineCount:=-1;
  FChangeStep:=0;
  Data:=nil;
  FChangeHooks:=nil;
  FChangeHookCount:=0;
  FReadOnly:=false;
end;

destructor TSourceLog.Destroy;
var
  i: Integer;
begin
  if FChangeHooks<>nil then begin
    FreeMem(FChangeHooks);
    FChangeHooks:=nil;
  end;
  Clear;
  for i:=FMarkers.Count-1 downto 0 do begin
    Markers[i].Log:=nil;
    Markers[i].Free;
  end;
  FMarkers.Free;
  FLog.Free;
  inherited Destroy;
end;

function TSourceLog.LineCount: integer;
begin
  if fLineCount<0 then BuildLineRanges;
  Result:=fLineCount;
end;

function TSourceLog.GetLine(Index: integer; WithLineEnd: boolean): string;
var LineLen: integer;
begin
  BuildLineRanges;
  if (Index>=0) and (Index<fLineCount) then begin
    if WithLineEnd then begin
      if Index<fLineCount-1 then
        LineLen:=fLineRanges[Index+1].StartPos-fLineRanges[Index].StartPos
      else
        LineLen:=fSrcLen-fLineRanges[Index].StartPos+1;
    end else begin
      LineLen:=fLineRanges[Index].EndPos-fLineRanges[Index].StartPos
    end;
    SetLength(Result{%H-},LineLen);
    if LineLen>0 then
      System.Move(fSource[fLineRanges[Index].StartPos],Result[1],LineLen);
  end else
    Result:='';
end;

function TSourceLog.GetLineLength(Index: integer): integer;
begin
  BuildLineRanges;
  if (Index>=0) and (Index<fLineCount) then
    Result:=fLineRanges[Index].EndPos-fLineRanges[Index].StartPos
  else
    Result:=0;
end;

procedure TSourceLog.GetLineRange(Index: integer; out LineRange: TLineRange);
begin
  BuildLineRanges;
  LineRange:=FLineRanges[Index];
end;

function TSourceLog.GetLineStart(Index: integer): integer;
begin
  BuildLineRanges;
  if Index<FLineCount then
    Result:=FLineRanges[Index].StartPos
  else
    Result:=FSrcLen;
end;

function TSourceLog.ClearEntries: boolean;
var i: integer;
begin
  if (Count=0) and (FLog.Count=0) then exit(false);
  Result:=true;
  for i:=0 to Count-1 do Items[i].Free;
  FLog.Clear;
end;

procedure TSourceLog.Clear;
var i: integer;
  m: TSourceLogMarker;
  SourceChanged: Boolean;
begin
  ClearEntries; // ignore if entries change
  // markers are owned by someone else, do not free them
  for i:=0 to FMarkers.Count-1 do begin
    m:=Markers[i];
    if m.Position>1 then
      m.Deleted:=true;
  end;
  SourceChanged:=FSource<>'';
  FSource:='';
  FSrcLen:=0;
  FModified:=false;
  if FLineRanges<>nil then begin
    FreeMem(FLineRanges);
    FLineRanges:=nil;
  end;
  FLineCount:=-1;
  Data:=nil;
  FReadOnly:=false;
  IncreaseChangeStep;
  if SourceChanged then
    DoSourceChanged;
  NotifyHooks(nil);
end;

function TSourceLog.GetItems(Index: integer): TSourceLogEntry;
begin
  Result:=TSourceLogEntry(FLog[Index]);
end;

procedure TSourceLog.SetItems(Index: integer; AnItem: TSourceLogEntry);
begin
  FLog[Index]:=AnItem;
end;

function TSourceLog.Count: integer;
begin
  Result:=fLog.Count;
end;

function TSourceLog.GetMarkers(Index: integer): TSourceLogMarker;
begin
  Result:=TSourceLogMarker(FMarkers[Index]);
end;

function TSourceLog.MarkerCount: integer;
begin
  Result:=fMarkers.Count;
end;

procedure TSourceLog.NotifyHooks(Entry: TSourceLogEntry);
var i: integer;
begin
  if (FChangeHooks=nil) or (FChangeHookLock>0) then begin
    FChangeHookDelayed:=true;
    exit;
  end;
  FChangeHookDelayed:=false;
  for i:=0 to FChangeHookCount-1 do
    FChangeHooks[i](Self,Entry);
end;

procedure TSourceLog.IncreaseHookLock;
begin
  inc(FChangeHookLock);
end;

procedure TSourceLog.DecreaseHookLock;
begin
  if FChangeHookLock<=0 then exit;
  dec(FChangeHookLock);
  if (FChangeHookLock=0) and FChangeHookDelayed then
    NotifyHooks(nil);
end;

procedure TSourceLog.SetSource(const NewSrc: string);
begin
  //DebugLn('TSourceLog.SetSource A ',length(NewSrc),' LineCount=',fLineCount,' SrcLen=',fSrcLen);
  if NewSrc<>FSource then begin
    inc(FChangeHookLock);
    try
      Clear;
      FSource:=NewSrc;
      FSrcLen:=length(FSource);
      FLineCount:=-1;
      FReadOnly:=false;
      DoSourceChanged;
    finally
      dec(FChangeHookLock);
    end;
    NotifyHooks(nil);
  end;
end;

procedure TSourceLog.Insert(Pos: integer; const Txt: string);
var i: integer;
  NewSrcLogEntry: TSourceLogEntry;
begin
  if Txt='' then exit;
  if Assigned(FOnInsert) then FOnInsert(Self,Pos,Txt);
  NewSrcLogEntry:=TSourceLogEntry.Create(Pos,length(Txt),-1,Txt,sleoInsert);
  FLog.Add(NewSrcLogEntry);
  NotifyHooks(NewSrcLogEntry);
  FSource:=copy(FSource,1,Pos-1)
          +Txt
          +copy(FSource,Pos,length(FSource)-Pos+1);
  FSrcLen:=length(FSource);
  FLineCount:=-1;
  for i:=0 to FMarkers.Count-1 do begin
    if (not Markers[i].Deleted) then
      NewSrcLogEntry.AdjustPosition(Markers[i].NewPosition);
  end;
  FModified:=true;
  DoSourceChanged;
end;

procedure TSourceLog.Delete(Pos, Len: integer);
var i: integer;
  NewSrcLogEntry: TSourceLogEntry;
begin
  if Len=0 then exit;
  if Assigned(FOnDelete) then FOnDelete(Self,Pos,Len);
  NewSrcLogEntry:=TSourceLogEntry.Create(Pos,Len,-1,'',sleoDelete);
  FLog.Add(NewSrcLogEntry);
  NotifyHooks(NewSrcLogEntry);
  System.Delete(FSource,Pos,Len);
  FSrcLen:=length(FSource);
  FLineCount:=-1;
  for i:=0 to FMarkers.Count-1 do begin
    if (Markers[i].Deleted=false) then begin
      if (Markers[i].NewPosition<=Pos) and (Markers[i].NewPosition<Pos+Len) then
        Markers[i].Deleted:=true
      else begin
        NewSrcLogEntry.AdjustPosition(Markers[i].NewPosition);
      end;
    end;
  end;
  FModified:=true;
  DoSourceChanged;
end;

procedure TSourceLog.Replace(Pos, Len: integer; const Txt: string);
var i: integer;
  DeleteSrcLogEntry, InsertSrcLogEntry: TSourceLogEntry;
begin
  if (Len=0) and (Txt='') then exit;
  if Len=length(Txt) then begin
    i:=1;
    while (i<=Len) and (FSource[Pos+i-1]=Txt[i]) do inc(i);
    if i>Len then exit;
  end;
  if Assigned(FOnDelete) then FOnDelete(Self,Pos,Len);
  if Assigned(FOnInsert) then FOnInsert(Self,Pos,Txt);
  DeleteSrcLogEntry:=TSourceLogEntry.Create(Pos,Len,-1,'',sleoDelete);
  FLog.Add(DeleteSrcLogEntry);
  NotifyHooks(DeleteSrcLogEntry);
  InsertSrcLogEntry:=TSourceLogEntry.Create(Pos,length(Txt),-1,Txt,sleoInsert);
  FLog.Add(InsertSrcLogEntry);
  NotifyHooks(InsertSrcLogEntry);
  FSource:=copy(FSource,1,Pos-1)
          +Txt
          +copy(FSource,Pos+Len,length(FSource)-Pos-Len+1);
  FSrcLen:=length(FSource);
  FLineCount:=-1;
  for i:=0 to FMarkers.Count-1 do begin
    if (Markers[i].Deleted=false) then begin
      if (Markers[i].NewPosition>=Pos) and (Markers[i].NewPosition<Pos+Len) then
        Markers[i].Deleted:=true
      else begin
        DeleteSrcLogEntry.AdjustPosition(Markers[i].NewPosition);
        InsertSrcLogEntry.AdjustPosition(Markers[i].NewPosition);
      end;
    end;
  end;
  FModified:=true;
  DoSourceChanged;
end;

procedure TSourceLog.Move(Pos, Len, MoveTo: integer);
var i: integer;
  NewSrcLogEntry: TSourceLogEntry;
begin
  if Assigned(FOnMove) then FOnMove(Self,Pos,Len,MoveTo);
  if (MoveTo>=Pos) and (MoveTo<Pos+Len) then exit;
  NewSrcLogEntry:=TSourceLogEntry.Create(Pos,Len,MoveTo,'',sleoMove);
  FLog.Add(NewSrcLogEntry);
  NotifyHooks(NewSrcLogEntry);
  if MoveTo<Pos then begin
    FSource:=copy(FSource,1,MoveTo-1)
            +copy(FSource,Pos,Len)
            +copy(FSource,MoveTo,Pos-MoveTo)
            +copy(FSource,Pos+Len,length(FSource)-Pos-Len+1);
  end else begin
    FSource:=copy(FSource,1,Pos-1)
            +copy(FSource,Pos+Len,MoveTo-Pos-Len)
            +copy(FSource,Pos,Len)
            +copy(FSource,MoveTo,length(FSource)-MoveTo+1);
  end;
  FSrcLen:=length(FSource);
  FLineCount:=-1;
  for i:=0 to FMarkers.Count-1 do begin
    if (Markers[i].Deleted=false) then
      NewSrcLogEntry.AdjustPosition(Markers[i].NewPosition);
  end;
  FModified:=true;
  DoSourceChanged;
end;

function TSourceLog.AddMarker(Position: integer; SomeData: Pointer
  ): TSourceLogMarker;
begin
  Result:=TSourceLogMarker.Create;
  Result.Position:=Position;
  Result.NewPosition:=Result.Position;
  Result.Data:=SomeData;
  Result.Deleted:=false;
  Result.Log:=Self;
  FMarkers.Add(Result);
end;

function TSourceLog.AddMarkerXY(Line, Column: integer; SomeData: Pointer
  ): TSourceLogMarker;
begin
  Result:=TSourceLogMarker.Create;
  LineColToPosition(Line,Column,Result.Position);
  Result.NewPosition:=Result.Position;
  Result.Data:=SomeData;
  Result.Deleted:=false;
  Result.Log:=Self;
  FMarkers.Add(Result);
end;

procedure TSourceLog.AdjustPosition(var APosition: integer);
var i: integer;
begin
  for i:=0 to Count-1 do
    Items[i].AdjustPosition(APosition);
end;

{$IFOPT R+}{$DEFINE RangeChecking}{$ENDIF}
{$R-}
procedure TSourceLog.BuildLineRanges;
var
  line:integer;
  Cap: Integer;
  SrcEnd: PChar;
  SrcStart: PChar;
  p: PChar;
begin
  //DebugLn(['[TSourceLog.BuildLineRanges] A Self=',DbgS(Self),',LineCount=',FLineCount,' Len=',SourceLength]);
  if FLineCount>=0 then exit;
  // build line range list
  FLineCount:=0;
  if FSource='' then begin
    ReAllocMem(FLineRanges,0);
    exit;
  end;
  Cap:=FSrcLen div 20+100;
  ReAllocMem(FLineRanges,Cap*SizeOf(TLineRange));
  line:=0;
  FLineRanges[line].StartPos:=1;
  SrcStart:=PChar(FSource);
  SrcEnd:=SrcStart+FSrcLen;
  p:=SrcStart;
  repeat
    if (not (p^ in [#10,#13])) then begin
      if (p^=#0) and (p>=SrcEnd) then break;
      inc(p);
    end else begin
      // new line
      FLineRanges[line].EndPos:=p-SrcStart+1;
      inc(line);
      if line>=Cap then begin
        Cap:=Cap*2;
        ReAllocMem(FLineRanges,Cap*SizeOf(TLineRange));
      end;
      if (p[1] in [#10,#13]) and (p^<>p[1]) then
        inc(p,2)
      else
        inc(p);
      FLineRanges[line].StartPos:=p-SrcStart+1;
    end;
  until false;
  FLineRanges[line].EndPos:=fSrcLen+1;
  FLineCount:=line;
  if not (FSource[FSrcLen] in [#10,#13]) then
    inc(FLineCount);
  ReAllocMem(FLineRanges,FLineCount*SizeOf(TLineRange));
  //DebugLn('[TSourceLog.BuildLineRanges] END ',FLineCount);
end;
{$IFDEF RangeChecking}{$R+}{$ENDIF}

procedure TSourceLog.LineColToPosition(Line, Column: integer;
  out Position: integer);
begin
  BuildLineRanges;
  if (Line>=1) and (Line<=FLineCount) and (Column>=1) then begin
    if (Line<FLineCount) then begin
      // not the last line
      if (Column<=FLineRanges[Line-1].EndPos-FLineRanges[Line-1].StartPos+1)
      then begin
        Position:=FLineRanges[Line-1].StartPos+Column-1;
      end else begin
        Position:=FLineRanges[Line-1].EndPos;
      end;
    end else begin
      // last line
      if (Column<=fSrcLen-FLineRanges[Line-1].StartPos) then begin
        Position:=FLineRanges[Line-1].StartPos+Column-1;
      end else begin
        Position:=FLineRanges[Line-1].EndPos;
      end;
    end;
  end else begin
    Position:=-1;
  end;
end;

procedure TSourceLog.AbsoluteToLineCol(Position: integer;
  out Line, Column: integer);
var l,r,m:integer;
begin
  BuildLineRanges;
  if (FLineCount=0) or (Position<1) or (Position>fSrcLen+1) then begin
    Line:=-1;
    Column:=-1;
    exit;
  end;
  if (Position>=FLineRanges[FLineCount-1].StartPos) then begin
    Line:=FLineCount;
    Column:=Position-FLineRanges[Line-1].StartPos+1;
    exit;
  end;
  // binary search for the line
  l:=0;
  r:=FLineCount-1;
  repeat
    m:=(l+r) shr 1;
    if FLineRanges[m].StartPos>Position then begin
      // too high, search lower
      r:=m-1;
    end else if FLineRanges[m+1].StartPos<=Position then begin
      // too low, search higher
      l:=m+1;
    end else begin
      // line found
      Line:=m+1;
      Column:=Position-FLineRanges[Line-1].StartPos+1;
      exit;
    end;
  until false;
end;

function TSourceLog.LineColIsOutside(Line, Column: integer): boolean;
begin
  BuildLineRanges;
  Result:=true;
  if (Line<1) or (Column<1) then exit;
  if (Line>LineCount+1) then exit;
  if (Line<=fLineCount)
  and (Column>fLineRanges[Line-1].EndPos-fLineRanges[Line-1].StartPos+1) then
    exit;
  // check if on empty last line
  if (Line=FLineCount+1)
  and ((Column>1) or (FSource='') or (not (FSource[FSrcLen] in [#10,#13]))) then
    exit;
  Result:=false;
end;

function TSourceLog.LineColIsSpace(Line, Column: integer): boolean;
// check if there is a non space character in front of or at Line,Column
var
  p: PChar;
  rg: PLineRange;
begin
  BuildLineRanges;
  Result:=true;
  if (Line<1) or (Column<1) or (Line>LineCount) then exit;
  rg:=@fLineRanges[Line-1];
  if (Column>rg^.EndPos-rg^.StartPos+1) then
    exit;
  p:=@fSource[rg^.StartPos];
  if (p[Column-1]>' ') then exit(false);
  if (Column>1) and (p[Column-2]>' ') then exit(false);
end;

function TSourceLog.AbsoluteToLineColStr(Position: integer): string;
var
  Line: integer;
  Column: integer;
begin
  AbsoluteToLineCol(Position,Line,Column);
  Result:='p='+IntToStr(Position)+',line='+IntToStr(Line)+',col='+IntToStr(Column);
end;

function TSourceLog.LoadFromFile(const Filename: string): boolean;
var
  s: string;
  fs: TFileStream;
  p: Integer;
begin
  Result := False;
  LastError:='';
  try
    fs := TFileStream.Create(Filename, fmOpenRead or fmShareDenyNone);
    try
      SetLength(s{%H-}, fs.Size);
      if s <> '' then
        fs.Read(s[1], length(s));
      FDiskEncoding := '';
      FMemEncoding := '';
      DecodeLoaded(Filename, s, FDiskEncoding, FMemEncoding);

      // get line ending
      FDiskLineEnding:=LineEnding;
      p:=1;
      while p<=length(s) do begin
        if s[p] in [#10,#13] then begin
          if s[p]=#10 then fDiskLineEnding:=#10
          else if (p<length(s)) and (s[p+1]=#10) then fDiskLineEnding:=#13#10
          else fDiskLineEnding:=#13;
          break;
        end;
        inc(p);
      end;

      Source := s;
    finally
      fs.Free;
    end;
    Result := True;
  except
    on E: Exception do
      LastError:=E.Message;
  end;
end;

procedure TSourceLog.IncreaseChangeStep;
begin
  if FChangeStep<High(FChangeStep) then
    inc(FChangeStep)
  else
    FChangeStep:=low(FChangeStep);
  //DebugLn('[TSourceLog.IncreaseChangeStep] ',FChangeStep,',',DbgS(Self));
end;

procedure TSourceLog.DoSourceChanged;
begin
  IncreaseChangeStep;
  //debugln(['TSourceLog.DoSourceChanged ']);
end;

function TSourceLog.SaveToFile(const Filename: string): boolean;
var 
  fs: TFileStream;
  s: String;
begin
  {$IFDEF VerboseCTSave}
  DebugLn(['TSourceLog.SaveToFile Self=',DbgS(Self),' ',Filename,' Size=',length(Source)]);
  CTDumpStack;
  {$ENDIF}
  Result := False;
  LastError:='';
  try
    s := Source;
    // convert encoding
    EncodeSaving(Filename, s);
    // convert line ending to disk line ending
    if (DiskLineEnding<>'') and (LineEnding <> DiskLineEnding) then
      s := ChangeLineEndings(s, DiskLineEnding);

    // keep filename case on disk
    if FileExistsUTF8(Filename) then begin
      InvalidateFileStateCache(Filename);
      fs := TFileStream.Create(Filename, fmOpenWrite or fmShareDenyNone);
      fs.Size := 0;
    end else begin
      InvalidateFileStateCache; // invalidate all (samba shares)
      fs := TFileStream.Create(Filename, fmCreate);
    end;
    try
      if s <> '' then
        fs.Write(s[1], length(s));
    finally
      fs.Free;
    end;
    Result := True;
  except
    on E: Exception do
      LastError:=E.Message;
  end;
end;

function TSourceLog.GetLines(StartLine, EndLine: integer): string;
var
  StartPos: Integer;
  EndPos: Integer;
begin
  BuildLineRanges;
  if StartLine<1 then StartLine:=1;
  if EndLine>LineCount then EndLine:=LineCount;
  if StartLine<=EndLine then begin
    StartPos:=FLineRanges[StartLine-1].StartPos;
    if EndLine<LineCount then
      EndPos:=FLineRanges[EndLine].StartPos
    else
      EndPos:=FLineRanges[EndLine-1].EndPos;
    SetLength(Result{%H-},EndPos-StartPos);
    System.Move(FSource[StartPos],Result[1],length(Result));
  end else
    Result:='';
end;

function TSourceLog.IsEqual(sl: TStrings): boolean;
var
  p: PChar;
  Line: String;
  l: PChar;
  y: Integer;
begin
  Result:=false;
  if sl=nil then exit;
  if (FSrcLen=0) and (sl.Count>0) then exit;
  if (FLineCount>=0) and (sl.Count<>FLineCount) then exit;
  p:=PChar(FSource);
  y:=0;
  while (y<sl.Count) do begin
    Line:=sl[y];
    if (Line<>'') then begin
      l:=PChar(Line);
      while (l^=p^) do begin
        if (l^=#0) then begin
          if l-PChar(Line)=length(Line) then begin
            // end of Line
            if (p-PChar(FSource)=FSrcLen) then begin
              // end of source
              Result:=y=sl.Count-1;
              exit;
            end;
            break;
          end else if p-PChar(FSource)=FSrcLen then begin
            // not at end of Line, end of source
            exit;
          end;
        end;
        inc(p);
        inc(l);
      end;
      if l^<>#0 then exit;
    end;
    // at end of Line
    if not (p^ in [#10,#13]) then begin
      // not between two lines in Source
      Result:=(y=sl.Count-1) and (p-PChar(FSource)=FSrcLen);
      exit;
    end;
    // skip line end
    if (p[1] in [#10,#13]) and (p^<>p[1]) then
      inc(p,2)
    else
      inc(p);
    inc(y);
  end;
  Result:=p-PChar(FSource)=FSrcLen;
end;

function TSourceLog.OldIsEqual(sl: TStrings): boolean;
var x,y,p,LineLen: integer;
  Line: string;
begin
  Result:=false;
  if sl=nil then exit;
  p:=1;
  x:=1;
  y:=0;
  while (y<sl.Count) do begin
    Line:=sl[y];
    LineLen:=length(Line);
    if fSrcLen-p+1<LineLen then exit;
    x:=1;
    while (x<=LineLen) do begin
      if Line[x]<>fSource[p] then exit;
      inc(x);
      inc(p);
    end;
    if (p<=fSrcLen) and (not (fSource[p] in [#10,#13])) then exit;
    inc(p);
    if (p<=fSrcLen) and (fSource[p] in [#10,#13]) and (fSource[p]<>fSource[p-1])
    then inc(p);
    inc(y);
  end;
  if p<FSrcLen then exit;
  Result:=true;
end;

procedure TSourceLog.Assign(sl: TStrings);
begin
  if sl=nil then exit;
  if IsEqual(sl) then exit;
  IncreaseHookLock;
  try
    Clear;
    fSource := sl.Text;
    fSrcLen := Length(fSource);
    DoSourceChanged;
    NotifyHooks(nil);
  finally
    DecreaseHookLock;
  end;
end;

procedure TSourceLog.AssignTo(sl: TStrings; UseAddStrings: Boolean);
var y: integer;
  s: string;
  TempList: TStringList;
begin
  if sl=nil then exit;
  if IsEqual(sl) then exit;
  if UseAddStrings then begin
    TempList:=TStringList.Create;
    AssignTo(TempList,false);
    sl.BeginUpdate;
    sl.Clear;
    sl.AddStrings(TempList);
    sl.EndUpdate;
    TempList.Free;
  end else begin
    sl.BeginUpdate;
    sl.Clear;
    BuildLineRanges;
    sl.Capacity:=fLineCount;
    for y:=0 to fLineCount-1 do begin
      s:='';
      SetLength(s,fLineRanges[y].EndPos-fLineRanges[y].StartPos);
      if s<>'' then
        System.Move(fSource[fLineRanges[y].StartPos],s[1],length(s));
      sl.Add(s);
    end;
    sl.EndUpdate;
  end;
end;

procedure TSourceLog.LoadFromStream(aStream: TStream);
var
  NewSrcLen: integer;
  NewSource: String;
begin
  IncreaseHookLock;
  try
    if aStream=nil then exit;
    aStream.Position:=0;
    NewSrcLen:=aStream.Size-aStream.Position;
    NewSource:='';
    if NewSrcLen>0 then begin
      SetLength(NewSource,NewSrcLen);
      aStream.Read(NewSource[1],NewSrcLen);
    end;
    Source:=NewSource;
  finally
    DecreaseHookLock;
  end;
end;

procedure TSourceLog.SaveToStream(aStream: TStream);
begin
  if fSource<>'' then aStream.Write(fSource[1],fSrcLen);
end;

procedure TSourceLog.SetReadOnly(const Value: boolean);
begin
  FReadOnly := Value;
end;

procedure TSourceLog.IncWriteLock;
begin
  inc(FWriteLock);
end;

procedure TSourceLog.DecWriteLock;
begin
  if FWriteLock>0 then dec(FWriteLock);
end;

function TSourceLog.ConsistencyCheck: integer;
begin
  if fSrcLen<>length(fSource) then begin
    Result:=-1;  exit;
  end;
  Result:=0;
end;

function TSourceLog.CalcMemSize: PtrUInt;
begin
  Result:=PtrUInt(InstanceSize)
    +MemSizeString(FDiskEncoding)
    +MemSizeString(FDiskLineEnding)
    +PtrUint(FLineCount)*SizeOf(TLineRange)
    +MemSizeString(FMemEncoding)
    +PtrUInt(FChangeHookCount)*SizeOf(TOnSourceChange)
    +MemSizeString(FSource)
    +PtrUint(FLog.Count)*SizeOf(TSourceLogEntry)
    +PtrUInt(FMarkers.Count*TSourceLogMarker.InstanceSize);
end;

function TSourceLog.IndexOfChangeHook(AChangeHook: TOnSourceChange): integer;
begin
  Result:=FChangeHookCount-1;
  while (Result>=0) and (FChangeHooks[Result]<>AChangeHook) do dec(Result);
end;

procedure TSourceLog.DecodeLoaded(const AFilename: string;
  var ASource, ADiskEncoding, AMemEncoding: string);
begin
  if Assigned(OnDecodeLoaded) then
    OnDecodeLoaded(Self,AFilename,ASource,ADiskEncoding,AMemEncoding);
end;

procedure TSourceLog.EncodeSaving(const AFilename: string; var ASource: string);
begin
  if Assigned(OnEncodeSaving) then
    OnEncodeSaving(Self,AFilename,ASource);
end;

procedure TSourceLog.AddChangeHook(AnOnSourceChange: TOnSourceChange);
var i: integer;
begin
  i:=IndexOfChangeHook(AnOnSourceChange);
  if i>=0 then exit;
  inc(FChangeHookCount);
  if FChangeHooks=nil then
    GetMem(FChangeHooks, SizeOf(TOnSourceChange))
  else
    ReallocMem(FChangeHooks, SizeOf(TOnSourceChange) * FChangeHookCount);
  FChangeHooks[FChangeHookCount-1]:=AnOnSourceChange;
end;

procedure TSourceLog.RemoveChangeHook(AnOnSourceChange: TOnSourceChange);
var i,j: integer;
begin
  i:=IndexOfChangeHook(AnOnSourceChange);
  if i<0 then exit;
  dec(FChangeHookCount);
  if FChangeHookCount=1 then
    FreeMem(FChangeHooks)
  else begin
    for j:=i to FChangeHookCount-2 do
      FChangeHooks[j]:=FChangeHooks[j+1];
    ReAllocMem(FChangeHooks,SizeOf(TOnSourceChange) * FChangeHookCount);
  end;
end;

{ TSourceLogMarker }

destructor TSourceLogMarker.Destroy;
begin
  if Log<>nil then Log.FMarkers.Remove(Self);
  Log:=nil;
  inherited Destroy;
end;

end.

