////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2001-2026 Alexey Kuryakin daqgroup@mail.ru under MIT license //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// This file is part of the CRW-DAQ project by DaqGroup - component CRWLIB.   //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// Purpose:                                                                   //
// Geolocation Identifiers:                                                   //
// 1) International Country Codes: ISO-3166.                                  //
//    https://github.com/lukes/ISO-3166-Countries-with-Regional-Codes         //
// 2) Internet country top-level domains (TLDs).                              //
//    Provides ccTLDs - country code top-level domains.                       //
//    https://www.worldstandards.eu/other/tlds/                               //
//    https://github.com/gh0stwizard/iana-tld-extractor                       //
// 3) IANA Root Zone Database                                                 //
//    https://www.iana.org/domains/root/db                                    //
// 4) https://github.com/jonasraoni/tld-to-iso                                //
// 5) https://stefangabos.github.io/world_countries/                          //
// 6) https://github.com/mledoze/countries/                                   //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// History:                                                                   //
// 20240105 - Created by A.K.                                                 //
////////////////////////////////////////////////////////////////////////////////

unit _crw_geoid; //  Geolocation Identifiers.

{$I _crw_sysdef.inc}

{$I _crw_sysmode.inc}

interface

uses
 //////////////////////////////////////////////////////
 {$I _crw_uses_first.inc} // NB: MUST BE FIRST USES !!!
 //////////////////////////////////////////////////////
 sysutils, classes, math,
 _crw_alloc, _crw_rtc, _crw_str, _crw_fio, _crw_sect;

 {
 ISO-3166-1 standard provides a table of country codes to identify geolocation.
 -------------------------------------------------------------------------------
 Field                  | Description                  | Example
 -----------------------+------------------------------+------------------------
 Name                   | Country Name                 | Russian Federation
 Alpha2                 | 2-char Country Code          | RU
 Alpha3                 | 3-char Country Code          | RUS
 CountryCode            | 3-digit numeric Country Code | 643
 Iso3166_2              |                              | ISO 3166-2:RU
 Region                 | Name of Country Region       | Europe
 SubRegion              | Name of SubRegion            | Eastern Europe
 IntermediateRegion     |                              | ""
 RegionCode             | 3-digit Region Code          | 150
 SubRegionCode          | 3-digit SubRegion Code       | 151
 IntermediateRegionCode | 3-digit Region Code          | ""
 -------------------------------------------------------------------------------
 }
type
 TIsoCountryInfo=class(TMasterObject)
 private
  myName                   : LongString;
  myAlpha2                 : LongString;
  myAlpha3                 : LongString;
  myCountryCode            : LongString;
  myIso3166_2              : LongString;
  myRegion                 : LongString;
  mySubRegion              : LongString;
  myIntermediateRegion     : LongString;
  myRegionCode             : LongString;
  mySubRegionCode          : LongString;
  myIntermediateRegionCode : LongString;
  myNameRu                 : LongString;
  myTlds                   : LongString;
  myIndex                  : Integer;
 private
  function  GetName                   : LongString;
  function  GetAlpha2                 : LongString;
  function  GetAlpha3                 : LongString;
  function  GetCountryCode            : LongString;
  function  GetIso3166_2              : LongString;
  function  GetRegion                 : LongString;
  function  GetSubRegion              : LongString;
  function  GetIntermediateRegion     : LongString;
  function  GetRegionCode             : LongString;
  function  GetSubRegionCode          : LongString;
  function  GetIntermediateRegionCode : LongString;
  function  GetNameRu                 : LongString;
  procedure SetNameRu(aName:LongString);
  function  GetTlds                   : LongString;
  procedure SetTlds(aTlds:LongString);
  function  GetIndex                  : Integer;
  procedure SetIndex(aIndex:Integer);
 public
  constructor Create(const aLine:LongString);
  destructor  Destroy; override;
 public
  property Name                   : LongString read GetName;
  property Alpha2                 : LongString read GetAlpha2;
  property Alpha3                 : LongString read GetAlpha3;
  property CountryCode            : LongString read GetCountryCode;
  property Iso3166_2              : LongString read GetIso3166_2;
  property Region                 : LongString read GetRegion;
  property SubRegion              : LongString read GetSubRegion;
  property IntermediateRegion     : LongString read GetIntermediateRegion;
  property RegionCode             : LongString read GetRegionCode;
  property SubRegionCode          : LongString read GetSubRegionCode;
  property IntermediateRegionCode : LongString read GetIntermediateRegionCode;
  property NameRu                 : LongString read GetNameRu;
  property Tlds                   : LongString read GetTlds;
  property Index                  : Integer    read GetIndex;
 public
  function CsvLine:LongString;
  function SimpleTable:LongString;
 end;

const
 Iso3166CsvHeader='name,alpha-2,alpha-3,country-code,iso_3166-2,'
                 +'region,sub-region,intermediate-region,region-code,'
                 +'sub-region-code,intermediate-region-code,name-ru,'
                 +'top-level-domains';

 {
 Iso3166:TIsoCountryCodes provides a table of ISO-3166 country codes.
 }
type
 TIsoCountryCodes=class(TMasterObject)
 private
  myCsvText : LongString;
  myList    : TStringList;
  myDict    : TStringList;
  function  GetCount:Integer;
  function  GetCsvText:LongString;
  function  GetItems(i:Integer):TIsoCountryInfo;
  function  ParseCsv(const Csv,Ru,Tlds:LongString):Integer;
 public
  constructor Create;
  destructor Destroy; override;
 public
  property Count:Integer read GetCount;
  property CsvText:LongString read GetCsvText;
  property Items[i:Integer]:TIsoCountryInfo read GetItems; default;
 public
  function Reload(Force:Boolean=false):Integer;
  function IndexOf(const alpha:LongString):Integer;
  function CsvTable:LongString;
 public
  class function ExtractCsvField(n:Integer; const aLine:LongString):LongString;
  class function ExtractName(const aLine:LongString):LongString;
  class function ExtractAlpha2(const aLine:LongString):LongString;
  class function ExtractAlpha3(const aLine:LongString):LongString;
  class function ExtractCountryCode(const aLine:LongString):LongString;
  class function ExtractIso3166_2(const aLine:LongString):LongString;
  class function ExtractRegion(const aLine:LongString):LongString;
  class function ExtractSubRegion(const aLine:LongString):LongString;
  class function ExtractIntermediateRegion(const aLine:LongString):LongString;
  class function ExtractRegionCode(const aLine:LongString):LongString;
  class function ExtractSubRegionCode(const aLine:LongString):LongString;
  class function ExtractIntermediateRegionCode(const aLine:LongString):LongString;
 end;

function Iso3166:TIsoCountryCodes;

procedure Init_CountryCodes;

implementation

constructor TIsoCountryInfo.Create(const aLine:LongString);
begin
 inherited Create;
 myName                   := TIsoCountryCodes.ExtractName(aLine);
 myAlpha2                 := TIsoCountryCodes.ExtractAlpha2(aLine);
 myAlpha3                 := TIsoCountryCodes.ExtractAlpha3(aLine);
 myCountryCode            := TIsoCountryCodes.ExtractCountryCode(aLine);
 myIso3166_2              := TIsoCountryCodes.ExtractIso3166_2(aLine);
 myRegion                 := TIsoCountryCodes.ExtractRegion(aLine);
 mySubRegion              := TIsoCountryCodes.ExtractSubRegion(aLine);
 myIntermediateRegion     := TIsoCountryCodes.ExtractIntermediateRegion(aLine);
 myRegionCode             := TIsoCountryCodes.ExtractRegionCode(aLine);
 mySubRegionCode          := TIsoCountryCodes.ExtractSubRegionCode(aLine);
 myIntermediateRegionCode := TIsoCountryCodes.ExtractIntermediateRegionCode(aLine);
 myNameRu                 := '';
 myTlds                   := '';
end;

destructor  TIsoCountryInfo.Destroy;
begin
 myName                   :='';
 myAlpha2                 :='';
 myAlpha3                 :='';
 myCountryCode            :='';
 myIso3166_2              :='';
 myRegion                 :='';
 mySubRegion              :='';
 myIntermediateRegion     :='';
 myRegionCode             :='';
 mySubRegionCode          :='';
 myIntermediateRegionCode :='';
 myNameRu                 :='';
 myTlds                   :='';
 inherited Destroy;
end;

function TIsoCountryInfo.GetName:LongString;
begin
 if Assigned(Self) then Result:=myName else Result:='';
end;

function TIsoCountryInfo.GetAlpha2:LongString;
begin
 if Assigned(Self) then Result:=myAlpha2 else Result:='';
end;

function TIsoCountryInfo.GetAlpha3:LongString;
begin
 if Assigned(Self) then Result:=myAlpha3 else Result:='';
end;

function TIsoCountryInfo.GetCountryCode:LongString;
begin
 if Assigned(Self) then Result:=myCountryCode else Result:='';
end;

function TIsoCountryInfo.GetIso3166_2:LongString;
begin
 if Assigned(Self) then Result:=myIso3166_2 else Result:='';
end;

function TIsoCountryInfo.GetRegion:LongString;
begin
 if Assigned(Self) then Result:=myRegion else Result:='';
end;

function TIsoCountryInfo.GetSubRegion:LongString;
begin
 if Assigned(Self) then Result:=mySubRegion else Result:='';
end;

function TIsoCountryInfo.GetIntermediateRegion:LongString;
begin
 if Assigned(Self) then Result:=myIntermediateRegion else Result:='';
end;

function TIsoCountryInfo.GetRegionCode:LongString;
begin
 if Assigned(Self) then Result:=myRegionCode else Result:='';
end;

function TIsoCountryInfo.GetSubRegionCode:LongString;
begin
 if Assigned(Self) then Result:=mySubRegionCode else Result:='';
end;

function TIsoCountryInfo.GetIntermediateRegionCode:LongString;
begin
 if Assigned(Self) then Result:=myIntermediateRegionCode else Result:='';
end;

function TIsoCountryInfo.GetNameRu:LongString;
begin
 if Assigned(Self) then Result:=myNameRu else Result:='';
end;

procedure TIsoCountryInfo.SetNameRu(aName:LongString);
begin
 if Assigned(Self) then myNameRu:=aName;
end;

function TIsoCountryInfo.GetTlds:LongString;
begin
 if Assigned(Self) then Result:=myTlds else Result:='';
end;

procedure TIsoCountryInfo.SetTlds(aTlds:LongString);
begin
 if Assigned(Self) then myTlds:=aTlds;
end;

function TIsoCountryInfo.GetIndex:Integer;
begin
 if Assigned(Self) then Result:=myIndex else Result:=-1;
end;

procedure TIsoCountryInfo.SetIndex(aIndex:Integer);
begin
 if Assigned(Self) then myIndex:=aIndex;
end;

function TIsoCountryInfo.CsvLine:LongString;
 function fs(const s:LongString):LongString;
 begin
  Result:=TrimDef(s,'""');
 end;
begin
 Result:='';
 if Assigned(Self) then
 Result:=Format('%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s',
 [fs(Name),fs(Alpha2),fs(Alpha3),fs(CountryCode),fs(Iso3166_2),
  fs(Region),fs(SubRegion),fs(IntermediateRegion),fs(RegionCode),
  fs(SubRegionCode),fs(IntermediateRegionCode),fs(NameRu),fs(Tlds)]);
end;

function TIsoCountryInfo.SimpleTable:LongString;
 function fs(n:Integer; const s:LongString):LongString;
 begin
  Result:=Pad(ExtractWord(n,Iso3166CsvHeader,ScanSpaces)+':',25)+' '+TrimDef(s,'""');
 end;
begin
 Result:='';
 if Assigned(Self) then
 Result:=fs(1,Name)+EOL
        +fs(2,Alpha2)+EOL
        +fs(3,Alpha3)+EOL
        +fs(4,CountryCode)+EOL
        +fs(5,Iso3166_2)+EOL
        +fs(6,Region)+EOL
        +fs(7,SubRegion)+EOL
        +fs(8,IntermediateRegion)+EOL
        +fs(9,RegionCode)+EOL
        +fs(10,SubRegionCode)+EOL
        +fs(11,IntermediateRegionCode)+EOL
        +fs(12,NameRu)+EOL
        +fs(13,Tlds);
end;

//////////////////////////////////
// TIsoCountryCodes implementation
//////////////////////////////////

constructor TIsoCountryCodes.Create;
begin
 inherited Create;
 myList:=TStringList.Create;
 myList.Duplicates:=dupAccept;
 myList.OwnsObjects:=true;
 myList.Sorted:=true;
 myDict:=TStringList.Create;
 myDict.Duplicates:=dupIgnore;
 myDict.OwnsObjects:=false;
 myDict.Sorted:=true;
 myCsvText:='';
end;

destructor TIsoCountryCodes.Destroy;
begin
 Kill(myList);
 Kill(myDict);
 myCsvText:='';
 inherited Destroy;
end;

function CsvParser(n:SizeInt; Line:LongString; Custom:Pointer):Boolean;
var a2,a3,d3:LongString; cc:TIsoCountryCodes; ci:TIsoCountryInfo; dn:Integer;
begin
 Result:=true; cc:=Custom;
 if not Assigned(cc) then Exit;
 a2:=TIsoCountryCodes.ExtractAlpha2(Line);
 a3:=TIsoCountryCodes.ExtractAlpha3(Line);
 d3:=TIsoCountryCodes.ExtractCountryCode(Line);
 if (Length(a2)=2) and (Length(a3)=3) then begin
  ci:=TIsoCountryInfo.Create(Line);
  cc.myList.AddObject(a2,ci);
  cc.myDict.AddObject(a2,ci);
  cc.myDict.AddObject(a3,ci);
  cc.myDict.AddObject(d3,ci);
  if TryStrToInt(d3,dn) and (dn>0)
  then cc.myDict.AddObject(IntToStr(dn),ci);
 end;
end;

function RuWorldParser(n:SizeInt; Line:LongString; Custom:Pointer):Boolean;
var a2,ru:LongString; cc:TIsoCountryCodes; i:Integer;
begin
 Result:=true; cc:=Custom;
 if not Assigned(cc) then Exit;
 a2:=TIsoCountryCodes.ExtractCsvField(2,Line);
 ru:=TIsoCountryCodes.ExtractCsvField(4,Line);
 if (Length(a2)=2) then i:=cc.IndexOf(a2) else i:=-1;
 if (i>=0) then cc.Items[i].SetNameRu(ru);
end;

function TldsParser(n:SizeInt; Line:LongString; Custom:Pointer):Boolean;
var a2,tl,dn:LongString; cc:TIsoCountryCodes; i,j:Integer;
begin
 Result:=true; cc:=Custom;
 if not Assigned(cc) then Exit;
 a2:=TIsoCountryCodes.ExtractCsvField(1,Line);
 tl:=TIsoCountryCodes.ExtractCsvField(2,Line);
 tl:=StringReplace(tl,':',' ',[rfReplaceAll]);
 if (Length(a2)=2) then i:=cc.IndexOf(a2) else i:=-1;
 if (i>=0) then begin
  cc.Items[i].SetTlds(Trim(tl));
  for j:=1 to WordCount(tl,ScanSpaces) do begin
   dn:=ExtractWord(j,tl,ScanSpaces);
   if (Pos('.',dn)=1) then cc.myDict.AddObject(dn,cc.Items[i]);
  end;
 end;
end;

function TIsoCountryCodes.ParseCsv(const Csv,Ru,Tlds:LongString):Integer;
var i:Integer;
begin
 if Assigned(Self) then
 try
  myList.Clear; myDict.Clear;
  ForEachStringLine(Csv,CsvParser,Self);
  for i:=0 to Count-1 do Items[i].SetIndex(i);
  ForEachStringLine(Tlds,TldsParser,Self);
  ForEachStringLine(Ru,RuWorldParser,Self);
  Result:=Count;
 except
  on E:Exception do BugReport(E,Self,'ParseCsv');
 end;
end;

function TIsoCountryCodes.Reload(Force:Boolean=false):Integer;
var ru,tl:LongString;
begin
 Result:=0;
 if Assigned(Self) then
 try
  if Force then myCsvText:='';
  if (myCsvText<>'') then Result:=Count else begin
   myCsvText:=ExtractTextSection(SysIniFile,SectI18nCodesIso3166Csv,efConfigNC);
   ru:=ExtractTextSection(SysIniFile,SectI18nCodesRuWorldCsv,efConfigNC);
   tl:=ExtractTextSection(SysIniFile,SectI18nCodesCca2TldCsv,efConfigNC);
   Result:=ParseCsv(myCsvText,ru,tl);
  end;
 except
  on E:Exception do BugReport(E,Self,'Reload');
 end;
end;

function TIsoCountryCodes.GetCsvText:LongString;
begin
 Result:='';
 if Assigned(Self) then begin
  if (myCsvText='') then Reload;
  Result:=myCsvText;
 end;
end;

function TIsoCountryCodes.GetCount:Integer;
begin
 if Assigned(Self)
 then Result:=myList.Count
 else Result:=0;
end;

function TIsoCountryCodes.GetItems(i:Integer):TIsoCountryInfo;
begin
 if Assigned(Self) and InRange(i,0,myList.Count-1)
 then Result:=(myList.Objects[i] as TIsoCountryInfo)
 else Result:=nil;
end;

function TIsoCountryCodes.IndexOf(const alpha:LongString):Integer;
begin
 if Assigned(Self) and (Length(alpha)=2)
 then Result:=myList.IndexOf(alpha)
 else Result:=-1;
 if (Result>=0) then Exit; // Found in Alpha2 list
 if Assigned(Self) and (alpha<>'')
 then Result:=myDict.IndexOf(alpha)
 else Result:=-1;
 if (Result>=0) then Result:=(myDict.Objects[Result] as TIsoCountryInfo).Index;
end;

function TIsoCountryCodes.CsvTable:LongString;
var List:TStringList; i:Integer;
begin
 Result:='';
 try
  List:=TStringList.Create;
  try
   for i:=0 to Count-1 do List.Add(Items[i].CsvLine);
   Result:=List.Text;
  finally
   Kill(List);
  end;
 except
  on E:Exception do BugReport(E,Self,'CsvTable');
 end;
end;

procedure Init_CountryCodes;
var ms:QWord;
begin
 ms:=GetTickCount64;
 DebugOutText(stdfDebug,EOL+SectI18nCodesIso3166Csv);
 if (iso3166.Reload(true)>0) then begin
  DebugOutText(stdfDebug,iso3166.CsvText+EOL);
  DebugOutText(stdfDebug,iso3166.CsvTable+EOL);
 end;
 ms:=GetTickCount64-ms;
 DebugOutText(stdfDebug,'Init_CountryCodes takes '+IntToStr(ms)+' ms'+EOL);
end;

////////////////////////////////////////////////////////////////////////////////
// 1:  name                       2:  alpha-2
// 3:  alpha-3                    4:  country-code
// 5:  iso_3166-2                 6:  region
// 7:  sub-region                 8:  intermediate-region
// 9:  region-code                10: sub-region-code
// 11: intermediate-region-code
////////////////////////////////////////////////////////////////////////////////

class function TIsoCountryCodes.ExtractCsvField(n:Integer; const aLine:LongString):LongString;
var s:LongString; i:Integer; const JustComma=[','];
begin
 s:=aLine;
 for i:=1 to n do begin
  Result:=Trim(ExtractFirstParam(Trim(s),QuoteMark,JustComma));
  s:=Trim(SkipFirstParam(Trim(s),QuoteMark,JustComma));
 end;
 Exit; // Skip obsolete:
 Result:=Trim(ExtractWord(n,aLine,[',']));
 if (StrFetch(Result,1)=QuoteMark) then Result:=AnsiDequotedStr(Result,QuoteMark);
end;

class function TIsoCountryCodes.ExtractName(const aLine:LongString):LongString;
begin
 Result:=ExtractCsvField(1,aLine);
end;

class function TIsoCountryCodes.ExtractAlpha2(const aLine:LongString):LongString;
begin
 Result:=ExtractCsvField(2,aLine);
end;

class function TIsoCountryCodes.ExtractAlpha3(const aLine:LongString):LongString;
begin
 Result:=ExtractCsvField(3,aLine);
end;

class function TIsoCountryCodes.ExtractCountryCode(const aLine:LongString):LongString;
begin
 Result:=ExtractCsvField(4,aLine);
end;

class function TIsoCountryCodes.ExtractIso3166_2(const aLine:LongString):LongString;
begin
 Result:=ExtractCsvField(5,aLine);
end;

class function TIsoCountryCodes.ExtractRegion(const aLine:LongString):LongString;
begin
 Result:=ExtractCsvField(6,aLine);
end;

class function TIsoCountryCodes.ExtractSubRegion(const aLine:LongString):LongString;
begin
 Result:=ExtractCsvField(7,aLine);
end;

class function TIsoCountryCodes.ExtractIntermediateRegion(const aLine:LongString):LongString;
begin
 Result:=ExtractCsvField(8,aLine);
end;

class function TIsoCountryCodes.ExtractRegionCode(const aLine:LongString):LongString;
begin
 Result:=ExtractCsvField(9,aLine);
end;

class function TIsoCountryCodes.ExtractSubRegionCode(const aLine:LongString):LongString;
begin
 Result:=ExtractCsvField(10,aLine);
end;

class function TIsoCountryCodes.ExtractIntermediateRegionCode(const aLine:LongString):LongString;
begin
 Result:=ExtractCsvField(11,aLine);
end;

/////////////////////////
// Iso3166 implementation
/////////////////////////

const
 TheIso3166:TIsoCountryCodes=nil;

function Iso3166:TIsoCountryCodes;
begin
 if not Assigned(TheIso3166) then begin
  TheIso3166:=TIsoCountryCodes.Create;
  TheIso3166.Master:=@TheIso3166;
 end;
 Result:=TheIso3166;
end;

///////////////////////////////////////
// Unit initialization and finalization
///////////////////////////////////////

procedure Init_crw_geoid;
begin
 Iso3166.Ok;
end;

procedure Free_crw_geoid;
begin
 Kill(TObject(TheIso3166));
end;

initialization

 Init_crw_geoid;

finalization

 Free_crw_geoid;

end.

//////////////
// END OF FILE
//////////////

