unit Acronym;

interface

uses
  Classes, SysUtils, XMLRead, XMLWrite, DOM, XML, fphttpclient, opensslsockets,
  Dialogs, odbcconn, sqldb, LazUTF8, Generics.Collections, ListViewSort;

type
  TAcronymCategories = class;
  TAcronymMeanings = class;
  TAcronymDb = class;
  TImportSource = class;
  TImportSources = class;
  TImportFormats = class;

  TSearchFlag = (sfCaseInsensitive);
  TSearchFlags = set of TSearchFlag;

  { TAcronym }

  TAcronym = class
    Db: TAcronymDb;
    Name: string;
    Meanings: TAcronymMeanings;
    procedure Assign(Source: TAcronym);
    procedure SaveToNode(Node: TDOMNode);
    procedure LoadFromNode(Node: TDOMNode);
    constructor Create;
    destructor Destroy; override;
  end;

  { TAcronyms }

  TAcronyms = class(TObjectList<TAcronym>)
    Db: TAcronymDb;
    procedure Assign(Source: TAcronyms);
    procedure SaveToNode(Node: TDOMNode);
    procedure LoadFromNode(Node: TDOMNode);
    function SearchByName(Name: string; Flags: TSearchFlags = []): TAcronym;
    function AddAcronym(Name: string): TAcronym;
  end;

  { TAcronymMeaning }

  TAcronymMeaning = class
    Id: Integer;
    Name: string;
    Description: string;
    Language: string;
    Acronym: TAcronym;
    Categories: TAcronymCategories;
    Sources: TImportSources;
    procedure Assign(Source: TAcronymMeaning);
    procedure MergeCategories(MergedCategories: TAcronymCategories);
    procedure SaveToNode(Node: TDOMNode);
    procedure LoadFromNode(Node: TDOMNode);
    constructor Create;
    destructor Destroy; override;
  end;

  { TAcronymMeanings }

  TAcronymMeanings = class(TObjectList<TAcronymMeaning>)
  public
    Acronym: TAcronym;
    procedure Assign(Source: TAcronymMeanings);
    procedure UpdateIds;
    procedure SaveToNode(Node: TDOMNode);
    procedure LoadFromNode(Node: TDOMNode);
    function SearchByName(Name: string; Flags: TSearchFlags = []): TAcronymMeaning;
    function AddMeaning(Name: string): TAcronymMeaning;
    function GetNames: string;
  end;

  { TAcronymCategory }

  TAcronymCategory = class
    Id: Integer;
    Name: string;
    Enabled: Boolean;
    AcronymDb: TAcronymDb;
    function GetAcronymMeanings: TAcronymMeanings;
    function GetAcronymMeaningsCount: Integer;
    function GetImportSources: TImportSources;
    function GetImportSourcesCount: Integer;
    procedure Assign(Source: TAcronymCategory);
    procedure SaveToNode(Node: TDOMNode);
    procedure LoadFromNode(Node: TDOMNode);
    constructor Create;
  end;

  { TAcronymCategories }

  TAcronymCategories = class(TObjectList<TAcronymCategory>)
    Db: TAcronymDb;
    procedure UpdateIds;
    procedure SaveToNode(Node: TDOMNode);
    procedure LoadFromNode(Node: TDOMNode);
    procedure SaveRefToNode(Node: TDOMNode);
    procedure LoadRefFromNode(Node: TDOMNode);
    function SearchByName(Name: string): TAcronymCategory;
    function SearchById(Id: Integer): TAcronymCategory;
    function AddContext(Name: string): TAcronymCategory;
    procedure AssignToStrings(Strings: TStrings);
    procedure AssignFromStrings(Strings: TStrings);
    procedure AddFromStrings(Strings: TStrings);
    procedure AssignToList(List: TObjects);
    procedure Assign(Source: TAcronymCategories);
    function GetString: string;
    function IsAnyEnabled: Boolean;
  end;

  { TAcronymEntry }

  TAcronymEntry = class
    Name: string;
    Meaning: string;
    Description: string;
    Categories: TStringList;
    Sources: TStringList;
    constructor Create;
    destructor Destroy; override;
  end;

  TImportPatternFlag = (ipfSet, ipfNewItem, ipfSkip, ipfRemove, ipfCleanSet);
  TImportVariable = (ivNone, ivAcronym, ivMeaning, ivDescription, ivCategory);

  { TImportPattern }

  TImportPattern = class
    StartString: string;
    EndString: string;
    Variable: TImportVariable;
    Flag: TImportPatternFlag;
    Repetition: Boolean;
    procedure Assign(Source: TImportPattern);
    procedure SaveToNode(Node: TDOMNode);
    procedure LoadFromNode(Node: TDOMNode);
  end;

  { TImportPatterns }

  TImportPatterns = class(TObjectList<TImportPattern>)
    procedure SaveToNode(Node: TDOMNode);
    procedure LoadFromNode(Node: TDOMNode);
  end;

  TImportFormatKind = (ifkParseURL, ifkMSAccess, ifkParseFile);

  { TImportFormat }

  TImportFormat = class
    Id: Integer;
    Name: string;
    Kind: TImportFormatKind;
    Block: TImportPattern;
    ItemPatterns: TImportPatterns;
    Formats: TImportFormats;
    procedure Assign(Source: TImportFormat);
    procedure SaveToNode(Node: TDOMNode);
    procedure LoadFromNode(Node: TDOMNode);
    constructor Create;
    destructor Destroy; override;
  end;

  { TImportFormats }

  TImportFormats = class(TObjectList<TImportFormat>)
    procedure Assign(Source: TImportFormats);
    procedure UpdateIds;
    procedure SaveToNode(Node: TDOMNode);
    procedure LoadFromNode(Node: TDOMNode);
    function SearchByName(Name: string): TImportFormat;
    function SearchById(Id: Integer): TImportFormat;
  end;

  { TImportSource }

  TImportSource = class
  private
    ResponseStream: TMemoryStream;
    procedure DoPassword(Sender: TObject; var RepeatRequest : Boolean);
    procedure TextParse(AcronymDb: TAcronymDb; S: string);
  public
    Id: Integer;
    Enabled: Boolean;
    Name: string;
    URL: string;
    Format: TImportFormat;
    Sources: TImportSources;
    ItemCount: Integer;
    LastImportTime: TDateTime;
    Categories: TAcronymCategories;
    UserName: string;
    Password: string;
    function DownloadHTTP(URL: string; Stream: TStream): Boolean;
    procedure Process(AcronymDb: TAcronymDb);
    procedure ProcessTextParseURL(AcronymDb: TAcronymDb);
    procedure ProcessTextParseFile(AcronymDb: TAcronymDb);
    procedure ProcessMSAccess(AcronymDb: TAcronymDb);
    procedure Assign(Source: TImportSource);
    procedure SaveToNode(Node: TDOMNode);
    procedure LoadFromNode(Node: TDOMNode);
    constructor Create;
    destructor Destroy; override;
  end;

  { TImportSources }

  TImportSources = class(TObjectList<TImportSource>)
    AcronymDb: TAcronymDb;
    procedure UpdateIds;
    function SearchById(Id: Integer): TImportSource;
    procedure SaveRefToNode(Node: TDOMNode);
    procedure LoadRefFromNode(Node: TDOMNode);
    procedure AssignToStrings(Strings: TStrings);
    procedure Assign(Source: TImportSources);
    function SearchByName(Name: string): TImportSource;
    procedure SaveToNode(Node: TDOMNode);
    procedure LoadFromNode(Node: TDOMNode);
    procedure AssignToList(List: TObjects);
  end;

  { TAcronymDb }

  TAcronymDb = class
  private
    FUpdateCount: Integer;
  public
    FileName: string;
    Acronyms: TAcronyms;
    Categories: TAcronymCategories;
    ImportSources: TImportSources;
    ImportFormats: TImportFormats;
    Modified: Boolean;
    AddedCount: Integer;
    OnUpdate: TList<TNotifyEvent>;
    constructor Create;
    destructor Destroy; override;
    procedure Assign(Source: TAcronymDb);
    procedure LoadFromFile(FileName: string);
    procedure SaveToFile(FileName: string);
    procedure LoadFromFileCSV(FileName: string);
    procedure SaveToFileCSV(FileName: string);
    procedure FilterList(AName: string; Items: TAcronymMeanings);
    function GetMeaningsCount: Integer;
    function AddAcronym(AcronymName, MeaningName: string): TAcronymMeaning;
    function SearchAcronym(AcronymName, MeaningName: string; Flags: TSearchFlags = []): TAcronymMeaning;
    procedure RemoveMeaning(Meaning: TAcronymMeaning);
    procedure RemoveAcronym(AcronymName, MeaningName: string);
    procedure AssignToList(List: TObjects; EnabledCategoryOnly: Boolean = False);
    procedure BeginUpdate;
    procedure EndUpdate;
    procedure Update;
  end;

var
  ImportVariableString: array [TImportVariable] of string;
  ImportPatternFlagString: array [TImportPatternFlag] of string;

procedure Translate;


implementation

resourcestring
  SWrongFileFormat = 'Wrong file format';
  SUnsupportedImportFormat = 'Unsupported import format';
  SDescription = 'Description';
  SMeaning = 'Meaning';
  SAcronym = 'Acronym';
  SCategory = 'Category';
  SNone = 'None';
  SNewItem = 'New item';
  SSkip = 'Skip';
  SRemoveOnStart = 'Remove on start';
  SCleanSet = 'Clean set';
  SUnsupportedAuthMethod = 'Unsupported HTTP authorization method';
  SFileNotFound = 'File %s not found';


procedure Translate;
begin
  ImportVariableString[ivAcronym] := SAcronym;
  ImportVariableString[ivNone] := SNone;
  ImportVariableString[ivMeaning] := SMeaning;
  ImportVariableString[ivDescription] := SDescription;
  ImportVariableString[ivCategory] := SCategory;
  ImportPatternFlagString[ipfSet] := SNone;
  ImportPatternFlagString[ipfNewItem] := SNewItem;
  ImportPatternFlagString[ipfSkip] := SSkip;
  ImportPatternFlagString[ipfRemove] := SRemoveOnStart;
  ImportPatternFlagString[ipfCleanSet] := SCleanSet;
end;

function StripHTML(S: string): string;
var
  TagBegin, TagEnd, TagLength: Integer;
begin
  TagBegin := Pos( '<', S);      // search position of first <

  while (TagBegin > 0) do begin  // while there is a < in S
    TagEnd := Pos('>', S);              // find the matching >
    if TagEnd = 0 then TagLength := Length(S) - TagBegin
      else TagLength := TagEnd - TagBegin + 1;
    if TagLength > 0 then
      Delete(S, TagBegin, TagLength)     // delete the tag
      else Delete(S, 1, TagEnd);     // delete the tag
    TagBegin := Pos( '<', S);            // search for next <
  end;

  Result := S;                   // give the result
end;

{ TImportSourceMSAccess }

procedure TImportSource.ProcessMSAccess(AcronymDb: TAcronymDb);
var
  ODBCConnection1: TODBCConnection;
  SQLTransaction1: TSQLTransaction;
  SQLQuery1: TSQLQuery;
  NewAcronym: TAcronymEntry;
  AddedAcronym: TAcronymMeaning;
begin
  ItemCount := 0;
  ODBCConnection1 := TODBCCOnnection.Create(nil);
  SQLQuery1 := TSQLQuery.Create(nil);
  SQLTransaction1 := TSQLTransaction.Create(nil);
  try
    ODBCConnection1.Driver := 'Microsoft Access Driver (*.mdb, *.accdb)';
    ODBCConnection1.Params.Add('DBQ=' + URL);
    ODBCConnection1.Params.Add('Locale Identifier=1031');
    ODBCConnection1.Params.Add('ExtendedAnsiSQL=1');
    ODBCConnection1.Params.Add('CHARSET=ansi');
    ODBCConnection1.Connected := True;
    ODBCConnection1.KeepConnection := True;

    SQLTransaction1.DataBase := ODBCConnection1;
    SQLTransaction1.Action := caCommit;
    SQLTransaction1.Active := True;

    SQLQuery1.DataBase := ODBCConnection1;
    SQLQuery1.UsePrimaryKeyAsKey := False;
    SQLQuery1.SQL.Text := 'SELECT Acronym,Meaning FROM data1';
    SQLQuery1.Open;

    NewAcronym := TAcronymEntry.Create;
    while not SQLQuery1.EOF do begin
      NewAcronym.Name := Trim(WinCPToUTF8(SQLQuery1.FieldByName('Acronym').AsString));
      NewAcronym.Meaning := Trim(WinCPToUTF8(SQLQuery1.FieldByName('Meaning').AsString));
      if (NewAcronym.Name <> '') and (NewAcronym.Meaning <> '') then begin
        AddedAcronym := AcronymDb.AddAcronym(NewAcronym.Name, NewAcronym.Meaning);
        AddedAcronym.MergeCategories(Categories);
        if AddedAcronym.Sources.IndexOf(Self) = -1 then
          AddedAcronym.Sources.Add(Self);
        end;
      SQLQuery1.Next;
      Inc(ItemCount);
    end;
    NewAcronym.Free;
  finally
    SQLQuery1.Free;
    SQLTransaction1.Free;
    ODBCConnection1.Free;
  end;
end;

{ TImportPatterns }

procedure TImportPatterns.SaveToNode(Node: TDOMNode);
var
  I: Integer;
  NewNode2: TDOMNode;
begin
  for I := 0 to Count - 1 do
  with Items[I] do begin
    NewNode2 := Node.OwnerDocument.CreateElement('Pattern');
    Node.AppendChild(NewNode2);
    SaveToNode(NewNode2);
  end;
end;

procedure TImportPatterns.LoadFromNode(Node: TDOMNode);
var
  Node2: TDOMNode;
  NewItem: TImportPattern;
begin
  Count := 0;
  Node2 := Node.FirstChild;
  while Assigned(Node2) and (Node2.NodeName = 'Pattern') do begin
    NewItem := TImportPattern.Create;
    NewItem.LoadFromNode(Node2);
    Add(NewItem);
    Node2 := Node2.NextSibling;
  end;
end;

{ TImportPattern }

procedure TImportPattern.Assign(Source: TImportPattern);
begin
  StartString := Source.StartString;
  EndString := Source.EndString;
  Variable := Source.Variable;
  Flag := Source.Flag;
  Repetition := Source.Repetition;
end;

procedure TImportPattern.SaveToNode(Node: TDOMNode);
begin
  WriteString(Node, 'StartString', StartString);
  WriteString(Node, 'EndString', EndString);
  WriteInteger(Node, 'Variable', Integer(Variable));
  WriteInteger(Node, 'Flag', Integer(Flag));
  WriteBoolean(Node, 'Repetition', Repetition);
end;

procedure TImportPattern.LoadFromNode(Node: TDOMNode);
begin
  StartString := ReadString(Node, 'StartString', '');
  EndString := ReadString(Node, 'EndString', '');
  Variable := TImportVariable(ReadInteger(Node, 'Variable', 0));
  Flag := TImportPatternFlag(ReadInteger(Node, 'Flag', 0));
  Repetition := ReadBoolean(Node, 'Repetition', False);
end;

procedure TImportSource.DoPassword(Sender: TObject; var RepeatRequest: Boolean);
var
  H: string;
begin
  with TFPHttpClient(Sender) do begin
    H := GetHeader(ResponseHeaders, 'WWW-Authenticate');
    if Pos(' ', H) > 0 then H := Copy(H, 1, Pos(' ', H) - 1);

    if H <> 'Basic' then
      raise Exception.Create(SUnsupportedAuthMethod);

    if (Self.UserName <> '') and (UserName = '') then begin
      UserName := Self.UserName;
      Password := Self.Password;
      ResponseStream.Clear;
      RepeatRequest := True;
    end else RepeatRequest := False;
  end;
end;

procedure TImportSource.TextParse(AcronymDb: TAcronymDb; S: string);
var
  SS: string;
  NewAcronym: TAcronymEntry;
  P: Integer;
  P1, P2: Integer;
  Q: Integer;
  I: Integer;
  T: string;
  TT: string;
  LastLength: Integer;
  AddedAcronym: TAcronymMeaning;
  NewCategory: TAcronymCategory;
begin
  NewAcronym := TAcronymEntry.Create;
  try

  // Find main block
  if Format.Block.StartString <> '' then begin
    P := Pos(Format.Block.StartString, S);
    if P > 0 then
      Delete(S, 1, P + Length(Format.Block.StartString) - 1);
  end;
  if Format.Block.EndString <> '' then begin
    P := Pos(Format.Block.EndString, S);
    if P > 0 then
      Delete(S, P, Length(S));
  end;

  // Remove unneeded items
  repeat
    LastLength := Length(S);
    for I := 0 to Format.ItemPatterns.Count - 1 do
    with Format.ItemPatterns[I] do
    if Flag = ipfRemove then begin
      P := Pos(StartString, S);
      if P > 0 then begin
        SS := Copy(S, P + Length(StartString), Length(S));
        Q := Pos(EndString, SS);
        if Q > 0 then begin
          Delete(S, P, Q + Length(EndString) + Length(StartString) - 1);
        end;
      end;
    end;
  until Length(S) = LastLength;

  // Find items
  repeat
    LastLength := Length(S);
    I := 0;
    while I < Format.ItemPatterns.Count do
    with Format.ItemPatterns[I] do begin
      if Flag <> ipfRemove then begin
        TT := StartString;
        if Length(StartString) > 0 then begin
          P := Pos(StartString, S);
          if P > 0 then Delete(S, 1, P + Length(StartString) - 1);
        end;

        if ((Length(StartString) > 0) and (P > 0)) or (Length(StartString) = 0) then begin
          P := Pos(EndString, S);
          T := Copy(S, 1, P - 1);
          if Flag <> ipfSkip then begin
            T := StripHTML(T);
            T := StringReplace(T, '&quot;', '"', [rfReplaceAll]);
            T := StringReplace(T, '&trade;', 'TM', [rfReplaceAll]);
            T := StringReplace(T, '&amp;', '&', [rfReplaceAll]);
            T := StringReplace(T, '&#160;', ' ', [rfReplaceAll]); // Non-breaking space
            T := StringReplace(T, #$C2#$A0, ' ', [rfReplaceAll]); // Non-breaking space
            T := StringReplace(T, '&lt;', '<', [rfReplaceAll]);
            T := StringReplace(T, '&gt;', '>', [rfReplaceAll]);
            T := Trim(T);
            case Variable of
              ivAcronym: NewAcronym.Name := T;
              ivMeaning: NewAcronym.Meaning := T;
              ivDescription: NewAcronym.Description := T;
              ivCategory: begin
                NewAcronym.Categories.Clear;
                while T <> '' do begin
                  if Pos(',', T) > 0 then begin
                    TT := Copy(T, 1, Pos(',', T) - 1);
                    Delete(T, 1, Length(TT) + 1);
                  end else begin
                    TT := T;
                    T := '';
                  end;
                  TT := Trim(TT);
                  NewCategory := AcronymDb.Categories.SearchByName(TT);
                  if not Assigned(NewCategory) then begin
                    NewCategory := TAcronymCategory.Create;
                    NewCategory.Name := TT;
                    AcronymDb.Categories.Add(NewCategory);
                  end;
                  NewAcronym.Categories.AddObject(TT, NewCategory);
                end;
              end;
            end;
          end;
          Delete(S, 1, P + Length(EndString) - 1);

          if (Flag = ipfNewItem) and (NewAcronym.Name <> '') and
            (NewAcronym.Meaning <> '') then begin
              AddedAcronym := AcronymDb.AddAcronym(NewAcronym.Name, NewAcronym.Meaning);
              AddedAcronym.Description := NewAcronym.Description;
              AddedAcronym.MergeCategories(Categories);
              AddedAcronym.Categories.AddFromStrings(NewAcronym.Categories);
              if AddedAcronym.Sources.IndexOf(Self) = -1 then
                AddedAcronym.Sources.Add(Self);

              Inc(ItemCount);
            end;

          if Repetition then begin
            if Length(StartString) > 0 then begin
              P1 := Pos(StartString, S);
              if P1 > 0 then begin
                P2 := Pos(Format.ItemPatterns[(I + 1) mod
                  Format.ItemPatterns.Count].StartString, S);
                if (P2 > 0) and (P1 < P2) then Continue;
              end;
            end;
          end;
        end;
      end;
      Inc(I);
    end;
  until Length(S) = LastLength;
  finally
    NewAcronym.Free;
  end;
end;


function TImportSource.DownloadHTTP(URL: string; Stream: TStream): Boolean;
var
  HTTPClient: TFPHTTPClient;
  FormData: TStringList;
begin
  Result := False;
  HTTPClient := TFPHttpClient.Create(nil);
  HTTPClient.AllowRedirect := True;
  HTTPClient.OnPassword := DoPassword;
  FormData := TStringList.Create;
  try
    HTTPClient.Get(URL, Stream);
    Result := True;
  finally
    FormData.Free;
    HTTPClient.Free;
  end;
end;

procedure TImportSources.AssignToList(List: TObjects);
var
  I: Integer;
begin
  List.Count := Count;
  for I := 0 to Count - 1 do
    List[I] := Items[I];
end;

procedure TImportSource.Process(AcronymDb: TAcronymDb);
begin
  ItemCount := 0;
  case Format.Kind of
    ifkParseURL: ProcessTextParseURL(AcronymDb);
    ifkMSAccess: ProcessMSAccess(AcronymDb);
    ifkParseFile: ProcessTextParseFile(AcronymDb);
    else raise Exception.Create(SUnsupportedImportFormat);
  end;
  LastImportTime := Now;
end;

{ TImportFormat }

procedure TImportFormat.Assign(Source: TImportFormat);
var
  I: Integer;
begin
  Kind := Source.Kind;
  Name := Source.Name;
  Block.StartString := Source.Block.StartString;
  Block.EndString := Source.Block.EndString;
  while ItemPatterns.Count < Source.ItemPatterns.Count do
    ItemPatterns.Add(TImportPattern.Create);
  if ItemPatterns.Count > Source.ItemPatterns.Count then
    ItemPatterns.Count := Source.ItemPatterns.Count;
  for I := 0 to ItemPatterns.Count - 1 do begin
    ItemPatterns[I].Assign(Source.ItemPatterns[I]);
  end;
end;

procedure TImportFormat.SaveToNode(Node: TDOMNode);
var
  NewNode: TDOMNode;
begin
  WriteInteger(Node, 'Id', Id);
  WriteString(Node, 'Name', Name);
  WriteInteger(Node, 'Kind', Integer(Kind));
  WriteString(Node, 'BlockStartString', Block.StartString);
  WriteString(Node, 'BlockEndString', Block.EndString);

  NewNode := Node.OwnerDocument.CreateElement('Patterns');
  Node.AppendChild(NewNode);
  ItemPatterns.SaveToNode(NewNode);
end;

procedure TImportFormat.LoadFromNode(Node: TDOMNode);
var
  NewNode: TDOMNode;
begin
  Id := ReadInteger(Node, 'Id', 0);
  Name := ReadString(Node, 'Name', '');
  Kind := TImportFormatKind(ReadInteger(Node, 'Kind', 0));
  Block.StartString := ReadString(Node, 'BlockStartString', '');
  Block.EndString := ReadString(Node, 'BlockEndString', '');

  NewNode := Node.FindNode('Patterns');
  if Assigned(NewNode) then
    ItemPatterns.LoadFromNode(NewNode);
end;

constructor TImportFormat.Create;
begin
  Block := TImportPattern.Create;
  ItemPatterns := TImportPatterns.Create;
end;

destructor TImportFormat.Destroy;
begin
  FreeAndNil(Block);
  FreeAndNil(ItemPatterns);
  inherited;
end;

{ TImportSources }

procedure TImportSources.UpdateIds;
var
  LastId: Integer;
  I: Integer;
begin
  // Get highest used ID
  LastId := 0;
  for I := 0 to Count - 1 do begin
    if Items[I].Id > LastId then LastId := Items[I].Id;
  end;
  // Add ID to new items without ID
  for I := 0 to Count - 1 do begin
    if Items[I].Id = 0 then begin
      Inc(LastId);
      Items[I].Id := LastId;
    end;
  end;
end;

function TImportSources.SearchById(Id: Integer): TImportSource;
var
  I: Integer;
begin
  I := 0;
  while (I < Count) and (Items[I].Id <> Id) do Inc(I);
  if I < Count then Result := Items[I]
    else Result := nil;
end;

procedure TImportSources.SaveRefToNode(Node: TDOMNode);
var
  I: Integer;
  NewNode: TDOMNode;
begin
  for I := 0 to Count - 1 do begin
    NewNode := Node.OwnerDocument.CreateElement('Ref');
    Node.AppendChild(NewNode);
    NewNode.TextContent := WideString(IntToStr(Items[I].Id));
  end;
end;

procedure TImportSources.LoadRefFromNode(Node: TDOMNode);
var
  Node2: TDOMNode;
  Id: Integer;
  Source: TImportSource;
begin
  Node2 := Node.FirstChild;
  while Assigned(Node2) and (Node2.NodeName = 'Ref') do begin
    if TryStrToInt(string(Node2.TextContent), Id) then begin
      Source := AcronymDb.ImportSources.SearchById(Id);
      if Assigned(Source) then begin
        Add(Source);
      end;
    end;
    Node2 := Node2.NextSibling;
  end;
end;

procedure TImportSources.AssignToStrings(Strings: TStrings);
var
  I: Integer;
begin
  Strings.Clear;
  for I := 0 to Count - 1 do
    Strings.AddObject(Items[I].Name, Items[I]);
end;

procedure TImportSources.Assign(Source: TImportSources);
begin

end;

function TImportSources.SearchByName(Name: string): TImportSource;
var
  I: Integer;
begin
  I := 0;
  while (I < Count) and (Items[I].Name <> Name) do Inc(I);
  if I < Count then Result := Items[I]
    else Result := nil;
end;

procedure TImportSources.SaveToNode(Node: TDOMNode);
var
  I: Integer;
  NewNode2: TDOMNode;
begin
  UpdateIds;
  for I := 0 to Count - 1 do
  with Items[I] do begin
    NewNode2 := Node.OwnerDocument.CreateElement('ImportSource');
    Node.AppendChild(NewNode2);
    SaveToNode(NewNode2);
  end;
end;

procedure TImportSources.LoadFromNode(Node: TDOMNode);
var
  Node2: TDOMNode;
  NewItem: TImportSource;
begin
  Count := 0;
  Node2 := Node.FirstChild;
  while Assigned(Node2) and (Node2.NodeName = 'ImportSource') do begin
    NewItem := TImportSource.Create;
    NewItem.Sources := Self;
    NewItem.LoadFromNode(Node2);
    Add(NewItem);
    Node2 := Node2.NextSibling;
  end;
  UpdateIds;
end;

{ TImportFormats }

function TImportFormats.SearchByName(Name: string): TImportFormat;
var
  I: Integer;
begin
  I := 0;
  while (I < Count) and (Items[I].Name <> Name) do Inc(I);
  if I < Count then Result := Items[I]
    else Result := nil;
end;

procedure TImportFormats.Assign(Source: TImportFormats);
begin

end;

procedure TImportFormats.UpdateIds;
var
  LastId: Integer;
  I: Integer;
begin
  // Get highest used ID
  LastId := 0;
  for I := 0 to Count - 1 do begin
    if Items[I].Id > LastId then LastId := Items[I].Id;
  end;
  // Add ID to new items without ID
  for I := 0 to Count - 1 do begin
    if Items[I].Id = 0 then begin
      Inc(LastId);
      Items[I].Id := LastId;
    end;
  end;
end;

procedure TImportFormats.SaveToNode(Node: TDOMNode);
var
  I: Integer;
  NewNode2: TDOMNode;
begin
  UpdateIds;
  for I := 0 to Count - 1 do
  with Items[I] do begin
    NewNode2 := Node.OwnerDocument.CreateElement('ImportFormat');
    Node.AppendChild(NewNode2);
    SaveToNode(NewNode2);
  end;
end;

procedure TImportFormats.LoadFromNode(Node: TDOMNode);
var
  Node2: TDOMNode;
  NewItem: TImportFormat;
begin
  Count := 0;
  Node2 := Node.FirstChild;
  while Assigned(Node2) and (Node2.NodeName = 'ImportFormat') do begin
    NewItem := TImportFormat.Create;
    NewItem.LoadFromNode(Node2);
    Add(NewItem);
    Node2 := Node2.NextSibling;
  end;
  UpdateIds;
end;

function TImportFormats.SearchById(Id: Integer): TImportFormat;
var
  I: Integer;
begin
  I := 0;
  while (I < Count) and (Items[I].Id <> Id) do Inc(I);
  if I < Count then Result := Items[I]
    else Result := nil;
end;

{ TImportSource }

procedure TImportSource.ProcessTextParseURL(AcronymDb: TAcronymDb);
var
  S: string;
begin
  ResponseStream.Clear;
  if DownloadHTTP(URL, ResponseStream) then begin
    ResponseStream.Position := 0;
    S := default(string);
    SetLength(S, ResponseStream.Size);
    ResponseStream.Read(S[1], Length(S));

    TextParse(AcronymDb, S);
  end;
end;

procedure TImportSource.ProcessTextParseFile(AcronymDb: TAcronymDb);
var
  S: TStringList;
begin
  if FileExists(URL) then begin
    S := TStringList.Create;
    try
      S.LoadFromFile(URL);
      TextParse(AcronymDb, S.Text);
    finally
      S.Free;
    end;
  end else ShowMessage(SysUtils.Format(SFileNotFound, [URL]));
end;

procedure TImportSource.Assign(Source: TImportSource);
begin
  Enabled := Source.Enabled;
  Name := Source.Name;
  URL := Source.URL;
  Format := Source.Format;
  ItemCount := Source.ItemCount;
  Categories.Assign(Source.Categories);
  UserName := Source.UserName;
  Password := Source.Password;
  LastImportTime := Source.LastImportTime;
end;

procedure TImportSource.SaveToNode(Node: TDOMNode);
var
  NewNode: TDOMNode;
begin
  WriteString(Node, 'Name', Name);
  WriteString(Node, 'URL', URL);
  if Assigned(Format) then WriteInteger(Node, 'ImportFormat', Format.Id)
    else WriteInteger(Node, 'ImportFormat', -1);
  WriteBoolean(Node, 'Enabled', Enabled);
  WriteInteger(Node, 'ItemCount', ItemCount);
  WriteDateTime(Node, 'LastImportTime', LastImportTime);
  WriteString(Node, 'UserName', UserName);

  NewNode := Node.OwnerDocument.CreateElement('Categories');
  Node.AppendChild(NewNode);
  Categories.SaveRefToNode(NewNode);
end;

procedure TImportSource.LoadFromNode(Node: TDOMNode);
var
  Node2: TDOMNode;
begin
  Name := ReadString(Node, 'Name', '');
  URL := ReadString(Node, 'URL', '');
  Format := Sources.AcronymDb.ImportFormats.SearchById(ReadInteger(Node, 'ImportFormat', -1));
  Enabled := ReadBoolean(Node, 'Enabled', True);
  ItemCount := ReadInteger(Node, 'ItemCount', 0);
  UserName := ReadString(Node, 'UserName', '');
  LastImportTime := ReadDateTime(Node, 'LastImportTime', 0);

  Categories.Db := Sources.AcronymDb;
  Node2 := Node.FindNode('Categories');
  if Assigned(Node2) then
    Categories.LoadRefFromNode(Node2);
end;

constructor TImportSource.Create;
begin
  Format := nil;
  Enabled := True;
  Categories := TAcronymCategories.Create;
  Categories.OwnsObjects := False;
  ResponseStream := TMemoryStream.Create;
end;

destructor TImportSource.Destroy;
begin
  FreeAndNil(Categories);
  FreeAndNil(ResponseStream);
  inherited;
end;

{ TAcronymEntry }

constructor TAcronymEntry.Create;
begin
  Categories := TStringList.Create;
  Sources := TStringList.Create;
  Name := '';
  Meaning := '';
  Description := '';
end;

destructor TAcronymEntry.Destroy;
begin
  FreeAndNil(Categories);
  FreeAndNil(Sources);
  inherited;
end;

{ TAcronymMeanings }

procedure TAcronymMeanings.Assign(Source: TAcronymMeanings);
begin

end;

procedure TAcronymMeanings.UpdateIds;
var
  LastId: Integer;
  I: Integer;
begin
  // Get highest used ID
  LastId := 0;
  for I := 0 to Count - 1 do begin
    if Items[I].Id > LastId then LastId := Items[I].Id;
  end;
  // Add ID to new items without ID
  for I := 0 to Count - 1 do begin
    if Items[I].Id = 0 then begin
      Inc(LastId);
      Items[I].Id := LastId;
    end;
  end;
end;

procedure TAcronymMeanings.SaveToNode(Node: TDOMNode);
var
  I: Integer;
  NewNode2: TDOMNode;
begin
  UpdateIds;
  for I := 0 to Count - 1 do
  with Items[I] do begin
    NewNode2 := Node.OwnerDocument.CreateElement('Meaning');
    Node.AppendChild(NewNode2);
    SaveToNode(NewNode2);
  end;
end;

procedure TAcronymMeanings.LoadFromNode(Node: TDOMNode);
var
  Node2: TDOMNode;
  NewItem: TAcronymMeaning;
begin
  Count := 0;
  Node2 := Node.FirstChild;
  while Assigned(Node2) and (Node2.NodeName = 'Meaning') do begin
    NewItem := TAcronymMeaning.Create;
    NewItem.Acronym := Acronym;
    NewItem.LoadFromNode(Node2);
    Add(NewItem);
    Node2 := Node2.NextSibling;
  end;
  UpdateIds;
end;

function TAcronymMeanings.SearchByName(Name: string; Flags: TSearchFlags
  ): TAcronymMeaning;
var
  I: Integer;
begin
  I := 0;
  if sfCaseInsensitive in Flags then begin
    while (I < Count) and (LowerCase(Items[I].Name) <> LowerCase(Name)) do Inc(I);
  end else begin
    while (I < Count) and (Items[I].Name <> Name) do Inc(I);
  end;
  if I < Count then Result := Items[I]
    else Result := nil;
end;

function TAcronymMeanings.AddMeaning(Name: string): TAcronymMeaning;
begin
  Result := TAcronymMeaning.Create;
  Result.Name := Name;
  Result.Acronym := Acronym;
  Add(Result);
end;

function TAcronymMeanings.GetNames: string;
var
  I: Integer;
begin
  Result := '';
  for I := 0 to Count - 1 do
    Result := Result + ', "' + Items[I].Name + '"';
  System.Delete(Result, 1, 2);
end;

{ TAcronymMeaning }

procedure TAcronymMeaning.Assign(Source: TAcronymMeaning);
begin
  Name := Source.Name;
  Description := Source.Description;
  Language := Source.Language;
  Categories.Assign(Source.Categories);
  Sources.Assign(Source.Sources);
end;

procedure TAcronymMeaning.MergeCategories(MergedCategories: TAcronymCategories);
var
  I: Integer;
begin
  for I := 0 to MergedCategories.Count - 1 do
  if Categories.IndexOf(MergedCategories[I]) = -1 then begin
    Categories.Add(MergedCategories[I]);
  end;
end;

procedure TAcronymMeaning.SaveToNode(Node: TDOMNode);
var
  NewNode: TDOMNode;
begin
  WriteString(Node, 'Name', Name);
  WriteString(Node, 'Description', Description);
  WriteString(Node, 'Language', Language);

  NewNode := Node.OwnerDocument.CreateElement('Categories');
  Node.AppendChild(NewNode);
  Categories.SaveRefToNode(NewNode);

  NewNode := Node.OwnerDocument.CreateElement('Sources');
  Node.AppendChild(NewNode);
  Sources.SaveRefToNode(NewNode);
end;

procedure TAcronymMeaning.LoadFromNode(Node: TDOMNode);
var
  Node2: TDOMNode;
begin
  Name := ReadString(Node, 'Name', '');
  Description := ReadString(Node, 'Description', '');
  Language := ReadString(Node, 'Language', '');

  Categories.Db := Acronym.Db;
  Node2 := Node.FindNode('Categories');
  if Assigned(Node2) then begin
    Categories.LoadRefFromNode(Node2);
  end;

  Sources.AcronymDb := Acronym.Db;
  Node2 := Node.FindNode('Sources');
  if Assigned(Node2) then begin
    Sources.LoadRefFromNode(Node2);
  end;
end;

constructor TAcronymMeaning.Create;
begin
  Categories := TAcronymCategories.Create(False);
  Sources := TImportSources.Create(False);
end;

destructor TAcronymMeaning.Destroy;
begin
  FreeAndNil(Categories);
  FreeAndNil(Sources);
  inherited;
end;

{ TAcronyms }

procedure TAcronyms.Assign(Source: TAcronyms);
begin

end;

procedure TAcronyms.SaveToNode(Node: TDOMNode);
var
  I: Integer;
  NewNode2: TDOMNode;
begin
  for I := 0 to Count - 1 do
  with Items[I] do begin
    NewNode2 := Node.OwnerDocument.CreateElement('Acronym');
    Node.AppendChild(NewNode2);
    SaveToNode(NewNode2);
  end;
end;

procedure TAcronyms.LoadFromNode(Node: TDOMNode);
var
  Node2: TDOMNode;
  NewItem: TAcronym;
begin
  Count := 0;
  Node2 := Node.FirstChild;
  while Assigned(Node2) and (Node2.NodeName = 'Acronym') do begin
    NewItem := TAcronym.Create;
    NewItem.Db := Db;
    NewItem.LoadFromNode(Node2);
    Add(NewItem);
    Node2 := Node2.NextSibling;
  end;
end;

function TAcronyms.SearchByName(Name: string; Flags: TSearchFlags = []): TAcronym;
var
  I: Integer;
begin
  I := 0;
  if sfCaseInsensitive in Flags then begin
    while (I < Count) and (LowerCase(Items[I].Name) <> LowerCase(Name)) do Inc(I);
  end else begin
    while (I < Count) and (Items[I].Name <> Name) do Inc(I);
  end;
  if I < Count then Result := Items[I]
    else Result := nil;
end;

function TAcronyms.AddAcronym(Name: string): TAcronym;
begin
  Result := TAcronym.Create;
  Result.Name := Name;
  Result.Db := Db;
  Add(Result);
end;

{ TAcronymCategories }

procedure TAcronymCategories.UpdateIds;
var
  LastId: Integer;
  I: Integer;
begin
  // Get highest used ID
  LastId := 0;
  for I := 0 to Count - 1 do begin
    if Items[I].Id > LastId then LastId := Items[I].Id;
  end;

  // Add ID to new items without ID
  for I := 0 to Count - 1 do begin
    if Items[I].Id = 0 then begin
      Inc(LastId);
      Items[I].Id := LastId;
    end;
  end;
end;

procedure TAcronymCategories.SaveToNode(Node: TDOMNode);
var
  I: Integer;
  NewNode2: TDOMNode;
begin
  UpdateIds;
  for I := 0 to Count - 1 do
  with Items[I] do begin
    NewNode2 := Node.OwnerDocument.CreateElement('Category');
    Node.AppendChild(NewNode2);
    SaveToNode(NewNode2);
  end;
end;

procedure TAcronymCategories.LoadFromNode(Node: TDOMNode);
var
  Node2: TDOMNode;
  NewItem: TAcronymCategory;
begin
  Count := 0;
  Node2 := Node.FirstChild;
  while Assigned(Node2) and (Node2.NodeName = 'Category') do begin
    NewItem := TAcronymCategory.Create;
    NewItem.AcronymDb := Db;
    NewItem.LoadFromNode(Node2);
    Add(NewItem);
    Node2 := Node2.NextSibling;
  end;
  UpdateIds;
end;

procedure TAcronymCategories.SaveRefToNode(Node: TDOMNode);
var
  I: Integer;
  NewNode: TDOMNode;
begin
  for I := 0 to Count - 1 do begin
    NewNode := Node.OwnerDocument.CreateElement('Ref');
    Node.AppendChild(NewNode);
    NewNode.TextContent := WideString(IntToStr(Items[I].Id));
  end;
end;

procedure TAcronymCategories.LoadRefFromNode(Node: TDOMNode);
var
  Node2: TDOMNode;
  Id: Integer;
  Category: TAcronymCategory;
begin
  Node2 := Node.FirstChild;
  while Assigned(Node2) and (Node2.NodeName = 'Ref') do begin
    if TryStrToInt(string(Node2.TextContent), Id) then begin
      Category := SearchById(Id);
      if not Assigned(Category) then begin
        Category := Db.Categories.SearchById(Id);
        if Assigned(Category) then begin
          Add(Category);
        end;
      end;
    end;
    Node2 := Node2.NextSibling;
  end;

  // Old way to store ref Id, remove in future
  Node2 := Node.FirstChild;
  while Assigned(Node2) and (Node2.NodeName = 'Category') do begin
    Id := ReadInteger(Node2, 'Id', 0);
    Category := Db.Categories.SearchById(Id);
    if Assigned(Category) then begin
      Add(Category);
    end;
    Node2 := Node2.NextSibling;
  end;
end;

function TAcronymCategories.SearchByName(Name: string): TAcronymCategory;
var
  I: Integer;
begin
  I := 0;
  while (I < Count) and (Items[I].Name <> Name) do Inc(I);
  if I < Count then Result := Items[I]
    else Result := nil;
end;

function TAcronymCategories.SearchById(Id: Integer): TAcronymCategory;
var
  I: Integer;
begin
  I := 0;
  while (I < Count) and (Items[I].Id <> Id) do Inc(I);
  if I < Count then Result := Items[I]
    else Result := nil;
end;

function TAcronymCategories.AddContext(Name: string): TAcronymCategory;
begin
  Result := TAcronymCategory.Create;
  Result.Name := Name;
  Add(Result);
end;

procedure TAcronymCategories.AssignToStrings(Strings: TStrings);
var
  I: Integer;
begin
  Strings.Clear;
  for I := 0 to Count - 1 do
    Strings.AddObject(Items[I].Name, Items[I]);
end;

procedure TAcronymCategories.AssignFromStrings(Strings: TStrings);
begin
  Clear;
  AddFromStrings(Strings);
end;

procedure TAcronymCategories.AddFromStrings(Strings: TStrings);
var
  I: Integer;
begin
  for I := 0 to Strings.Count - 1 do begin
    Add(TAcronymCategory(Strings.Objects[I]));
  end;
end;

procedure TAcronymCategories.AssignToList(List: TObjects);
var
  I: Integer;
begin
  List.Count := Count;
  for I := 0 to Count - 1 do
    List[I] := Items[I];
end;

procedure TAcronymCategories.Assign(Source: TAcronymCategories);
var
  I: Integer;
  NewCategory: TAcronymCategory;
begin
  Clear;
  for I := 0 to Source.Count - 1 do begin
    NewCategory := TAcronymCategory.Create;
    NewCategory.AcronymDb := Db;
    NewCategory.Assign(Source[I]);
    Add(NewCategory);
  end;
end;

function TAcronymCategories.GetString: string;
var
  I: Integer;
begin
  Result := '';
  for I := 0 to Count - 1 do
    Result := Result + Items[I].Name + ', ';
  System.Delete(Result, Length(Result) - 1, 2);
end;

function TAcronymCategories.IsAnyEnabled: Boolean;
var
  I: Integer;
begin
  Result := False;
  for I := 0 to Count - 1 do
  if Items[I].Enabled then begin
    Result := True;
    Break;
  end;
end;

{ TAcronym }

procedure TAcronym.Assign(Source: TAcronym);
begin
  Name := Source.Name;
  Meanings.Assign(Source.Meanings);
end;

procedure TAcronym.SaveToNode(Node: TDOMNode);
var
  NewNode: TDOMNode;
begin
  WriteString(Node, 'Name', Name);

  NewNode := Node.OwnerDocument.CreateElement('Meanings');
  Node.AppendChild(NewNode);
  Meanings.SaveToNode(NewNode);
end;

procedure TAcronym.LoadFromNode(Node: TDOMNode);
var
  NewNode: TDOMNode;
begin
  Name := ReadString(Node, 'Name', '');

  NewNode := Node.FindNode('Meanings');
  if Assigned(NewNode) then
    Meanings.LoadFromNode(NewNode);
end;

constructor TAcronym.Create;
begin
  Meanings := TAcronymMeanings.Create;
  Meanings.Acronym := Self;
end;

destructor TAcronym.Destroy;
begin
  FreeAndNil(Meanings);
  inherited;
end;

{ TAcronymCategory }

function TAcronymCategory.GetAcronymMeanings: TAcronymMeanings;
var
  I: Integer;
  J: Integer;
begin
  Result := TAcronymMeanings.Create(False);
  for I := 0 to AcronymDb.Acronyms.Count - 1 do
  with AcronymDb.Acronyms[I] do begin
    for J := 0 to Meanings.Count - 1 do
    with Meanings[J] do
    if Assigned(Categories.SearchById(Self.Id)) then
      Result.Add(Meanings[J]);
  end;
end;

function TAcronymCategory.GetAcronymMeaningsCount: Integer;
var
  I: Integer;
  J: Integer;
begin
  Result := 0;
  for I := 0 to AcronymDb.Acronyms.Count - 1 do
  with AcronymDb.Acronyms[I] do begin
    for J := 0 to Meanings.Count - 1 do
    with Meanings[J] do
    if Assigned(Categories.SearchById(Self.Id)) then
      Inc(Result);
  end;
end;

function TAcronymCategory.GetImportSources: TImportSources;
var
  I: Integer;
begin
  Result := TImportSources.Create(False);
  for I := 0 to AcronymDb.ImportSources.Count - 1 do
  with AcronymDb.ImportSources[I] do
  if Assigned(Categories.SearchById(Self.Id)) then
    Result.Add(AcronymDb.ImportSources[I]);
end;

function TAcronymCategory.GetImportSourcesCount: Integer;
var
  I: Integer;
begin
  Result := 0;
  for I := 0 to AcronymDb.ImportSources.Count - 1 do
  with AcronymDb.ImportSources[I] do
  if Assigned(Categories.SearchById(Self.Id)) then
    Inc(Result);
end;

procedure TAcronymCategory.Assign(Source: TAcronymCategory);
begin
  Id := Source.Id;
  Name := Source.Name;
  Enabled := Source.Enabled;
end;

procedure TAcronymCategory.SaveToNode(Node: TDOMNode);
begin
  WriteString(Node, 'Name', Name);
  WriteInteger(Node, 'Id', Id);
  WriteBoolean(Node, 'Enabled', Enabled);
end;

procedure TAcronymCategory.LoadFromNode(Node: TDOMNode);
begin
  Name := ReadString(Node, 'Name', '');
  Id := ReadInteger(Node, 'Id', 0);
  Enabled := ReadBoolean(Node, 'Enabled', True);
end;

constructor TAcronymCategory.Create;
begin
  Id := 0;
  Name := '';
  Enabled := True;
end;

{ TAcronymDb }

constructor TAcronymDb.Create;
begin
  Acronyms := TAcronyms.Create;
  Acronyms.Db := Self;
  Categories := TAcronymCategories.Create;
  Categories.Db := Self;
  ImportSources := TImportSources.Create;
  ImportSources.AcronymDb := Self;
  ImportFormats := TImportFormats.Create;
  FUpdateCount := 0;
  OnUpdate := TList<TNotifyEvent>.Create;
end;

destructor TAcronymDb.Destroy;
begin
  FreeAndNil(OnUpdate);
  FreeAndNil(ImportFormats);
  FreeAndNil(ImportSources);
  FreeAndNil(Acronyms);
  FreeAndNil(Categories);
  inherited;
end;

procedure TAcronymDb.Assign(Source: TAcronymDb);
begin
  Modified := Source.Modified;
  AddedCount := Source.AddedCount;
  FileName := Source.FileName;
  Categories.Assign(Source.Categories);
  Acronyms.Assign(Source.Acronyms);
  ImportFormats.Assign(Source.ImportFormats);
  ImportSources.Assign(Source.ImportSources);
end;

procedure TAcronymDb.LoadFromFile(FileName: string);
var
  NewNode: TDOMNode;
  Doc: TXMLDocument;
  RootNode: TDOMNode;
begin
  if ExtractFileExt(FileName) = '.csv' then begin
    LoadFromFileCSV(FileName);
    Exit;
  end;
  Self.FileName := FileName;
  ReadXMLFile(Doc, FileName);
  with Doc do try
    if Doc.DocumentElement.NodeName <> 'AcronymDecoderProject' then
      raise Exception.Create(SWrongFileFormat);
    RootNode := Doc.DocumentElement;
    with RootNode do begin
      NewNode := FindNode('Categories');
      if Assigned(NewNode) then
        Categories.LoadFromNode(NewNode);

      NewNode := FindNode('ImportFormats');
      if Assigned(NewNode) then
        ImportFormats.LoadFromNode(NewNode);

      NewNode := FindNode('ImportSources');
      if Assigned(NewNode) then
        ImportSources.LoadFromNode(NewNode);

      // Load acronyms after categories and import formats and sources because of references
      NewNode := FindNode('Acronyms');
      if Assigned(NewNode) then
        Acronyms.LoadFromNode(NewNode);
    end;
  finally
    Doc.Free;
  end;
end;

procedure TAcronymDb.SaveToFile(FileName: string);
var
  NewNode: TDOMNode;
  Doc: TXMLDocument;
  RootNode: TDOMNode;
begin
  if ExtractFileExt(FileName) = '.csv' then begin
    SaveToFileCSV(FileName);
    Exit;
  end;
  Self.FileName := FileName;
  Doc := TXMLDocument.Create;
  with Doc do try
    RootNode := CreateElement('AcronymDecoderProject');
    AppendChild(RootNode);
    with RootNode do begin
      NewNode := OwnerDocument.CreateElement('Categories');
      AppendChild(NewNode);
      Categories.SaveToNode(NewNode);

      NewNode := OwnerDocument.CreateElement('ImportFormats');
      AppendChild(NewNode);
      ImportFormats.SaveToNode(NewNode);

      NewNode := OwnerDocument.CreateElement('ImportSources');
      AppendChild(NewNode);
      ImportSources.SaveToNode(NewNode);

      // Save acronyms after categories, import formats and sources because of references
      NewNode := OwnerDocument.CreateElement('Acronyms');
      AppendChild(NewNode);
      Acronyms.SaveToNode(NewNode);
    end;
    ForceDirectories(ExtractFileDir(FileName));
    WriteXMLFile(Doc, FileName);
  finally
    Doc.Free;
  end;
  Modified := False;
end;

procedure TAcronymDb.LoadFromFileCSV(FileName: string);
var
  F: TStringList;
  Line: TStringList;
  CategoryStrings: TStringList;
  NewAcronym: TAcronym;
  NewMeaning: TAcronymMeaning;
  I: Integer;
  J: Integer;
  AcronymCategory: TAcronymCategory;
begin
  Self.FileName := FileName;
  Acronyms.Clear;
  F := TStringList.Create;
  Line := TStringList.Create;
  Line.StrictDelimiter := True;
  CategoryStrings := TStringList.Create;
  CategoryStrings.Delimiter := ';';
  try
    F.LoadFromFile(FileName);
    for I := 0 to F.Count - 1 do begin
      Line.CommaText := F[I];
      NewAcronym := Acronyms.SearchByName(Line[0]);
      if not Assigned(NewAcronym) then begin
        NewAcronym := TAcronym.Create;
        NewAcronym.Name := Line[0];
        Acronyms.Add(NewAcronym);
      end;
      NewMeaning := NewAcronym.Meanings.SearchByName(Line[1]);
      if not Assigned(NewMeaning) then begin
        NewMeaning := TAcronymMeaning.Create;
        NewMeaning.Name := Line[1];
        NewMeaning.Acronym := NewAcronym;
        NewAcronym.Meanings.Add(NewMeaning);
      end;
      CategoryStrings.DelimitedText := Line[2];
      for J := 0 to CategoryStrings.Count - 1 do begin
        AcronymCategory := Categories.SearchByName(CategoryStrings[J]);
        if not Assigned(AcronymCategory) then begin
          AcronymCategory := TAcronymCategory.Create;
          AcronymCategory.Name := CategoryStrings[J];
          Categories.Add(AcronymCategory);
        end;
        NewMeaning.Categories.Add(AcronymCategory);
      end;
    end;
  finally
    F.Free;
    Line.Free;
    CategoryStrings.Free;
  end;
  Modified := False;
end;

procedure TAcronymDb.SaveToFileCSV(FileName: string);
var
  I: Integer;
  J: Integer;
  K: Integer;
  F: TStringList;
  Line: TStringList;
  Context: TStringList;
begin
  Self.FileName := FileName;
  F := TStringList.Create;
  Line := TStringList.Create;
  Line.StrictDelimiter := True;
  Context := TStringList.Create;
  Context.Delimiter := ';';
  try
    Line.Clear;
    for I := 0 to Acronyms.Count - 1 do
    with Acronyms[I] do begin
      for K := 0 to Meanings.Count - 1 do
      with Meanings[K] do begin
        Line.Clear;
        Line.Add(Acronym.Name);
        Line.Add(Name);
        Context.Clear;
        for J := 0 to Categories.Count - 1 do
          Context.Add(Categories[J].Name);
        Line.Add(Context.DelimitedText);
        F.Add(Line.CommaText);
      end;
    end;
    F.SaveToFile(FileName);
  finally
    F.Free;
    Line.Free;
    Context.Free;
  end;
  Modified := False;
end;

procedure TAcronymDb.FilterList(AName: string; Items: TAcronymMeanings);
var
  I: Integer;
  J: Integer;
begin
  AName := LowerCase(AName);
  Items.Clear;
  for I := 0 to Acronyms.Count - 1 do
  with Acronyms[I] do begin
    for J := 0 to Meanings.Count - 1 do
    with Meanings[J] do begin
      if (AName = '') or (Pos(AName, LowerCase(Acronyms[I].Name)) > 0)
        or (Pos(AName, LowerCase(Name)) > 0) then Items.Add(Meanings[J])
    end;
  end;
end;

function TAcronymDb.GetMeaningsCount: Integer;
var
  I: Integer;
begin
  Result := 0;
  for I := 0 to Acronyms.Count - 1 do
    Result := Result + Acronyms[I].Meanings.Count;
end;

function TAcronymDb.AddAcronym(AcronymName, MeaningName: string): TAcronymMeaning;
var
  Acronym: TAcronym;
  Meaning: TAcronymMeaning;
begin
  Acronym := Acronyms.SearchByName(AcronymName);
  if not Assigned(Acronym) then begin
    Acronym := TAcronym.Create;
    Acronym.Db := Self;
    Acronym.Name := AcronymName;
    Acronyms.Add(Acronym);
  end;
  Meaning := Acronym.Meanings.SearchByName(MeaningName);
  if not Assigned(Meaning) then begin
    Meaning := TAcronymMeaning.Create;
    Meaning.Name := MeaningName;
    Meaning.Acronym := Acronym;
    Acronym.Meanings.Add(Meaning);
    Inc(AddedCount);
  end;
  Result := Meaning;
  Modified := True;
end;

function TAcronymDb.SearchAcronym(AcronymName, MeaningName: string;
  Flags: TSearchFlags = []): TAcronymMeaning;
var
  Acronym: TAcronym;
  Meaning: TAcronymMeaning;
begin
  Result := nil;
  Acronym := Acronyms.SearchByName(AcronymName);
  if Assigned(Acronym) then begin
    Meaning := Acronym.Meanings.SearchByName(MeaningName, Flags);
    if Assigned(Meaning) then begin
      Result := Meaning;
    end;
  end;
end;

procedure TAcronymDb.RemoveMeaning(Meaning: TAcronymMeaning);
var
  Acronym: TAcronym;
begin
  Acronym := Meaning.Acronym;
  Acronym.Meanings.Remove(Meaning);
  if Acronym.Meanings.Count = 0 then
    Acronyms.Remove(Acronym);
  Modified := True;
end;

procedure TAcronymDb.RemoveAcronym(AcronymName, MeaningName: string);
var
  Acronym: TAcronym;
  Meaning: TAcronymMeaning;
begin
  Acronym := Acronyms.SearchByName(AcronymName);
  if Assigned(Acronym) then begin
    Meaning := Acronym.Meanings.SearchByName(MeaningName);
    if Assigned(Meaning) then RemoveMeaning(Meaning);
  end;
end;

procedure TAcronymDb.AssignToList(List: TObjects; EnabledCategoryOnly: Boolean = False);
var
  I: Integer;
  J: Integer;
begin
  List.Clear;
  for I := 0 to Acronyms.Count - 1 do
  with Acronyms[I] do begin
    for J := 0 to Meanings.Count - 1 do
    with Meanings[J] do
    if not EnabledCategoryOnly or (EnabledCategoryOnly and Categories.IsAnyEnabled) then begin
      List.Add(Meanings[J]);
    end;
  end;
end;

procedure TAcronymDb.BeginUpdate;
begin
  Inc(FUpdateCount);
end;

procedure TAcronymDb.EndUpdate;
begin
  if FUpdateCount > 0 then Dec(FUpdateCount);
  if FupdateCount = 0 then Update;
end;

procedure TAcronymDb.Update;
var
  I: Integer;
begin
  for I := 0 to OnUpdate.Count - 1 do
    OnUpdate[I](Self);
end;

end.

