package terse.talk;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import terse.talk.Expr.Block;

public class Parser extends Talk {
    Cls cls;
    Terp terp;
    TLex lex;
    HashMap<String, Integer> localVars; // maps to Local Var index.
    HashMap<String, String> localVarSpelling;
    int temp = 0;

    static Pattern doubleSingeQuote = Pattern.compile("''");

    // This is the public function to parse a string.
    public static Expr.Top parseMethod(Cls cls, String methName, String code) {
        Parser p = new Parser(cls, methName, code);
        int numArgs = 0;
        for (int i = 0; i < methName.length(); i++) {
            if (methName.charAt(i) == ':') {
                String paramName = Character.toString((char) ((int) 'a' + numArgs));
                p.localVars.put(paramName, numArgs);
                p.localVarSpelling.put(paramName, paramName);
                ++numArgs;
            }
        }
        p.parseExpr(); // Once to learn names of variables.
        if (p.lex.w.trim().length() > 0) {
            toss("Parser:  Leftover word after parsing: <%s>", p.lex.w.trim());
        }
        if (p.lex.sub.trim().length() > 0) {
            toss("Parser:  Leftover junk after parsing: <%s>", p.lex.sub.trim());
        }
        p.lex = new TLex(code); // Ugly.
        Expr expr = p.parseExpr(); // Again for code generation.
        Expr.Top top = new Expr.Top(p.localVars.size(), numArgs, p.localVars, p.localVarSpelling, cls, methName, expr);
//        say("parseMethod <<< %s.%s <<< %s", cls, methName, code);
//        say("parseMethod >>> %s", top);
        return top;
    }

    private Parser(Cls cls, String methName, String code) {
        this.cls = cls;
        this.terp = cls.terp;
        this.lex = new TLex(code);
        this.localVars = new HashMap<String, Integer>();
        this.localVarSpelling = new HashMap<String, String>();
    }

    private static String replace(Pattern p, String replacement, String a) {
        Matcher m = p.matcher(a);
        StringBuffer sb = new StringBuffer();
        while (m.find()) {
            m.appendReplacement(sb, replacement);
        }
        m.appendTail(sb);
        return sb.toString();
    }
    private static String reduceDoubleSingleQuotes(String a) {
        return replace(doubleSingeQuote, "'", a);
    }
    private void skipStmtEnders() { // i.e. dots & semicolons
        while (lex.endsStmt()) {
            lex.advance();
        }
    }

    private Expr parseExpr() {
        Expr[] v = emptyExprs;
        skipStmtEnders();
        while (lex.t != null) {
            if (lex.closesBlock() || lex.closesParen())
                break;

            Expr s = parseStmt();
            v = append(v, s);
            skipStmtEnders();
        }
        return new Expr.Seq(v);
    }

    private Expr parseStmt() {
        if (lex.isAssignish()) {
            // We just peeked with a raw Pattern Matcher.
            // We did not consume any actual tokens.
            boolean isDestructuring = false;
            assert lex.t == Pat.NAME;
            String[] varNames = strs(lex.w);
            lex.advance();

            while (lex.isTupler()) {
                isDestructuring = true;
                lex.advance();

                if (lex.isAssign()) {
                    // Trailing ',' before '=' is OK, especially for the
                    // singleton case.
                    break;
                } else if (lex.t == Pat.NAME) {
                    varNames = append(varNames, lex.w);
                    lex.advance();
                } else {
                    toss("Was expecting a Name or '@' or '=', but got %s <%s>", lex.t, lex.w);
                }
            }
            assert lex.isAssign();
            lex.advance();
            Expr chain = parseTuple();

            int[] indices = emptyInts;
            for (int i = 0; i < varNames.length; i++) {
                String varName = varNames[i];
                String varKey = varName.toLowerCase();
                indices = append(indices, grokAssignedVariable(varName, varKey));
//                if (index < 0) {
//                    putters = append(putters, new Expr.PutInstVar(varName, cls.allVars.get(varKey), chain));
//                } else {
//                    putters = append(putters, new Expr.PutLocalVar(varName, index, chain));
//                }
            }
            
            if (isDestructuring) {
                ++temp;
                String tempVar = fmt("temporaryzzz%dzzz", temp);
                int tempIndex = grokAssignedVariable(tempVar, tempVar);
                Expr getTemp = new Expr.GetLocalVar(tempVar, tempIndex);
                
                // A block with one param, the temp Var.
                Expr.Seq seq = new Expr.Seq(emptyExprs);
                Expr.Block block = new Block(seq, strs(tempVar), ints(tempIndex));
                
                for (int i = 0; i < varNames.length; i++) {
                    int index = indices[i];
                    Expr gotAt = new Expr.Send(getTemp, "at:", exprs(new Expr.Lit(terp.newNum(i))));
                    if (index < 0) {
                        seq.body = append(seq.body, new Expr.PutInstVar(varNames[i], cls.allVars.get(varNames[i].toLowerCase()), gotAt));
                    } else {
                        seq.body = append(seq.body, new Expr.PutLocalVar(varNames[i], index, gotAt));
                    }
                }
                return new Expr.Send(block, "run:", exprs(chain));
                
            } else {
                assert indices.length == 1;
                int index = indices[0];
                if (index < 0) {
                    return new Expr.PutInstVar(varNames[0], cls.allVars.get(varNames[0].toLowerCase()), chain);
                } else {
                    return new Expr.PutLocalVar(varNames[0], index, chain);
                }
            }
        } else {
            return parseTuple();
        }
    }
    // When a variable is assigned, it must be either Inst or Local.
    // If it is in the class's allVars, then it is Inst.
    // Otherwise if we don't have it as a local yet, add it.
    // Returns -1 if inst var, or Local Var index if local.
    private int grokAssignedVariable(String varName, String varKey) {
        // Determine Inst or Local. Globals are never assigned.
        if (cls.allVars.containsKey(varKey)) {
            return -1;
        }
        int index;
        if (localVars.containsKey(varKey)) {
            index = localVars.get(varKey);
        } else {
            // Does not exist yet, so make it.
            index = localVars.size();
            localVars.put(varKey, index);
            localVarSpelling.put(varKey, varName);
        }
        return index;
    }
    private Expr parseTuple() {
        boolean itsATuple = false;
        Expr[] v = exprs(parseChain());
        while (lex.isTupler()) {
            itsATuple = true;
            lex.advance();
            if (lex.endsStmt() || lex.closesBlock() || lex.closesParen()) {
                break;
            }
            v = append(v, parseChain());
        }
        if (itsATuple) {
            // Use "[ ... ]vec" to construct the Tuple.
            return new Expr.Send(new Expr.Block(new Expr.Seq(v), emptyStrs, emptyInts), "vec", emptyExprs);
        } else {
            // It's not a tuple at all.
            return v[0];
        }
    }
    private Expr parseChain() {
        // Normal SmallTalk "cascades" using ";", but we don't.
        // Instead we "chain" with "--".
        // Whereas cascade sends subsequent messages to the same receiver,
        // this chaining sends subsequent messages to the previous result.
        Expr unary = parseUnary();
        Expr expr = parseKeyed(unary, 999);
        while (lex.isChain()) {
            lex.advance();
            while (lex.t == Pat.NAME) { // parse Unary messages at front of
                // chain.
                expr = new Expr.Send(expr, lex.w.toLowerCase(), emptyExprs);
                lex.advance();
            }
            if (lex.t == Pat.KEYWORD) {
                expr = parseKeyed(expr, 999);
            }
        }
        return expr;
    }
    private Expr parseKeyed(Expr receiver, int cap) {
        if (lex.t == Pat.KEYWORD && lex.keywordStrength() < cap) {
            while (lex.t == Pat.KEYWORD && lex.keywordStrength() < cap) {
                String keywords = "";
                Expr[] args = emptyExprs;
                int strength = lex.keywordStrength();
                while (lex.t == Pat.KEYWORD && lex.keywordStrength() == strength) {
                    keywords += lex.keywordName().toLowerCase() + ":";
                    lex.advance();
                    Expr unary = parseUnary();
                    Expr arg = parseKeyed(unary, strength); // Do keywords of
                    // smaller strength.
                    args = append(args, arg);
                }
                receiver = new Expr.Send(receiver, keywords, args);
            }
            return receiver;
        } else {
            return receiver; // No keyword with strength less than cap.
        }
    }
    private Expr parseUnary() {
        Expr expr = parsePrim();
        while (lex.t == Pat.NAME) {
            expr = new Expr.Send(expr, lex.w.toLowerCase(), emptyExprs);
            lex.advance();
        }
        return expr;
    }
    private Expr parsePrim() {
        Expr z = null;
        switch (lex.t) {
        case RESERVED:
            String lower = lex.w.toLowerCase();
            if (lower.equals("nil"))
                z = new Expr.Lit(terp.instNil);
            else if (lower.equals("se"))
                z = new Expr.GetSelf();
            else if (lower.equals("self"))
                z = new Expr.GetSelf();
            else if (lower.equals("sup"))
                z = new Expr.Lit(terp.instSuper);
            else if (lower.equals("super"))
                z = new Expr.Lit(terp.instSuper);
            break;
        case NUMBER:
            z = new Expr.Lit(new Pro.Num(terp, Double.parseDouble(lex.w)));
            break;
        case STRING:
            // For old regexp that included quotes:
            // z = new Expr.Lit(new Pro.Str(terp,
            // reduceDoubleSingleQuotes(lex.w.substring(1, lex.w.length() -
            // 1))));
            // For new one that leaves them out:
            z = new Expr.Lit(new Pro.Str(terp, lex.w));
            break;
        case OTHER:
            z = lex.opensBlock() ? parseBlock() : lex.opensParen() ? parseParen() : null;
            break;
        case NAME:
            z = lex.isMacro() ? parseMacro() : getVar(lex.w, lex.w.toLowerCase());
            break;
        case KEYWORD:
            toss("Misplaced keyword, was expecting a variable or literal or block: <%s>", lex.w);
            break;
        }
        if (z == null) {
            toss("Syntax Error in parsePrim: <%s>", lex.w);
        }
        // NOTA BENE: All of the above cases do not advance. We do it here.
        lex.advance();
        return z;
    }

    private Expr getVar(String varName, String varKey) {
        // TODO: factor this out.
        // Determine Inst or Local. Globals are never assigned.
        if (cls.allVars.containsKey(varKey)) {
            return new Expr.GetInstVar(varName, cls.allVars.get(varKey));
        }
        int index;
        if (localVars.containsKey(varKey)) {
            index = localVars.get(varKey);
            return new Expr.GetLocalVar(varName, index);
        } else {
            return new Expr.GetGlobalVar(varKey);
        }
    }
    private Expr parseBlock() {
        assert lex.opensBlock();
        lex.advance();
        String[] params = emptyStrs;
        int[] paramsLocalIndex = emptyInts;
        while (lex.marksBlockParam()) {
            String marker = lex.w; // Just for error message.
            lex.advance(); // Beyond the marker.
            if (lex.t != Pat.NAME) {
                toss("Expected a variable name for Block Parameter after <%s> but got <%s>", marker, lex.w);
            }
            params = append(params, lex.w); // Keep case, since this is
            Integer localIndex = localVars.get(lex.w.toLowerCase());
            // On first pass, we don't know locals, so say -1, to make sure it
            // is never eval'ed.
            int index = (localIndex == null) ? -1 : localIndex.intValue();
            paramsLocalIndex = append(paramsLocalIndex, index);
            // definition point.
            // Params count as Assignments, from the point of view of
            // allocating local variables. Forbid using inst var for param.
            int negativeIfInstanceVar = grokAssignedVariable(lex.w, lex.w.toLowerCase());
            if (negativeIfInstanceVar < 0) {
                toss("Instance variable <%s> not allowed as param in Blk.", lex.w);
            }

            lex.advance(); // Beyond the var name.
        }
        Expr body = parseExpr();
//        say("@@@@@@@ body=<%s>", body);
        assert lex.closesBlock() : fmt("lex=%s<%s> body=<%s>", lex.t, lex.w, body);
        // Do not advance; parsePrim() does that after switch().
        return new Expr.Block(body, params, paramsLocalIndex);
    }
    private Expr parseParen() {
        assert lex.opensParen();
        lex.advance();
        if (lex.closesParen()) {
            // The unit tuple (). It's an empty but mutable vector, not a
            // literal.
            return new Expr.Send(new Expr.Block(new Expr.Seq(emptyExprs), emptyStrs, emptyInts), "vec", emptyExprs);
        }
        Expr body = parseExpr();
        assert lex.closesParen();
        // Do not advance; parsePrim() does that after switch().
        return new Expr.Send(new Expr.Block(body, emptyStrs, emptyInts), "run", emptyExprs);
    }
    private Expr parseMacro() {
        ArrayList<String> names = new ArrayList<String>();
        ArrayList<Expr.Block> blocks = new ArrayList<Expr.Block>();
        while (lex.t == Pat.NAME && lex.isMacro()) {
            names.add(lex.w);
            lex.advance();
            Expr.Block b = (Expr.Block) parseBlock();
            blocks.add(b);
        }
        final int sz = names.size();
        if (sz == 1) {
            String name = names.get(0);
            return new Expr.Send(blocks.get(0), name.toLowerCase(), emptyExprs);
        } else {
            String name = names.get(0) + names.get(1);
            name += ":";
            for (int i = 2; i < sz; i++) {
                String nextName = names.get(0);
                name += nextName + ":";
            }
            Expr[] args = new Expr[sz - 1];
            for (int i = 1; i < sz; i++)
                args[i - 1] = blocks.get(i); // Leave off the first.
            return new Expr.Send(blocks.get(0), name.toLowerCase(), args);
        }
    }

    enum Pat {
        WHITE("\\s\\s*"), COMMENT("\"[^\"]*\""), STRING("'[^']*'"), NUMBER(
                "-?[0-9][0-9]*(\\.[0-9][0-9]*)?([Ee][+-]?[0-9][0-9]*)?"), NAME("[A-Za-z][A-Za-z0-9]*"), KEYWORD(
                "([A-Za-z][A-Za-z0-9]*)\\s*([:/]+)"), OTHER("--|."), RESERVED("(self|se|super|sup|nil)\\b",
                Pattern.CASE_INSENSITIVE);

        Pattern p;

        Pat(String s) {
            this.p = Pattern.compile(s, 0);
        }
        Pat(String s, int flags) {
            this.p = Pattern.compile(s, flags);
        }

        Matcher matcher(String x) {
            // say("Attempting %s pattern %s ON %s", this, p, x);
            return p.matcher(x);
        }
    }

    static final class TLex extends Talk {
        static Pattern ASSIGNISH = Pattern.compile("[A-Za-z][A-Za-z0-9,\\s]*[@=]");

        // The parser will use these fields:
        public String w; // current word
        public Pat t; // type that matched
        public Matcher m;

        String putback_w;
        Pat putback_t;
        Matcher putback_m;
        String putback_sub;

        // These will be used for finding errors.
        final String prog; // program to parse
        String sub; // Remaining substring of p to parse

        TLex(String prog) {
            this.prog = prog;
            this.w = "";
            this.t = null;
            this.m = null;
            this.putback_w = null;
            this.putback_t = null;
            this.putback_m = null;
            this.putback_sub = null;
            this.sub = prog;
            advance();
        }
        // You can back up one token.
        void back() {
            w = putback_w;
            t = putback_t;
            m = putback_m;
            sub = putback_sub;
        }
        private boolean attempt(Pat pat) {
            m = pat.matcher(sub);
            if (m.lookingAt()) {
                w = sub.substring(m.start(), m.end()); // Detected word.
                sub = sub.substring(m.end() - m.start()); // New substring of p
                // to match.
                t = pat; // Which pattern matched.

                return true;
            }
            return false;
        }
        void advance() {
            // Save state before advancing, so we can go back.
            putback_w = w;
            putback_t = t;
            putback_m = m;
            putback_sub = sub;

            while (true) {
                if (sub.length() == 0) {
                    // EOF condition.
                    w = "";
                    t = null;
                    return;
                }
                // Skip white space & comments.
                if (!attempt(Pat.WHITE) && !attempt(Pat.COMMENT))
                    break;
            }

            if (sub.length() == 0) {
                // EOF condition.
                w = "";
                t = null;
                return;
            }

            // Check for reserved words first.
            // Must check KEYWORD before NAME, or NAME will overtake it.
            if (attempt(Pat.RESERVED) || attempt(Pat.KEYWORD) || attempt(Pat.NAME) || attempt(Pat.NUMBER)) {
                return;
            } else if (attempt(Pat.STRING)) {
                StringBuffer sb = new StringBuffer();
                while (true) {
                    sb.append(w.substring(1, w.length() - 1)); // leave off
                    // initial &
                    // final <'>
                    // If another string starts immediately, it was a doubled
                    // <''>.
                    if (Pat.STRING.matcher(sub).lookingAt()) {
                        sb.append("'"); // Add one <'> for the doubled <''>
                        // input.
                        attempt(Pat.STRING);
                    } else {
                        break;
                    }
                }
                w = sb.toString();
                return;
            } else if (attempt(Pat.OTHER)) {
                return;
            } else {
                StringBuffer sb = new StringBuffer();
                for (int i = 0; i < prog.length(); i++) {
                    sb.append(fmt("%d ", (int) prog.charAt(i)));
                }
                say("PROG = %s", sb);
                say("Entire Program (%d chars): <%s>", prog.length(), prog);
                say("Previous Token: %s <%s>", putback_t, putback_w);
                toss("Parser Error at char <%d>, remaining unparsed code: <%s>", prog.length() - sub.length(), sub);
            }
        }
        boolean isAssignish() {
            return ASSIGNISH.matcher(w + sub).lookingAt();
        }
        boolean endsStmt() {
            if (t != Pat.OTHER)
                return false;
            char c = w.charAt(0);
            return c == '.' || c == ';';
        }
        boolean marksBlockParam() {
            if (t != Pat.OTHER)
                return false;
            char c = w.charAt(0);
            return c == ':' || c == '/' || c == '=' || c == '@';
        }
        boolean isAssign() {
            if (t != Pat.OTHER)
                return false;
            char c = w.charAt(0);
            return c == '=' || c == '@';
        }
        boolean isAssignedVaraible() {
            if (t != Pat.NAME)
                return false;
            advance();
            boolean z = isAssign();
            back();
            return z;
        }
        boolean isTupler() {
            if (t != Pat.OTHER)
                return false;
            return w.equals(",");
        }
        boolean isChain() {
            if (t != Pat.OTHER)
                return false;
            return w.equals("--");
        }
        boolean opensBlock() {
            if (t != Pat.OTHER)
                return false;
            char c = w.charAt(0);
            return c == '[' || c == '{' || c == '!';
        }
        boolean closesBlock() {
            if (t != Pat.OTHER)
                return false;
            char c = w.charAt(0);
            return c == ']' || c == '}' || c == '?';
        }
        boolean opensParen() {
            if (t != Pat.OTHER)
                return false;
            char c = w.charAt(0);
            return c == '(';
        }
        boolean closesParen() {
            if (t != Pat.OTHER)
                return false;
            char c = w.charAt(0);
            return c == ')';
        }
        /** Name without the trailing colons or slashes. */
        String keywordName() {
            assert (t == Pat.KEYWORD);
            return m.group(1);
        }
        /** Number of trailing colons or slashes */
        int keywordStrength() {
            assert (t == Pat.KEYWORD);
            return m.group(2).length();
        }
        /** Look ahead at the next token, to distinguish MACRO. */
        boolean isMacro() {
            assert (t == Pat.NAME);
            advance();
            boolean z = opensBlock();
            back();
            return z;
        }
    }

    public static String[] separateSlashedMethodIntoNameAndAbbrev(String s) {
        // TODO
        return null;
    }
}
