unit FppkgHelper;

{$mode objfpc}{$H+}

interface

uses
  Classes,
  SysUtils,
  {$IF FPC_FULLVERSION>30100}
  pkgFppkg,
  fpmkunit,
  fprepos,
  LazarusIDEStrConsts,
  {$ENDIF}
  // LazUtils
  LazLogger, LazFileCache, FileUtil, LazFileUtils;

type

  TFppkgPackageVariantArray = array of TStringArray;

  TFppkgPropConfigured = (fpcUnknown, fpcYes, fpcNo);

  { TFppkgHelper }

  TFppkgHelper = class
  private
    {$IF FPC_FULLVERSION>30100}
    FFPpkg: TpkgFPpkg;
    {$ENDIF}
    FIsProperlyConfigured: TFppkgPropConfigured;
    FConfStatusMessage: string;
    FOverrideConfigurationFilename: string;
    function HasFPCPackagesOnly(const PackageName: string): Boolean;
    procedure InitializeFppkg;
    procedure SetOverrideConfigurationFilename(AValue: string);
  public
    constructor Create;
    destructor Destroy; override;
    class function Instance: TFppkgHelper;
    function HasPackage(const PackageName: string): Boolean;
    procedure ListPackages(AList: TStringList);
    function GetPackageUnitPath(const PackageName: string): string;
    function IsProperlyConfigured(out Message: string): Boolean;
    function GetCompilerFilename: string;
    function GetConfigurationFileName: string;
    function GetCompilerConfigurationFileName: string;
    // Temporary solution, because fpc 3.2.0 does not has support for package-variants
    // in TFPPackage
    function GetPackageVariantArray(const PackageName: string): TFppkgPackageVariantArray;
    procedure ReInitialize;
    property OverrideConfigurationFilename: string read FOverrideConfigurationFilename write SetOverrideConfigurationFilename;
  end;

implementation

var
  GFppkgHelper: TFppkgHelper = nil;

{ TFppkgHelper }

procedure TFppkgHelper.InitializeFppkg;
{$IF NOT (FPC_FULLVERSION>30100)}
begin
end;
{$ELSE}
var
  FPpkg: TpkgFPpkg;
begin
  FPpkg := TpkgFPpkg.Create(nil);
  try
    try
      if not Assigned(Defaults) then
        Defaults := TBasicDefaults.Create;

      FPpkg.InitializeGlobalOptions(FOverrideConfigurationFilename);
      FPpkg.InitializeCompilerOptions;

      FPpkg.CompilerOptions.CheckCompilerValues;
      FPpkg.FpmakeCompilerOptions.CheckCompilerValues;

      FPpkg.LoadLocalAvailableMirrors;

      FPpkg.ScanPackages;

      FFPpkg := FPpkg;
      FPpkg := nil;
    except
      on E: Exception do
        debugln(['InitializeFppkg failed: '+E.Message]);
    end;
  finally
    FPpkg.Free;
  end;
end;
{$ENDIF FPC_FULLVERSION>30100}

constructor TFppkgHelper.Create;
begin
  InitializeFppkg;
end;

destructor TFppkgHelper.Destroy;
begin
{$IF FPC_FULLVERSION>30100}
  FFPpkg.Free;
{$ENDIF FPC_FULLVERSION>30100}
  inherited Destroy;
end;

class function TFppkgHelper.Instance: TFppkgHelper;
begin
  if not Assigned(GFppkgHelper) then
    GFppkgHelper := TFppkgHelper.Create;
  Result := GFppkgHelper;
end;

function TFppkgHelper.HasPackage(const PackageName: string): Boolean;
{$IF FPC_FULLVERSION>30100}
var
  Msg: string;
{$ENDIF}
begin
{$IF NOT (FPC_FULLVERSION>30100)}
  Result := HasFPCPackagesOnly(PackageName);
{$ELSE }
  if IsProperlyConfigured(Msg) then
    begin
    Result :=
      Assigned(FFPpkg.FindPackage(PackageName,pkgpkInstalled)) or
      Assigned(FFPpkg.FindPackage(PackageName,pkgpkAvailable)) or
      Assigned(FFPpkg.FindPackage(PackageName,pkgpkBoth));

    if not Result then
      begin
      // rescan and try again
      FFppkg.LoadLocalAvailableMirrors;
      FFppkg.ScanPackages;

      Result :=
        Assigned(FFPpkg.FindPackage(PackageName,pkgpkInstalled)) or
        Assigned(FFPpkg.FindPackage(PackageName,pkgpkAvailable)) or
        Assigned(FFPpkg.FindPackage(PackageName,pkgpkBoth));
      end;
    end
  else
    Result := HasFPCPackagesOnly(PackageName);
{$ENDIF FPC_FULLVERSION>30100}
end;

procedure TFppkgHelper.ListPackages(AList: TStringList);
{$IF FPC_FULLVERSION>30100}
var
  I, J: Integer;
  Repository: TFPRepository;
{$ENDIF FPC_FULLVERSION>30100}
begin
  AList.Clear;
{$IF FPC_FULLVERSION>30100}
  if not Assigned(FFPpkg) then
    Exit;
  for I := 0 to FFPpkg.RepositoryList.Count -1 do
    begin
    Repository := FFPpkg.RepositoryList.Items[I] as TFPRepository;
    for J := 0 to Repository.PackageCount -1 do
      begin
      AList.AddObject(Repository.Packages[J].Name, Repository.Packages[J]);
      end;
    end;
{$ENDIF FPC_FULLVERSION>30100}
end;

function TFppkgHelper.GetPackageUnitPath(const PackageName: string): string;
{$IF FPC_FULLVERSION>30100}
var
  FppkgPackage: TFPPackage;
{$IF not (FPC_FULLVERSION>30300)}
  PackageVariantsArray: TFppkgPackageVariantArray;
{$ENDIF}
  i: Integer;
{$ENDIF FPC_FULLVERSION>30100}
begin
{$IF NOT (FPC_FULLVERSION>30100)}
  if PackageName='' then ;
  Result := '';
{$ELSE}
  if not Assigned(FFPpkg) then
    begin
    Result := '';
    Exit;
    end;
  FppkgPackage := FFPpkg.FindPackage(PackageName, pkgpkInstalled);
  if Assigned(FppkgPackage) then
    begin
    Result := FppkgPackage.PackagesStructure.GetUnitDirectory(FppkgPackage);

    {$IF FPC_FULLVERSION>30300}
    for i := 0 to FppkgPackage.PackageVariants.Count -1 do
      begin
      Result := ConcatPaths([Result, FppkgPackage.PackageVariants.Items[i].Options[0]]);
      end;
    {$ELSE}
    PackageVariantsArray := GetPackageVariantArray(PackageName);
    for i := 0 to High(PackageVariantsArray) do
      begin
      Result := ConcatPaths([Result, PackageVariantsArray[i][1]]);
      end;
    {$ENDIF FPC_FULLVERSION>30300}
    end
  else
    begin
    // The package has not been installed, so there is no unit-path yet.
    // ToDo: if this leads to problems, we could 'guess' the repository it will
    // be installed into, and use the corresponding packagestructure.
    Result := '';
    end;
{$ENDIF FPC_FULLVERSION>30100}
end;

function TFppkgHelper.GetPackageVariantArray(const PackageName: string): TFppkgPackageVariantArray;
{$IF FPC_FULLVERSION>30100}
var
  FppkgPackage: TFPPackage;
  UnitConfigFile: TStringList;
  PackageVariantStr, PackageVariant, UnitConfigFilename: String;
  PackageVariantOptions: TStringArray;
  i: Integer;
{$ENDIF FPC_FULLVERSION>30100}
begin
  {$IF FPC_FULLVERSION>30100}
  Result := [];
  {$ELSE}
  SetLength(Result, 0);
  if PackageName='' then ;
  {$ENDIF FPC_FULLVERSION>30100}

  {$IF FPC_FULLVERSION>30100}
  if not Assigned(FFPpkg) then
    begin
    Result := [];
    Exit;
    end;

  FppkgPackage := FFPpkg.FindPackage(PackageName, pkgpkInstalled);
  if Assigned(FppkgPackage) then
    begin
    UnitConfigFilename := FppkgPackage.PackagesStructure.GetConfigFileForPackage(FppkgPackage);
    if FileExists(UnitConfigFilename) then
      begin
      UnitConfigFile := TStringList.Create;
      try
        UnitConfigFile.LoadFromFile(UnitConfigFilename);
        i := 1;
        repeat
        PackageVariantStr := UnitConfigFile.Values['PackageVariant_'+IntToStr(i)];
        if PackageVariantStr<>'' then
          begin
          PackageVariant := Copy(PackageVariantStr, 1, pos(':', PackageVariantStr) -1);
          if RightStr(PackageVariant, 1) = '*' then
            PackageVariant := Copy(PackageVariant, 1, Length(PackageVariant) -1);
          PackageVariantOptions := Copy(PackageVariantStr, pos(':', PackageVariantStr) +1).Split(',');
          Insert(PackageVariant, PackageVariantOptions, -1);
          Insert(PackageVariantOptions, Result, 100);
          end;
        inc(i);
        until PackageVariantStr='';
      finally
        UnitConfigFile.Free;
      end;
      end
    end
  {$ENDIF FPC_FULLVERSION>30100}
end;

function TFppkgHelper.IsProperlyConfigured(out Message: string): Boolean;
{$IF FPC_FULLVERSION>30100}
var
  CompilerFilename: string;
{$ENDIF FPC_FULLVERSION>30100}
begin
  Message := '';
  {$IF FPC_FULLVERSION>30100}
  if Assigned(FFPpkg) and (FIsProperlyConfigured=fpcUnknown) then
    begin
    FIsProperlyConfigured := fpcYes;
    FConfStatusMessage := '';

    if not HasPackage('rtl') then
      begin
      FIsProperlyConfigured := fpcNo;
      FConfStatusMessage := lisFppkgRtlNotFound;
      end
    else
      begin
      CompilerFilename := FFPpkg.CompilerOptions.Compiler;
      if Pos(PathDelim, CompilerFilename) > 0 then
        begin
        if not FileExistsCached(CompilerFilename) then
          begin
          FIsProperlyConfigured := fpcNo;
          FConfStatusMessage := Format(lisFppkgCompilerNotExists, [CompilerFilename]);
          end
        else if not FileIsExecutableCached(CompilerFilename) then
          begin
          FIsProperlyConfigured := fpcNo;
          FConfStatusMessage := Format(lisFppkgCompilerNotExecutable, [CompilerFilename]);
          end;
        end
      else
        begin
        CompilerFilename := ExeSearch(CompilerFilename);
        if CompilerFilename = '' then
          begin
          FIsProperlyConfigured := fpcNo;
          FConfStatusMessage := Format(lisFppkgCompilerNotFound, [FFPpkg.CompilerOptions.Compiler]);
          end
        else if not FileIsExecutableCached(CompilerFilename) then
          begin
          FIsProperlyConfigured := fpcNo;
          FConfStatusMessage := Format(lisFppkgCompilerNotExecutable, [CompilerFilename]);
          end;
        end
      end;
    end;
  result := FIsProperlyConfigured=fpcYes;
  {$ELSE}
  result := True;
  {$ENDIF FPC_FULLVERSION>30100}
  Message := FConfStatusMessage;
end;

function TFppkgHelper.HasFPCPackagesOnly(const PackageName: string): Boolean;
const
  FpcPackages: array[0..120] of String = (
    // All packages of fpc-trunk from 20181231
    'rtl',
    'rtl-generics',
    'fcl-res',
    'fpindexer',
    'lua',
    'regexpr',
    'fcl-db',
    'cdrom',
    'paszlib',
    'libgc',
    'libtar',
    'fcl-report',
    'libcups',
    'sqlite',
    'libsee',
    'newt',
    'sdl',
    'gnome1',
    'ldap',
    'openssl',
    'libpng',
    'graph',
    'bzip2',
    'fcl-extra',
    'dbus',
    'symbolic',
    'rtl-objpas',
    'mad',
    'httpd24',
    'fcl-process',
    'fcl-sound',
    'gdbint',
    'rtl-unicode',
    'gtk1',
    'fcl-net',
    'utils-lexyacc',
    'mysql',
    'ptc',
    'libvlc',
    'fcl-image',
    'webidl',
    'fcl-base',
    'oggvorbis',
    'a52',
    'fcl-pdf',
    'opencl',
    'pthreads',
    'libgd',
    'tcl',
    'xforms',
    'iconvenc',
    'dts',
    'gmp',
    'httpd22',
    'jni',
    'syslog',
    'pasjpeg',
    'users',
    'postgres',
    'rtl-extra',
    'pxlib',
    'fv',
    'ncurses',
    'zlib',
    'fastcgi',
    'aspell',
    'rtl-console',
    'googleapi',
    'fpgtk',
    'bfd',
    'libusb',
    'unzip',
    'libenet',
    'x11',
    'libcurl',
    'utils-pas2js',
    'chm',
    'numlib',
    'fcl-registry',
    'libxml2',
    'fcl-web',
    'imlib',
    'fpmkunit',
    'libmicrohttpd',
    'pcap',
    'utmp',
    'odbc',
    'fcl-xml',
    'fcl-fpcunit',
    'ibase',
    'fcl-passrc',
    'cairo',
    'ide',
    'fppkg',
    'gtk2',
    'fcl-async',
    'pastojs',
    'hermes',
    'ggi',
    'openal',
    'opengl',
    'zorba',
    'hash',
    'fcl-json',
    'gdbm',
    'oracle',
    'fftw',
    'uuid',
    'libfontconfig',
    'modplug',
    'rsvg',
    'fcl-sdo',
    'fcl-js',
    'proj4',
    'dblib',
    'svgalib',
    'opengles',
    'libffi',
    'odata',
    'fcl-stl',
    'imagemagick'
  );
var
  i: Integer;
begin
  for i := 0 to High(FpcPackages) do
    if SameText(PackageName, FpcPackages[i]) then
      Exit(True);
  Result := False;
end;

function TFppkgHelper.GetCompilerFilename: string;
begin
  Result := '';
  {$IF FPC_FULLVERSION>30100}
  if Assigned(FFPpkg) then
    begin
    Result := FFPpkg.CompilerOptions.Compiler;
    end;
  {$ENDIF}
end;

procedure TFppkgHelper.ReInitialize;
begin
  FIsProperlyConfigured := fpcUnknown;
  {$IF FPC_FULLVERSION>30100}
  FreeAndNil(FFPpkg);
  {$ENDIF}
  InitializeFppkg;
end;

function TFppkgHelper.GetCompilerConfigurationFileName: string;
{$IF FPC_FULLVERSION>30100}
var
  FPpkg: TpkgFPpkg;
{$ENDIF}
begin
  Result := '';
  {$IF FPC_FULLVERSION>30100}
  if Assigned(FFPpkg) then
    Result:=ConcatPaths([FFPpkg.Options.GlobalSection.CompilerConfigDir, FFPpkg.Options.CommandLineSection.CompilerConfig])
  else
    begin
    FPpkg := TpkgFPpkg.Create(nil);
    try
      try
        FPpkg.InitializeGlobalOptions(FOverrideConfigurationFilename);
        Result:=ConcatPaths([FPpkg.Options.GlobalSection.CompilerConfigDir, FPpkg.Options.CommandLineSection.CompilerConfig])
      except
        on E: Exception do
          debugln(['Fppkg initialize global options failed: '+E.Message]);
      end;
    finally
      FPpkg.Free;
    end;
    end
  {$ENDIF}
end;

function TFppkgHelper.GetConfigurationFileName: string;
begin
  Result := '';
  {$IF FPC_FULLVERSION>30200}
  if Assigned(FFPpkg) then
    Result:=FFPpkg.ConfigurationFilename;
  {$ENDIF}
end;

procedure TFppkgHelper.SetOverrideConfigurationFilename(AValue: string);
begin
  if FOverrideConfigurationFilename = AValue then Exit;
  FOverrideConfigurationFilename := AValue;
  ReInitialize;
end;

finalization
  GFppkgHelper.Free;
end.
