package terse.talk;

import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.regex.Pattern;

import terse.talk.Pro.Blk;
import terse.talk.Pro.Buf;
import terse.talk.Pro.Dict;
import terse.talk.Pro.Undefined;
import terse.talk.Pro.Num;
import terse.talk.Pro.Obj;
import terse.talk.Pro.Str;
import terse.talk.Pro.Sys;
import terse.talk.Pro.Vec;
import terse.talk.Usr.Tmp;

public class Talk {
    public static final String STANDARD_INIT_FILENAME = "src/terse/talk/default.tti";

    public static Pro toss(String s, Object... objects) {
        try {
            say("TOSSING <%s>", s);
        } catch (Exception ex) {
            // Ignore.
        }
        throw new TalkException(s, objects);
    }
    public static Pro tossNotUnderstood(Cls c, String msg) {
        throw new TalkException("Message <%s> not understood by class <%s>", msg, c.name);
    }

    static FileWriter logWriter;

    static void say(String s, Object... objects) {
        try {
            if (logWriter == null) {
                logWriter = new FileWriter("__log.txt");
            }
            String msg = fmt(s, objects);
            System.out.println(msg);

            logWriter.write(msg);
            logWriter.write("\n");
            logWriter.flush();
        } catch (IOException e) {
            e.printStackTrace();
            throw new AssertionError(e.toString());
        }
    }
    public static String fmt(String s, Object... objects) {
        return String.format(s, objects);
    }
    public static String htmlEscape(String s) {
        return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
    }
    public static String arrayToString(Expr[] a) {
        String z = "[ ";
        for (Expr e : a) {
            z += fmt("%s, ", e.toString());
        }
        return z + "]";
    }
    public static String arrayToString(Pro[] a) {
        String z = "[ ";
        for (Pro e : a) {
            z += fmt("%s, ", e.toString());
        }
        return z + "]";
    }
    public static String arrayToString(String[] a) {
        if (a == null)
            return "<NULL[]>";
        String z = "[ ";
        for (String e : a) {
            z += fmt("\"%s\", ", e);
        }
        return z + "]";
    }

    public static String repeat(int n, String s) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < n; i++) {
            sb.append(s);
        }
        return sb.toString();
    }

    // Little functions to avoid List objects for small arrays.
    // append() builds arrays in O(n**2) time, but n is usually very small.
    public static String[] strs(String... strings) {
        return strings;
    }

    public static String[] emptyStrs = new String[0];

    public static String[] append(String[] arr, String s) {
        if (arr == null || arr.length == 0) {
            return new String[] { s };
        }
        String[] z = Arrays.copyOf(arr, arr.length + 1);
        z[arr.length] = s;
        return z;
    }
    public static String[] filterOutEmptyStrings(String[] a) {
        int count = 0;
        for (int i = 0; i < a.length; i++) {
            if (a[i].length() > 0)
                ++count;
        }
        String[] z = new String[count];
        int j = 0;
        for (int i = 0; i < a.length; i++) {
            if (a[i].length() > 0) {
                z[j] = a[i];
                ++j;
            }
        }
        return z;
    }
    public static Expr[] exprs(Expr... exprs) {
        return exprs;
    }

    public static Expr[] emptyExprs = new Expr[0];

    public static Expr[] append(Expr[] arr, Expr s) {
        if (arr == null || arr.length == 0) {
            return new Expr[] { s };
        }
        Expr[] z = Arrays.copyOf(arr, arr.length + 1);
        z[arr.length] = s;
        return z;
    }

    public static Pro[] pros(Pro... pros) {
        return pros;
    }

    public static Pro[] emptyPros = new Pro[0];

    public static Pro[] append(Pro[] arr, Pro s) {
        if (arr == null || arr.length == 0) {
            return new Pro[] { s };
        }
        Pro[] z = Arrays.copyOf(arr, arr.length + 1);
        z[arr.length] = s;
        return z;
    }

    public static int[] ints(int... ints) {
        return ints;
    }

    public static int[] emptyInts = new int[0];

    public static int[] append(int[] arr, int s) {
        if (arr == null || arr.length == 0) {
            return new int[] { s };
        }
        int[] z = Arrays.copyOf(arr, arr.length + 1);
        z[arr.length] = s;
        return z;
    }

    // I got infinite recursion trying to use Java split() with quoted pattern.
    // So i write a simple one myself:
    public static String[] splitNonEmpty(String src, char delim) {
        String[] z = emptyStrs;
        while (src.length() > 0) {
            int i = src.indexOf(delim);
            if (i < 0) {
                // Not found; take the rest.
                z = append(z, src);
                break;
            } else if (i > 0) {
                z = append(z, src.substring(0, i));
                src = src.substring(i + 1);
            } else { // i == 0
                // Don't append empty string.
                src = src.substring(1);
            }
        }
        return z;
    }

    static Pattern htmlTagP = Pattern.compile("[A-Za-z][-A-Za-z0-9_.:]*");

    public static class TalkException extends RuntimeException {
        private static final long serialVersionUID = 1L;
        String msg;
        Object[] args;

        public TalkException(String msg, Object... args) {
            this.msg = msg;
            this.args = args;
            say("TalkException: msg=<%s>", msg);
            for (int i = 0; i < args.length; i++) {
                say("TalkException: args[%d] : %s : <%s>", i, (args[i] instanceof Pro) ? ((Pro) args[i]).cls.name
                        : args[i].getClass().getName(), args[i].toString());
            }
        }

        @Override
        public String toString() {
            return fmt(msg, args);
        }
    }

    public static final class Terp {
        String initFilename;
        boolean loadingInitFile = false;
        HashMap<String, Cls> clss;
        HashMap<String, String> env;
        Cls tPro;
        Cls tProCls;
        Cls tObj;
        Cls tObjCls;
        Cls tCls;
        Cls tClsCls;
        Cls tMetacls;
        Cls tMetaclsCls;
        Cls tSuper;
        Cls tSuperCls;
        Cls tMeth;
        Cls tMethCls;
        Cls tJavMeth;
        Cls tJavMethCls;
        Cls tUsrMeth;
        Cls tUsrMethCls;
        Cls tSys;
        Cls tSysCls;
        Cls tNum;
        Cls tNumCls;
        Cls tBuf;
        Cls tBufCls;
        Cls tStr;
        Cls tStrCls;
        Cls tExc;
        Cls tExcCls;
        Cls tErr;
        Cls tErrCls;
        Cls tRtx;
        Cls tRtxCls;
        Cls tRtn;
        Cls tRtnCls;
        Cls tBrk;
        Cls tBrkCls;
        Cls tUndefined;
        Cls tUndefinedCls;
        Cls tBlk;
        Cls tBlkCls;
        Cls tVec;
        Cls tVecCls;
        Cls tDict;
        Cls tDictCls;
        Cls tUsr;
        Cls tUsrCls;
        Cls tTmp;
        Cls tTmpCls;
        Num instTrue;
        Num instFalse;
        Undefined instNil;
        Pro.Super instSuper;

        /** The Interpreter with no init file. */
        public Terp() {
            this("", "");
        }

        /** The Interpreter with an init file. */
        public Terp(String imageName, String initFilename) {
            this.initFilename = initFilename;
            this.clss = new HashMap<String, Cls>();
            this.env = new HashMap<String, String>();
            this.env.put("image", imageName);

            this.tProCls = new Cls(null/* Metacls */, this, "ProCls", null/* Cls */);
            this.tPro = new Cls(tProCls, this, "Pro", null/* Pro has no super */);

            this.tObjCls = new Cls(null/* Metacls */, this, "ObjCls", null/* Cls */);
            this.tObj = new Cls(tObjCls, this, "Obj", tPro);

            this.tClsCls = new Cls(null/* MetaCls */, this, "ClsCls", tObjCls);
            this.tCls = new Cls(tClsCls, this, "Cls", tObj);

            this.tMetaclsCls = new Cls(null/* MetaCls */, this, "MetaclsCls", tObjCls);
            this.tMetacls = new Cls(tMetaclsCls, this, "Metacls", tCls);

            // Patch up: All *Cls's are instances of Metacls.
            this.tProCls.cls = this.tMetacls;
            this.tObjCls.cls = this.tMetacls;
            this.tClsCls.cls = this.tMetacls;
            this.tMetaclsCls.cls = this.tMetacls;

            // Patch up: All *Cls's ultimately derive from Cls.
            this.tProCls.supercls = tCls;
            this.tObjCls.supercls = tCls;

            // Super is a very special class, from Pro, not from Obj.
            this.tSuperCls = new Cls(tMetaclsCls, this, "SuperCls", tProCls);
            this.tSuper = new Cls(tSuperCls, this, "Super", tPro);

            this.tMethCls = new Cls(tMetaclsCls, this, "MethCls", tObjCls);
            this.tMeth = new Cls(tMethCls, this, "Meth", tObj);
            this.tJavMethCls = new Cls(tMetaclsCls, this, "JavMethCls", tObjCls);
            this.tJavMeth = new Cls(tJavMethCls, this, "JavMeth", tObj);
            this.tUsrMethCls = new Cls(tMetaclsCls, this, "UsrMethCls", tObjCls);
            this.tUsrMeth = new Cls(tUsrMethCls, this, "UsrMeth", tObj);
            this.tSysCls = new Cls(tMetaclsCls, this, "SysCls", tObjCls);
            this.tSys = new Cls(tSysCls, this, "Sys", tObj);
            this.tNumCls = new Cls(tMetaclsCls, this, "NumCls", tObjCls);
            this.tNum = new Cls(tNumCls, this, "Num", tObj);
            this.tBufCls = new Cls(tMetaclsCls, this, "BufCls", tObjCls);
            this.tBuf = new Cls(tBufCls, this, "Buf", tObj);
            this.tStrCls = new Cls(tMetaclsCls, this, "StrCls", tObjCls);
            this.tStr = new Cls(tStrCls, this, "Str", tObj);
            this.tUsrCls = new Cls(tMetaclsCls, this, "UsrCls", tObjCls);
            this.tUsr = new Cls(tUsrCls, this, "Usr", tObj);
            this.tTmpCls = new Cls(tMetaclsCls, this, "TmpCls", tUsrCls);
            this.tTmp = new Cls(tTmpCls, this, "Tmp", tUsr);
            this.tExcCls = new Cls(tMetaclsCls, this, "ExcCls", tObjCls);
            this.tExc = new Cls(tExcCls, this, "Exc", tObj);
            this.tErrCls = new Cls(tMetaclsCls, this, "ErrCls", tObjCls);
            this.tErr = new Cls(tErrCls, this, "Err", tObj);
            this.tRtxCls = new Cls(tMetaclsCls, this, "RtxCls", tObjCls);
            this.tRtx = new Cls(tRtxCls, this, "Rtx", tObj);
            this.tRtnCls = new Cls(tMetaclsCls, this, "RtnCls", tObjCls);
            this.tRtn = new Cls(tRtnCls, this, "Rtn", tObj);
            this.tBrkCls = new Cls(tMetaclsCls, this, "BrkCls", tObjCls);
            this.tBrk = new Cls(tBrkCls, this, "Brk", tObj);
            this.tUndefinedCls = new Cls(tMetaclsCls, this, "UndefinedCls", tObjCls);
            this.tUndefined = new Cls(tUndefinedCls, this, "Undefined", tObj);
            this.tBlkCls = new Cls(tMetaclsCls, this, "BlkCls", tObjCls);
            this.tBlk = new Cls(tBlkCls, this, "Blk", tObj);
            this.tVecCls = new Cls(tMetaclsCls, this, "VecCls", tObjCls);
            this.tVec = new Cls(tVecCls, this, "Vec", tObj);
            this.tDictCls = new Cls(tMetaclsCls, this, "DictCls", tObjCls);
            this.tDict = new Cls(tDictCls, this, "Dict", tObj);

            this.instTrue = newNum(1);
            this.instFalse = newNum(0);
            this.instNil = new Undefined(tUndefined);
            this.instSuper = new Pro.Super(this);

            Pro.addBuiltinMethodsForPro(this);
            Obj.addBuiltinMethodsForObj(this);
            Cls.addBuiltinMethodsForCls(this);
            Sys.addBuiltinMethodsForSys(this);
            Num.addBuiltinMethodsForNum(this);
            Buf.addBuiltinMethodsForBuf(this);
            Str.addBuiltinMethodsForStr(this);
            Blk.addBuiltinMethodsForBlk(this);
            Vec.addBuiltinMethodsForVec(this);
            Dict.addBuiltinMethodsForDict(this);
            Usr.addBuiltinMethodsForUsr(this);

            if (initFilename.length() > 0) {
                try {
                    loadInitFile(initFilename);
                } catch (IOException e) {
                    e.printStackTrace();
                    toss("Cannot loadInitFile: " + e.toString());
                }
            }
        }

        public Frame newFrame(Frame prev, Pro self, Expr.Top top) {
            return new Frame(prev, self, top);
        }

        public class Frame { // Important: non-static, tied to a Terp.
            Frame prev; // for listing the stack
            Pro self;
            Pro[] locals;
            Expr.Top top;
            int level;

            // If we did Lexical Binding, we would have a parent frame.
            // But we're not doing that (yet) so I deleted it.

            private Frame(Frame prev, Pro self, Expr.Top top) {
                this.prev = prev;
                this.self = self;
                this.top = top;
                this.level = (prev == null) ? 0 : prev.level + 1;
                this.locals = new Pro[top.numLocals];
                for (int i = 0; i < locals.length; i++) {
                    this.locals[i] = getTerp().instNil;
                }
            }

            @Override
            public String toString() {
                StringBuffer sb = new StringBuffer("FRAME{");
                sb.append(fmt("self=<%s>; ", self));
                for (int i = 0; i < locals.length; i++) {
                    sb.append(fmt("\"%d\"<%s>; ", i, locals[i]));
                }
                sb.append("}");
                return sb.toString();
            }
            public Terp terp() {
                return getTerp();
            }
        }

        public Terp getTerp() {
            return this;
        }
        public Num newNum(double a) {
            return new Num(this, a);
        }
        public Str newStr(String s) {
            return new Str(this, s);
        }
        public Tmp newTmp() {
            return new Tmp(this);
        }
        public Dict newDict(Pro[] arr) {
            Dict z = new Dict(this);
            for (int i = 0; i < arr.length; i++) {
                if (arr[i] instanceof Vec) {
                    Vec v = (Vec) arr[i];
                    if (v.vec.size() == 2) {
                        z.dict.put(v.vec.get(0), v.vec.get(1));
                    } else {
                        toss("To initialize assoc in Dict, expected Vec of length 2, but got <%s>; inside <%s>", v,
                                arrayToString(arr));
                    }
                } else {
                    toss("To initialize assoc in Dict, expected Vec of length 2, but got <%s>; inside <%s>", arr[i],
                            arrayToString(arr));
                }
            }
            return z;
        }
        public Vec newVec(Pro[] a) {
            Vec z = new Vec(this);
            for (int i = 0; i < a.length; ++i) {
                z.vec.add(a[i]);
            }
            return z;
        }
        public Vec mkStrVec(String... strs) {
            Pro[] arr = new Pro[strs.length];
            for (int i = 0; i < strs.length; i++) {
                arr[i] = newStr(strs[i]);
            }
            return newVec(arr);
        }
        public Vec mkSingletonStrVecVec(String... strs) {
            Pro[] arr = new Pro[strs.length];
            for (int i = 0; i < strs.length; i++) {
                arr[i] = mkStrVec(strs[i]);
            }
            return newVec(arr);
        }
        public Dict handleUrl(String url, HashMap<String, String> query) {
            say("runUrl: %s", url);
            query = (query == null) ? new HashMap<String, String>() : query;
            Pro[] queryArr = new Pro[query.size()];
            int i = 0;
            for (String k : query.keySet()) {
                say("  query %s = %s", k, query.get(k));
                Pro queryKey = newStr(k);
                Pro queryValue = newStr(query.get(k).replaceAll("\r\n", "\n"));
                queryArr[i] = new Vec(pros(queryKey, queryValue));
                ++i;
            }
            Dict qDict = newDict(queryArr);
            assert url.startsWith("/");
            if (url.equals("/")) {
                url = "/Home";
            }

            // To get app name, mit the initial '/', and split on dots.
            String[] word = url.substring(1).split("[.]");
            assert word.length > 0;
            String appName = word[0];

            Dict result = null;
            try {
                Cls cls = getTerp().clss.get(appName.toLowerCase());
                if (cls == null) {
                    toss("Rendering class does not exist: <%s>", appName);
                }
                Pro self = cls.eval("self new");
                Pro result_pro = self.asObj().eval(fmt("self handle: (%s) query: (%s)", newStr(url), qDict));
                result = result_pro.asDict();
                if (result == null) {
                    toss("Sending <handle:> to instance of <%s> did not return a Dict: <%s>", appName, result_pro);
                }

            } catch (Exception ex) {
                StringBuffer sb = new StringBuffer(ex.toString());
                StackTraceElement[] elems = ex.getStackTrace();
                for (StackTraceElement e : elems) {
                    sb.append("\n  * ");
                    sb.append(e.toString());
                }
                Pro[] dict_arr = pros(new Vec(pros(newStr("type"), newStr("text"))), new Vec(pros(newStr("title"),
                        newStr(ex.toString()))), new Vec(pros(newStr("value"), newStr(sb.toString()))));
                result = newDict(dict_arr);
            }
            return result;

        }

        Pattern INITIAL_WHITE = Pattern.compile("^\\s.*");

        public void loadInitFile(String initFilename) throws IOException {
            BufferedReader in = new BufferedReader(new FileReader(initFilename));
            loadInitFile(in);
        }
        public void loadInitFile(BufferedReader in) throws IOException {
            loadingInitFile = true;
            try {
                int lineNum = 0;
                String line = in.readLine();
                while (true) {
                    // Break at EOF.
                    if (line == null)
                        break;
                    ++lineNum;
                    // Skip blank lines.
                    String trimmed = line.trim();
                    if (trimmed.length() == 0) {
                        line = in.readLine();
                        continue;
                    }
                    // Skip comments.
                    if (line.charAt(0) == '#') {
                        line = in.readLine();
                        continue;
                    }

                    if (line.charAt(0) < 'a' || line.charAt(0) > 'z') {
                        toss("loadInitFile: Should have started with some lowercase letter a-z: <%s>", line);
                    }

                    // Store the command line in words[] and command.
                    String[] words = line.trim().split("\\s+");
                    if (words.length < 1) {
                        toss("loadInitFile: Not enough words: <%s>", line.trim());
                    }
                    String command = words[0];

                    // Now slurp any lines beginning with white space into more.
                    StringBuffer sb = new StringBuffer();
                    line = in.readLine();
                    while (line != null && (INITIAL_WHITE.matcher(line).matches() || line.length() == 0)) {
                        if (line.length() == 0 || line.charAt(0) != '#') {
                            sb.append(line);
                            sb.append("\n");
                        }
                        line = in.readLine();
                    }
                    // Keep line for next time through big loop.
                    String more = sb.toString();

                    if (command.equals("meth")) {
                        String className = words[1];
                        String methodName = words[2];

                        Cls cls = clss.get(className.toLowerCase());
                        if (cls == null) {
                            toss("loadInitFile: Global <%s> not a class, for meth <%s> :: %s", className, methodName);
                        }
                        cls.eval(fmt("self definemethod: %s abbrev: %s doc: %s code: %s", newStr(methodName),
                                newStr(""), newStr(""), newStr(more)));
                    } else if (command.equals("class")) {
                        String className = words[1];
                        String superName = words[2];
                        Cls clsObj = clss.get(className.toLowerCase());
                        Cls supObj = clss.get(superName.toLowerCase());
                        if (more.trim().length() > 0) {
                            toss("Was not expecting more: <%s>");
                        }

                        if (clsObj == null && supObj != null) {
                            // We can create the class.
                            supObj.defineSubclass(className);
                        } else {
                            toss("loadInitFile: Cannot define subclass <%s> of <%s>", className, superName);
                        }
                    } else if (command.equals("equals")) {
                        String expected = "";
                        for (int i = 1; i < words.length; i++) {
                            expected += words[i] + " ";
                        }
                        Pro p1 = getTerp().newTmp().eval(expected);
                        Pro p2 = getTerp().newTmp().eval(more);
                        if (!p1.equals(p2)) {
                            toss("loadInitFile: eq check failed: line %d: <%s> --> <%s> but <%s> --> <%s>", lineNum,
                                    expected, p1, more, p2);
                        }
                    } else {
                        toss("loadInitFile: Unknown command: <%s>", line);
                    }
                }
            } finally {
                loadingInitFile = false;
            }
        }
        void appendImageFile(String firstLine, String[] rest) throws IOException {
            if (loadingInitFile) {
                return; // Don't cause infinite loop!
            }
            if (initFilename.length() == 0) {
                return; // For unit tests.
            }
            if (initFilename.endsWith("/default.tti")) {
                return; // Never modify default.
            }
            FileOutputStream fos = new FileOutputStream(initFilename, true/* append */);
            PrintStream ps = new PrintStream(fos);
            ps.println(firstLine);
            if (rest != null) {
                for (String s : rest) {
                    ps.println(" " + s); // Stuff 1 space before each line.
                }
            }
            ps.flush();
            fos.flush();
            fos.close();
        }

    }

    public static class Visitor {
        public void visitPro(Pro a) {
            toss("SubclassResponsibility(Visitor::visitPro)");
        }
        public void visitCls(Cls a) {
            visitPro(a);
        }
        public void visitNum(Num a) {
            visitPro(a);
        }
        public void visitStr(Str a) {
            visitPro(a);
        }
        public void visitUndefined(Undefined a) {
            visitPro(a);
        }
        public void visitVec(Vec a) {
            visitPro(a);
        }
        public void visitDict(Dict a) {
            visitPro(a);
        }
        public void visitUsr(Usr a) {
            visitPro(a);
        }
    }
}
