package terse.talk;

import java.io.IOException;
import java.util.HashMap;
import java.util.SortedSet;
import java.util.TreeSet;

import terse.talk.Pro.Obj;
import terse.talk.Talk.Terp.Frame;

public final class Cls extends Obj {
    public final Terp terp;
    public final String name;
    public Cls supercls; // tObjCls.supercls will be patched up, so not final.
    final HashMap<String, Meth> meths;
    public boolean trace;
    long countInstances;
    HashMap<String, Integer> allVars; // Calculated.
    String[] myVars; // Authoritative.
    SortedSet<String> subclasses; // keep them lower.

    static int generationCounter = 1;
    public int generation;

    Cls(Cls cls, Terp terp, String name, Cls supercls) {
        super(cls);
        this.terp = terp;
        this.name = name;
        this.supercls = supercls;
        this.meths = new HashMap<String, Meth>();
        this.trace = false;
        this.countInstances = 0;
        this.subclasses = new TreeSet<String>();
        this.myVars = emptyStrs;

        String key = name.toLowerCase();
        if (terp.clss.containsKey(key)) {
            Pro existing = terp.clss.get(name);
            if (existing instanceof Cls) {
                toss("Class named <%s> already exists", ((Cls) existing).name);
            } else {
                toss("Object named <%s> already exists in Globals: <%s>", name, existing.toString());
            }
        }
        terp.clss.put(name.toLowerCase(), this);

        this.allVars = new HashMap<String, Integer>();
        if (supercls != null) {
            // Object has no supercls, and during bootstrapping it can be null.
            supercls.subclasses.add(this.name.toLowerCase());
            recalculateAllVars(); // Not used.
        }
    }

    private void setNextGeneration() {
        this.generation = generationCounter;
        ++generationCounter;
    }
    private void recalculateAllVars() {
        this.allVars.clear();
        if (supercls != null) {
            this.allVars.putAll(supercls.allVars);
        }
        int varNum = this.allVars.size();
        for (String k : myVars) {
            this.allVars.put(k, varNum);
            ++varNum;
        }
    }
    // Send this to tPro, and let it recurse.
    void recalculateAllVarsHereAndBelow() {
        // First do my own (trusting that supercls.allVars is correct).
        recalculateAllVars();
        // Bust cache.
        // TODO: Right idea, but wrong method. Really we need to recompile all
        // methods to get correct field indices.
        setNextGeneration();
        // Recurse to subclasses.
        for (String s : subclasses) {
            Pro p = terp.clss.get(s.toLowerCase());
            if (p == null) {
                toss("Cls <%s> not found in globals", s);
            }
            Cls c = p.asCls();
            if (c == null) {
                toss("Cls <%s> is not a Cls in globals", s);
            }
            c.recalculateAllVarsHereAndBelow();
        }
    }
    Cls asCls() {
        return this;
    }
    public String toString() {
        return name;
    }
    public boolean instOf(Cls query) {
        for (Cls p = this; p != null; p = p.supercls) {
            if (p == query)
                return true;
        }
        return false;
    }
    void addMethod(Meth m) {
        assert m != null;
        assert m.abbrev != null;
        assert meths != null;
        meths.put(m.abbrev.toLowerCase(), m);
        meths.put(m.name.toLowerCase(), m);

        if (m instanceof UsrMeth) {
            String[] lines = ((UsrMeth)m).src.split("\n");
            try {
                terp.appendImageFile(fmt("meth %s %s", name, m.name), lines);
                if (m.abbrev.length() > 0 && ! m.abbrev.equals(m.name)) {
                    terp.appendImageFile(fmt("meth %s %s", name, m.abbrev), lines);
                }
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
                toss("Cannot write image file: " + e);
            }
        }
    }
    Cls defineSubclass(String newname) {
        if (newname.endsWith("Cls")) {
            toss("Don't define classes ending with 'Cls' using defineClass: name=<%s>", newname);
        }
        if (this.cls == terp().tMetacls) {
            toss("Don't define classes on Metaclasses using defineClass: self=<%s>", this.cls.name);
        }
        if (terp.clss.containsKey(newname.toLowerCase())) {
            toss("Name <%s> already exists in Global Frame", newname);
        }
        if (terp.clss.containsKey(newname.toLowerCase() + "cls")) {
            toss("Name <%sCls> already exists in Global Frame", newname);
        }
        Cls newMetaCls = new Cls(terp.tMetacls, terp, newname + "Cls", this.cls);
        Cls newCls = new Cls(newMetaCls, terp, newname, this);
        terp.clss.put(newMetaCls.name.toLowerCase(), newMetaCls);
        terp.clss.put(newCls.name.toLowerCase(), newCls);

        try {
            terp.appendImageFile(fmt("class %s %s", newname, name), null);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            toss("Cannot write image file: " + e);
        }

        return newCls;
    }
    static void addBuiltinMethodsForCls(final Terp terp) {
        terp.tCls.addMethod(new JavaMeth(terp.tCls, "name", null, "Return the name of the class.") {
            public Pro apply(Frame f, Pro r, Pro[] args) {
                Cls self = r.asCls();
                assert self != null;
                return terp.newStr(self.name);
            }
        });
        terp.tCls.addMethod(new JavaMeth(terp.tCls, "superClass", "sup", "Return the superClass of the class.") {
            public Pro apply(Frame f, Pro r, Pro[] args) {
                Cls self = r.asCls();
                assert self != null;
                return self.supercls == null ? terp.instNil : self.supercls;
            }
        });
        terp.tCls.addMethod(new JavaMeth(terp.tCls, "methodNames", "meths",
                "Return a Vec of the method names defined on the class.") {
            public Pro apply(Frame f, Pro r, Pro[] args) {
                Cls self = r.asCls();
                assert self != null;
                Vec z = new Vec(terp);
                for (String s : self.meths.keySet()) {
                    z.vec.add(new Str(terp, s));
                }
                return z;
            }
        });
        terp.tCls.addMethod(new JavaMeth(terp.tCls, "methodsDefs", "methDefs",
                "Return a Dict of the method names defined on the class.") {
            public Pro apply(Frame f, Pro r, Pro[] args) {
                Cls self = r.asCls();
                assert self != null;
                Dict z = new Dict(terp);
                for (String s : self.meths.keySet()) {
                    z.dict.put(terp.newStr(s), self.meths.get(s));
                }
                return z;
            }
        });
        terp.tCls
                .addMethod(new JavaMeth(terp.tCls, "defineSubclass:", "defSub:", "Define a new subclass of the class.") {
                    public Pro apply(Frame f, Pro r, Pro[] args) {
                        Str newnameStr = args[0].asStr();
                        assert newnameStr != null;
                        String newname = newnameStr.str;
                        Cls self = r.asCls();
                        return self.defineSubclass(newname);
                    }
                });
        terp.tCls.addMethod(new JavaMeth(terp.tCls, "defineInstanceVars:", "defVars:", "Define instance variables, "
                + "using space-separated string.") {
            public Pro apply(Frame f, Pro r, Pro[] args) {
                Str varsStr = args[0].asStr();
                if (varsStr == null) {
                    say("Expected Str with inst var names");
                }
                String[] myVarNames = filterOutEmptyStrings(varsStr.str.split("\\s+"));
                Cls self = r.asCls();

                // Check for Ignore-Case uniqueness.
                for (int i = 0; i < myVarNames.length; i++) {
                    String iStr = myVarNames[i];
                    String iLow = iStr.toLowerCase();
                    for (Cls c = self.supercls; c != null; c = c.supercls) {
                        for (String y : c.myVars) {
                            if (iLow.equals(y.toLowerCase())) {
                                toss("Cannot add inst var <%s> because Cls <%s> also has <%s>", iStr, c.name, y);
                            }
                        }
                        for (int j = i + 1; j < myVarNames.length; j++) {
                            if (iLow.equals(myVarNames[j].toLowerCase())) {
                                toss("Cannot add 2 inst vars <%s> <%s> with same name.", iStr, myVarNames[j]);
                            }
                        }
                    }
                }

                self.myVars = myVarNames;
                terp.tPro.recalculateAllVarsHereAndBelow();
                return r;
            }
        });
        terp.tCls.addMethod(new JavaMeth(terp.tCls, "defineMethod:abbrev:doc:code:", "defMeth:a:d:c:", "") {
            public Pro apply(Frame f, Pro r, Pro[] args) {
                assert args.length == 4;
                Cls self = r.asCls();

                Str methNameStr = args[0].asStr();
                assert methNameStr != null;
                String methName = methNameStr.str;

                // I've been indicisive about using "/" or ":". Allow both.
                methName = methName.replaceAll("/", ":");

                Str abbrevStr = args[1].asStr();
                String abbrev = abbrevStr == null ? "" : abbrevStr.str;

                Str docStr = args[2].asStr();
                String doc = docStr == null ? "" : docStr.str;

                Str methBodyStr = args[3].asStr();
                assert methBodyStr != null;
                String methBody = methBodyStr.str;

                Expr.Top top = Parser.parseMethod(self, methName, methBody);
                Meth m = new UsrMeth(self, methName, abbrev, doc, methBody, top);
                self.addMethod(m);
                return m;
            }
        });
        terp.tCls.addMethod(new JavaMeth(terp.tCls, "setTrace:", "trace:",
                "Set true to trace all calls of this method.") {
            public Pro apply(Frame f, Pro r, Pro[] args) {
                assert args.length == 1;
                r.asCls().trace = args[0].truth();
                return r;
            }
        });
        terp.tClsCls.addMethod(new JavaMeth(terp.tClsCls, "allClassesDict", "all") {
            public Pro apply(Frame f, Pro r, Pro[] args) {
                assert args.length == 0;
                Pro[] arr = new Pro[terp.clss.size()];
                int i = 0;
                for (String k : terp.clss.keySet()) {
                    Pro key = terp.newStr(k);
                    Pro value = terp.clss.get(k);
                    arr[i] = new Vec(pros(key, value));
                    ++i;
                }
                return terp.newDict(arr);
            }
        });
    }

    public static abstract class Meth extends Obj {
        Cls onCls;
        String name;
        String abbrev;
        String doc;
        boolean trace;
        long countMessages;

        public Meth(Cls cls, Cls onCls, String name, String abbrev, String doc) {
            super(cls);
            this.onCls = onCls;
            this.name = name;
            this.abbrev = abbrev == null || abbrev.length() == 0 ? name : abbrev;
            this.doc = doc;
            this.trace = false;
            this.countMessages = 0;
            assert this.name != null;
            assert this.abbrev != null;

        }
        public abstract Pro apply(Frame f, Pro r, Pro[] args);
    }

    /**
     * Meth implemented in Java. Notice that the CTOR automatically adds the
     * method to onCls!
     */
    public static abstract class JavaMeth extends Meth {
        JavaMeth(Cls onCls, String name, String abbrev) {
            super(onCls.terp.tJavMeth, onCls, name, abbrev, "");
            onCls.addMethod(this); // NOTA BENE
        }
        JavaMeth(Cls onCls, String name, String abbrev, String doc) {
            super(onCls.terp.tJavMeth, onCls, name, abbrev, doc);
        }
        public abstract Pro apply(Frame f, Pro r, Pro[] args);
        public String toString() {
            return fmt("\"<%s %s> Built-In Method\"", onCls, name);
        }
    }

    public static abstract class StrMeth extends JavaMeth {
        StrMeth(Cls onCls, String name, String abbrev, String doc) {
            super(onCls, name, abbrev, doc);
        }
        public Pro apply(Frame f, Pro r, Pro[] args) {
            assert args.length == 1;
            return apply1(f, r, (Str) args[0]);
        }
        public abstract Pro apply1(Frame f, Pro r, Str a);
    }

    public static class UsrMeth extends Meth {
        String src;
        Expr.Top top;

        UsrMeth(Cls onCls, String name, String abbrev, String doc, String src, Expr.Top expr) {
            // N.B. cls is the class of the UsrMeth instance (probably
            // terp.tUsrMeth),
            // not the class that it is a method on.
            super(onCls.terp.tJavMeth, onCls, name, abbrev, doc);
            this.src = src;
            this.top = expr;
        }
        @Override
        public Pro apply(Frame f, Pro r, Pro[] args) {
            assert args.length == top.numArgs;

            Frame f2 = f.terp().newFrame(f, r, top);

            for (int i = 0; i < args.length; i++) {
                f2.locals[i] = args[i];
            }

            Pro z = top.eval(f2);
            return z;
        }
        public String toString() {
            return fmt("%s \n\n##########\n\n%s", src, top.toPrettyString());
        }
    }
}
