{
 /***************************************************************************
   diffmerge.pas - functions to merge new text with the JCF changes into the
   editor

 ***************************************************************************/

 ***************************************************************************
 *                                                                         *
 *   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: Domingo Galmés

  Abstract:
    Methods for creating diffs and applying them to the editor.

  Uses TextDiff
    https://github.com/rickard67/TextDiff
    http://www.angusj.com/delphi/textdiff.html
    Author : Angus Johnson - angusj-AT-myrealbox-DOT-com
    Copyright : © 2001-2008 Angus Johnson
    Updated by : Rickard Johansson (RJ TextEd)
}
unit diffmerge;

{$mode objfpc}{$H+}
{$WARN 6058 off}  // supress note: call function/procedure marked as inline is not inlined

interface

uses
  Classes, SysUtils, crc,
  // LazUtils
  IntegerList,
  // IdeIntf
  SrcEditorIntf,
  Diff;

procedure DiffMergeEditor(aEdit: TSourceEditorInterface; const aNewText: string; aFromLine: integer = 1; aToLine: integer = maxint);

implementation

function HashLine(const line: string; aIgnoreFinalSpaces: boolean = False): cardinal;
var
  lLen: integer;
begin
  lLen := Length(line);
  if aIgnoreFinalSpaces then
  begin
    while (lLen > 0) and (line[lLen] in [' ', #9]) do
      Dec(lLen);
  end;
  if lLen = 0 then
    Result := CRC32(0, nil, lLen)
  else
    Result := CRC32(0, pbyte(@line[1]), lLen);
end;

//Line number 1 based as TSourceEditorInterface.
procedure FillHashList(aList: TCardinalList; aLines: TStrings; aFirstLine: integer = 1; aLastLine: integer = maxint);
var
  i: integer;
begin
  aList.Clear;
  Dec(aFirstLine);
  Dec(aLastLine);
  if aFirstLine < 0 then
    aFirstLine := 0;
  if aLastLine > aLines.Count - 1 then
    aLastLine := aLines.Count - 1;
  for i := aFirstLine to aLastLine do
    aList.Add(HashLine(aLines[i]));
end;

function EditorGetLastColumn(aEdit: TSourceEditorInterface; aLineNumber: integer): integer;
begin
  Result := length(aEdit.Lines[aLineNumber - 1]) + 1;
end;

procedure EditorDeleteLine(aEdit: TSourceEditorInterface; aLineNumber: integer);
var
  lStartPoint, lEndPoint: Tpoint;
begin
  if (aLineNumber < 0) or (aLineNumber > aEdit.Lines.Count) then
    Exit;
  lStartPoint.X := 1;
  lStartPoint.Y := aLineNumber;
  lEndPoint.X := 1;
  lEndPoint.Y := aLineNumber + 1;
  if lEndPoint.Y > aEdit.Lines.Count then
  begin
    lEndPoint.Y := aLineNumber;
    lEndPoint.X := EditorGetLastColumn(aEdit, aLineNumber);
    if aLineNumber > 1 then
    begin
      lStartPoint.X := EditorGetLastColumn(aEdit, aLineNumber - 1);
      lStartPoint.Y := aLineNumber - 1;
    end;
  end;
  aEdit.ReplaceText(lStartPoint, lEndPoint, '');
end;


procedure EditorInsertLine(aEdit: TSourceEditorInterface; aLineNumber: integer; aText: string);
begin
  aEdit.InsertLine(aLineNumber, aText, True);
end;

procedure EditorReplaceLine(aEdit: TSourceEditorInterface; aLineNumber: integer; aNewText: string);
begin
  aEdit.ReplaceLines(aLineNumber, aLineNumber, aNewText, True);
end;

procedure DiffMergeEditor(aEdit: TSourceEditorInterface; const aNewText: string; aFromLine: integer = 1; aToLine: integer = maxint);
var
  lDiff: TDiff;
  lI: integer;
  lListOldTextHashes, lListNewTextHashes: TCardinalList;
  lDeltaLines: integer;
  lStartLine: integer;
  lNewText: TStringList;
  lCursor: TPoint;
  lCursorAdjusted: boolean;
  lCursorNeedAdjustX: boolean;
begin
  lDiff := nil;
  lNewText := nil;
  lListOldTextHashes := nil;
  lListNewTextHashes := nil;
  lDeltaLines := 0;
  lStartLine := aFromLine - 1;  // 0 based offset.
  lCursor := aEdit.CursorTextXY;
  lCursorAdjusted := False;
  lCursorNeedAdjustX := False;
  try
    aEdit.BeginUpdate;
    aEdit.BeginUndoBlock;
    lDiff := TDiff.Create(nil);
    lNewText := TStringList.Create;
    lNewText.Text := aNewText;
    lListOldTextHashes := TCardinalList.Create;
    lListNewTextHashes := TCardinalList.Create;
    FillHashList(lListOldTextHashes, aEdit.Lines, aFromLine, aToLine);
    FillHashList(lListNewTextHashes, lNewText);
    lDiff.Execute(lListOldTextHashes, lListNewTextHashes);
    for lI := 0 to lDiff.Count - 1 do
    begin
      with lDiff.Compares[lI] do
      begin
        if (lCursorAdjusted = False) and (lCursor.Y = lStartLine + oldIndex1 + 1) then
        begin
          lCursor.Y := lCursor.Y + lDeltaLines;
          lCursorAdjusted := True;
          lCursorNeedAdjustX := Kind <> ckNone;
        end;
        case Kind of
          ckAdd:
          begin
            EditorInsertLine(aEdit, lStartLine + oldIndex2 + 1, lNewText[oldIndex2]);
            Inc(lDeltaLines);
          end;
          ckDelete:
          begin
            EditorDeleteLine(aEdit, lStartLine + oldIndex1 + 1 + lDeltaLines);
            Dec(lDeltaLines);
          end;
          ckModify:
          begin
            EditorReplaceLine(aEdit, lStartLine + oldIndex2 + 1, lNewText[oldIndex2]);
          end;
        end;
      end;
    end;
    if lCursorNeedAdjustX then
      lCursor.X := EditorGetLastColumn(aEdit, lCursor.Y);
    aEdit.CursorTextXY := lCursor;
  finally
    lDiff.Free;
    lListOldTextHashes.Free;
    lListNewTextHashes.Free;
    lNewText.Free;
    aEdit.EndUndoBlock;
    aEdit.EndUpdate;
  end;
end;

end.
