#############################################################################
##
#W  rat-def.gi                         Manuel Delgado <mdelgado@fc.up.pt>
#W                                    Jose Morais    <josejoao@fc.up.pt>
##
##
##
#Y  Copyright (C)  2004,  CMUP, Universidade do Porto, Portugal
##
##
##          Rational Expressions
##
##  RatExpOnnLetters( n, operation, list )
##
## Constructs a rational expression on n letters.
##
## The name of the operation is "product", "union" or "star"
##  list is a list (with one element in the case of "star") of rational
## expressions or a list consisting of an integer when the rational
## expression is a single letter.
##
##  Example
##
## r:=RatExpOnnLetters(2,"union",[RatExpOnnLetters(2,[],[1]),
##                       RatExpOnnLetters(2,[],[2])]);
##  (Due to the function PrintObj that follows, GAP writes  aUb )
##
## ProductRatExp(r,r);
##  GAP writes (aUb)(aUb)
##
## UnionRatExp(StarRatExp(r),ProductRatExp(r,StarRatExp(r)));
##  GAP writes (aUb)*U(aUb)(aUb)*
##
## The empty word is the rational expression:
##  RatExpOnnLetters(5,[],[]);
##  GAP writes empty set
##  (5 may be replaced by any natural number)
## The empty set is considered a rational expression:
## RatExpOnnLetters(n,[],"empty_set");
############################################################################
DeclareRepresentation( "IsRationalExpressionRep", IsComponentObjectRep,
        [ "op", "list_exp" ] );


############################################################################
InstallGlobalFunction( RatExpOnnLettersObj, function( Fam, ratexpression )
    return Objectify( NewType( Fam, IsRatExpOnnLettersObj and
                   IsRationalExpressionRep and IsAttributeStoringRep),  ratexpression );
end );

############################################################################
##
#GF RatExpOnnLetters
##
## Contructs a rational expression on n letters. See example above.
##
InstallGlobalFunction( RatExpOnnLetters, function( n, operation, list )

    local   k,  F,  l,  exp,  list_exp,  R;

    if not (operation = "product" or operation = "union" or
            operation = "star" or operation = [ ]) then
        Error("Wrong operation given in RatExpOnnLetters");
    fi;

    if not (IsPosInt( n ) or IsList(n)) then
      Error( "<n> must be a positive integer or a list" );
    fi;

    if operation = "star" then
        if not IsRationalExpression(list) then
            Error("The star must be on a rational expression");
        fi;
    elif not IsList(list) then
        Error("list must be a list");
    elif operation = [] then
        if not(list = [ ] or list = "empty_set" or (Length(list)=1
                   and IsPosInt(list[1]))) then
            Error("list must be the empty list, a list of one positive integer or \"empty_set\"");
        fi;
    elif  not  ForAll(list, x -> IsPosInt(x) or IsRationalExpression(x)) then
        Error("list must be a list of positive integers/rational expressions");
    fi;

    if operation <> []  and operation <> "star" then
        for k in [1..Length(list)] do
            if IsPosInt(list[k]) then
                list[k] := RatExpOnnLetters(n, [ ], [k]);
            fi;
        od;
    fi;

    if operation = [] and Length(list) > 1 and list <> "empty_set" then
        Error("When <operation> is [], the third argument must be the empty list, a list of length 1 or \"empty_list\"");
    fi;

    # Construct the family of rational expressions on n letters.
    if IsInt(n) then
        F:= NewFamily( Concatenation( "RatExp", String(n), "Letters" ),
                    IsRatExpOnnLettersObj );
    else
        F:= NewFamily( Concatenation( "RatExp", String(Length(n)), "Letters" ),
                    IsRatExpOnnLettersObj );
    fi;

    # Install the data.
    if IsPosInt(n) then
        F!.alphabet := n;
        if n < 27 then
           F!.alphabet2 := List([1..n], i -> jascii[68+i]);
        else
           F!.alphabet2 := List([1..n], i -> Concatenation("a", String(i)));
        fi;
    else
        #        l := SSortedList(n);
        l := n;
        if '@' in l then
            Error("'@' must not be in the alphabet");
        fi;
        F!.alphabet := Length(l);
        F!.alphabet2 := l;
    fi;

    exp := rec(op := operation,
               list_exp := list );
    R := RatExpOnnLettersObj( F, exp );

    # Return the rational expression.
    return R;
end );

############################################################################
##
#F RatExpToString(r)
##
InstallGlobalFunction( RatExpToString, function( r, count )
    local   A,  i,  hasUnion,  xstr,  xout,  str,  e,  ret,  flag;

    if not IsRatExpOnnLettersObj(r) then
        Error("The argument to RatExpToString must be a rational expression");
    fi;


    A := [];
    if IsChar(AlphabetOfRatExpAsList(r)[1]) then
        for i in AlphabetOfRatExpAsList(r) do
            Add(A, [i]);
        od;
    else
        for i in AlphabetOfRatExpAsList(r) do
            Add(A, i);
        od;
    fi;

    # Tests if a rational expression has a union rational subexpression
    hasUnion := function(r)
        if r!.op = [] then
            return(false);
        elif r!.op = "star" then
            return(false);
        elif r!.op = "union" then
            return(true);
        else
            return(ForAny(r!.list_exp, e -> hasUnion(e)));
        fi;
    end;

    if r!.list_exp = "empty_set" then
        return(["empty_set", count]);
    elif r!.op = [] then
        count := count + 1;
        if r!.list_exp = [] then
            return(["@", count]);
        else
            if IsString(A[r!.list_exp[1]]) then
                return([String(ShallowCopy(A[r!.list_exp[1]])), count]);
            else
                xstr := "";
                xout := OutputTextString( xstr, false );
                PrintTo( xout, ShallowCopy(A[r!.list_exp[1]]));
                CloseStream( xout );
                return([xstr, count]);
            fi;
        fi;
    elif r!.op = "product" then
        str := "";
        for e in r!.list_exp do
            ret := RatExpToString(e, count);
            count := ret[2];
            if hasUnion(e) then
                str := Concatenation(str, "(", ret[1], ")");
            else
                str := Concatenation(str, ret[1]);
            fi;
        od;
        return([str, count]);
    elif r!.op = "union" then
        flag := false;
        str := "";
        for e in r!.list_exp do
            ret := RatExpToString(e, count);
            count := ret[2];
            if e!.op = "union" then
                if str = "" then
                    str := ret[1];
                else
                    str := Concatenation(str, "U", ret[1]);
                fi;
            else
                if str = "" then
                    str := ret[1];
                else
                    str := Concatenation(str, "U", ret[1]);
                fi;
            fi;
        od;
        return([str, count]);
    else
        if r!.list_exp!.op = [] then  # This is star of a single letter
            ret := RatExpToString(r!.list_exp, count);
            return([Concatenation(ret[1], "*"), ret[2]]);
        else
            ret := RatExpToString(r!.list_exp, count);
            return([Concatenation("(", ret[1], ")*"), ret[2]]);
        fi;
    fi;
end);


#######################################################################
##
#M  PrintObj(R)
##
## Is used to display a rational expression <R> in a form that is readable by
## a human being
##
InstallMethod( PrintObj,
"for rational expressions",
true,
[ IsRatExpOnnLettersObj and IsRationalExpressionRep ], 0,
function(r)
    local ret, string;

    ret := RatExpToString(r, 0);
    string := ret[1];
    Setter(SizeRatExp)(r,ret[2]);
    Print(string);
end );

#############################################################################
##
#F  RationalExpression(arg)
##
##  Given the number of alphabet symbols and a string (S) returns
##  the rational expression represented by S
##
InstallGlobalFunction(RationalExpression, function(arg)
    local i, I0, S0, RationalExp, processPowers;


#	This function parses a string and replaces a^n by aaa.. (n times)
	processPowers := function(S)
		local S2, lc, i, j, k, num;

		if S = [] then
			return [];
		fi;
		lc := S[1];		# The last character read
		S2 := [lc];		# The converted string
		i := 2;
		while IsBound(S[i]) do
			if S[i] = '^' and IsBound(S[i+1]) and IsDigitChar(S[i+1]) then
				j := i + 1;
				num := [];
#				Get the power
				while IsBound(S[j]) and IsDigitChar(S[j]) do
					Add(num, S[j]);
					j := j + 1;
				od;
				num := Int(num);
				for k in [2..num] do
					Add(S2, lc);
				od;
				i := j - 1;
			else
				Add(S2, S[i]);
			fi;
			lc := S[i];
			i := i + 1;
		od;
		return S2;
	end;
##	End of processPowers()  --


    if Length(arg) = 0 then
        Error("Please specify a rational expression as a string");
    elif IsBound(arg[2]) then
        I0 := arg[2];
        S0 := processPowers(arg[1]);
        if not ((IsPosInt(I0) or IsString(I0)) and IsString(S0)) then
            Error("The arguments to RationalExpression must be a string [and a positive integer or a string]");
        fi;
        if IsString(I0) then
            if '@' in I0 or '*' in I0 or '(' in I0 or ')' in I0 or 'U' in I0 then
                Error("'@', '*', '(', ')' and 'U' must not be in the alphabet");
            fi;
#            I0 := SSortedList(I0);
        else
            if Length(SSortedList(Concatenation(S0, "U*()@"))) > I0+5 then
                Error("The expression contains more different letters than the alphabet");
            fi;
        fi;
    else
        I0 := [];
        S0 := processPowers(arg[1]);
        if S0 = "@" then
            Error("When defining @, please specify the alphabet");
        fi;
        for i in S0 do
            if not (i = '@' or i = '*' or i = 'U' or i = '(' or i = ')') then
                AddSet(I0, i);
            fi;
        od;
    fi;


    RationalExp := function(S, I)
        local i, op, ord, I0, S0,
              getTopOp, buildStar, buildProduct,
              sub_exps, union_inds, star_inds, union_sub_exps;

        if S = "" or S = "@" then
            return(RatExpOnnLetters(I, [], []));
        elif S = "empty_set" then
            return(RatExpOnnLetters(I, [], "empty_set"));
        elif Length(S) = 1 then
            if IsInt(I) then
                ord := Ord(S[1]) - Ord('a') + 1;
            else
                ord := Position(I, S[1]);
                if ord = fail then
                    Error(Concatenation("Letter of expression not in alphabet: ", S[1]));
                fi;
            fi;
            return(RatExpOnnLetters(I, [], [ord]));
        fi;
        sub_exps       := [];
        union_inds     := [];
        star_inds      := [];
        union_sub_exps := [];

##  This functions returns the "top" most operation of the given
##  expression
##  getTopOp("aaUab*") returns "union"
##  getTopOp("(bUa)*") returns "star"
##  getTopOp("(bUa)*b") returns "product"
##
##  it also fills the array sub_exps with the sub expressions,
##  union_inds with the indexes in sub_exps of the positions of
##  the U symbols
##  and star_inds with the indexes in sub_exps of the positions
##  of the * symbols
        getTopOp := function(e)
            local i, c, stack, istack, star, nonstar, union, len, sub;

            if e = "" or Length(e) = 1 then
                return([]);
            else
                stack   := [];
                istack  := 0;
                star    := false;
                nonstar := false;
                union   := false;
                len     := Length(e);
                sub     := [];
                for i in [1 .. len] do
                    c := e[i];
                    if c = '(' then
                        if istack > 0 then
                            Add(sub, c);
                        fi;
                        istack := istack + 1;
                        stack[istack] := c;
                    elif c = ')' then
                        if istack > 0 then
                            istack := istack - 1;
                            if istack = 0 then
                                if i < len - 1 then
                                    nonstar := true;
                                fi;
                                Add(sub_exps, String(sub));
                                sub := [];
                            else
                                Add(sub, c);
                            fi;
                        else
                            Error("Mismatched parenthesis");
                        fi;
                    elif istack = 0 then
                        if c = 'U' then
                            union := true;
                            Add(union_inds, Length(sub_exps) + 1);
                        elif c = '*' then
                            star  := true;
                            Add(star_inds, Length(sub_exps) + 1);
                        else
                            Add(sub_exps, [c]);
                        fi;
                    else
                        Add(sub, c);
                    fi;
                od;
                if istack > 0 then
                    Error("Mismatched parenthesis");
                fi;
                if union then
                    return("union");
                elif star and ((e[1] = '(' and e[len-1] = ')' and nonstar = false) or (len = 2)) then
                    return("star");
                else
                    return("product");
                fi;
            fi;
        end;
##  End of getTopOp()  --

##  This function scans the array sub_exps and replaces
##  all the strings that are meant to be star expressions
##  by the corresponding star rational subexpression
        buildStar := function()
            local i;

            if not star_inds = [] then
                for i in star_inds do
                    if i-1 > 0 then
                        if IsString(sub_exps[i-1]) then
                            sub_exps[i-1] := RatExpOnnLetters(I, "star", RationalExp(sub_exps[i-1], I));
                        else
                            Error("Malformed expression");
                        fi;
                    else
                        Error("Malformed expression");
                    fi;
                od;
            fi;
        end;
##  End of buildStar()  --

##  When the "top" most operation is "union", this functions
##  groups the product subexpressions of the union, building
##  the list union_sub_exps to be used in
##  RatExpOnnLetters(I, "union", union_sub_exps
        buildProduct := function()
            local i, j, k, list, len;

            list := [];
            j    := 1;
            len  := Length(sub_exps);
            for i in union_inds do
                if i > len then
                    Error("Malformed expression");
                fi;
                for k in [j .. i-1] do
                    Add(list, sub_exps[k]);
                od;
                if Length(list) = 1 then
                    Add(union_sub_exps, list[1]);
                else
                    Add(union_sub_exps, RatExpOnnLetters(I, "product", list));
                fi;
                j := i;
                list := [];
            od;
            for k in [j .. len] do
                Add(list, sub_exps[k]);
            od;
            if Length(list) = 1 then
                Add(union_sub_exps, list[1]);
            else
                Add(union_sub_exps, RatExpOnnLetters(I, "product", list));
            fi;
        end;
##  End of buildProduct()  --

        op := getTopOp(S);
        buildStar();
        if op = "star" then
            return(sub_exps[1]);
        elif op = "product" then
            for i in [1 .. Length(sub_exps)] do
                if IsString(sub_exps[i]) then
                    sub_exps[i] := RationalExp(sub_exps[i], I);
                fi;
            od;
            return(RatExpOnnLetters(I, "product", sub_exps));
        else    # op = "union"
            for i in [1 .. Length(sub_exps)] do
                if IsString(sub_exps[i]) then
                    sub_exps[i] := RationalExp(sub_exps[i], I);
                fi;
            od;
            buildProduct();
            return(RatExpOnnLetters(I, "union", union_sub_exps));
        fi;
    end;

    return(RationalExp(S0, I0));
end);


#######################################################################
##
#M  String(R)
##
##  Outputs a rational expression as a string
##
InstallMethod( String,
"for rational expressions",
true,
[ IsRatExpOnnLettersObj and IsRationalExpressionRep ], 0,
function(r)
    local ret, string;
    ret := RatExpToString(r, 0);
    string := ret[1];
    Setter(SizeRatExp)(r,ret[2]);
    return(string);
end );

#############################################################################
##
#F  RandomRatExp(arg)
##
##  Given the number of symbols of the alphabet and (possibly) a factor m,
##  returns a pseudo random rational expression over that alphabet.
##
InstallGlobalFunction(RandomRatExp, function(arg)
    local aut, m, n;

    if Length(arg) = 0 then
        Error("Please specify the number of letters of the alphabet of the rational expression or a string");
    fi;
    n := arg[1];
    if not (IsPosInt( n ) or IsList(n)) then
        Error( "The argument must be a positive integer or a string" );
    fi;
    if IsString(n) then
        if '@' in n then
            Error("@ must not be in the alphabet");
        fi;
    fi;
    if IsBound(arg[2]) then
        m := arg[2];
    else
        m := 1;
    fi;
    aut := RandomAutomaton("det", (Runtime() mod (m*7)) + 2, n);
    return(FAtoRatExp(aut));
end);

#########################################################################
##
#M  SizeRatExp(r)
##
##  Computes the size of the rational expression r
##
##
InstallMethod(SizeRatExp,
        "size of RatExp",
        [IsRatExpOnnLettersObj],
        function( r )
    local   count,  e;

    if not IsRatExpOnnLettersObj(r) then
        Error("The argument to RatExpToString must be a rational expression");
    fi;
    count := 0;
    if r!.list_exp = "empty_set" then
        return(0);
    elif r!.op = [] then
        return(1);
    elif r!.op = "product" or r!.op = "union" then
        for e in r!.list_exp do
            count := count + SizeRatExp(e);
        od;
        return(count);
    else
        return(SizeRatExp(r!.list_exp));
    fi;
end);

#########################################################################
##
#M  AlphabetOfRatExp(r)
##
##  Returns the number of symbols in the alphabet of the rational expression r
##
##
InstallGlobalFunction(AlphabetOfRatExp, function( R )
    return(ShallowCopy(FamilyObj(R)!.alphabet));
end);


#############################################################################
##
#F  AlphabetOfRatExpAsList(R)
##
##  Returns the alphabet of the rational expression as a list.
##  If the alphabet of the rational expression is given by means of an integer
##  less than 27 it returns the list "abcd....",
##  otherwise returns [ "a1", "a2", "a3", "a4", ... ].
##
InstallGlobalFunction(AlphabetOfRatExpAsList, function( R )
    if not IsRatExpOnnLettersObj(R) then
        Error("The argument must be a rational expression");
    fi;
    return(ShallowCopy(FamilyObj(R)!.alphabet2));
end);

############################################################################
##
#F IsRationalExpression
##
InstallGlobalFunction(IsRationalExpression, function(R)
    return IsRatExpOnnLettersObj(R) and IsRationalExpressionRep(R);
end);
############################################################################
##
#M Methods for the comparison operations.
##
InstallMethod( \=,
    "for two rational sets",
    ReturnTrue,
        [ IsRatExpOnnLettersObj and IsRationalExpressionRep,
          IsRatExpOnnLettersObj and IsRationalExpressionRep,  ],
    0,
        function( L1, L2 )
    return( AlphabetOfRatExp(L1) = AlphabetOfRatExp(L2) and L1!.op = L2!.op and L1!.list_exp = L2!.list_exp );
end );

InstallMethod( \<,
        "for two rational expressions",
            #    IsIdenticalObj,
        true,
        [ IsRatExpOnnLettersObj and IsRationalExpressionRep,
          IsRatExpOnnLettersObj and IsRationalExpressionRep,  ],
        0,
        function( x, y )
    if x = y then
        return(false);
    elif x!.list_exp = "empty_set" then
        return(true);
    elif x!.op = [] and y!.op = [] and not y!.list_exp = "empty_set" then
        return(x!.list_exp < y!.list_exp);
    elif x!.op = [] and (y!.op = "star" or y!.op = "product" or y!.op = "union") then
        return(true);
    elif x!.op = "star" and (y!.op = "product" or y!.op = "union") then
        return(true);
    elif x!.op = "product" and y!.op = "union" then
        return(true);
    elif x!.op = y!.op then
        return(x!.list_exp < y!.list_exp);
    else
        return(false);
    fi;
end );

##
#E







