package danga.s2;

import java.util.LinkedList;
import java.util.ListIterator;

public class NodeTerm extends NodeExpr
{
    int type = 0;

    public final static int INTEGER = 1;
    TokenIntegerLiteral tokInt;

    public final static int STRING = 2;
    TokenStringLiteral tokStr;
    Node nodeString;
    String ctorclass;  // if not null, then we're not a string, but calling a class ctor with a string

    public final static int BOOL = 3;
    boolean boolValue;

    public final static int VARREF = 4;
    NodeVarRef var;

    public final static int SUBEXPR = 5;
    public final static int DEFINEDTEST = 6;
    public final static int SIZEFUNC = 7;
    public final static int REVERSEFUNC = 8;
    public final static int ISNULLFUNC = 12;
    NodeExpr subExpr;
    Type subType;      // for backend, set by getType()

    public final static int NEW = 9;
    public final static int NEWNULL = 13; // Like NEW, but sets object to be null
    TokenIdent newClass;

    public final static int FUNCCALL = 10;
    public final static int METHCALL = 11;
    int derefLine;      // keep track of where we saw the deref token
    TokenIdent funcIdent;
    String funcClass;      // null or classname of the method call
    NodeArguments funcArgs;
    boolean funcBuiltin;   // is this function call a builtin?
                           // (if so, don't use vtable)
    boolean parentMethod;  // is this a method call on a super class?  if so,
                           // can't optimize method call since instance class won't
                           // necessarily be known until run-time (without a lot of analysis)

    String funcID;         // used by backend; set after getType()
    String funcID_noclass;
    int funcNum;    // used by perl backend for function vtables

    public final static int ARRAY = 14;

    public static boolean canStart (Tokenizer toker) throws Exception
    {
	Token t = toker.peek();
	if (t instanceof TokenIntegerLiteral ||
	    t instanceof TokenStringLiteral ||
	    t instanceof TokenIdent ||
	    t.equals(TokenPunct.LPAREN) ||
	    t.equals(TokenPunct.DOLLAR) ||
	    t.equals(TokenKeyword.DEFINED) ||
	    t.equals(TokenKeyword.TRUE) ||
	    t.equals(TokenKeyword.FALSE) ||
	    t.equals(TokenKeyword.NEW) ||
	    t.equals(TokenKeyword.SIZE) ||
	    t.equals(TokenKeyword.REVERSE) ||
	    t.equals(TokenKeyword.ISNULL) ||
	    t.equals(TokenKeyword.NULL) ||
	    t.equals(TokenPunct.LBRACK) ||
	    t.equals(TokenPunct.LBRACE)
	    )
	    return true;
	return false;
    }

    public Type getType (Checker ck) throws Exception
    {
        return getType(ck, null);
    }

    public Type getType (Checker ck, Type wanted) throws Exception
    {
	if (type == INTEGER) return Type.INT;
	if (type == STRING) {
            if (nodeString != null) {
                return nodeString.getType(ck, Type.STRING);
            }
            if (ck.isStringCtor(wanted)) {
                ctorclass = wanted.baseType();
                return wanted;
            }
            return Type.STRING;
        }
	if (type == SUBEXPR) return subExpr.getType(ck);
	if (type == BOOL) return Type.BOOL;

	if (type == DEFINEDTEST) {
	    System.err.println("FIXME: check type of defined expression");
	    return Type.BOOL;
	}
	if (type == SIZEFUNC) {
	    subType = subExpr.getType(ck);
	    if (subType.equals(Type.STRING))
		return Type.INT;
	    if (subType.isArrayOf())
		return Type.INT;
	    // complain
	    throw new Exception("Can't use size on expression that's "+
				"not a string or array at "+getFilePos());

	}
	if (type == REVERSEFUNC) {
	    subType = subExpr.getType(ck);

	    // reverse a string
	    if (subType.equals(Type.STRING))
		return Type.STRING;

	    // reverse an array
	    if (subType.isArrayOf())
		return subType;

	    // complain
	    throw new Exception("Can't use reverse on expression that's "+
				"not a string or array at "+getFilePos());
	}

        if (type == ISNULLFUNC) {
            subType = subExpr.getType(ck);
	    if (subExpr instanceof NodeTerm) {
                NodeTerm nt = (NodeTerm) subExpr;
                if (nt.type != VARREF && nt.type != FUNCCALL && nt.type != METHCALL)
                    throw new Exception("isnull must only be used on an object variable, "+
                                        "function call or method call at "+getFilePos());
	    } else {
                throw new Exception("isnull must only be used on an object variable, "+
                                    "function call or method call at "+getFilePos());
	    }

            // can't be used on arrays and hashes
            if (subType.isArrayOf() || subType.isHashOf())
                throw new Exception("Can't use isnull on an array or hash at "+getFilePos());

            // not primitive types either
            if (subType.equals(Type.BOOL) || subType.equals(Type.STRING) || subType.equals(Type.INT))
                throw new Exception("Can't use isnull on primitive types at "+getFilePos());

            // nor void
            if (subType.equals(Type.VOID))
                throw new Exception("Can't use isnull on a void value at "+getFilePos());

            return Type.BOOL;
        }

	if (type == NEW || type == NEWNULL) {
	    String clas = newClass.getIdent();
	    NodeClass nc = ck.getClass(clas);
	    if (nc == null) {
		throw new Exception("Can't instantiate unknown class at "+
				    getFilePos());
	    }
	    return new Type(clas);
	}

	if (type == VARREF) {
            if (! ck.getInFunction()) {
                throw new Exception("Can't reference a variable outside of a function at "+getFilePos());
            }
	    return var.getType(ck, wanted);
	}

	if (type == METHCALL || type == FUNCCALL) {
            if (! ck.getInFunction()) {
                throw new Exception("Can't call a function or method outside of a function at "+getFilePos());
            }

	    // find the classname of the variable the method was being called on
	    if (type == METHCALL) {
		Type vartype = var.getType(ck);
		if (! vartype.isSimple()) {
		    throw new Exception("Cannot call a method on an array or hash of "+
					"objects at "+getFilePos());
		}
		funcClass = vartype.toString();

		NodeClass methClass = ck.getClass(funcClass);
		if (methClass == null) {
		    throw new Exception("Can't call a method on an instance of an "+
					"undefined class at "+getFilePos());
		}

		if (ck.hasDerClasses(funcClass))
		    parentMethod = true;
	    }

	    funcID = Checker.functionID(funcClass, funcIdent.getIdent(),
					funcArgs.typeList(ck));
	    funcBuiltin = ck.isFuncBuiltin(funcID);

	    // and remember the funcID without a class for use later when
	    // we have to generate a funcID at run-time by concatenating
	    // this to the end of a $instance->{'_type'}
	    funcID_noclass = Checker.functionID(null, funcIdent.getIdent(),
						funcArgs.typeList(ck));

	    Type t = ck.functionType(funcID);
	    if (! funcBuiltin)
		funcNum = ck.functionNum(funcID);
	    if (t == null) {
	      throw new Exception("Unknown function "+funcID+" at "+
				  funcIdent.getFilePos());
	    }
	    return t;
	}

        if (type == ARRAY) {
            return subExpr.getType(ck, wanted);
        }

	throw new Exception("ERROR: unknown NodeTerm type at "+getFilePos());
    }

    public boolean isLValue ()
    {
	if (type == VARREF) return true;
	if (type == SUBEXPR) {
	    return subExpr.isLValue();
	}
        return false;
    }

    public boolean makeAsString(Checker ck)
    {
	if (type == VARREF) {
	    try {
		Type t = var.getType(ck);
		if (t.isSimple()) {
		    String bt = t.baseType();

		    // class has .toString() method
		    if (ck.classHasToString(bt)) {
			// let's change this VARREF into a METHCALL!
			// warning: ugly hacks ahead...
			type = METHCALL;
			funcIdent = new TokenIdent("toString");
			funcClass = bt;
			funcArgs = (NodeArguments)
			    NodeArguments.makeEmptyArgs();
			funcID_noclass = "toString()";
			funcID = bt + "::" + funcID_noclass;
			funcBuiltin = ck.isFuncBuiltin(funcID);
			funcNum = ck.functionNum(funcID);
			if (ck.hasDerClasses(funcClass))
			    parentMethod = true;

			return true;
		    }

		    // class has $.as_string string member
		    if (ck.classHasAsString(bt)) {
			var.useAsString();
			return true;
		    }
		}
	    } catch (Exception e) {
		return false;
	    }
	}
	return false;
    }

    public static Node parse (Tokenizer toker) throws Exception
    {
	NodeTerm nt = new NodeTerm();

	Token t = toker.peek();

	// integer literal
	if (t instanceof TokenIntegerLiteral) {
	    nt.type = NodeTerm.INTEGER;
	    nt.tokInt = (TokenIntegerLiteral) nt.eatToken(toker);
	    return nt;
	}

	// boolean literal
	if (t.equals(TokenKeyword.TRUE) || t.equals(TokenKeyword.FALSE)) {
	    nt.type = NodeTerm.BOOL;
	    nt.boolValue = t.equals(TokenKeyword.TRUE);
	    nt.eatToken(toker);
	    return nt;
	}

	// string literal
	if (t instanceof TokenStringLiteral) {
	    TokenStringLiteral ts = (TokenStringLiteral) t;
	    int ql = ts.getQuotesLeft();
	    int qr = ts.getQuotesRight();

	    if (qr != 0) {
		// whole string literal
		nt.type = NodeTerm.STRING;
		nt.tokStr = (TokenStringLiteral) nt.eatToken(toker);
                nt.setStart(nt.tokStr);
		return nt;
	    }

            // interpolated string literal (turn into a subexpr)
            LinkedList toklist = new LinkedList();

            nt.type = NodeTerm.STRING;
            nt.tokStr = (TokenStringLiteral) nt.eatToken(toker);
            toklist.add(nt.tokStr.clone());  // cloned before it's changed.
            nt.tokStr.setQuotesRight(ql);
            Node lhs = nt;
            FilePos filepos = (FilePos) nt.tokStr.getFilePos();

            boolean loop = true;
            while (loop) {
                Node rhs = null;
                Token tok = toker.peek();
                if (tok instanceof TokenStringLiteral) {
                    rhs = new NodeTerm();
                    NodeTerm rhsnt = (NodeTerm) rhs;
                    ts = (TokenStringLiteral) tok;
                    rhsnt.type = NodeTerm.STRING;
                    rhsnt.tokStr = (TokenStringLiteral) rhsnt.eatToken(toker);
                    toklist.add(rhsnt.tokStr.clone());  // cloned before it's changed.

                    if (ts.getQuotesRight() == ql) {
                        loop = false;
                    }
                    ts.setQuotesRight(ql);
                    ts.setQuotesLeft(ql);
                }
                else if (tok.equals(TokenPunct.DOLLAR)) {
                    rhs = NodeTerm.parse(toker);
                    toklist.add(rhs);
                }
                else {
                    throw new Exception("Error parsing "+
                                        "interpolated string.");
                }

                // don't make a sum out of a blank string on either side
                boolean join = true;
                if (lhs instanceof NodeTerm) {
                    NodeTerm lhst = (NodeTerm) lhs;
                    if (lhst.type == STRING &&
                        lhst.tokStr.getString().length() == 0) {
                        lhs = rhs;
                        join = false;
                    }
                }
                if (rhs instanceof NodeTerm) {
                    NodeTerm rhst = (NodeTerm) rhs;
                    if (rhst.type == STRING &&
                        rhst.tokStr.getString().length() == 0) {
                        join = false;
                    }
                }
                if (join) {
                    lhs = new NodeSum(lhs, TokenPunct.PLUS, rhs);
                }
            }
            
            lhs.setTokenList(toklist);
            lhs.setStart(filepos);

            NodeTerm rnt = new NodeTerm();
            rnt.type = NodeTerm.STRING;
            rnt.nodeString = lhs;
            rnt.addNode(lhs);
            return rnt;
	}

	// Sub-expression (in parenthesis)
	if (t.equals(TokenPunct.LPAREN)) {
	    nt.type = NodeTerm.SUBEXPR;
	    nt.setStart(nt.eatToken(toker));

	    nt.subExpr = (NodeExpr) NodeExpr.parse(toker);
	    nt.addNode(nt.subExpr);

	    nt.requireToken(toker, TokenPunct.RPAREN);
	    return nt;
	}

	// defined test
	if (t.equals(TokenKeyword.DEFINED)) {
	    nt.type = NodeTerm.DEFINEDTEST;
	    nt.eatToken(toker);
	    nt.subExpr = (NodeTerm) NodeTerm.parse(toker);
	    nt.addNode(nt.subExpr);
	    return nt;
	}

	// reverse function
	if (t.equals(TokenKeyword.REVERSE)) {
	    nt.type = NodeTerm.REVERSEFUNC;
	    nt.eatToken(toker);
	    nt.subExpr = (NodeTerm) NodeTerm.parse(toker);
	    nt.addNode(nt.subExpr);
	    return nt;
	}

	// size function
        if (t.equals(TokenKeyword.SIZE)) {
            nt.type = NodeTerm.SIZEFUNC;
            nt.eatToken(toker);
            nt.subExpr = (NodeTerm) NodeTerm.parse(toker);
            nt.addNode(nt.subExpr);
            return nt;
        }

        // isnull function
        if (t.equals(TokenKeyword.ISNULL)) {
            nt.type = NodeTerm.ISNULLFUNC;
            nt.eatToken(toker);
            nt.subExpr = (NodeTerm) NodeTerm.parse(toker);
            nt.addNode(nt.subExpr);
            return nt;
        }

	// new and null
	if (t.equals(TokenKeyword.NEW) || t.equals(TokenKeyword.NULL)) {
	    nt.type = (t.equals(TokenKeyword.NEW) ? NodeTerm.NEW : NodeTerm.NEWNULL);
	    nt.eatToken(toker);
	    nt.newClass = nt.getIdent(toker);
	    return nt;
	}

	// VarRef
	if (t.equals(TokenPunct.DOLLAR)) {
	    nt.type = VARREF;
	    nt.var = (NodeVarRef) NodeVarRef.parse(toker);
	    nt.addNode(nt.var);

	    // check for -> after, like: $object->method(arg1, arg2, ...)
	    if (toker.peek().equals(TokenPunct.DEREF)) {
                nt.derefLine = toker.peek().getFilePos().line;

		nt.eatToken(toker);
		nt.type = METHCALL;
		// don't return... parsing continues below.
	    } else {
		return nt;
	    }
	}

	// function/method call
	if (t instanceof TokenIdent || nt.type == METHCALL) {
	    if (nt.type != METHCALL) nt.type = FUNCCALL;

	    nt.funcIdent = nt.getIdent(toker);
	    nt.funcArgs = (NodeArguments) NodeArguments.parse(toker);
	    nt.addNode(nt.funcArgs);
	    return nt;
	}

        // array/hash literal
        if (NodeArrayLiteral.canStart(toker)) {
            nt.type = ARRAY;
            nt.subExpr = (NodeExpr) NodeArrayLiteral.parse(toker);
            nt.addNode(nt.subExpr);
            return nt;
        }

	throw new Exception("Can't finish parsing NodeTerm at " +
			    toker.locationString() +
			    ", toker.peek() = " + t.toString());
    }

    public void asS2 (Indenter o)
    {
	if (type == INTEGER) {
	    tokInt.asS2(o);
	    return;
	}
	if (type == STRING) {
            if (nodeString != null) {
                nodeString.asS2(o);
                return;
            }
	    tokStr.asS2(o);
	    return;
	}
	if (type == BOOL) {
	    if (boolValue)
		o.write("true");
	    else
		o.write("false");
	    return;
	}
	if (type == SUBEXPR) {
	    o.write("(");
	    subExpr.asS2(o);
	    o.write(")");
	    return;
	}
	if (type == NEW) {
	    o.write("new ");
	    o.write(newClass.getIdent());
	    return;
	}
        if (type == NEWNULL) {
            o.write("null ");
            o.write(newClass.getIdent());
            return;
        }
	if (type == DEFINEDTEST) {
	    o.write("defined ");
	    subExpr.asS2(o);
	    return;
	}
	if (type == SIZEFUNC) {
	    o.write("size ");
	    subExpr.asS2(o);
	    return;
	}
        if (type == REVERSEFUNC) {
            o.write("reverse ");
            subExpr.asS2(o);
            return;
        }
        if (type == ISNULLFUNC) {
            o.write("isnull ");
            subExpr.asS2(o);
            return;
        }
	if (type == VARREF || type == METHCALL) {
	    var.asS2(o);
	}
	if (type == METHCALL) {
	    o.write("->");
	}
	if (type == METHCALL || type == FUNCCALL) {
	    o.write(funcIdent.getIdent());
	    funcArgs.asS2(o);
	}
	if (type == VARREF || type == METHCALL || type == FUNCCALL)
	    return;
        if (type == ARRAY) {
            subExpr.asS2(o);
            return;
        }
    }

    public void asPerl (BackendPerl bp, Indenter o)
    {
	if (type == INTEGER) {
	    tokInt.asPerl(bp, o);
	    return;
	}
	if (type == STRING) {
            if (nodeString != null) {
                o.write("(");
                nodeString.asPerl(bp, o);
                o.write(")");
                return;
            }
            if (ctorclass != null)
                o.write("S2::Builtin::"+ctorclass+"__"+ctorclass+"(");
	    tokStr.asPerl(bp, o);
            if (ctorclass != null)
                o.write(")");
	    return;
	}
	if (type == BOOL) {
	    if (boolValue)
		o.write("1");
	    else
		o.write("0");
	    return;
	}
	if (type == SUBEXPR) {
	    o.write("(");
	    subExpr.asPerl(bp, o);
	    o.write(")");
	    return;
	}
        if (type == ARRAY) {
            subExpr.asPerl(bp, o);
            return;
        }
	if (type == NEW) {
	    o.write("{'_type'=>" +
		    bp.quoteString(newClass.getIdent())+
		    "}");
	    return;
	}
	if (type == NEWNULL) {
	    o.write("{'_type'=>" +
		    bp.quoteString(newClass.getIdent())+
		    ", '_isnull'=>1}");
	    return;
	}
	if (type == DEFINEDTEST) {
	    o.write("defined(");
	    subExpr.asPerl(bp, o);
	    o.write(")");
	    return;
	}
	if (type == REVERSEFUNC) {
	    if (subType.isArrayOf()) {
		o.write("[reverse(");
		o.write("@{");
		subExpr.asPerl(bp, o);
		o.write("})");
		o.write("]");
	    } else if (subType.equals(Type.STRING)) {
		o.write("reverse(");
		subExpr.asPerl(bp, o);
		o.write(")");
	    }
	    return;
	}
	if (type == SIZEFUNC) {
	    if (subType.equals(Type.STRING)) {
		o.write("length(");
		subExpr.asPerl(bp, o);
		o.write(")");
	    }
	    else if (subType.isArrayOf()) {
		o.write("scalar(@{");
		subExpr.asPerl(bp, o);
		o.write("})");
	    }
	    return;
	}
	if (type == ISNULLFUNC) {
	    o.write("(ref ");
	    subExpr.asPerl(bp, o);
            o.write(" ne \"HASH\" || ");
	    subExpr.asPerl(bp, o);
	    o.write("->{'_isnull'})");
	    return;
	}
	if (type == VARREF) {
	    var.asPerl(bp, o);
	    return;
	}

	if (type == FUNCCALL || type == METHCALL) {

	    boolean funcDumped = false;

	    // builtin functions can be optimized.
	    if (funcBuiltin) {
		// these built-in functions can be inlined.
		if (funcID.equals("string(int)")) {
		    funcArgs.asPerl(bp, o, false);
		    return;
		}
		if (funcID.equals("int(string)")) {
		    // cast from string to int by adding zero to it
		    o.write("(0+");
		    funcArgs.asPerl(bp, o, false);
		    o.write(")");
		    return;
		}

		// otherwise, call the builtin function (avoid a layer
		// of indirection), unless it's for a class that has
		// children (won't know until run-time which class to call)
		if(funcClass == null || (funcClass != null && ! parentMethod)) {
		    o.write("S2::Builtin::");
		    if (funcClass != null) {
			o.write(funcClass + "__");
		    }
		    o.write(funcIdent.getIdent());
		    funcDumped = true;
		}
	    }

	    if (funcDumped == false) {
		if (type == METHCALL && ! funcClass.equals("string")) {
		    o.write("$_ctx->[VTABLE]->{get_object_func_num(");
		    o.write(bp.quoteString(funcClass));
		    o.write(",");
                    var.asPerl(bp, o);
		    o.write(",");
		    o.write(bp.quoteString(funcID_noclass));
		    o.write(",");
                    o.write(bp.getLayerID());
		    o.write(",");
                    o.write(derefLine);
                    if (var.isSuper()) {
                        o.write(",1");
                    }
		    o.write(")}->");
		} else if (type == METHCALL) {
                    o.write("$_ctx->[VTABLE]->{get_func_num(");
                    o.write(bp.quoteString(funcID));
                    o.write(")}->");
		} else {
		    o.write("$_ctx->[VTABLE]->{$_l2g_func["+funcNum+"]}->");
		}
	    }

	    o.write("($_ctx, ");

	    // this pointer
	    if (type == METHCALL) {
		var.asPerl(bp, o);
		o.write(", ");
	    }

	    funcArgs.asPerl(bp, o, false);

	    o.write(")");
	    return;
	}
    }


}
