package net.yak.stalk;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Pattern;

import net.yak.stalk.Cls.Meth;
import net.yak.stalk.Expr.Send;
import net.yak.stalk.Pro.Assoc;
import net.yak.stalk.Pro.Blk;
import net.yak.stalk.Pro.Buf;
import net.yak.stalk.Pro.Map;
import net.yak.stalk.Pro.Undefined;
import net.yak.stalk.Pro.Num;
import net.yak.stalk.Pro.Obj;
import net.yak.stalk.Pro.Str;
import net.yak.stalk.Pro.Sys;
import net.yak.stalk.Pro.Vec;

public class Talk {
    static Pro toss(String s, Object... objects) {
        throw new TalkException(s, objects);
    }
    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());
        }
    }
    static String fmt(String s, Object... objects) {
        return String.format(s, objects);
    }
    static String htmlEscape(String s) {
        return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
    }
    static String arrayToString(Expr[] a) {
        String z = "[ ";
        for (Expr e : a) {
            z += fmt("%s, ", e.toString());
        }
        return z + "]";
    }
    static String arrayToString(Pro[] a) {
        String z = "[ ";
        for (Pro e : a) {
            z += fmt("%s, ", e.toString());
        }
        return z + "]";
    }
    static String arrayToString(String[] a) {
        if (a == null)
            return "<NULL[]>";
        String z = "[ ";
        for (String e : a) {
            z += fmt("\"%s\", ", e);
        }
        return z + "]";
    }
    static String[] toArray(List<String> v) {
        return (String[]) v.toArray();
    }

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

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

    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;
    }
    static boolean contains(String[] arr, String s) {
        for (String x : arr) {
            if (x.equals(s))
                return true;
        }
        return false;
    }
    static boolean containsIgnoreCase(String[] arr, String s) {
        s = s.toLowerCase();
        for (String x : arr) {
            if (x.toLowerCase().equals(s))
                return true;
        }
        return false;
    }
    static String[] nonemptyStrings(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;
    }
    static Expr[] exprs(Expr... exprs) {
        return exprs;
    }

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

    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;
    }
    static Pro[] pros(Pro... pros) {
        return pros;
    }

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

    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;
    }

    // static Pattern unaryNameP;
    // static Pattern keywordNameP;
    static Pattern htmlTagP;
    static {
        // // Unary name cannot be 2 letters. Can be 1 letter, or 3 or more.
        // // Must begin with a lower.
        // unaryNameP = Pattern.compile("[a-z]|[a-zA-Z0-9]{3,}");
        // // Keyword name must have 2 or more letters in each part, beginning
        // with
        // // lower.
        // keywordNameP = Pattern.compile("([a-z][a-zA-Z0-9]+[:])+");
        // Tags in HTML: safe chars in names of tags and params.
        htmlTagP = Pattern.compile("[-A-Za-z0-9_.:]+");
    }

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

        public TalkException(String s, Object... args) {
            this.msg = s;
            this.args = args;
            System.err.println("TalkException: msg=" + msg);
            for (int i = 0; i < args.length; i++) {
                System.err.println("TalkException: arg:" + args[i]);
            }
        }

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

    public static class Terp {
        HashMap<String, Cls> clss;
        Cls tPro;
        Cls tProCls;
        Cls tObj;
        Cls tObjCls;
        Cls tCls;
        Cls tClsCls;
        Cls tMetacls;
        Cls tMetaclsCls;
        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 tMap;
        Cls tMapCls;
        Cls tAssoc;
        Cls tAssocCls;
        Cls tUsr;
        Cls tUsrCls;
        // Cls tPage;
        // Cls tPageCls;
        // Cls tApp;
        // Cls tAppCls;
        Num instTrue;
        Num instFalse;
        Undefined instNil;

        public Terp() {
            this.clss = new HashMap<String, Cls>();
            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;

            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.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.tMapCls = new Cls(tMetaclsCls, this, "MapCls", tObjCls);
            this.tMap = new Cls(tMapCls, this, "Map", tObj);
            this.tAssocCls = new Cls(tMetaclsCls, this, "AssocCls", tObjCls);
            this.tAssoc = new Cls(tAssocCls, this, "Assoc", tObj);

            // this.tPageCls = new Cls(tMetaclsCls, this, "PageCls", tUsrCls);
            // this.tPage = new Cls(tPageCls, this, "Page", tUsr);
            // this.tAppCls = new Cls(tMetaclsCls, this, "AppCls", tPageCls);
            // this.tApp = new Cls(tAppCls, this, "App", tPage);

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

            Pro.addBuiltinMethodsForPro(this);
            Obj.addBuiltinMethodsForObj(this);
            Cls.addBuiltinMethodsForCls(this);
            Sys.addBuiltinMethodsForSys(this);
            Num.addBuiltinMethodsForNum(this);
            Buf.addBuiltinMethodsForBuf(this);
            Str.addBuiltinMethodsForStr(this);
            // True.addBuiltinMethodsForTru(this);
            // False.addBuiltinMethodsForFal(this);
            Blk.addBuiltinMethodsForBlk(this);
            Vec.addBuiltinMethodsForVec(this);
            Map.addBuiltinMethodsForMap(this);
            Usr.addBuiltinMethodsForUsr(this);

            try {
                loadInitFile();
            } catch (IOException e) {
                e.printStackTrace();
                toss("Cannot loadInitFile: " + e.toString());
            }
        }
        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;
            HashMap<String, Pro> dict;
            int numLocals;
            Expr.Top top;

            // 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.numLocals = top.numLocals;
                this.top = top;
                this.dict = new HashMap<String, Pro>();
                // Hack until we get rid of the dict:
                this.dict.put("self", self);
                this.dict.put("se", self);
            }

            @Override
            public String toString() {
                return "Frame [dict=" + dict + ", numLocals=" + numLocals + ", self=" + self
                        + ", top=" + top + "]";
            }

            Pro get(String key) {
                Pro value = this.dict.get(key);
                // if (value == null) {
                // toss("No variable <%s> in %s Frame.", key,
                // this == glob ? "Global" : "Local");
                // }
                return value;
            }
            boolean containsKey(String key) {
                return this.dict.containsKey(key);
            }

            void put(String key, Pro value) {
                this.dict.put(key, value);
            }
            Terp terp() {
                return getTerp();
            }
        }

        Terp getTerp() {
            return this;
        }
        Num newNum(double a) {
            return new Num(this, a);
        }
        Str newStr(String s) {
            return new Str(this, s);
        }
        Map newMap(Pro[] kvArray) {
            Map z = new Map(this);
            for (int i = 0; i < kvArray.length; i += 2) {
                z.map.put(kvArray[i], kvArray[i + 1]);
            }
            return z;
        }
        Vec newVec(Pro[] a) {
            Vec z = new Vec(this);
            for (int i = 0; i < a.length; ++i) {
                z.vec.add(a[i]);
            }
            return z;
        }
        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);
        }
        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);
        }
        Map handleUrl(String url, HashMap<String, String> query) {
            say("runUrl: %s", url);
            Pro[] kvArray = new Pro[query.size() * 2];
            int i = 0;
            for (String k : query.keySet()) {
                say("  query %s = %s", k, query.get(k));
                kvArray[i] = newStr(k);
                kvArray[i + 1] = newStr(query.get(k));
                i += 2;
            }
            Map qMap = newMap(kvArray);
            assert url.startsWith("/");
            if (url.equals("/")) {
                url = "/TopApp";
            }
            ;

            String[] word = url.substring(1).split("[.]");
            assert word.length > 0;
            String appName = word[0];

            Map result = null;
            try {
                Cls cls = getTerp().clss.get(appName.toLowerCase());
                Pro self = cls.eval("self new");
                Pro result_pro = self.asObj().eval(fmt("self handle: %s quer: %s", newStr(url), qMap));
                result = result_pro.asMap();
                if (result == null) {
                    toss("Sending <handle:> to instance of <%s> did not return a Map: <%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[] map_arr = new Pro[] { newStr("type"), newStr("text"), newStr("title"), newStr(ex.toString()),
                    newStr("value"), newStr(sb.toString()) };
                result = newMap(map_arr);
            }
            return result;

        }
        void loadInitFile() throws IOException {
            BufferedReader in = new BufferedReader(new FileReader(
                    "/home/strick/workspace/one/src/net/yak/stalk/init.txt"));
            while (true) {
                String line = in.readLine();
                // Break at EOF.
                if (line == null)
                    break;
                // Skip blank lines.
                if (line.trim().length() == 0)
                    continue;
                // Skip comments.
                if (line.trim().charAt(0) == '#')
                    continue;

                String[] words = line.split("\\s+");
                assert words.length > 2;
                assert words[0].equals("{");
                String command = words[1];

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

                    Cls cls = clss.get(className.toLowerCase());
                    if (cls == null) {
                        toss("Global <%s> not a class, for meth <%s> :: %s", className, methodName);
                    }
                    StringBuffer sb = new StringBuffer();
                    line = in.readLine();
                    while (!line.startsWith("}")) {
                        sb.append(line);
                        line = in.readLine();
                    }
                    String body = sb.toString();
                    cls.eval(fmt("self definemethod: %s abbrev: %s docstr: %s code: %s", newStr(methodName), newStr(""), newStr(""), newStr(body)));
                } else if (command.equals("class")) {
                    String className = words[2];
                    String superName = words[3];
                    Cls clsObj = clss.get(className.toLowerCase());
                    Cls supObj = clss.get(superName.toLowerCase());

                    if (clsObj == null && supObj != null) {
                        // We can create the class.
                        supObj.defineSubclass(className);
                    } else {
                        toss("Cannot define subclass <%s> of <%s>", className, superName);
                    }
                }
            }
        }

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