(****************************************************************************
 * WANT - A build management tool.                                          *
 * Copyright (c) 2001-2003 Juancarlo Anez, Caracas, Venezuela.              *
 * All rights reserved.                                                     *
 *                                                                          *
 * This library is free software; you can redistribute it and/or            *
 * modify it under the terms of the GNU Lesser General Public               *
 * License as published by the Free Software Foundation; either             *
 * version 2.1 of the License, or (at your option) any later version.       *
 *                                                                          *
 * This library is distributed in the hope that it will be useful,          *
 * but WITHOUT ANY WARRANTY; without even the implied warranty of           *
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU        *
 * Lesser General Public License for more details.                          *
 *                                                                          *
 * You should have received a copy of the GNU Lesser General Public         *
 * License along with this library; if not, write to the Free Software      *
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA *
 ****************************************************************************)
{
    @brief

    @author Juancarlo Aņez
}

unit ScriptRunner;

interface

uses
  SysUtils,
  Classes,

  JclFileUtils,

  JALStrings,

  WildPaths,

  WantUtils,
  WantClasses,
  BuildListeners,
  ScriptParser;

type
  TScriptRunner = class
  protected
    FListener: TBuildListener;
    FListenerCreated: boolean;

    procedure DoCreateListener; virtual;
    procedure CreateListener; virtual;
    procedure SetListener(Value: TBuildListener);

    procedure BuildTarget(Target: TTarget);
    procedure ExecuteTask(Task: TTask);
    procedure GetAttributeInfo; virtual;

  public
    constructor Create;
    destructor Destroy; override;

    procedure LoadProject(Project: TProject; BuildFile: TPath; SearchUp: boolean = false);

    procedure BuildProject(Project: TProject; Target: string = ''); overload;
    procedure BuildProject(Project: TProject; Targets: TStringArray); overload;

    procedure Build(BuildFile: TPath; Targets: TStringArray; Level: TLogLevel = vlNormal); overload;
    procedure Build(BuildFile: TPath; Target: string; Level: TLogLevel = vlNormal); overload;
    procedure Build(BuildFile: TPath; Level: TLogLevel = vlNormal); overload;

    procedure Log(Level: TLogLevel; Msg: string);

    class function DefaultBuildFileName: TPath;
    function FindBuildFile(BuildFile: TPath; SearchUp: boolean = False): TPath; overload;
    function FindBuildFile(SearchUp: boolean = False): TPath; overload;

    property Listener: TBuildListener read FListener write SetListener;
    property ListenerCreated: boolean read FListenerCreated;
  end;

implementation

{ TScriptRunner }

constructor TScriptRunner.Create;
begin
  inherited Create;
  DoCreateListener;
end;

destructor TScriptRunner.Destroy;
begin
  if ListenerCreated then
  begin
    FListener.BuildFinished;
    FreeAndNil(FListener);
  end;
  inherited Destroy;
end;

procedure TScriptRunner.LoadProject(Project: TProject; BuildFile: TPath; SearchUp: boolean);
begin
  if not IsLocalPath(BuildFile) then
    BuildFile := ToPath(BuildFile);
  BuildFile := FindBuildFile(BuildFile, SearchUp);

  try
    TScriptParser.Parse(Project, BuildFile);
    Listener.BuildFileLoaded(Project, WildPaths.ToRelativePath(BuildFile, CurrentDir));
  except
    on e: Exception do
    begin
      Listener.BuildFailed(Project, e.Message);
      raise;
    end;
  end;
end;

procedure TScriptRunner.Build(BuildFile: TPath;
  Targets: TStringArray;
  Level: TLogLevel = vlNormal);
var
  Project: TProject;
begin
  Listener.Level := Level;
  Project := TProject.Create;
  try
    Project.Listener := Listener;
    try
      LoadProject(Project, BuildFile);
      BuildProject(Project, Targets);
    except
      on e: EWantException do
      begin
        Log(vlDebug, e.Message);
        raise;
      end;
      on e: Exception do
      begin
        Log(vlDebug, e.Message);
        Listener.BuildFailed(Project, e.Message);
        raise;
      end;
    end;
  finally
    Project.Free;
  end;
end;

procedure TScriptRunner.DoCreateListener;
begin
  if Listener = nil then
  begin
    CreateListener;
    FListenerCreated := True;
  end;
end;

procedure TScriptRunner.CreateListener;
begin
  FListener := TBasicListener.Create;
end;

procedure TScriptRunner.Build(BuildFile: TPath; Level: TLogLevel);
begin
  Build(BuildFile, nil, Level);
end;

procedure TScriptRunner.Build(BuildFile: TPath; Target: string; Level: TLogLevel);
var
  T: TStringArray;
begin
  SetLength(T, 1);
  T[0] := Target;
  Build(BuildFile, T, Level);
end;

procedure TScriptRunner.BuildProject(Project: TProject; Target: string);
begin
  if Trim(Target) <> '' then
    BuildProject(Project, StringArray(Target))
  else
    BuildProject(Project, nil);
end;

procedure TScriptRunner.BuildProject(Project: TProject; Targets: TStringArray);
var
  i: Integer;
  Sched: TTargetArray;
  LastDir: TPath;
begin
  Sched := nil;
  Listener.ProjectStarted(Project);
  try
    try
      Sched := nil;
      Project.Listener := Listener;

      Project.Configure;

      Log(vlDebug, Format('rootdir="%s"', [Project.RootPath]));
      Log(vlDebug, Format('basedir="%s"', [Project.BaseDir]));
      Log(vlDebug, Format('basepath="%s"', [Project.BasePath]));

      if Length(Targets) = 0 then
      begin
        if Project._Default <> '' then
          Targets := StringArray(Project._Default)
        else
          raise ENoDefaultTargetError.Create('No default target');
      end;
    except
      on e: Exception do
      begin
        Log(vlDebug, e.Message);
        if e is ETaskException then
          Listener.BuildFailed(Project)
        else
          Listener.BuildFailed(Project, e.Message);
        raise;
      end;
    end;

    try
      Sched := Project.Schedule(Targets);

      if Length(Sched) = 0 then
        Listener.Log(vlWarnings, 'Nothing to build')
      else
      begin
        LastDir := CurrentDir;
        try
          for i := Low(Sched) to High(Sched) do
          begin
            ChangeDir(Project.BasePath);
            BuildTarget(Sched[i]);
          end;
        finally
          ChangeDir(LastDir);
        end;
      end;
    except
      on e: Exception do
      begin
        Log(vlDebug, e.Message);
        if e is ETaskException then
          Listener.BuildFailed(Project)
        else
          Listener.BuildFailed(Project, e.Message);
        raise;
      end;
    end;
  finally
    Listener.ProjectFinished(Project);
    Project.Listener := nil;
  end;
end;

procedure TScriptRunner.BuildTarget(Target: TTarget);
var
  i: Integer;
  LastDir: TPath;
begin
  if not Target.Enabled then
    EXIT;

  Listener.TargetStarted(Target);

  Log(vlDebug, Format('basedir="%s"', [Target.BaseDir]));
  Log(vlDebug, Format('basepath="%s"', [Target.BasePath]));

  LastDir := CurrentDir;
  try
    ChangeDir(Target.BasePath);

    for i := 0 to Target.TaskCount - 1 do
      ExecuteTask(Target.Tasks[i]);

    Listener.TargetFinished(Target);
  finally
    ChangeDir(LastDir)
  end;
end;

procedure TScriptRunner.ExecuteTask(Task: TTask);
begin
  if not Task.Enabled then
  begin
    Log(vlVerbose, Format('skipping disabled task <%s>', [Task.TagName]));
    EXIT;
  end;
  Listener.TaskStarted(Task);

  Log(vlDebug, Format('basedir="%s"', [Task.BaseDir]));
  Log(vlDebug, Format('basepath="%s"', [Task.BasePath]));

  try
    Task.DoExecute;
    Listener.TaskFinished(Task);
  except
    on e: Exception do
    begin
      Log(vlDebug, 'caught: ' + e.Message);
      if e is EWantException then
      begin
        Listener.TaskFailed(Task, e.Message);
        raise
      end
      else
      begin
        Log(vlErrors, e.Message);
        Listener.TaskFailed(Task, e.Message);
        raise ETaskError.Create(e.Message);
      end;
    end;
  end;
end;

procedure TScriptRunner.Log(Level: TLogLevel; Msg: string);
begin
  Assert(Listener <> nil);
  Listener.Log(Level, Msg);
end;

procedure TScriptRunner.SetListener(Value: TBuildListener);
begin
  if FListener <> Value then
  begin
    if FListenerCreated then
    begin
      FListener.Free;
      FListener := nil;
    end;
    FListenerCreated := False;
    FListener := Value;
  end;
end;

class function TScriptRunner.DefaultBuildFileName: TPath;
var
  AppName: string;
begin
  AppName := ExtractFileName(GetModulePath(hInstance));
  Result := ChangeFileExt(LowerCase(AppName), '.xml');
end;

function TScriptRunner.FindBuildFile(BuildFile: TPath; SearchUp: boolean): TPath;
var
  Dir: TPath;
begin
  if BuildFile = '' then
    Result := FindBuildFile(SearchUp)
  else
  begin
    Log(vlDebug, Format('Findind buildfile %s', [BuildFile]));
    Result := PathConcat(CurrentDir, BuildFile);
    Dir := SuperPath(Result);

    Log(vlDebug, Format('Looking for "%s in "%s"', [BuildFile, Dir]));
    while not PathIsFile(Result)
      and SearchUp
      and (Dir <> '')
      and (Dir <> SuperPath(Dir))
      do
    begin
      if PathIsDir(Dir) then
      begin
        Result := PathConcat(Dir, BuildFile);
        Dir := SuperPath(Dir);
        Log(vlDebug, Format('Looking for "%s in "%s"', [BuildFile, Dir]));
      end
      else
        break;
    end;

    if not PathIsFile(Result) then
      Result := BuildFile;
  end;
end;

function TScriptRunner.FindBuildFile(SearchUp: boolean): TPath;
begin
  Result := FindBuildFile(DefaultBuildFileName, SearchUp);
  if not PathIsFile(Result) then
    Result := FindBuildFile(AntBuildFileName, SearchUp);
  if not PathIsFile(Result) then
    Result := DefaultBuildFileName;
end;

procedure TScriptRunner.GetAttributeInfo;
var
  i, j, k: integer;
  TagName: string;
  EClass, AClass: TScriptElementClass;
  FClasses: TList;
  FAttrs, FElems: TStringlist;
begin
  j := WantClasses.GetElementCount - 1;
  FClasses := TList.Create;
  FElems := TStringlist.Create;
  FAttrs := TStringlist.Create;
  try
    for i := 0 to j do
    begin
      FAttrs.Clear;
      FElems.Clear;
      if not WantClasses.GetElementInfo(i, TagName, EClass, AClass) then
        Continue;
      if (AClass <> nil) then
      begin
        if (FClasses.IndexOf(Pointer(AClass)) < 0) then
        begin
          TTaskClass(AClass).EnumAttributes(FAttrs);
//          TTaskClass(AClass).EnumElements(FElems);
        end;
        if FAttrs.Count > 0 then
        begin
          Log(vlNormal, Format('%s:%s', [AClass.TagName, AClass.ClassName]));
          Log(vlNormal, 'Attributes:');
          for k := 0 to FAttrs.Count - 1 do
            Log(vlNormal, Format('  %s:%s', [FAttrs.Names[k], FAttrs.Values[FAttrs.Names[k]]]));
        end;
        if (AClass <> nil) and (EClass <> nil) then
        begin
          FAttrs.Clear;
          TScriptElementClass(EClass).EnumAttributes(FAttrs);
//          TScriptElementClass(EClass).EnumElements(FElems);
          if FAttrs.Count > 0 then
          begin
           if (FClasses.IndexOf(Pointer(AClass)) < 0) then
              Log(vlNormal, '  Elements:');
            Log(vlNormal, Format('    %s:%s', [EClass.TagName, EClass.ClassName]));
            Log(vlNormal, '    Attributes:');
            for k := 0 to FAttrs.Count - 1 do
              Log(vlNormal, Format('      %s:%s', [FAttrs.Names[k], FAttrs.Values[FAttrs.Names[k]]]));
          end;
        end;
        FClasses.Add(Pointer(AClass));
      end;

    end;
  finally
    FClasses.Free;
    FAttrs.Free;
    FElems.Free;
  end;
end;

end.

