{ pas2js options

  Author: Mattias Gaertner
}
unit PJSDsgnOptions;

{$mode objfpc}{$H+}
{$Inline on}

interface

uses
  Classes, SysUtils,
  // LazUtils
  LazFileCache, LazConfigStorage, LazFileUtils, FileUtil,
  // Codetools
  DefineTemplates,
  // IdeIntf
  MacroIntf, BaseIDEIntf;

const
  PJSDsgnOptsFile = 'pas2jsdsgnoptions.xml';
  PJSDefaultCompiler = '$MakeExe(IDE,pas2js)';
  PJSDefaultWebServerName = 'compileserver';
  PJSDefaultWebServer = '$MakeExe(IDE,'+PJSDefaultWebServerName+')';
  PJSDefaultStartAtPort = 3000; // compileserver default port
  PJSDefaultHTTPServerParams = '-s -p $()';
  PJSDefaultBrowser = '$MakeExe(IDE,firefox)';
  PJSDefaultNodeJS = '$MakeExe(IDE,nodejs)';

Type
  TPas2jsCachedOption = (
    p2jcoCompilerFilename,
    p2jcoBrowserFilename,
    p2jcoHTTPServerFilename,
    p2jcoHTTPServerPort,
    p2jcoNodeJSFilename,
    p2jcoAtomTemplateDir,
    p2jcoVSCodeTemplateDir
    );
  TPas2jsCachedOptions = set of TPas2jsCachedOption;
const
  p2jcoFilenames = [
    p2jcoCompilerFilename,
    p2jcoBrowserFilename,
    p2jcoHTTPServerFilename,
    p2jcoNodeJSFilename,
    p2jcoAtomTemplateDir,
    p2jcoVSCodeTemplateDir
  ];

type
  TPas2jsCachedValue = record
    RawValue: string;
    Stamp: int64;
    ParsedValue: string;
  end;
  PPas2jsCachedValue = ^TPas2jsCachedValue;

  { TPas2jsOptions }

  TPas2jsOptions = class
  private
    FCachedOptions: array[TPas2jsCachedOption] of TPas2jsCachedValue;
    FChangeStamp: int64;
    FHTTPServerOpts: TStrings;
    FSavedStamp: int64;
    FStartAtPort: Word;
    procedure DoOptsChanged(Sender: TObject);
    function GetAtomTemplateDir: String;
    function GetBrowserFileName: String;
    function GetCompilerFilename: string;
    function GetStartAtPort: Word;
    function GetVSCodeTemplateDir: String;
    function GetWebServerFileName: string;
    function GetModified: boolean;
    function GetNodeJSFileName: string;
    function GetParsedOptionValue(Option: TPas2jsCachedOption): string;
    procedure SetAtomTemplateDir(AValue: String);
    procedure SetBrowserFileName(AValue: String);
    procedure SetVSCodeTemplateDir(AValue: String);
    procedure SetWebServerFileName(AValue: string);
    procedure SetHTTPServerOpts(AValue: TStrings);
    procedure SetModified(AValue: boolean);
    procedure SetCompilerFilename(AValue: string);
    procedure SetNodeJSFileName(AValue: string);
    procedure SetStartAtPort(AValue: Word);
    procedure SetCachedOption(Option: TPas2jsCachedOption; const AValue: string);
  public
    constructor Create;
    destructor Destroy; override;
    procedure IncreaseChangeStamp; inline;
    procedure Load;
    procedure Save;
    function GetParsedBrowserFilename: string;
    function GetParsedCompilerFilename: string;
    function GetParsedWebServerFilename: string;
    function GetParsedWebServerPort: string;
    function GetParsedNodeJSFilename: string;
    procedure LoadFromConfig(Cfg: TConfigStorage);
    procedure SaveToConfig(Cfg: TConfigStorage);
  public
    property CompilerFilename: string read GetCompilerFilename write SetCompilerFilename;
    Property WebServerFileName : string Read GetWebServerFileName Write SetWebServerFileName;
    Property HTTPServerOpts : TStrings Read FHTTPServerOpts Write SetHTTPServerOpts;
    Property NodeJSFileName : string Read GetNodeJSFileName Write SetNodeJSFileName;
    Property BrowserFileName : String Read GetBrowserFileName Write SetBrowserFileName;
    Property StartAtPort : Word Read GetStartAtPort Write SetStartAtPort;
    property ChangeStamp: int64 read FChangeStamp;
    property Modified: boolean read GetModified write SetModified;
    Property AtomTemplateDir : String Read GetAtomTemplateDir Write SetAtomTemplateDir;
    Property VSCodeTemplateDir : String Read GetVSCodeTemplateDir Write SetVSCodeTemplateDir;
  end;

var
  PJSOptions: TPas2jsOptions = nil;

function GetStandardPas2jsExe: string;
function GetStandardWebServerExe: string;
function GetStandardBrowser: string;
function GetStandardNodeJS: string;
function GetPas2jsQuality(Filename: string; out Msg: string): boolean;

implementation

uses
  strpas2jsdesign;

function GetStandardPas2jsExe: string;
begin
  Result:=PJSDefaultCompiler;
  if not IDEMacros.SubstituteMacros(Result) then
    Result:='pas2js'+GetExeExt;
end;

function GetStandardNodeJS: string;

begin
  Result:=PJSDefaultNodeJS;
  if not IDEMacros.SubstituteMacros(Result) then
    Result:='nodejs'+GetExeExt;
end;

function GetStandardWebServerExe: string;

begin
  Result:=PJSDefaultWebServer;
  if not IDEMacros.SubstituteMacros(Result) then
    Result:='PJSDefaultHTTPServerCmd'+GetExeExt;
end;

function GetStandardBrowser: string;

begin
  Result:='$MakeExe(IDE,firefox)';
  if not IDEMacros.SubstituteMacros(Result) then
    begin
    Result:='$MakeExe(IDE,chrome)';
    {$ifdef windows}
    if not IDEMacros.SubstituteMacros(Result) then
      Result:='$MakeExe(IDE,iexplore)';
    {$else}
    {$ifdef darwin}
     if not IDEMacros.SubstituteMacros(Result) then
       Result:='$MakeExe(IDE,xdg-open)';
    {$endif}
     if not IDEMacros.SubstituteMacros(Result) then
       Result:='';
    {$endif}
    end;
end;


function GetPas2jsQuality(Filename: string; out Msg: string): boolean;
var
  ShortFile: String;
begin
  Msg:='';
  Filename:=TrimFilename(Filename);
  if (Filename='') then begin
    Msg:=pjsdMissingPathToPas2js;
    exit(false);
  end;
  if not FileExistsCached(Filename) then begin
    Msg:=SafeFormat(pjsdFileNotFound, [Filename]);
    exit(false);
  end;
  if not DirPathExistsCached(ExtractFilePath(Filename)) then begin
    Msg:=SafeFormat(pjsdDirectoryNotFound, [ExtractFilePath(Filename)]);
    exit(false);
  end;
  if not FileIsExecutable(Filename) then begin
    Msg:=SafeFormat(pjsdFileNotExecutable, [Filename]);
    exit(false);
  end;
  ShortFile:=ExtractFileNameOnly(Filename);
  if not CompareText(LeftStr(ShortFile,length('pas2js')),'pas2js')<>0 then begin
    Msg:=pjsdFileNameDoesNotStartWithPas2js;
    exit(false);
  end;
  // run it
  //RunTool(Filename);
end;

{ TPas2jsOptions }

procedure TPas2jsOptions.SetModified(AValue: boolean);
begin
  if AValue then
    IncreaseChangeStamp
  else
    FSavedStamp:=FChangeStamp;
end;

function TPas2jsOptions.GetModified: boolean;
begin
  Result:=FSavedStamp<>FChangeStamp;
end;

function TPas2jsOptions.GetBrowserFileName: String;
begin
  Result:=FCachedOptions[p2jcoBrowserFilename].RawValue;
end;

procedure TPas2jsOptions.DoOptsChanged(Sender: TObject);
begin
  Self.Modified:=True;
end;

function TPas2jsOptions.GetAtomTemplateDir: String;
begin
  Result:=FCachedOptions[p2jcoAtomTemplateDir].RawValue;
end;

function TPas2jsOptions.GetCompilerFilename: string;
begin
  Result:=FCachedOptions[p2jcoCompilerFilename].RawValue;
end;

function TPas2jsOptions.GetStartAtPort: Word;
begin
  Result:=StrToIntDef(FCachedOptions[p2jcoHTTPServerPort].RawValue,PJSDefaultStartAtPort);
end;

function TPas2jsOptions.GetVSCodeTemplateDir: String;
begin
  Result:=FCachedOptions[p2jcoVSCodeTemplateDir].RawValue;
end;

function TPas2jsOptions.GetWebServerFileName: string;
begin
  Result:=FCachedOptions[p2jcoHTTPServerFilename].RawValue;
end;

function TPas2jsOptions.GetNodeJSFileName: string;
begin
  Result:=FCachedOptions[p2jcoNodeJSFilename].RawValue;
end;

procedure TPas2jsOptions.SetCompilerFilename(AValue: string);
begin
  AValue:=TrimFilename(AValue);
  SetCachedOption(p2jcoCompilerFilename,AValue);
end;

procedure TPas2jsOptions.SetNodeJSFileName(AValue: string);
begin
  AValue:=TrimFilename(AValue);
  SetCachedOption(p2jcoNodeJSFilename,AValue);
end;

constructor TPas2jsOptions.Create;
var
  o: TPas2jsCachedOption;
begin
  FChangeStamp:=LUInvalidChangeStamp64;
  FCachedOptions[p2jcoCompilerFilename].RawValue:=PJSDefaultCompiler;
  FCachedOptions[p2jcoHTTPServerFilename].RawValue:=PJSDefaultWebServer;
  FCachedOptions[p2jcoNodeJSFilename].RawValue:=PJSDefaultNodeJS;
  FCachedOptions[p2jcoBrowserFilename].RawValue:=PJSDefaultBrowser;
  FCachedOptions[p2jcoAtomTemplateDir].RawValue:='';
  FCachedOptions[p2jcoVSCodeTemplateDir].RawValue:='';
  for o in TPas2jsCachedOption do
    FCachedOptions[o].Stamp:=LUInvalidChangeStamp64;
  FHTTPServerOpts:=TStringList.Create;
  TStringList(FHTTPServerOpts).OnChange:=@DoOptsChanged;
end;

destructor TPas2jsOptions.Destroy;
begin
  FreeAndNil(FHTTPServerOpts);
  inherited Destroy;
end;

procedure TPas2jsOptions.IncreaseChangeStamp;
begin
  LUIncreaseChangeStamp64(FChangeStamp);
end;

procedure TPas2jsOptions.Load;
var
  Cfg: TConfigStorage;
begin
  Cfg:=GetIDEConfigStorage(PJSDsgnOptsFile,true);
  try
    LoadFromConfig(Cfg);
  finally
    Cfg.Free;
  end;
end;

procedure TPas2jsOptions.Save;
var
  Cfg: TConfigStorage;
begin
  Cfg:=GetIDEConfigStorage(PJSDsgnOptsFile,false);
  try
    SaveToConfig(Cfg);
  finally
    Cfg.Free;
  end;
end;

Const
  KeyCompiler = 'compiler/value';
  KeyHTTPServer = 'webserver/value';
  KeyHTTPServerOptions = 'webserver/extraoptions/value';
  KeyBrowser = 'webbrowser/value';
  KeyNodeJS = 'nodejs/value';
  KeyAtomTemplate = 'atomtemplate/value';
  KeyVSCodeTemplate = 'vscodetemplate/value';
  KeyStartPortAt = 'webserver/startatport/value';

procedure TPas2jsOptions.LoadFromConfig(Cfg: TConfigStorage);

begin
  CompilerFilename:=Cfg.GetValue(KeyCompiler,PJSDefaultCompiler);
  WebServerFileName:=Cfg.GetValue(KeyHTTPServer,PJSDefaultWebServer);
  BrowserFileName:=Cfg.GetValue(KeyBrowser,PJSDefaultBrowser);
  NodeJSFileName:=Cfg.GetValue(KeyNodeJS,PJSDefaultNodeJS);
  AtomTemplateDir:=Cfg.GetValue(KeyAtomTemplate,'');
  VSCodeTemplateDir:=Cfg.GetValue(KeyVSCodeTemplate,'');
  StartAtPort :=Cfg.GetValue(KeyStartPortAt,PJSDefaultStartAtPort);
  Cfg.GetValue(KeyHTTPServerOptions,FHTTPServerOpts);
  Modified:=false;
end;

procedure TPas2jsOptions.SaveToConfig(Cfg: TConfigStorage);

begin
  Cfg.SetDeleteValue(KeyCompiler,CompilerFilename,PJSDefaultCompiler);
  Cfg.SetDeleteValue(KeyHTTPServer,WebServerFileName,PJSDefaultWebServer);
  Cfg.SetDeleteValue(KeyStartPortAt,StartAtPort,PJSDefaultStartAtPort);
  Cfg.SetDeleteValue(KeyNodeJS,NodeJSFileName,PJSDefaultNodeJS);
  Cfg.SetDeleteValue(KeyBrowser,BrowserFileName,PJSDefaultBrowser);
  Cfg.SetDeleteValue(KeyAtomTemplate,AtomTemplateDir,'');
  Cfg.SetDeleteValue(KeyVSCodeTemplate,VSCodeTemplateDir,'');
  Cfg.SetValue(KeyHTTPServerOptions,FHTTPServerOpts);
  Modified:=false;
end;

function TPas2jsOptions.GetParsedCompilerFilename: string;
begin
  Result:=GetParsedOptionValue(p2jcoCompilerFilename);
end;

function TPas2jsOptions.GetParsedWebServerFilename: string;
begin
  Result:=GetParsedOptionValue(p2jcoHTTPServerFilename);
end;

function TPas2jsOptions.GetParsedWebServerPort: string;
begin
  Result:=GetParsedOptionValue(p2jcoHTTPServerPort);
end;

function TPas2jsOptions.GetParsedNodeJSFilename: string;
begin
  Result:=GetParsedOptionValue(p2jcoNodeJSFilename);
end;

function TPas2jsOptions.GetParsedOptionValue(Option: TPas2jsCachedOption
  ): string;
var
  p: PPas2jsCachedValue;
  IsFilename: Boolean;
begin
  p:=@FCachedOptions[Option];
  if p^.Stamp<>IDEMacros.BaseTimeStamp then
  begin
    p^.Stamp:=IDEMacros.BaseTimeStamp;
    p^.ParsedValue:=p^.RawValue;
    IDEMacros.SubstituteMacros(p^.ParsedValue);
    IsFilename:=Option in p2jcoFilenames;
    if IsFilename then
    begin
      p^.ParsedValue:=TrimFilename(p^.ParsedValue);
      if (p^.ParsedValue<>'')
          and not FilenameIsAbsolute(p^.ParsedValue) then
      begin
        if ExtractFilePath(p^.ParsedValue)='' then
          p^.ParsedValue:=FindDefaultExecutablePath(p^.ParsedValue)
        else
          p^.ParsedValue:=''; // not found
      end;
    end;
    if p^.ParsedValue='' then
    begin
      case Option of
        p2jcoCompilerFilename: p^.ParsedValue:=GetStandardPas2jsExe;
        p2jcoBrowserFilename: p^.ParsedValue:=GetStandardBrowser;
        p2jcoHTTPServerFilename: p^.ParsedValue:=GetStandardWebServerExe;
        p2jcoHTTPServerPort: p^.ParsedValue:=IntToStr(PJSDefaultStartAtPort);
        p2jcoNodeJSFilename: p^.ParsedValue:=GetStandardNodeJS;
      end;
      if IsFilename and (p^.ParsedValue<>'')
          and not FilenameIsAbsolute(p^.ParsedValue) then
      begin
        if ExtractFilePath(p^.ParsedValue)='' then
          p^.ParsedValue:=FindDefaultExecutablePath(p^.ParsedValue)
        else
          p^.ParsedValue:=''; // not found
      end;
    end;
  end;
  Result:=p^.ParsedValue;
end;

procedure TPas2jsOptions.SetAtomTemplateDir(AValue: String);
begin
  AValue:=TrimFilename(AValue);
  SetCachedOption(p2jcoAtomTemplateDir,AValue);
end;

function TPas2jsOptions.GetParsedBrowserFilename: string;
begin
  Result:=GetParsedOptionValue(p2jcoBrowserFilename);
end;

procedure TPas2jsOptions.SetBrowserFileName(AValue: String);
begin
  AValue:=TrimFilename(AValue);
  SetCachedOption(p2jcoBrowserFilename,AValue);
end;

procedure TPas2jsOptions.SetVSCodeTemplateDir(AValue: String);
begin
  AValue:=TrimFilename(AValue);
  SetCachedOption(p2jcoVSCodeTemplateDir,AValue);
end;

procedure TPas2jsOptions.SetWebServerFileName(AValue: string);
begin
  AValue:=TrimFilename(AValue);
  SetCachedOption(p2jcoHTTPServerFilename,AValue);
end;

procedure TPas2jsOptions.SetHTTPServerOpts(AValue: TStrings);
begin
  if FHTTPServerOpts=AValue then Exit;
  FHTTPServerOpts.Assign(AValue);
end;

procedure TPas2jsOptions.SetStartAtPort(AValue: Word);
begin
  if FStartAtPort=AValue then Exit;
  FStartAtPort:=AValue;
  IncreaseChangeStamp;
end;

procedure TPas2jsOptions.SetCachedOption(Option: TPas2jsCachedOption;
  const AValue: string);
begin
  if FCachedOptions[Option].RawValue=AValue then exit;
  FCachedOptions[Option].RawValue:=AValue;
  FCachedOptions[Option].Stamp:=InvalidIDEMacroStamp;
  IncreaseChangeStamp;
  IDEMacros.IncreaseBaseStamp;
end;

Procedure DonePSJOptions;

begin
  if PJSOptions<>nil then
  begin
    try
      if PJSOptions.Modified then
        PJSOptions.Save;
    except
    end;
    FreeAndNil(PJSOptions);
  end;
end;


finalization
  DonePSJOptions;
end.
