unit CS.Database.Main;
interface
uses
  Generics.Collections,
  CS.Database.Snippets,
  CS.Database.Types,
  CS.Database.Core.Lookups,
  CS.Database.Core.SnippetsTable,
  CS.Database.IO.Native,
  CS.SourceCode.Languages,
  CS.Utils.Dates,
  UBaseObjects,
  UExceptions;
type
  TDatabase = class(TNoConstructObject)
  strict private
    class var
      fSnippetsTable: TDBSnippetsTable;
      fTagLookup: TDBLookup<TDBTag>;
      fLanguageLookup: TDBLookup<TSourceCodeLanguageID>;
      fLastModified: TUTCDateTime;
      fDirty: Boolean;
    class procedure FlagUpdate;
    class procedure Clear;
    class function DatabasePath: string;
  public
    class constructor Create;
    class destructor Destroy;
    class procedure Load;
    class procedure Save;
    class function NewSnippet: ISnippet;
    class procedure InsertSnippet(ASnippet: ISnippet);
    class procedure UpdateSnippet(ASnippet: ISnippet);
    class procedure DeleteSnippet(const ASnippetID: TDBSnippetID);
    class function SnippetExists(const ASnippetID: TDBSnippetID): Boolean;
    class function GetReadOnlySnippet(const ASnippetID: TDBSnippetID;
      const RequiredProps: TDBSnippetProps = []): IReadOnlySnippet;
    class function GetSnippet(const ASnippetID: TDBSnippetID): ISnippet;
    class function GetSnippetIDs(Filter: IDBFilter): IDBSnippetIDList;
    class function IsDirty: Boolean;
  end;
implementation
uses
  SysUtils,
  CS.Database.Exceptions;
{ TDatabase }
class procedure TDatabase.Clear;
begin
  fSnippetsTable.Clear;
end;
class constructor TDatabase.Create;
begin
  fSnippetsTable := TDBSnippetsTable.Create;
  fTagLookup := TDBLookup<TDBTag>.Create(
    TDBTag.TEqualityComparer.Create
  );
  fLanguageLookup := TDBLookup<TSourceCodeLanguageID>.Create(
    TSourceCodeLanguageID.TEqualityComparer.Create
  );
  fLastModified := TUTCDateTime.CreateNull;
  fDirty := False;
end;
class function TDatabase.DatabasePath: string;
begin
  // TODO -cPRERELEASE: Replace this database path one from TAppInfo
  Result := ExtractFilePath(ParamStr(0));
end;
class procedure TDatabase.DeleteSnippet(const ASnippetID: TDBSnippetID);
var
  OldSnippet: TDBSnippet;
  Tag: TDBTag;
begin
  OldSnippet := fSnippetsTable.Get(ASnippetID);
  for Tag in OldSnippet.GetTags do
    fTagLookup.Delete(Tag, ASnippetID);
  fLanguageLookup.Delete(OldSnippet.GetLanguageID, ASnippetID);
  fSnippetsTable.Delete(ASnippetID);
  FlagUpdate;
end;
class destructor TDatabase.Destroy;
begin
  fLanguageLookup.Free;
  fTagLookup.Free;
  fSnippetsTable.Free;
end;
class procedure TDatabase.FlagUpdate;
begin
  fLastModified := TUTCDateTime.Now.RoundToNearestSecond;
  fDirty := True;
end;
class function TDatabase.GetReadOnlySnippet(const ASnippetID: TDBSnippetID;
  const RequiredProps: TDBSnippetProps): IReadOnlySnippet;
var
  Row: TDBSnippet;
begin
  Row := fSnippetsTable.Get(ASnippetID);
  Result := Row.CopyPartial(RequiredProps);
end;
class function TDatabase.GetSnippet(const ASnippetID: TDBSnippetID): ISnippet;
var
  Row: TDBSnippet;
begin
  Row := fSnippetsTable.Get(ASnippetID);
  Result := Row.Copy;
end;
class function TDatabase.GetSnippetIDs(Filter: IDBFilter): IDBSnippetIDList;
var
  Snippet: TDBSnippet;
begin
  Result := TDBSnippetIDList.Create;
  for Snippet in fSnippetsTable do
  begin
    if Filter.Match(
      TPartialSnippet.Create(Snippet, Filter.RequiredProperties)
    ) then
      Result.Add(Snippet.GetID);
  end;
end;
class procedure TDatabase.InsertSnippet(ASnippet: ISnippet);
var
  DBSnippet: TDBSnippet;
  Tag: TDBTag;
begin
  DBSnippet := TDBSnippet.CreateFrom(ASnippet);
  fSnippetsTable.Add(DBSnippet);
  FlagUpdate;
  DBSnippet.SetModified(fLastModified);
  for Tag in ASnippet.Tags do
    fTagLookup.Add(Tag, ASnippet.ID);
  fLanguageLookup.Add(ASnippet.LanguageID, ASnippet.ID);
end;
class function TDatabase.IsDirty: Boolean;
begin
  Result := fDirty;
end;
class procedure TDatabase.Load;
begin
  Clear;
  // TODO: implement load process
  // update last modification date to that from file
  // clear and load snippets table
  // clear and rebuild lookups
  fDirty := False;
end;
class function TDatabase.NewSnippet: ISnippet;
begin
  Result := TSnippet.CreateNew;
end;
class procedure TDatabase.Save;
var
  Writer: TDBNativeWriter;
begin
  if not fDirty then
    Exit;
  // TODO: backup database here
  try
    Writer := TDBNativeWriter.Create(DatabasePath);
    try
      Writer.Save(fSnippetsTable, fLastModified);
    finally
      Writer.Free;
    end;
  except
    // TODO: restore database on exception here
    raise;
  end;
  fDirty := False;
end;
class function TDatabase.SnippetExists(const ASnippetID: TDBSnippetID): Boolean;
begin
  Result := fSnippetsTable.Contains(ASnippetID);
end;
class procedure TDatabase.UpdateSnippet(ASnippet: ISnippet);
var
  OldSnippet: TDBSnippet;
  UpdatedSnippet: TDBSnippet;
  Tag: TDBTag;
begin
  OldSnippet := fSnippetsTable.Get(ASnippet.ID);
  UpdatedSnippet := TDBSnippet.CreateFrom(ASnippet);
  // Update snippets table
  fSnippetsTable.Update(UpdatedSnippet);
  FlagUpdate;
  UpdatedSnippet.SetModified(fLastModified);
  // Update tags lookup
  for Tag in OldSnippet.GetTags do
    fTagLookup.Delete(Tag, ASnippet.ID);
  for Tag in NewSnippet.GetTags do
    fTagLookup.Add(Tag, ASnippet.ID);
  // Update languages lookup
  fLanguageLookup.Update(
    OldSnippet.GetLanguageID, UpdatedSnippet.GetLanguageID, ASnippet.ID
  );
end;
end.