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

SPackGui (common files)
Copyright (C) 2013 Geoffray Levasseur <geoffray.levasseurbrandin@numericable.fr>.
Copyright (C) <date> <add your name and mail address here>

http://www.geoffray-levasseur.org/
http://0.tuxfamilly.org/

This software is governed by the CeCILL license under French law and
abiding by the rules of distribution of free software.  You can  use,
modify and/ or redistribute the software under the terms of the CeCILL
license as circulated by CEA, CNRS and INRIA at the following URL
"http://www.cecill.info".

As a counterpart to the access to the source code and  rights to copy,
modify and redistribute granted by the license, users are provided only
with a limited warranty  and the software's author,  the holder of the
economic rights,  and the successive licensors  have only  limited
liability.

In this respect, the user's attention is drawn to the risks associated
with loading,  using,  modifying and/or developing or reproducing the
software by the user in light of its specific status of free software,
that may mean  that it is complicated to manipulate,  and  that  also
therefore means  that it is reserved for developers  and  experienced
professionals having in-depth computer knowledge. Users are therefore
encouraged to load and test the software's suitability as regards their
requirements in conditions enabling the security of their systems and/or
data to be ensured and,  more generally, to use and operate it in the
same conditions as regards security.

The fact that you are presently reading this means that you have had
knowledge of the CeCILL license and that you accept its terms.

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

Description:
  some common vars and functions

********************************************************************************
}
unit uCommon;

{$include defines.inc}

interface

uses
  Classes, IniFiles, Forms;

type
  TCharSet = set of Char;

const
  //for test we don't touch to real system files
  {$IFDEF TEST}
  //you need to change everything before the ELSE compiler directive
  //accordingly to your own test path
  sDefaultLogFileName = '/share/src/pascal/0linux/log/spackgui.log';
  sLockFile = '/share/src/pascal/0linux/log/spackgui.lock';
  sIconBaseDir = '/share/src/pascal/0linux/icons';
  sRootUserName = 'fatalerrors'; //when testing we want to make change even if non root

  sDefaultSpackPkgDir = '/share/src/pascal/0linux/testpack';
  sDefaultConfDir = '/share/src/pascal/0linux/testconf';
  sDefaultRepoIndexDir = sDefaultConfDir;
  sDefaultDwlDir = '/share/src/pascal/0linux/packages';
  sRepoConfFile = 'depots.conf';
  sMainConfFile = 'spackgui.conf';
  {$ELSE}
  sDefaultLogFileName = {$LOCALSTATEDIR}'/log/spackgui.log';
  sLockFile = {$LOCALSTATEDIR}'/lock/spackgui.lock';
  sIconBaseDir = {$PREFIX}'/share/spackgui/icons';
  sRootUserName = 'root';

  sDefaultSpackPkgDir = {$LOCALSTATEDIR}'/lib/spack';
  sDefaultConfDir = {$SYSCONFDIR}'/spackgui';
  sDefaultRepoIndexDir = sDefaultConfDir;
  sDefaultDwlDir = {$LOCALSTATEDIR}'/cache/spack';
  sRepoConfFile = 'depots.conf';
  sMainConfFile = 'spackgui.conf';
  {$ENDIF}

  sDefaultInstallPackage = 'spackadd %s';
  sDefaultReinstallPackage = 'spackadd -f %s';
  sDefaultRemovePackage = 'spackrm %s';
  sDefaultUpdatePackage = 'spackadd %s';

  AnsiLineFeed = AnsiChar(#10);
  AnsiCarriageReturn = AnsiChar(#13);
  AnsiCrLf = AnsiString(#13#10);

  iDefaultProxyPort = 3128;

  RegExprOperator: TCharSet = ['&', '|', '#'];


var
  sConfDir: string;          //place where configuration files will be stored
  sSpackPkgDir: string;      //installed packages repository
  sRepoIndexDir: string;     //place where repositories metadata are stored
  sDwlDir: string;           //place where packages are downloaded before install
  sDefaultRepo: string;      //default repo used for upgrades

  bChangeAllowed: Boolean;   //global saying if change are allowed or not
  bUnitsAreDecimal: Boolean; //user may want decimal
  cThousandSep: Char;        //thousand separator (#0 means none)
  cDecimalSep: Char;         //decimal separator

  bNoColors: Boolean;        //colorize listview or not
  bShowGrig: Boolean;        //show a grid in listview or not

  sInstallPackage: string;   //command to install a package
  sReinstallPackage: string; //command to reinstall an installed package
  sRemovePackage: string;    //command to remove an installed package
  sUpdatePackage: string;    //command to update an installed package

  tLockFile: Text;           //pointer to lockfile
  iniMain: TIniFile;         //main configuration file object
  iniRepo: TIniFile;         //repositories configuration file object
  bReadOnly: Boolean;        //when non-root we are read-only
  slRepoList: TStringList;   //list of non installed packages repository
  slDeprecated: TStringList; //list of deprecated packages
  sArch: string;             //used architecture

  //proxy settings
  sHttpProxyAddress: string;
  iHttpProxyPort: Integer;
  sHttpProxyUser: string;
  sHttpProxyPass: string;
  sFtpProxyAddress: string;
  iFtpProxyPort: Integer;
  sFtpProxyUser: string;
  sFtpProxyPass: string;

//write a string list in an ini file, the TInifile must be initialized
procedure IniWriteStrings(IniFile: TIniFile; const Section, Ident: string;
  Str: TStrings);

//read a string list in an ini file, the TInifile must be initialized
procedure IniReadStrings(IniFile: TIniFile; const Section, Ident: string;
  Str: TStrings);

// SPack use that format: pkgname-version-arch-build{,.spack}
procedure DecomposePackageName(const S: ansistring;
  out Name, Version: ansistring; out Build: Byte; out Arch: ansistring);

// Extract package name a full spack format string
function ExtractPackageName(const S: ansistring): ansistring;

// Format numbers for file size display (with units)
// the base unit is Byte but spack have KB as base unit
function DispSize(const Size: integer): string;

//convert strings generated by the previous function to LongInt
function SizeStrToInt(const SzStr: string): LongInt;

// Get Major, minor and release number (sometimes there's letters so we keep that
// string)... Exotic version numbering gets remainings information in rem var
procedure DecomposeVersion(const V: string; out Maj, Min, Rel, Rem: string);

// If V1 > V2 result is 1, 0 if equal and -1 if V1 < V2
function CompareVersion(const V1, V2: string): ShortInt;

// Load/save window geometry in main ini file
procedure LoadWindowGeometry(Form: TForm);
procedure SaveWindowGeometry(Form: TForm);

// Load/save repository configuration informations
procedure LoadRepoSettings;
procedure SaveRepoSettings;

// Initialize config dir and inifiles
procedure InitConf;

// Get address of a repository from it's name
function GetRepoAddress(const RepoName: string): string;


implementation

uses
  SysUtils, uStrings, uUtils, uDebug, Controls;


procedure IniWriteStrings(IniFile: TIniFile; const Section, Ident: string;
  Str: TStrings);
var
  I: integer;
  OldCount: Integer;
begin
  //old number of line as needed
  OldCount := IniFile.ReadInteger(Section, Ident + rsConfNameCount, 0);
  if OldCount > Str.Count then //more lines in old config ?
    for I := Str.Count to OldCount do
      IniFile.DeleteKey(Section, Ident + IntToStr(I)); //erase old keys
  IniFile.WriteInteger(Section, Ident + rsConfNameCount, Str.Count); //new count
  for I := 0 to Str.Count - 1 do
    IniFile.WriteString(Section, Ident + IntToStr(I), Str[I]); //write each line
end;


procedure IniReadStrings(IniFile: TIniFile; const Section, Ident: string;
  Str: TStrings);
var
  I, N: Integer;
begin
  Str.Clear;
  N := IniFile.ReadInteger(Section, Ident + rsConfNameCount, 0);
  for I := 0 to N - 1 do
    Str.Add(IniFile.ReadString(Section, Ident + IntToStr(I), ''));
end;


procedure DecomposePackageName(const S: ansistring;
  out Name, Version: ansistring; out Build: Byte; out Arch: ansistring);
var
  I, J: integer;
  Tmp: string;
begin
  I := Length(S);
  while S[I] <> '-' do
    Dec(I);
  Tmp := Copy(S, I + 1, Length(S) - I + 1);
  if Trim(Tmp) <> '' then
    Build := StrToInt(Tmp)
  else
    Build := 0;
  Dec(I);
  J := I;
  while S[I] <> '-' do
    Dec(I);
  Arch := Copy(S, I + 1, J - I);
  Dec(I);
  J := I;
  while S[I] <> '-' do
    Dec(I);
  Version := Copy(S, I + 1, J - I);
  Name := Copy(S, 1, I - 1);
  //the following is unneeded as we shouldn't have '-' in versions
  //--------------------------------------------------------------
  //I := Length(Name);
  //while (I > 1) and (Name[I] <> '-') do //still some '-' in the name ?
  //  Dec(I);
  //if I > 1 then //name or version contains '-': try to analyse this correctly...
  //  if (Name[I + 1] in ['0'..'9']) and (Name[Length(Name)] in ['0'..'9']) then
  //  begin
  //    Tmp := Copy(Name, I + 1, Length(Name) - I + 1);
  //    Version := Tmp + '-' + Version;
  //    Name := Copy(S, 1, I - 1);
  //  end;
end;


function ExtractPackageName(const S: ansistring): ansistring;
var
  I, P: integer;
  Tmp: string;
begin
  if UpperCase(ExtractFileExt(S)) = rsSpackExt then
    Tmp := RemoveFileExt(S)
  else
    Tmp := S;
  P := Pos(' ', S);
  if P <> 0 then
  begin
    Tmp := Copy(Tmp, P + 1, Length(Tmp) - P);
    P := Pos(' ', Tmp);
    Tmp := Copy(Tmp, 1, P - 1);
  end;
  I := Length(Tmp);
  //skip build
  while Tmp[I] <> '-' do
    Dec(I);
  Dec(I);
  //skip arch
  while Tmp[I] <> '-' do
    Dec(I);
  Dec(I);
  //skip version
  while Tmp[I] <> '-' do
    Dec(I);
  Result := Copy(S, 1, I - 1);
end;


function DispSize(const Size: LongInt): string;
var
  Diviser, I, N: Integer;
  Unt: string;
begin
  if bUnitsAreDecimal then
    Diviser := 1000
  else
    Diviser := 1024;
  // we go up to GB as bigger is probably not needed for now in that kind of app
  if Size > (Diviser * 10) then //if true at least 10KB
    if (Size div Diviser) > (Diviser * 10) then //10MB
      if ((Size div Diviser) div Diviser) > (Diviser * 10) then //10GB
      begin
        Result := IntToStr(((Size div Diviser) div Diviser) div Diviser);
        if ((Size div Diviser) div Diviser) mod Diviser <> 0 then
          Result := Result + cDecimalSep +
            Copy(IntToStr(((Size div Diviser) div Diviser) mod Diviser), 1, 2);
        if bUnitsAreDecimal then
          Unt := rsGB
        else
          Unt := rsGiB;
      end else
      begin
        Result := IntToStr((Size div Diviser) div Diviser);
        if (Size div Diviser) mod Diviser <> 0 then
          Result := Result + cDecimalSep +
            Copy(IntToStr((Size div Diviser) mod Diviser), 1, 2);
        if bUnitsAreDecimal then
          Unt := rsMB
        else
          Unt := rsMiB;
      end
    else
    begin
      Result := IntToStr(Size div Diviser);
      if Size mod Diviser <> 0 then
        Result := Result + cDecimalSep + Copy(IntToStr(Size mod Diviser), 1, 2);
      if bUnitsAreDecimal then
        Unt :=  rsKB
      else
        Unt := rsKiB;
    end else
    begin
      Unt := rsByte;
      Result := IntToStr(Size);
    end;
  if (((Length(Result) > 3) and (Pos(cDecimalSep, Result) = 0)) or
    ((Pos(cDecimalSep, Result) <> 0) and (Length(Result) > 5))) and
    (cThousandSep <> #0) then
  begin
    N := Pos(cDecimalSep, Result) - 1;
    if N <= 0 then
      N := Length(Result);
    I := 0;
    while N >= 1 do
    begin
      if (I <> 0) and (I mod 3 = 0) then
        Insert(cThousandSep, Result, N + 1);
      Inc(I);
      Dec(N);
    end;
  end;
  Result := Result + ' ' + Unt;
end;


function SizeStrToInt(const SzStr: string): LongInt;
var
  Mul: Integer;
  Val: Single;
  S: string;
begin
  S := Trim(SzStr);
  if S = '' then
  begin
    Result := 0;
    Exit;
  end;
  Mul := 1;
  if Pos(rsKiB, S) > 1 then
  begin
    Mul := 1024;
    S := Copy(S, 1, Length(S) - 4);
  end else
    if Pos(rsKB, S) > 1 then
    begin
      Mul := 1000;
      S := Copy(S, 1, Length(S) - 3);
    end else
      if Pos(rsMiB, S) > 1 then
      begin
        Mul := 1024 * 1024;
        S := Copy(S, 1, Length(S) - 4);
      end else
        if Pos(rsMB, S) > 1 then
        begin
          Mul := 1000 * 1000;
          S := Copy(S, 1, Length(S) - 3);
        end else
          if Pos(rsGiB, S) > 1 then
          begin
            Mul := 1024 * 1024 * 1024;
            S := Copy(S, 1, Length(S) - 4);
          end else
            if Pos(rsGB, S) > 1 then
            begin
              Mul := 1000 * 1000 * 1000;
              S := Copy(S, 1, Length(S) - 3);
            end else
              if Pos(rsByte, S) > 1 then
                S := Copy(S, 1, Length(S) - 2);
  while Pos(cThousandSep, S) <> 0 do
    Delete(S, Pos(cThousandSep, S), 1);
  if Pos(cDecimalSep, S) <> 0 then
    S[Pos(cDecimalSep, S)] := '.';
  try
    Val := StrToFloat(S);
  except
    PrintLnDbg(Format('E Error while converting %s with intermediate %s to float',
      [SzStr, S]));
    Result := 0;
    Exit;
  end;
  try
    Result := Round(Val * Mul);
  except
    PrintLnDbg(Format('E Error while converting %s: float is to big',
      [SzStr, S]));
    Result := 0;
  end;
end;

procedure DecomposeVersion(const V: string; out Maj, Min, Rel, Rem: string);
var
  I: integer;
  Tmp: string;
begin
  Rem := '';
  Tmp := '';
  I := Pos('.', V);
  if (I = 0) then
    I := Pos('_', V);
  if I > 0 then
  begin
    Maj := Copy(V, 1, I - 1);
    Tmp := Copy(V, I + 1, Length(V) - I);
    if Length(V) = 0 then
    begin
      Min := '';
      Rel := '';
      Rem := '';
      Exit;
    end;
    I := Pos('.', Tmp);
    if (I = 0) then
      I := Pos('_', Tmp);
    if I > 0 then
    begin
      Min := Copy(Tmp, 1, I - 1);
      Tmp := Copy(Tmp, I + 1, Length(Tmp) - I);
      if Length(Tmp) = 0 then
      begin
        Rel := '';
        Rem := '';
        Exit;
      end;
      I := Pos('.', Tmp);
      if (I = 0) then
        I := Pos('_', Tmp);
      if I > 0 then
      begin
        Rel := Copy(Tmp, 1, I - 1);
        Tmp := Copy(Tmp, I + 1, Length(Tmp) - I);
        if Length(Tmp) = 0 then
          Exit;
        Rem := Tmp;
      end else
        Rel := Tmp;
    end else
      Min := Tmp;
  end else
    Maj := V;
end;


function CompareVersion(const V1, V2: string): ShortInt;
var
  Maj1, Maj2: string;
  Min1, Min2: string;
  Rel1, Rel2: string;
  Rem1, Rem2: string;
begin
  DecomposeVersion(V1, Maj1, Min1, Rel1, Rem1);
  DecomposeVersion(V2, Maj2, Min2, Rel2, Rem2);
  Result := 0;
  //test remainning informations
  if Rem1 > Rem2 then
    Result := 1
  else
    if Rem1 < Rem2 then
      Result := -1;
  //test release (overide previous test)
  if Rel1 > Rel2 then
    Result := 1
  else
    if Rel1 < Rel2 then
      Result := -1;
  //test minor
  if Min1 > Min2 then
    Result := 1
  else
    if Min1 < Min2 then
      Result := -1;
  //test major
  if Maj1 > Maj2 then
    Result := 1
  else
    if Maj1 < Maj2 then
      Result := -1;
end;


procedure LoadWindowGeometry(Form: TForm);
begin
  Form.Left := iniMain.ReadInteger(Form.Name, rsConfNameLeft, Form.Left);
  Form.Top := iniMain.ReadInteger(Form.Name, rsConfNameTop, Form.Top);
  if Form.BorderStyle <> bsDialog then
  begin
    Form.Width := iniMain.ReadInteger(Form.Name, rsConfNameWidth, Form.Width);
    Form.Height := iniMain.ReadInteger(Form.Name, rsConfNameHeight, Form.Height);
    if iniMain.ReadBool(Form.Name, rsConfNameMaximized, False) then
      Form.WindowState := wsMaximized;
  end;
end;


procedure SaveWindowGeometry(Form: TForm);
var
  Maxi: Boolean;
begin
  iniMain.WriteInteger(Form.Name, rsConfNameLeft, Form.Left);
  iniMain.WriteInteger(Form.Name, rsConfNameTop, Form.Top);
  if Form.BorderStyle <> bsDialog then
  begin
    iniMain.WriteInteger(Form.Name, rsConfNameWidth, Form.Width);
    iniMain.WriteInteger(Form.Name, rsConfNameHeight, Form.Height);
    Maxi := Form.WindowState = wsMaximized;
    iniMain.WriteBool(Form.Name, rsConfNameMaximized, Maxi);
  end;
end;


procedure InitConf;
var
  UseHome: Boolean;
  ProxyTmp, Port: string;
begin
  bReadOnly := False;
  sConfDir := sDefaultConfDir;
  if not DirectoryExists(sConfDir) then
    if DirectoryIsReadOnly(ExtractFileDir(sConfDir)) then
      UseHome := True
    else
      MkDir(sConfDir)
  else
    if DirectoryIsReadOnly(sConfDir) then
      UseHome := True;
  if UseHome then
    if not DirectoryIsReadOnly(GetHomePath) then
    begin
      sConfDir := GetHomePath + rsFreeDesktopConfig + '/' + rsHomeConfDir; //freedestop compliant
      if not DirectoryExists(sConfDir) then
      begin
        if not DirectoryExists(GetHomePath + rsFreeDesktopConfig) then
          MkDir(GetHomePath + rsFreeDesktopConfig);
        MkDir(sConfDir);
      end
    end else
      bReadOnly := True;
  iniMain := TIniFile.Create(sConfDir + '/' + sMainConfFile);
  PrintLnDbg(Format(rsInfoInitConfFile, [iniMain.FileName]), vlLow);
  iniRepo := TIniFile.Create(sConfDir + '/' + sRepoConfFile);
  PrintLnDbg(Format(rsInfoInitConfFile, [iniRepo.FileName]), vlLow);
  case GetUsedCPU of
    cpu386: sArch := 'i686';
    cpuX86_64: sArch := 'x86_64';
    cpuArm: sArch := 'arm';
  end;
  //environment settings
  sInstallPackage := iniMain.ReadString(rsConfSectionCommand,
    rsConfNameInstallCommand, sDefaultInstallPackage);
  sReinstallPackage := iniMain.ReadString(rsConfSectionCommand,
    rsConfNameReinstallCommand, sDefaultReinstallPackage);
  sRemovePackage := iniMain.ReadString(rsConfSectionCommand,
    rsConfNameRemoveCommand, sDefaultRemovePackage);
  sUpdatePackage := iniMain.ReadString(rsConfSectionCommand,
    rsConfNameUpdateCommand, sDefaultUpdatePackage);
  sSpackPkgDir := iniMain.ReadString(rsConfSectionPaths, rsConfNameInstPkgPath,
    sDefaultSpackPkgDir);
  sDwlDir := iniMain.ReadString(rsConfSectionPaths, rsConfNamePkgDownloadPath,
    sDefaultDwlDir);
  sRepoIndexDir := iniMain.ReadString(rsConfSectionPaths,
    rsConfNameRepoDownloadPath, sDefaultRepoIndexDir);
  ProxyTmp := iniMain.ReadString(rsConfSectionProxy, rsConfNameHttpProxy, '');
  GetProxyInfo(ProxyTmp, sHttpProxyUser, sHttpProxyPass, sHttpProxyAddress, Port);
  try
    if Trim(Port) <> '' then
      iHttpProxyPort := StrToInt(Port);
  finally
    //do nothing
  end;
  ProxyTmp := iniMain.ReadString(rsConfSectionProxy, rsConfNameFtpProxy, '');
  GetProxyInfo(ProxyTmp, sFtpProxyUser, sFtpProxyPass, sFtpProxyAddress, Port);
  try
    if Trim(Port) <> '' then
      iFtpProxyPort := StrToInt(Port);
  finally
    //do nothing
  end;
  //display settings
  try
    cThousandSep := Chr(iniMain.ReadInteger(rsConfSectionDisplay,
      rsConfNameThousandsSep, Ord(' ')));
  except
    cThousandSep := ' ';
  end;
  try
    cDecimalSep := Chr(iniMain.ReadInteger(rsConfSectionDisplay,
      rsConfNameDecimalSep, Ord(',')));
  except
    cDecimalSep := ',';
  end;
  bUnitsAreDecimal := iniMain.ReadBool(rsConfSectionDisplay,
    rsConfNameDecimalUnits, False);
  bShowGrig := iniMain.ReadBool(rsConfSectionDisplay, rsConfNameShowGrid, False);
  bNoColors := iniMain.ReadBool(rsConfSectionDisplay, rsConfNameNoColors, False);
end;


procedure LoadRepoSettings;
var
  I: Integer;
begin
  PrintLnDbg(rsInfoLoadingRepoSettings, vlLow);
  if iniRepo = nil then
    Exit; //exception goes here
  if slRepoList = nil then
    slRepoList := TStringList.Create
  else
    slRepoList.Clear;
  iniRepo.ReadSections(slRepoList);
  for I := 0 to slRepoList.Count - 1 do
  begin
    if iniRepo.ReadBool(slRepoList[I], rsConfNameDefault, False) then
      sDefaultRepo := slRepoList[I];
    slRepoList[I] := slRepoList[I] + '=' +
      iniRepo.ReadString(slRepoList[I], rsConfNameAddress, '');
  end;
  PrintLnDbg(Format(rsInfoDefaultRepo, [sDefaultRepo]), vlLow);
end;


procedure SaveRepoSettings;
var
  I, N: Integer;
  Name, Address: string;
begin
  if bReadOnly then
    Exit; //this should not happen: trigger an exception when conception is over
  if (slRepoList = nil) or (iniRepo = nil) then
    Exit;  //should not happen: an exception goes here
  for I := 0 to slRepoList.Count - 1 do
  begin
    N := Pos('=', slRepoList[I]);
    if N = 0 then
      Continue; //show an error message
    Name := Trim(Copy(slRepoList[I], 1, N - 1));
    Address := Trim(Copy(slRepoList[I], N + 1, Length(slRepoList[I]) - N));
    iniRepo.WriteString(Name, rsConfNameAddress, Address);
    iniRepo.WriteBool(Name, rsConfNameDefault, Name = sDefaultRepo);
  end;
  iniRepo.UpdateFile;
end;

function GetRepoAddress(const RepoName: string): string;
var
  I: Integer;
begin
  for I := 0 to slRepoList.Count - 1 do
    if Pos(RepoName, slRepoList[I]) = 1 then
      Result := Copy(slRepoList[I], Pos('=', slRepoList[I]) + 1,
        Length(slRepoList[I]) - Pos('=', slRepoList[I]) + 1);
end;

end.

