//==============================================================================
//  processes textmacros directives in code.
//  Input is JHInput.input, output is also JHInput.input
//==============================================================================
unit TextMacro;

//==============================================================================
//  INTERFACE
//==============================================================================
interface
uses TextMacroException, JHInput, TextOnly, TextCommon,
     StringHash, JASSerException,
     Constants, SysUtils, JHInterface;

//==============================================================================
type Ttextmacro = class(TObject)
private

public
   contents:array of string;
   contentssize:integer;
   args:array of string;
   argsn:integer;
   decl:integer;
   constructor create;
   procedure addArgument(const s:string);
   procedure add(const s:string);
   procedure reserveSpace(s:integer);
end;


//==============================================================================
procedure processTextMacros;

//==============================================================================
//  IMPLEMENTATION
//==============================================================================
implementation

var
    globaltextmacrotrace:integer;
    globaloffset:integer;

//==============================================================================
procedure AddMacroLine(const line:string);
begin
    JHInput.AddLineEx(line, globaloffset, globaltextmacrotrace);
end;

//==============================================================================
constructor Ttextmacro.Create;
begin
    contentssize:=0;
    argsn:=0;
    decl:=0;
    SetLength(contents,1);
    SetLength(args,1);
end;

//==============================================================================
procedure Ttextmacro.addArgument(const s:string);
var
   i:integer;
begin
    // TODO: rethrow as JASSerException when Ttextmacro.addArgument is called
    for i:=0 to argsn-1 do if (args[i]=s) then raise TTextMacroException.Create(decl,'The same argument name was used multiple times');
    SetLength(args,argsn+1);
    args[argsn]:=s;
    argsn:=argsn+1;
end;

//==============================================================================
procedure Ttextmacro.reserveSpace(s:integer);
begin
    if(Length(contents)<s) then SetLength(contents,s);
end;

//==============================================================================
procedure Ttextmacro.add(const s:string);
begin
    contentssize:=contentssize+1;
    if(Length(contents)<contentssize) then SetLength(contents,contentssize);
    contents[contentssize-1]:=s;
end;                    

//==============================================================================
procedure Ttextmacro_run(macro:Ttextmacro; var replaceto:array of string);
var
 i,k,L,t,j:integer;
 word:string;

begin
    i:=0;
    if(macro.argsn=0) then begin

        //textmacroes without arguments in the list can just do a blind replace
        while(i<macro.contentssize) do begin
            AddMacroLine(macro.contents[i]);
            i:=i+1;
        end;

    end else begin

        while(i<macro.contentssize) do begin
            L:=Length(macro.contents[i]);
            findchar(macro.contents[i],k,1,'$');
            if(k<=L) then begin
                word:=Copy(macro.contents[i],1,k-1);
                while(true) do begin
                    findchar(macro.contents[i],t,k+1,'$');
                    if(t>L) then begin
                        SWrite(word,Copy(macro.contents[i],k,L-k+1));
                        break;

                    end else begin
                        j:=0;
                        while(j<macro.argsn) do begin
                            if(comparesubstring(macro.contents[i],k+1,t-1,macro.args[j])) then break;
                            j:=j+1;
                        end;
                        if(j>=macro.argsn) then begin
                            SWrite(word,Copy(macro.contents[i],k,t-k+1));
                        end else SWrite(word,replaceto[j]);

                        findchar(macro.contents[i],k,t+1,'$');
                        if(k>L) then begin
                            SWrite(word,Copy(macro.contents[i],t+1, L-t  ));
                            break;
                        end else begin
                            SWrite(word,Copy(macro.contents[i],t+1, k-t-1  ));
                        end;
                    end;
                end;
                //JHInput.AddMacroLine(word);

            end else
                //JHInput.AddMacroLine(contents[i]);
                word:=macro.contents[i];
            {if(not evaluateTextmacroLine(word)) then}
            AddMacroLine(word);
            i:=i+1;
        end;


    end;

end;


//==============================================================================
procedure processTextMacros;
var
   incopy : array of string;
   blockCopy: array of string;
   word,tempish:string;
   i,k,h,c,wordend:integer;
   tmp,tmp2:integer;
   hash: TStringHash;
   macro:Ttextmacro;
   macros: array of TTextMacro;
   macron: integer;
   parsedarg:array of string;
   parsedargsn:integer;
   //period,nextperiod:integer;
   once,skip,optional:boolean;
   label kkk;


   procedure ParseCrazyCall(st:integer;const s:string);
   var
      L:integer;
      x:integer;
      ctr:boolean;
      owe:integer;


   begin
       L:=Length(s);
       x:=st;
       owe:=0;
       while(x<=L) do begin
           if(s[x]='(') then begin
               break;
           end else if (s[x]=' ') or (s[x]=#9) then
              x:=x+1
           else  raise JASSerLineExceptionNoTextMacro(i,'Unexpected symbol : "'+s[x]+'"');
       end;
       if(x>L) then raise JASSerLineExceptionNoTextMacro(i,'Expected (');
       //great we now get and count the args!!

       parsedargsn:=0;
       x:=x+1;
       while(x<=L) do begin
           if(s[x]=' ') or (s[x]=#9) then x:=x+1
           else if(s[x]='"') then begin
               owe:=0;
               //turns out it is an argument...
               word:='';
               x:=x+1;
               ctr:=false;
               while(x<=L) do begin
                   if(ctr) then begin
                        if(s[x]='"') then word:=word+'"'
                        else word:=word+'\'+s[x];
                        ctr:=false;
                   end
                   else if (s[x]='"') then break
                   else if(s[x]='\') then ctr:=true
                   else word:=word+s[x];
                   x:=x+1;
               end;
               if(x>L) then raise JASSerLineExceptionNoTextMacro(i,'Unclosed argument');
               {s[x]='"'}
               x:=x+1;
               while(x<=L) do begin
                   if(s[x]=',') or (s[x]=')') then break;
                   x:=x+1;
               end;
               if(x>L) then raise JASSerLineExceptionNoTextMacro(i,'Expected , or )');

               SetLength(parsedarg,parsedargsn+1);
               parsedarg[parsedargsn]:=word;
               parsedargsn:=parsedargsn+1;
               if (s[x]=',') then begin
                  x:=x+1;
                  owe:=1;
                end
               else if(s[x]=')') then break;


           end
           else if (s[x]=')') then break
           else raise JASSerLineExceptionNoTextMacro(i,'Unexpected symbol : "'+s[x]+'"  ['+s+']');
           //x:=x+1;
       end;
       if(owe>0) then raise JASSerLineExceptionNoTextMacro(i,'Expected an argument after ,');
       if(x>L) then raise JASSerLineExceptionNoTextMacro(i,'Expected )');




   end;

begin

     //period:=0;
     //nextperiod:=0;
    {input contains the script}
    {ln contains the number of lines}
    // period:=ln div UPDATEVALUE+1;
     //nextperiod:=period;
     //ProStatus('  TextMacros Loading: ');

    if(inputN>JHInput.StorageLength()) then raise Exception.Create('textmacro wtf');
    SetLength(offset,JHInput.StorageLength());
    SetLength(textmacrotrace,JHInput.StorageLength());
    SetLength(incopy,JHInput.StorageLength());
    SetLength(blockCopy,JHInput.StorageLength());
    for i := 0 to inputN - 1 do begin
        //ProPosition(i/inputN);
        //if (i>=nextperiod) then begin
        //    nextperiod:=i+period;
        //end;
        incopy[i]:=JHInput.Get(i);
        blockCopy[i]:=JHInput.getLineBlock(i);
        offset[i]:=0;
        textmacrotrace[i]:=0;
    end;
    //ProEnd();

    macron:=0;
    macro:=nil;
    hash:=TStringHash.Create;
try

    {copy contains a copy of the input script}

    //parse all of the TextMacros
    i:=0;
    //Why care about strings? the only danger would be an string that has line breaks and in one of its lines
    //begins with //! textmacro or //! endtextmacro, you got to accept that's pretty unlikelly to happen.

    //First step involves fetching the textmacros. We need to do it in 2 steps to avoid issues

    //period:=ln div UPDATEVALUE+1;
    //nextperiod:=period;
    //ProStatus('  Textmacros Parsing: ');
        //ProMax(ln-1);

    while(i<inputN) do begin
        //ProPosition(i/inputN);
        //if (i>=nextperiod) then begin
        //    nextperiod:=i+period;
        //end;


        //textmacro or not?
        if CompareLineWord('//!',JHInput.Get(i),wordend) then begin
            //begins with //! , is preprocessor
            GetLineWord(JHInput.Get(i),word,wordend,wordend);
            once:=(word='textmacro_once');
            if once or (word='textmacro') then begin
                //awesome! it is a textmacro declaration!

                GetLineWord(JHInput.Get(i),word,wordend,wordend);

                if(word='') then raise JASSerLineExceptionNoTextMacro(i,'Expected a name');

                {word holds the macro name}
                h:=hash.ValueOf(word);
                skip:=false;
                if(h>=0) then begin
                    if(once) then begin
                        skip:=true;
                        goto kkk;
                    end else
                            raise JASSerLineDoubleExceptionNoTextMacro(i,'Textmacro redeclared',macros[h].Decl,'`--- Previously declared here');
                end;
                hash.Add(word,macron);
                macro:=Ttextmacro.create;
                macro.decl:=i;
                macron:=macron+1;
                SetLength(macros,macron);
                macros[macron-1]:=macro;

                GetLineWord(JHInput.Get(i),word,wordend,wordend);

                if(word='takes') then begin

                    GetLineWordAlsoComma(JHInput.Get(i),word,wordend,wordend);
                    if(word='') then raise JASSerLineExceptionNoTextMacro(i,'Expected argument list');
                    while(word<>'') do begin
                        if (not validIdentifierName(word)) then raise JASSerLineExceptionNoTextMacro(i,'Invalid argument name: '+word);

                        try
                          macro.addArgument(word);
                        except on e:TTextMacroException do
                             raise JASSerLineException(e.linen, e.msg);
                        end;

                        GetLineWordAlsoComma(JHInput.Get(i),word,wordend,wordend);
                    end;
                end else if (word<>'') then raise JASSerLineExceptionNoTextMacro(i,'Expected "takes"');
                   kkk:
                    k:=i+1;
                    while(k<inputN) do begin
                        if CompareLineWord('//!',JHInput.Get(k),wordend) then begin
                            GetLineWord(JHInput.Get(k),word,wordend,wordend);
                            if(word='endtextmacro') then begin
                                break;
                            end else if (word='textmacro') then begin
                                raise JASSerLineDoubleExceptionNoTextMacro(k,'Nested textmacros are not allowed',i,'`--- textmacro began here');
                            end else if (word='runtextmacro') then begin
                                raise JASSerLineDoubleExceptionNoTextMacro(k,'runtextmacro inside a textmacro is not allowed',i,'`--- textmacro began here');
                            end;
                        end;
                        k:=k+1;
                    end;

                    if(k=inputN) then begin
                        raise JASSerLineExceptionNoTextMacro(i,'Missing endtextmacro');
                    end;
                    {k holds position of endtextmacro}

                    incopy[i]:='//! jasshelperskip '+IntToStr(k);

                    i:=i+1;
                    if(not skip) then begin
                        macro.reservespace(k-i);
                        while(i<k) do begin
                            macro.add(incopy[i]);
                            i:=i+1;
                        end;
                    end else i:=k;


            end
            else if (word='endtextmacro') then begin
                //For crying out loud syntax errors.
                raise JASSerLineExceptionNoTextMacro(i,'Unexpected endtextmacro');
            end;
        end;

        i:=i+1;
    end;
    //ProEnd();

        //period:=ln div UPDATEVALUE+1;
        //nextperiod:=period;
        //ProStatus('  Textmacros Writing: ');
        //ProMax(ln-1);

    //step 2 involves doing the actual textmacro executing
    i:=0;
    c:=inputN;
    JHInput.Reset2();
    globaltextmacrotrace:=0;
    globaloffset:=0;

    while(i<c) do begin
        //ProPosition(i/c);
        //if (i>=nextperiod) then begin
        //    nextperiod:=i+period;
        //end;

        if CompareLineWord('//!',incopy[i],wordend) then begin
            GetLineWord(incopy[i],word,wordend,wordend);
            if (word='jasshelperskip') then begin
                //skip to line k;
                GetLineWord(incopy[i],word,wordend,wordend);
                h:=StrToInt(word);
                //in theory, no bug should be here, but who knows?.
                if(h<i) then raise JASSerLineExceptionNoTextMacro(i,'[internal error] Wrong JASSHelper Skip');
                globaloffset := globaloffset + (h-i+1);
                i:=h;
            end else if (word='runtextmacro') then begin

                GetLineToken(incopy[i],word,wordend,wordend);
                optional := (word='optional');
                if(optional) then
                    GetLineToken(incopy[i],word,wordend,wordend);

                if(word='') then raise JASSerLineExceptionNoTextMacro(i,'Expected a name');
                h:=hash.ValueOf(word);
                tempish:=word;

                tempish:=word+Copy(incopy[i],wordend,Length(incopy));
                ParseCrazyCall(wordend,incopy[i]);

                if(h<0) then begin
                    if(not optional) then
                        raise JASSerLineExceptionNoTextMacro(i,'Unable to find textmacro: "'+word+'"');
                    AddMacroLine('//ignored textmacro command: '+tempish);
                    globaltextmacrotrace:=i+1;

                end else begin
                    macro:=macros[h];


                    if(parsedargsn<>macro.argsn) then raise JASSerLineDoubleExceptionNoTextMacro(i,'Invalid number of arguments (Expected: '+IntToStr(macro.argsn)+', got: '+IntToStr(parsedargsn)+')',macro.decl,'`--- macro declared here');
                    tmp:=globaloffset;
                    globaloffset:=macro.decl-inputN;
                    tmp2:=inputN;

                    AddMacroLine('//textmacro instance: '+tempish);
                    globaltextmacrotrace:=i+1;
                    Ttextmacro_run(macro, parsedarg);
                    globaltextmacrotrace:=0;
                    AddMacroLine('//end of: '+tempish);

                    globaloffset:=tmp-(inputN-tmp2)+1;
                end;
//                macro.destroy;

            end else begin
                AddMacroLine(incopy[i]);
            end;

        end else begin
            AddMacroLine(incopy[i]);
        end;
        i:=i+1;
    end;
    //ProEnd();

finally
{    if(macro<>nil) then begin
        macro.Destroy;
    end;}
    hash.destroy;
    for i := 0 to macron - 1 do macros[i].Destroy;
end;




end;

end.
