unit Scaffolding.Wizard.Form;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.ComCtrls,

  Vcl.Grids, Vcl.ValEdit, Vcl.CheckLst,

  Generics.Collections,

  XData.Model.Classes,
  XData.Web.Connection,

  Scaffolding.Types;

type
  TFScaffoldingWizard = class(TForm)
    pnlHeading: TPanel;
    lblHeading: TLabel;
    imgHeading: TImage;
    tabPages: TPageControl;
    tabServerSettings: TTabSheet;
    tabSelectEntities: TTabSheet;
    pnlButtons: TPanel;
    Bevel1: TBevel;
    btnCreate: TButton;
    btnCancel: TButton;
    btnPrev: TButton;
    btnNext: TButton;
    lbConfigurationTitle: TLabel;
    Label3: TLabel;
    edServerUrl: TEdit;
    Label1: TLabel;
    Bevel3: TBevel;
    Label2: TLabel;
    Bevel2: TBevel;
    Label4: TLabel;
    btRequestHeaders: TButton;
    btConnect: TButton;
    Label5: TLabel;
    ckbEntities: TCheckListBox;
    tabProjectLocation: TTabSheet;
    Label6: TLabel;
    Bevel4: TBevel;
    Label9: TLabel;
    edProjectLocation: TEdit;
    btSelectProjectLocation: TButton;
    lbEntityOptions: TLabel;
    ckCreateEntity: TCheckBox;
    ckUpdateEntity: TCheckBox;
    ckDeleteEntity: TCheckBox;
    procedure FormShow(Sender: TObject);
    procedure btRequestHeadersClick(Sender: TObject);
    procedure btConnectClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure btnPrevClick(Sender: TObject);
    procedure btnNextClick(Sender: TObject);
    procedure btnCreateClick(Sender: TObject);
    procedure btSelectProjectLocationClick(Sender: TObject);
    procedure tabProjectLocationShow(Sender: TObject);
    procedure ckbEntitiesClick(Sender: TObject);
    procedure ckbEntitiesClickCheck(Sender: TObject);
    procedure ckPermissionEntityClick(Sender: TObject);
    procedure ckbEntitiesKeyPress(Sender: TObject; var Key: Char);
  private
    { Private declarations }
    FModel: TXDataModel;
    FConfig: TScaffoldingConfig;
    FSelectedEntities: TList<TScaffoldingEntityConfig>;
    FHeaders: TStrings;
    procedure FillEntitiesList(AModel: TXDataModel);
    function RetrieveXDataModel(Url: string; AHeaders: TStrings): TXDataModel;
    function CheckModel(AModel: TXDataModel): Boolean;
    function GetProjectLocationBasePath: string;

    function DoUIChecks: Boolean;
    procedure UpdateNextPrevButtons;
    function GetNextTabIndex: Integer;
    function GetPrevTabIndex: Integer;

    function GetSelectedEntities: TList<TScaffoldingEntityConfig>;

    function IsSelectedEntity(AEntitySetName: string): Boolean;
    function AddSelectedEntity(AEntitySetName: string): TScaffoldingEntityConfig;
    procedure RemoveSelectedEntity(AEntitySetName: string);
    
    function GetSelectedEntity: TScaffoldingEntityConfig;
    procedure SetEntityPermission(AEntitySetName: string;
      APermissionIndex: Integer; AEnabled: Boolean);
  public
    { Public declarations }
    constructor Create(AConfig: TScaffoldingConfig); reintroduce;
    property Headers: TStrings read FHeaders;
    property Model: TXDataModel read FModel;
  end;

var
  FScaffoldingWizard: TFScaffoldingWizard;

implementation

uses
  System.UITypes,
  System.IOUtils,
  Scaffolding.Wizard.Headers;

{$R *.dfm}

function TFScaffoldingWizard.AddSelectedEntity(
  AEntitySetName: string): TScaffoldingEntityConfig;
begin
  if IsSelectedEntity(AEntitySetName) then
    raise EXDataScaffoldingException.CreateFmt('Entity %s is already selected', [AEntitySetName]);

  Result := TScaffoldingEntityConfig.Create(
    FModel.DefaultEntityContainer.FindEntitySet(AEntitySetName)
  );
    
  FSelectedEntities.Add(Result);    
end;

procedure TFScaffoldingWizard.btConnectClick(Sender: TObject);
var
  Model: TXDataModel;
begin
  Model := RetrieveXDataModel(edServerUrl.Text, FHeaders);
  try
    ShowMessage('Connection established successfully!');
  finally
    Model.Free;
  end;
end;

procedure TFScaffoldingWizard.btnCreateClick(Sender: TObject);
begin
  if DoUIChecks then
  begin
    FConfig.Model := Model;
    FConfig.ProjectDestinationPath := edProjectLocation.Text;

    FConfig.ProjectName := ExcludeTrailingPathDelimiter(FConfig.ProjectDestinationPath);
    FConfig.ProjectName := ExtractFileName(FConfig.ProjectName);

    FConfig.ApiUrl := edServerUrl.Text;
    FConfig.Entities.Clear;
    FConfig.Entities.AddRange(GetSelectedEntities);
  end
  else
    ModalResult := mrNone;
end;

procedure TFScaffoldingWizard.btnNextClick(Sender: TObject);
begin
  if not DoUIChecks then Exit;

  try
    // Moving the logo around and changing tabs can flicker
    SendMessage(tabPages.Handle, WM_SETREDRAW, 0, 0);
    tabPages.ActivePageIndex := GetNextTabIndex;
  finally
    SendMessage(tabPages.Handle, WM_SETREDRAW, 1, 0);
    tabPages.Repaint;
  end;

  UpdateNextPrevButtons;
end;

procedure TFScaffoldingWizard.btnPrevClick(Sender: TObject);
begin
  try
    // Moving the logo around and changing tabs can flicker
    SendMessage(tabPages.Handle, WM_SETREDRAW, 0, 0);

    tabPages.ActivePageIndex := GetPrevTabIndex;
  finally
    SendMessage(tabPages.Handle, WM_SETREDRAW, 1, 0);
    tabPages.Repaint;
  end;

  UpdateNextPrevButtons;
end;

procedure TFScaffoldingWizard.btRequestHeadersClick(Sender: TObject);
var
  FormHeaders: TFScaffoldingWizardHeaders;
begin
  FormHeaders := TFScaffoldingWizardHeaders.Create(FHeaders);
  try
    if FormHeaders.ShowModal = mrOk then
    begin
      FHeaders.Clear;
      FHeaders.AddStrings(FormHeaders.Headers);
    end;
  finally
    FormHeaders.Release;
  end;
end;

procedure TFScaffoldingWizard.btSelectProjectLocationClick(Sender: TObject);
var
  {$WARNINGS OFF}
  FileOpenDialog: TFileOpenDialog;
  DefaultDir: string;
begin
  FileOpenDialog := TFileOpenDialog.Create(nil);
  try
    FileOpenDialog.Options := [fdoPickFolders];
    FileOpenDialog.Title := 'Select Project Location';

    if edProjectLocation.Text = '' then
      DefaultDir := GetProjectLocationBasePath
    else
      DefaultDir := edProjectLocation.Text;

    if not DirectoryExists(DefaultDir) then
      DefaultDir := ExtractFilePath(ExcludeTrailingPathDelimiter(DefaultDir));

    FileOpenDialog.DefaultFolder := DefaultDir;

    if FileOpenDialog.Execute then
      edProjectLocation.Text := FileOpenDialog.FileName;
  finally
    FileOpenDialog.Free;
  end;
  {$WARNINGS ON}
end;

function TFScaffoldingWizard.CheckModel(AModel: TXDataModel): Boolean;
begin
  Result := True;

end;

procedure TFScaffoldingWizard.ckbEntitiesClick(Sender: TObject);
var
  ItemSelected: Integer;
begin
  ItemSelected := ckbEntities.ItemIndex;

  lbEntityOptions.Caption := Format('Options (%s):', [ckbEntities.Items[ItemSelected]]);

  ckCreateEntity.Enabled := ckbEntities.Checked[ItemSelected];
  ckUpdateEntity.Enabled := ckbEntities.Checked[ItemSelected];
  ckDeleteEntity.Enabled := ckbEntities.Checked[ItemSelected];

  if GetSelectedEntity <> nil then
  begin
    ckCreateEntity.Checked := GetSelectedEntity.CanInsert;
    ckUpdateEntity.Checked := GetSelectedEntity.CanModify;
    ckDeleteEntity.Checked := GetSelectedEntity.CanDelete;
  end
  else
  begin
    ckCreateEntity.Checked := False;
    ckUpdateEntity.Checked := False;
    ckDeleteEntity.Checked := False;
  end;
end;

procedure TFScaffoldingWizard.ckbEntitiesClickCheck(Sender: TObject);
var
  ItemSelected: Integer;
begin
  ItemSelected := ckbEntities.ItemIndex;

  if ckbEntities.Checked[ItemSelected] then
    AddSelectedEntity(ckbEntities.Items[ItemSelected])
  else
    RemoveSelectedEntity(ckbEntities.Items[ItemSelected]);
  
  ckCreateEntity.Checked := ckbEntities.Checked[ItemSelected];
  ckUpdateEntity.Checked := ckbEntities.Checked[ItemSelected];
  ckDeleteEntity.Checked := ckbEntities.Checked[ItemSelected];
end;

procedure TFScaffoldingWizard.ckbEntitiesKeyPress(Sender: TObject;
  var Key: Char);
begin
  if Key = #32 then
    ckbEntitiesClick(Sender);
end;

procedure TFScaffoldingWizard.ckPermissionEntityClick(Sender: TObject);
var
  EntitySetName: string;
begin
  EntitySetName := ckbEntities.Items[ckbEntities.ItemIndex];
  if Sender = ckCreateEntity then
    SetEntityPermission(EntitySetName, 0, TCheckBox(Sender).Checked)
  else if Sender = ckUpdateEntity then
    SetEntityPermission(EntitySetName, 1, TCheckBox(Sender).Checked)
  else if Sender = ckDeleteEntity then
    SetEntityPermission(EntitySetName, 2, TCheckBox(Sender).Checked);
end;

constructor TFScaffoldingWizard.Create(AConfig: TScaffoldingConfig);
begin
  inherited Create(nil);
  FConfig := AConfig;
end;

function TFScaffoldingWizard.DoUIChecks: Boolean;
var
  SelectedButton: integer;
begin
  Result := False;
  if tabPages.ActivePage = tabServerSettings then
  begin
    if edServerUrl.Text = '' then
    begin
      ShowMessage('Please enter the URL of the xdata server.');
      edServerUrl.SetFocus;
      Exit(False);
    end;

    try
      FModel := RetrieveXDataModel(edServerUrl.Text, FHeaders);
      FillEntitiesList(FModel);
      Result := True;
    except
      FModel := nil;
      raise;
    end;
  end
  else if tabPages.ActivePage = tabSelectEntities then
  begin
    if GetSelectedEntities.Count = 0 then
    begin
      ShowMessage('Please select at least one entity.');
      ckbEntities.SetFocus;
      Exit(False);
    end;
    Result := True;
  end
  else if tabPages.ActivePage = tabProjectLocation then
  begin
    if edProjectLocation.Text = '' then
    begin
      ShowMessage('Please enter the project location.');
      edProjectLocation.SetFocus;
      Exit(False);
    end
    else
    begin
      if not TDirectory.Exists(edProjectLocation.Text) then
        TDirectory.CreateDirectory(edProjectLocation.Text);

      if not TDirectory.IsEmpty(edProjectLocation.Text) then
      begin
        SelectedButton := messagedlg('The target folder is not empty. Are you sure you want to generate the web application into that folder?', mtInformation,
                              [mbYes, mbCancel], 0);

        if SelectedButton = mrCancel then
        begin
          edProjectLocation.SetFocus;
          Exit(False);
        end;
      end;
    end;
    Result := True;
  end;
end;

procedure TFScaffoldingWizard.FillEntitiesList(AModel: TXDataModel);
var
  EntitySet: TXDataEntitySet;
  I: integer;
begin
  if not CheckModel(AModel) then
    Exit;

  ckbEntities.Clear;
  FSelectedEntities.Clear;
  for I := 0 to AModel.DefaultEntityContainer.EntitySets.Count - 1 do
  begin
    EntitySet := AModel.DefaultEntityContainer.EntitySets[I];
    ckbEntities.Items.Add(EntitySet.Name);
    FSelectedEntities.Add(TScaffoldingEntityConfig.Create(EntitySet));
  end;
  ckbEntities.CheckAll(TCheckBoxState.cbChecked);
  ckbEntities.ItemIndex := 0;
  ckbEntitiesClick(ckbEntities);
end;

procedure TFScaffoldingWizard.FormCreate(Sender: TObject);
begin
  FModel := nil;
  FSelectedEntities := TObjectList<TScaffoldingEntityConfig>.Create;
  FHeaders := TStringList.Create;
end;

procedure TFScaffoldingWizard.FormDestroy(Sender: TObject);
begin
  if Assigned(FModel) then
    FModel.Free;
  FSelectedEntities.Free;
  FHeaders.Free;
end;

procedure TFScaffoldingWizard.FormShow(Sender: TObject);
const
  BORDER = 4;
var
  TabHeight : Integer;
begin
  tabPages.TabHeight := 1; // However, draws more than 1px
  TabHeight := 20; // Good safety margin

  // Arrange the page control so the tabs are not visible
  tabPages.Height := pnlButtons.Top - (pnlHeading.Top + pnlHeading.Height)
    + 2 * BORDER
    + TabHeight;

  tabPages.ActivePageIndex := 0;

  UpdateNextPrevButtons;
end;

function TFScaffoldingWizard.GetProjectLocationBasePath: string;
begin
  Result := FConfig.ProjectDestinationBasePath;
end;

function TFScaffoldingWizard.GetNextTabIndex: Integer;
var
  I : Integer;
begin
  Result := -1;

  // Find the first enabled tabsheet after the current one
  for I := tabPages.ActivePageIndex+1 to Pred(tabPages.PageCount) do
    if tabPages.Pages[i].Enabled then
      Exit(i);
end;

function TFScaffoldingWizard.GetPrevTabIndex: Integer;
var
  I : Integer;
begin
  Result := -1;

  // Find the first enabled tabsheet before the current one
  for I := tabPages.ActivePageIndex-1 downto 0 do
    if tabPages.Pages[i].Enabled then
      Exit(i);
end;

function TFScaffoldingWizard.GetSelectedEntities: TList<TScaffoldingEntityConfig>;
begin
  Result := FSelectedEntities;
end;

function TFScaffoldingWizard.GetSelectedEntity: TScaffoldingEntityConfig;
var
  EntityConfig: TScaffoldingEntityConfig;
begin
  Result := nil;
  for EntityConfig in FSelectedEntities do
  begin
    if EntityConfig.EntitySet.Name = ckbEntities.Items[ckbEntities.ItemIndex] then
    begin
      Result := EntityConfig;
      Break;
    end;
  end;
end;

function TFScaffoldingWizard.IsSelectedEntity(AEntitySetName: string): Boolean;
var
  EntityConfig: TScaffoldingEntityConfig;
begin
  Result := False;
  for EntityConfig in FSelectedEntities do
  begin
    if EntityConfig.EntitySet.Name = AEntitySetName then
    begin
      Result := True;
      Break;
    end;
  end;
end;

procedure TFScaffoldingWizard.RemoveSelectedEntity(AEntitySetName: string);
var
  EntityConfig: TScaffoldingEntityConfig;
begin
  if not IsSelectedEntity(AEntitySetName) then
    raise EXDataScaffoldingException.CreateFmt('Entity %s is not selected', [AEntitySetName]);
  
  for EntityConfig in FSelectedEntities do
  begin
    if EntityConfig.EntitySet.Name = AEntitySetName then
    begin
      FSelectedEntities.Remove(EntityConfig);
      Break;
    end;
  end;
end;

function TFScaffoldingWizard.RetrieveXDataModel(Url: string;
  AHeaders: TStrings): TXDataModel;
var
  Conn: TXDataWebConnection;
begin
  Conn := TXDataWebConnection.Create(nil);
  try
    Conn.URL := Url;
    if AHeaders.Count > 0 then
      Conn.DesignData.Headers.AddStrings(AHeaders);
    Result := Conn.RetrieveModel;
  finally
    Conn.Free;
  end;
end;

procedure TFScaffoldingWizard.SetEntityPermission(AEntitySetName: string;
  APermissionIndex: Integer; AEnabled: Boolean);
var
  EntityConfig: TScaffoldingEntityConfig;
begin
  // 0 = Create;   1 = Update;   2 = Delete
  for EntityConfig in FSelectedEntities do
  begin
    if EntityConfig.EntitySet.Name = AEntitySetName then
    begin
      case APermissionIndex of
        0: EntityConfig.CanInsert := AEnabled;
        1: EntityConfig.CanModify := AEnabled;
        2: EntityConfig.CanDelete := AEnabled;
      end;
      Break;
    end;
  end;
end;

procedure TFScaffoldingWizard.tabProjectLocationShow(Sender: TObject);
var
  I: integer;
  DestinationPath: string;
begin
  I := 1;
  DestinationPath := TPath.Combine(GetProjectLocationBasePath, 'XDataWebApplication');
  while DirectoryExists(DestinationPath + IntToStr(I)) do
    Inc(I);
  DestinationPath := DestinationPath + IntToStr(I);
  edProjectLocation.Text := DestinationPath;
end;

procedure TFScaffoldingWizard.UpdateNextPrevButtons;
begin
  // Position the buttons. Next morphs into Create, because they are in the same place
  btnNext.Left := btnCreate.Left;
  btnNext.Top := btnCreate.Top;
  btnPrev.Left := btnNext.Left - btnPrev.Width - 6;
  btnPrev.Top := btnNext.Top;

  // Are Next/Prev enabled?
  btnPrev.Enabled := GetPrevTabIndex <> -1;
  btnNext.Enabled := GetNextTabIndex <> -1;

  // If Next is not enabled, it should be Create
  btnNext.Visible := btnNext.Enabled;
  btnCreate.Visible := not btnNext.Enabled;
  btnCreate.Enabled := btnCreate.Visible;
end;

end.
