{
 ***************************************************************************
 *                                                                         *
 *   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: Tom Gregorovic
}
unit MainUnit;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Process,
  // LCL
  Forms, Controls, Graphics, Dialogs, StdCtrls, ComCtrls, ExtCtrls, EditBtn,
  LResources, LCLIntf, LCLType,
  // LazUtils
  UTF8Process, Laz_XMLCfg, LazUtf8, FileUtil, LazFileUtils, LazLoggerBase,
  FPDocFiles;

type

  { TFormMain }

  TFormMain = class(TForm)
    ButtonRefresh: TButton;
    ButtonUpdateNew: TButton;
    ButtonUpdate: TButton;
    ButtonUpdateAll: TButton;
    CheckBoxShowSummary: TCheckBox;
    CheckBoxBackup: TCheckBox;
    EditInclude: TDirectoryEdit;
    EditMakeSkel: TFileNameEdit;
    EditPackage: TEdit;
    EditBackup: TEdit;
    EditUnits: TDirectoryEdit;
    EditDocs: TDirectoryEdit;
    Label1: TLabel;
    LabelInclude: TLabel;
    LabelMakeSkel: TLabel;
    LabelPackage: TLabel;
    LabelBackup: TLabel;
    LabelUnits: TLabel;
    LabelDocs: TLabel;
    ListBox: TListBox;
    OpenDialog: TOpenDialog;
    StatusBar: TStatusBar;
    procedure ButtonRefreshClick(Sender: TObject);
    procedure ButtonUpdateAllClick(Sender: TObject);
    procedure ButtonUpdateClick(Sender: TObject);
    procedure ButtonUpdateNewClick(Sender: TObject);
    procedure EditDocsChange(Sender: TObject);
    procedure EditUnitsChange(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure ListBoxDrawItem(Control: TWinControl; Index: Integer;
      ARect: TRect; State: TOwnerDrawState);
  private
  public
    procedure BeginUpdate;
    procedure EndUpdate;
    procedure UpdateList;
    procedure UpdateFile(const AFileName: String);
    procedure BackupFile(const AFileName: String);
    procedure WriteStatus(const S: String);
    
    procedure MoveElement(const SrcPackage: TFPDocPackage;
      const SrcModule: TFPDocModule; const Src: TFPDocElement;
      const DestList: TStrings; var Dest: Integer);
  end; 

var
  FormMain: TFormMain;
  XMLConfig: TXMLConfig;
  BackupList: TStringList;

implementation

{$R *.lfm}

uses
  UnitMove, UnitSummary;

function FindFiles(const Path, Mask: String; OnlyFileName: Boolean = False): TStringList;
var
  I: Integer;
begin
  Result := FindAllFiles(Path, Mask, False);
  if OnlyFileName then
    for I := 0 to Result.Count - 1 do
      Result[I] := ExtractFileNameOnly(Result[I]);
end;

{ TFormMain }

procedure TFormMain.FormShow(Sender: TObject);
begin
  UpdateList;
end;

procedure TFormMain.ListBoxDrawItem(Control: TWinControl; Index: Integer;
  ARect: TRect; State: TOwnerDrawState);
begin
  if (Index < 0) or (Index >= ListBox.Items.Count) then Exit;
  
  with ListBox.Canvas do
  begin
    if odSelected in State then
      Brush.Color := clHighlight
    else
    begin
      Brush.Color := ListBox.Color;
      case PtrInt(ListBox.Items.Objects[Index]) of
      0: SetTextColor(ListBox.Canvas.Handle, ListBox.Canvas.Font.Color); // normal
      1: SetTextColor(ListBox.Canvas.Handle, clRed);                     // new
      end;
    end;
      
    FillRect(ARect);
    TextRect(ARect, ARect.Left + 8, ARect.Top + 2, ExtractFileNameOnly(ListBox.Items[Index]));
  end;
end;

procedure TFormMain.BeginUpdate;
begin
  BackupList := TStringList.Create;
  BackupList.Sorted := True;
  WriteStatus('Updating started.');
end;

procedure TFormMain.EndUpdate;
begin
  BackupList.Free;
  UpdateList;
  WriteStatus('Updating done.');
  Sleep(5000);
  WriteStatus('');
end;

procedure TFormMain.ButtonRefreshClick(Sender: TObject);
begin
  UpdateList;
end;

procedure TFormMain.ButtonUpdateAllClick(Sender: TObject);
var
  I: Integer;
begin
  BeginUpdate;
  try
    for I := 0 to ListBox.Items.Count - 1 do
      UpdateFile(ListBox.Items[I]);
  finally
    EndUpdate;
  end;
end;

procedure TFormMain.ButtonUpdateClick(Sender: TObject);
var
  I: Integer;
begin
  BeginUpdate;
  try
    for I := 0 to ListBox.Items.Count - 1 do
      if ListBox.Selected[I] then UpdateFile(ListBox.Items[I]);
  finally
    EndUpdate;
  end;
end;

procedure TFormMain.ButtonUpdateNewClick(Sender: TObject);
var
  I: Integer;
begin
  BeginUpdate;
  try
    for I := 0 to ListBox.Items.Count - 1 do
      if PtrInt(ListBox.items.Objects[I]) = 1 then UpdateFile(ListBox.Items[I]);
  finally
    EndUpdate;
  end;
end;

procedure TFormMain.EditDocsChange(Sender: TObject);
begin
  if DirectoryExistsUTF8(EditDocs.Text) then UpdateList;
end;

procedure TFormMain.EditUnitsChange(Sender: TObject);
begin
  if DirectoryExistsUTF8(EditUnits.Text) then UpdateList;
end;

procedure TFormMain.FormCreate(Sender: TObject);
begin
  XMLConfig := TXMLConfig.Create('FPDocUpdater.xml');

 EditDocs.Directory := XMLConfig.GetValue('FPDocsPath/Value', 'Please set path to <Docs\xml\lcl\>');
  EditUnits.Directory := XMLConfig.GetValue('UnitsPath/Value', 'Please set path to <LCL\>');
  EditInclude.Directory := XMLConfig.GetValue('IncludePath/Value', 'Please set path to <LCL\Include\>');
  EditMakeSkel.FileName := XMLConfig.GetValue('MakeSkelPath/Value', 'Please set path to <makeskel.exe>');
  CheckBoxBackup.Checked := XMLConfig.GetValue('BackupFPDocs/Value', True);
  EditBackup.Text := XMLConfig.GetValue('BackupExt/Value', 'bak');
  EditPackage.Text := XMLConfig.GetValue('Package/Value', 'lcl');
end;

procedure TFormMain.FormDestroy(Sender: TObject);
begin
  XMLConfig.Clear;
  
  XMLConfig.SetValue('FPDocsPath/Value', EditDocs.Directory);
  XMLConfig.SetValue('UnitsPath/Value', EditUnits.Directory);
  XMLConfig.SetValue('IncludePath/Value', EditInclude.Directory);
  XMLConfig.SetValue('MakeSkelPath/Value', EditMakeSkel.FileName);
  XMLConfig.SetValue('BackupFPDocs/Value', CheckBoxBackup.Checked);
  XMLConfig.SetValue('BackupExt/Value', EditBackup.Text);
  XMLConfig.SetValue('Package/Value', EditPackage.Text);
  
  XMLConfig.Free;
end;

procedure TFormMain.UpdateList;
var
  Docs, Units: TStringList;
  DocsPath, UnitsPath: String;
  I: Integer;
  N: String;
  State: PtrInt;
begin
  ListBox.Items.BeginUpdate;
  try
    ListBox.Items.Clear;
    
    DocsPath := AppendPathDelim(EditDocs.Directory);
    UnitsPath := AppendPathDelim(EditUnits.Directory);
    
    Docs := FindFiles(DocsPath, '*.xml', True);
    Units := FindFiles(UnitsPath, '*.pas;*.pp');
    try
      Units.Sorted := True;
      for I := 0 to Units.Count - 1 do
      begin
        N := ExtractFileNameOnly(Units[I]);

        if Docs.IndexOf(N) = -1 then
          State := 1
        else
          State := 0;
            
        ListBox.Items.AddObject(Units[I], TObject(State));
      end;
    finally
      Units.Free;
      Docs.Free;
    end;
  finally
    ListBox.Items.EndUpdate;
  end;
end;

procedure RaiseError(const S: String);
begin
  DebugLn(S);
  raise Exception.Create(S);
end;

procedure TFormMain.UpdateFile(const AFileName: String);
var
  DocFileName: String;
  MakeSkelPath: String;
  AProcess: TProcessUTF8;
  AStringList, AErrorList: TStringList;
  M: TMemoryStream;
  N, BytesRead: LongInt;
  OldDoc, NewDoc: TFPDocFile;
const
   READ_BYTES = 2048;
   
begin
  try
    if not FileExistsUTF8(AFileName) then
    begin
      RaiseError('Update ' + AFileName + ' failed!');
    end;

    MakeSkelPath := FindDefaultExecutablePath(EditMakeSkel.FileName);

    if not FileIsExecutable(MakeSkelPath) then
      RaiseError('Unable to find MakeSkel tool executable "' + EditMakeSkel.Text +'"!');

    DocFileName := AppendPathDelim(EditDocs.Directory) + ExtractFileNameOnly(AFileName) + '.xml';

    if CheckBoxBackup.Checked then BackupFile(DocFileName);

    WriteStatus('Updating ' + AFileName);

    AProcess := TProcessUTF8.Create(nil);
    AStringList := TStringList.Create;
    AErrorList := TStringList.Create;
    M := TMemoryStream.Create;
    try
      AProcess.CommandLine :=
        Format(MakeSkelPath + ' --package="%s" --input="%s -Fi%s"',
          [EditPackage.Text, AFileName, EditInclude.Directory]);
      AProcess.Options := AProcess.Options + [poUsePipes, poNoConsole, poStderrToOutPut];
      AProcess.Execute;

      BytesRead := 0;
      while AProcess.Running do
      begin
        M.SetSize(BytesRead + READ_BYTES);
        N := AProcess.Output.Read((M.Memory + BytesRead)^, READ_BYTES);
        if N > 0 then Inc(BytesRead, N)
        else Sleep(100);
      end;

      repeat
        M.SetSize(BytesRead + READ_BYTES);
        N := AProcess.Output.Read((M.Memory + BytesRead)^, READ_BYTES);
        if N > 0 then Inc(BytesRead, N);
      until N <= 0;
      M.SetSize(BytesRead);

      AStringList.LoadFromStream(M);

      while (AStringList.Count > 0) and
        (AStringList.Strings[AStringList.Count - 1] = '') do
        AStringList.Delete(AStringList.Count - 1);

      while (AStringList.Count > 0) and
        (AStringList.Strings[AStringList.Count - 1] <> '') and
        (AStringList.Strings[AStringList.Count - 1] <> '</fpdoc-descriptions>') do
      begin
        AErrorList.Add(AStringList.Strings[AStringList.Count - 1]);
        AStringList.Delete(AStringList.Count - 1);
      end;

      while (AStringList.Count > 0) and
        (AStringList.Strings[AStringList.Count - 1] <> '</fpdoc-descriptions>') do
        AStringList.Delete(AStringList.Count - 1);

      if AStringList.Count = 0 then
        RaiseError('Update ' + AFileName + ' failed, because' + AErrorList.Text);

      M.Clear;
      AStringList.SaveToStream(M);
      M.Position := 0;
      NewDoc := TFPDocFile.Create(M);
      if FileExistsUTF8(DocFileName) then OldDoc := TFPDocFile.Create(DocFileName)
      else OldDoc := nil;

      try
        if OldDoc <> nil then
        begin
          FormSummary.OldInfo := OldDoc.GetInfo;
          OldDoc.AssignToSkeleton(NewDoc, @MoveElement);
          FormSummary.NewInfo := NewDoc.GetInfo;
        end
        else
        begin
          FormSummary.OldInfo := EmptyFPDocInfo;
          FormSummary.NewInfo := NewDoc.GetInfo;
        end;

        if CheckBoxShowSummary.Checked then
        begin
          FormSummary.LabelFileName.Caption := DocFileName;
          if FormSummary.ShowModal = mrOk then
          begin
            NewDoc.SaveToFile(DocFileName);
            WriteStatus('Update ' + AFileName + ' to ' + DocFileName + ' succeeds!');
          end
          else
            WriteStatus('Update ' + AFileName + ' to ' + DocFileName + ' cancelled!');
        end
        else
        begin
          NewDoc.SaveToFile(DocFileName);
          WriteStatus('Update ' + AFileName + ' to ' + DocFileName + ' succeeds!');
        end;

      finally
        if OldDoc <> nil then OldDoc.Free;
        NewDoc.Free;
      end;

    finally
      M.Free;
      AStringList.Free;
      AErrorList.Free;
      AProcess.Free;
    end;
  except
    on E: Exception do
      MessageDlg('Error', E.Message, mtError, [mbOK], '');
  end;
end;

procedure TFormMain.BackupFile(const AFileName: String);
var
  BackupFileName: String;
begin
  if not FileExistsUTF8(AFileName) then Exit;

  if BackupList.IndexOf(AFileName) = -1 then
  begin
    BackupFileName := ChangeFileExt(AFileName, '.' + EditBackup.Text);

    if CopyFile(AFileName, BackupFileName, True) then
    begin
      WriteStatus('Backup ' + AFileName + ' to ' + BackupFileName + ' succeeds.');
      BackupList.Add(AFileName);
    end
    else
      RaiseError('Backup ' + AFileName + ' to ' + BackupFileName + ' failed!');
  end;
end;

procedure TFormMain.WriteStatus(const S: String);
begin
  DebugLn(S);
  StatusBar.SimpleText := S;
  StatusBar.Update;
end;

procedure TFormMain.MoveElement(const SrcPackage: TFPDocPackage;
  const SrcModule: TFPDocModule; const Src: TFPDocElement;
  const DestList: TStrings; var Dest: Integer);
var
  JumpList: TStringList;
  I: Integer;
  Prefix: String;
begin
  FormMove.LabelSrc.Caption := Format('Package: %sModule: %s',
    [SrcPackage.Name + LineEnding, SrcModule.Name]);
  FormMove.LabelSrcElement.Caption := 'Element: ' + Src.Name;

  JumpList := TStringList.Create;
  try
    for I := 0 to DestList.Count - 1 do
    begin
      if Pos('.', DestList[I]) = 0 then Continue;
      Prefix := Copy(DestList[I], 1, LastDelimiter('.', DestList[I]) - 1);
      if JumpList.IndexOf(Prefix) = -1 then JumpList.Add(Prefix);
    end;
    
    FormMove.ComboBoxJump.Items.Assign(JumpList);
  finally
    JumpList.Free;
  end;
  
  FormMove.ListBoxDest.Items.Assign(DestList);
  
  case FormMove.ShowModal of
    mrYes: Dest := FormMove.ListBoxDest.ItemIndex;
  end;
  
  if Dest <> -1 then
    WriteStatus('Move Element: ' + SrcPackage.Name + '\' + SrcModule.Name + '\' + Src.Name +
      ' Dest: ' + DestList[Dest]);
end;

end.

