package terse.talk;

import java.util.HashMap;

import terse.talk.Cls.Meth;
import terse.talk.Pro.Blk;
import terse.talk.Talk.Terp.Frame;

public abstract class Expr extends Talk {

    abstract Pro eval(Frame f);
    /** Does it need parens around it, in toString()? */
    boolean isComplicated() {
        return false;
    }
    int depth() {
        return 0;
    }
    void pretty(int level, StringBuffer sb) {
        sb.append(this.toString());
    }
    public String toPrettyString() {
        StringBuffer sb = new StringBuffer();
        pretty(0, sb);
        return sb.toString();
    }

    static final class Top extends Expr {
        int numLocals; // Counts args. Does not count "self".
        int numArgs; // Does not count "self".
        HashMap<String, Integer> localVars; // Keys are lowercase.
        HashMap<String, String> localVarSpelling; // Values retain case.
        Cls cls;
        String methName;
        Expr body;

        public Top(int numVars, int numArgs, HashMap<String, Integer> localVars,
                HashMap<String, String> localVarSpelling, Cls cls, String methName, Expr body) {
            super();
            this.numLocals = numVars;
            this.numArgs = numArgs;
            this.localVars = localVars;
            this.localVarSpelling = localVarSpelling;
            this.cls = cls;
            this.methName = methName;
            this.body = body;
        }
        @Override
        Pro eval(Frame f) {
            return body.eval(f);
        }
        public String toString() {
            return fmt("%s", cls, methName, numLocals, numArgs, body);
        }
        void pretty(int level, StringBuffer sb) {
            body.pretty(1, sb);
        }
    }

    static final class PutInstVar extends Expr {
        String name;
        Expr expr;
        int index;

        public PutInstVar(String name, int index, Expr expr) {
            super();
            this.name = name;
            this.index = index;
            this.expr = expr;
        }
        Pro eval(Frame f) {
            Pro x = expr.eval(f);
            if (f.self.cls.trace) {
                say("PutInstVar: %s#%s \"i%d\" %s := %s#%s", f.self.cls.name, f.self.instId, index, name, x.cls.name, x
                        .toString());
            }
//            say("PutInstVar [%s.%s] self=<%s> len=%d name=%s i=%d <<< %s", f.top.cls, f.top.methName, f.self,
//                    f.self.instVars.length, name, index, x);
            f.self.instVars[index] = x;
            return x;
        }
        public String toString() {
            return fmt("%s= %s", name, expr);
        }
    }

    static final class GetInstVar extends Expr {
        String name;
        int index;

        public GetInstVar(String name, int index) {
            super();
            this.name = name;
            this.index = index;
        }
        Pro eval(Frame f) {
            Pro z = f.self.instVars[index];
//            say("GetInstVar [%s.%s] self=<%s> len=%d name=%s i=%d >>> %s", f.top.cls, f.top.methName, f.self,
//                    f.self.instVars.length, name, index, z);
            return z;
        }
        public String toString() {
            return fmt("%s", name);
        }
    }

    static final class GetLocalVar extends Expr {
        String name;
        int index;

        GetLocalVar(String key, int index) {
            this.name = key;
            this.index = index;
        }
        Pro eval(Frame f) {
            if (index >= f.locals.length) {
                toss("GetLocalVar OutOfBounds name=%s index=%s frame=%s", name, index, f);
            }
            return f.locals[index];
        }
        public String toString() {
            return fmt("%s", name);
        }
    }

    static final class GetSelf extends Expr {
        GetSelf() {
        }
        Pro eval(Frame f) {
            return f.self;
        }
        public String toString() {
            return fmt("self");
        }
    }

    static final class GetGlobalVar extends Expr {
        String key;

        GetGlobalVar(String key) {
            this.key = key;
        }
        Pro eval(Frame f) {
            Pro z = f.terp().clss.get(key);
            if (z == null) {
                toss("Global Variable <%s> was never set.", key);
            }
            return z;
        }
        public String toString() {
            return fmt("%s", key);
        }
    }

    static final class PutLocalVar extends Expr {
        String key;
        int index;
        Expr expr;

        PutLocalVar(String key, int index, Expr expr) {
            this.key = key;
            this.index = index;
            this.expr = expr;
        }
        Pro eval(Frame f) {
            Pro value = expr.eval(f);
            f.locals[index] = value;
            return value;
        }
        public String toString() {
            return fmt("%s= %s", key, expr);
        }
    }
    
    static final class Send extends Expr {
        Expr rcvr;
        String msg;
        Expr args[];

        Send(Expr rcvr, String msg, Expr args[]) {
            this.rcvr = rcvr;
            this.msg = msg;
            this.args = args;
        }

        Meth findMeth(Pro r) {
            for (Cls c = r.cls; c != null; c = c.supercls) {
                Meth m = c.meths.get(msg);
                if (m != null)
                    return m;
            }
            // If above fails, list the possibilities.
            for (Cls c = r.cls; c != null; c = c.supercls) {
                say("Visiting class <%s>", c.name);
                for (String k : c.meths.keySet()) {
                    say("Method <%s> exits on class <%s>", k, c.name);
                }
            }
            say("Couldn't find method <%s> on class <%s>", msg, r.cls.name);
            return null;
        }

        Meth findSuperMeth(Frame f) {
            Pro self = f.self;
            Top top = f.top;
            for (Cls c = top.cls.supercls; c != null; c = c.supercls) {
                Meth m = c.meths.get(msg);
                if (m != null)
                    return m;
            }
            say("Couldn't find super method <%s> on current class <%s>", msg, top.cls);
            return null;
        }

        @Override
        Pro eval(Frame f) {
            Pro r = rcvr.eval(f);
            assert r != null;
            Pro a[] = new Pro[args.length];
            for (int i = 0; i < args.length; i++) {
                a[i] = args[i].eval(f);
                assert a[i] != null;
            }
            Meth m;
            if (r.cls == r.cls.terp.tSuper) {
                m = findSuperMeth(f);
            } else {
                m = findMeth(r);
            }
            if (m == null) {
                toss("MessageNotUnderstood: message %s to instance of class %s: <%s>", msg, r.cls.name, r);
            }
            if (r.cls.trace || m.trace) {
                say("Sending message %s to %s#%s with %s", m.name, r.cls.name, r.toString(), arrayToString(args));
            }
            Pro z = null;
            try {
                z = m.apply(f, r, a);
            } catch (TalkException ex) {
                String what = fmt("\n  * During SEND <%s %s> TO %s WITH %s", r.cls.name, m.name, r.toString(),
                        arrayToString(a));
                toss(ex.toString() + what);
            }
            if (r.cls.trace || m.trace) {
                say("Message %s to %s#%s returns %s#%s", m.name, r.cls.name, r.toString(), z.cls.name, z.toString());
            }
            return z;
        }

        public String toString() {
            if (args.length == 0) {
                if (rcvr instanceof Block) {
                    // Special presentation of unary message to Block:
                    // Use MACRO syntax, with UpperCase message, and curly
                    // braces.
                    if (msg.toLowerCase().equals("r") || msg.toLowerCase().equals("run")) {
                        // Recogize RUN Block, and just print parens.
                        return fmt("(%s) ", ((Block) rcvr).gutsToString());
                    } else {
                        return fmt("%s{%s} ", msg.toUpperCase(), ((Block) rcvr).gutsToString());
                    }
                } else if (rcvr.isComplicated()) {
                    return fmt("(%s) %s ", rcvr, msg);
                } else {
                    return fmt("%s %s ", rcvr, msg);
                }
            } else {
                StringBuffer sb = new StringBuffer();
                if (rcvr.isComplicated()) {
                    sb.append(fmt("(%s) ", rcvr.toString()));
                } else {
                    sb.append(fmt("%s ", rcvr.toString()));
                }
                String[] words = msg.split(":");
                for (int i = 0; i < args.length; i++) {
                    if (args[i].isComplicated()) {
                        sb.append(fmt("%s/ (%s) ", words[i], args[i].toString()));
                    } else {
                        sb.append(fmt("%s/ %s ", words[i], args[i].toString()));
                    }
                }
                return sb.toString();
            }
        }
        boolean isComplicated() {
            return args.length > 0;
        }
    }

    static final class Block extends Expr {
        Expr body;
        String[] params;
        int[] paramsLocalIndex;

        public Block(Expr body, String[] params, int[] paramsLocalIndex) {
            this.body = body;
            this.params = params;
            this.paramsLocalIndex = paramsLocalIndex;
        }
        @Override
        Pro eval(Frame f) {
            return new Blk(this, f);
        }
        /** toString but without the brackets */
        String gutsToString() {
            String plist = "";
            for (String param : params) {
                plist += "/" + param + " ";
            }
            return plist + body.toString();
        }
        public String toString() {
            return "[ " + gutsToString() + "] ";
        }
        void pretty(int level, StringBuffer sb) {
            sb.append("[ ");
            for (String param : params) {
                sb.append("/" + param + " ");
            }
            sb.append("\n");
            body.pretty(level + 1, sb);
            sb.append("]");
        }
        int depth() {
            return 1 + body.depth();
        }
    }

    static final class Seq extends Expr {
        Expr[] body; // Eval these in order; return value of last.

        public Seq(Expr[] body) {
            this.body = body;
        }
        Pro eval(Frame f) {
            Pro z = f.terp().instNil;
            for (Expr expr : body) {
                z = expr.eval(f);
                assert z != null;
            }
            return z;
        }
        public String toString() {
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < body.length; i++) {
                sb.append(body[i].toString());
                sb.append(". ");
            }
            return sb.toString();
        }
        void pretty(int level, StringBuffer sb) {
            for (int i = 0; i < body.length; i++) {
                sb.append(repeat(level, "  "));
                sb.append(body[i].toString());
                sb.append(".\n");
            }
        }
        boolean isComplicated() {
            return true;
        }
        int depth() {
            int z = 0;
            for (int i = 0; i < body.length; i++) {
                z = Math.max(z, 1 + body[i].depth());
            }
            return z;
        }
    }

    static final class Lit extends Expr {
        Pro value; // Literal value.

        public Lit(Pro value) {
            this.value = value;
        }
        Pro eval(Frame f) {
            return value;
        }
        public String toString() {
            return value.toString();
        }
    }
}
