package terse.talk;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

import terse.talk.Pro.Dict;
import terse.talk.Pro.Undefined;
import terse.talk.Pro.Num;
import terse.talk.Pro.Str;
import terse.talk.Pro.Vec;
import terse.talk.Talk.Terp;
import terse.talk.Talk.Visitor;

public class WebServer extends Talk {
    Async async;
    Terp terp;
    Str TYPE;
    Str LIST;
    Str VALUE;
    Str ACTION;
    Str TITLE;
    Str TEXT;
    Str EDIT;
    Str DRAW;
    Str UPPER;
    Str LOWER;
    Str FIELD1;
    Str FIELD2;
    Str VALUE1;
    Str VALUE2;
    String[] TABLE_PARAMS;
    Pattern LINK_P;

    public WebServer() {
        super();
        async = new Async("src/terse/talk");
        async.start();

        // TODO: Can we do without an initial terp?
        beginNewTerp(Talk.STANDARD_INIT_FILENAME);
    }
    void beginNewTerp(String initFile) {
        // TODO: get rid of this default terp.
        Terp t = new Terp("", initFile);
        setTerp(t);
    }
    void setTerp(Terp t) {
        this.terp = t;
        this.TYPE = t.newStr("type");
        this.LIST = t.newStr("list");
        this.VALUE = t.newStr("value");
        this.ACTION = t.newStr("action");
        this.TITLE = t.newStr("title");
        this.TEXT = t.newStr("text");
        this.EDIT = t.newStr("edit");
        this.DRAW = t.newStr("draw");
        this.UPPER = t.newStr("upper");
        this.LOWER = t.newStr("lower");
        this.FIELD1 = t.newStr("field1");
        this.FIELD2 = t.newStr("field2");
        this.VALUE1 = t.newStr("value1");
        this.VALUE2 = t.newStr("value2");
        this.TABLE_PARAMS = new String[] { "cellpadding", "8", "border", "1" };
        this.LINK_P = Pattern.compile("[|]link[|](/[-A-Za-z_0-9.:]*)[|]([^|]+)[|](.*)");
    }

    public static class Ht extends Talk { // For XSS Safety.
        private StringBuffer sb;

        public Ht() {
            this.sb = new StringBuffer();
        }
        public Ht(Ht that) {
            this.sb = new StringBuffer(that.sb.toString());
        }
        public Ht(String s) {
            this.sb = new StringBuffer(Talk.htmlEscape(s));
        }
        public String toString() {
            return sb.toString();
        }
        public Ht append(String s) {
            sb.append(htmlEscape(s));
            return this;
        }
        public Ht append(Ht that) {
            sb.append(that.toString());
            return this;
        }
        public Ht append(Pro that) {
            sb.append(htmlEscape(that.toString()));
            return this;
        }
        static public Ht entity(String name) {
            Ht ht = new Ht();
            ht.sb.append(fmt("&%s;", name));
            return ht;
        }
        static public Ht tag(Ht appendMe, String type, String[] args, String body) {
            return tag(appendMe, type, args, new Ht(body));
        }
        static public Ht tag(Ht appendMe, String type, String[] args, Ht body) {
            Ht z = appendMe == null ? new Ht() : appendMe;
            assert htmlTagP.matcher(type).matches();
            z.sb.append(fmt("<%s ", type));
            if (args != null) {
                for (int i = 0; i < args.length; i += 2) {
                    assert htmlTagP.matcher(args[i]).matches();
                    z.sb.append(fmt("%s=\"%s\" ", args[i], htmlEscape(args[i + 1])));
                }
            }
            z.sb.append(fmt(">%s</%s>", body, type));
            return z;
        }
        static public Ht tag(Ht appendMe, String type, String[] args) {
            Ht z = appendMe == null ? new Ht() : appendMe;
            assert htmlTagP.matcher(type).matches();
            z.sb.append(fmt("<%s ", type));
            if (args != null) {
                for (int i = 0; i < args.length; i += 2) {
                    assert htmlTagP.matcher(args[i]).matches();
                    z.sb.append(fmt("%s=\"%s\" ", args[i], htmlEscape(args[i + 1])));
                }
            }
            z.sb.append(" />");
            return z;
        }
    }

    public static class InspectorVisitor extends Visitor {
        Ht ht;

        InspectorVisitor() {
            ht = new Ht();
        }

        public void visitPro(Pro a) {
            ht.append("\"Pro:\"" + a.toString());
        }
        public void visitCls(Cls a) {
            ht.append("\"Cls:\"" + a.toString());
        }
        public void visitNum(Num a) {
            Ht.tag(ht, "span", new String[] { "style", "color=#555555" }, "" + a.toString());
        }
        public void visitStr(Str a) {
            Ht.tag(ht, "span", new String[] { "style", "color: #338033; font-family: courier" }, a.toString());
        }
        public void visitUndefined(Undefined a) {
            ht.append("Nil");
        }
        public void visitVec(Vec a) {
            ht.append("Vec{");
            Ht defs = new Ht();
            for (int i = 0; i < a.vec.size(); i++) {
                InspectorVisitor valueVisitor = new InspectorVisitor();
                valueVisitor.ht.append(" ( ");
                a.vec.get(i).visit(valueVisitor);
                Ht.tag(defs, "li", null, valueVisitor.ht.append(" );"));
            }
            Ht.tag(ht, "ol", null, defs);
            ht.append("}");
        }
        public void visitDict(Dict a) {
            ht.append("Dict{");
            Ht defs = new Ht();
            Vec[] assocs = a.sortedAssocs();
            for (int i = 0; i < assocs.length; i++) {
                InspectorVisitor keyVisitor = new InspectorVisitor();
                assocs[i].vec.get(0).visit(keyVisitor);
                InspectorVisitor valueVisitor = new InspectorVisitor();
                assocs[i].vec.get(1).visit(valueVisitor);
                Ht.tag(defs, "dt", null, keyVisitor.ht.append(";"));
                Ht.tag(defs, "dd", null, valueVisitor.ht.append(";"));
            }
            Ht.tag(ht, "dl", null, defs);
            ht.append("}");
        }
        public void visitUsr(Usr a) {
            ht.append("\"Usr:\"" + a.toString());
        }
    }
    
    String addSingleLetterQueryPartsToLink(String link, HashMap<String, String> query) {
        // Copy all single-letter query parts to the new link; they are sticky.
        boolean needsQuestion = link.indexOf('?') < 0;  // If no '?' then it needs a '?'
        for (String k : query.keySet()) {
            if (k.length() == 1) {
                link = link + (needsQuestion ? "?" : "&") + k + "=" + query.get(k);
                needsQuestion = false;
            }
        }
        return link;
    }

    Ht maybeLink(String s, HashMap<String, String> query) {
        Matcher m = LINK_P.matcher(s);
        if (m.matches()) {
            String link = addSingleLetterQueryPartsToLink(m.group(1), query);
            String label = m.group(2);
            String text = m.group(3);
            String[] params = new String[] { "href", link };
            Ht z = Ht.tag(null, "a", params, label);
            z.append(" ");
            z.append(text);
            return z;
        } else {
            return new Ht(s);
        }
    }
    Ht maybeLink(Pro x, HashMap<String, String> query) {
        return maybeLink(maybeString(x, null), query);
    }
    String maybeString(Pro x, String defaultString) {
        if (x == null) {
            return defaultString == null ? "nil" : defaultString;
        }
        Str s = x.asStr();
        if (s == null) {
            return defaultString == null ? x.toString() : defaultString;
        }
        return s.str;
    }

    public class EditRenderer extends BaseRenderer {
        public EditRenderer(Dict dict, HashMap<String, String> query) {
            super(dict, query);
        }
        public Ht innerHt() {
            String text = maybeString(dict.dict.get(VALUE), "ERROR: Missing value in dict.");
            String action = maybeString(dict.dict.get(ACTION), "ERROR: Missing action in dict.");
            Ht page = new Ht();

            // name=text wrap=virtual rows=25 cols=70 style=" width: 95%;  "
            Ht form = new Ht();
            String[] textareaParams = new String[] { "name", "text", "wrap", "virtual", "rows", "25", "cols", "80",
                "style", "width; 95%" };
            if (dict.dict.containsKey(FIELD1)) {
                String fname = maybeString(dict.dict.get(FIELD1), "?");
                String fvalue = maybeString(dict.dict.get(VALUE1), "?");
                Ht.tag(form, "b", null, fname);
                form.append(Ht.entity("nbsp"));
                Ht.tag(form, "input", new String[] { "name", fname, "value", fvalue }, "");
                Ht.tag(form, "p", null, "");
            }
            if (dict.dict.containsKey(FIELD2)) {
                String fname = maybeString(dict.dict.get(FIELD2), "?");
                String fvalue = maybeString(dict.dict.get(VALUE2), "?");
                Ht.tag(form, "b", null, fname);
                form.append(Ht.entity("nbsp"));
                Ht.tag(form, "input", new String[] { "name", fname, "value", fvalue }, "");
                Ht.tag(form, "p", null, "");
            }
            Ht.tag(form, "textarea", textareaParams, text);
            Ht.tag(form, "p", null, "");
            Ht.tag(form, "input", new String[] { "type", "submit", "value", "Submit" }, "");
            Ht.tag(form, "input", new String[] { "type", "reset" }, "");

            Ht.tag(page, "form", new String[] { "method", "POST", "action", addSingleLetterQueryPartsToLink(action, query) }, form);
            return page;
        }
    }

    public class DrawRenderer extends BaseRenderer {
        public DrawRenderer(Dict dict, HashMap<String, String> query) {
            super(dict, query);
        }
        public Ht innerHt() {
            Vec v = dict.dict.get(VALUE).asVec();
            Ht svg = new Ht();

            for (int i = 0; i < v.vec.size(); i++) {
                Vec u = v.vec.get(i).asVec();
                if (u.vec.get(0).asStr().str.equals("line")) {
                    String x1 = u.vec.get(1).asNum().toString();
                    String y1 = u.vec.get(2).asNum().toString();
                    String x2 = u.vec.get(3).asNum().toString();
                    String y2 = u.vec.get(4).asNum().toString();
                    String wid = "1";
                    if (u.vec.size() > 5)
                        wid = u.vec.get(5).asNum().toString();
                    String rgb = "rgb(0,0,0)";
                    if (u.vec.size() > 6)
                        rgb = u.vec.get(6).asStr().str;
                    String[] how = strs("x1", x1, "y1", y1, "x2", x2, "y2", y2, "style", fmt(
                            "stroke:%s;stroke-width:%s", rgb, wid));
                    Ht.tag(svg, "line", how);
                }
                if (u.vec.get(0).asStr().str.equals("rect")) {
                    String x = u.vec.get(1).asNum().toString();
                    String y = u.vec.get(2).asNum().toString();
                    String width = u.vec.get(3).asNum().toString();
                    String height = u.vec.get(4).asNum().toString();
                    String wid = "1";
                    if (u.vec.size() > 5)
                        wid = u.vec.get(5).asNum().toString();
                    String rgb = "rgb(0,0,0)";
                    if (u.vec.size() > 6)
                        rgb = u.vec.get(6).asStr().str;
                    String[] how = strs("x", x, "y", y, "width", width, "height", height, "stroke-width", wid, "fill",
                            rgb);
                    Ht.tag(svg, "rect", how);
                }
            }

            String[] params = strs("xmlns", "http://www.w3.org/2000/svg", "version", "1.1", "xmlns:xlink",
                    "http://www.w3.org/1999/xlink");
            Pro width = dict.dict.get(terp.newStr("width"));
            Pro height = dict.dict.get(terp.newStr("height"));
            if (width != null)
                params = append(append(params, "width"), width.asNum().toString());
            if (height != null)
                params = append(append(params, "height"), height.asNum().toString());

            return Ht.tag(null, "svg", params, svg);
        }
    }

    public class TextRenderer extends BaseRenderer {
        public TextRenderer(Dict dict, HashMap<String, String> query) {
            super(dict, query);
        }
        public Ht innerHt() {
            String text = maybeString(dict.dict.get(VALUE), "ERROR: Missing value in dict.");
            Ht page = new Ht();
            Ht.tag(page, "pre", null, text);
            return page;
        }
    }

    public class ListRenderer extends BaseRenderer {
        public ListRenderer(Dict dict, HashMap<String, String> query) {
            super(dict, query);
        }
        public Ht innerHt() {
            Ht rows = new Ht();
            Vec list = (Vec) dict.dict.get(VALUE);
            int llen = list.vec.size();
            for (int i = 0; i < llen; i++) {
                Vec tuple = (Vec) list.vec.get(i);
                Ht cols = new Ht();
                for (int j = 0; j < tuple.vec.size(); j++) {
                    Pro x = tuple.vec.get(j);
                    Ht.tag(cols, "td", null, maybeLink(x, query));
                }
                Ht.tag(rows, "tr", null, cols);
            }
            Ht page = new Ht();
            Ht.tag(page, "table", TABLE_PARAMS, rows);
            return page;
        }
    }

    public class BaseRenderer {
        Dict dict;
        HashMap<String, String> query;

        public BaseRenderer(Dict dict, HashMap<String, String> query) {
            this.dict = dict;
            this.query = query;
        }
        public Ht toHt() {
            try {
                Ht head = new Ht();
                Ht.tag(head, "title", null, maybeString(dict.dict.get(TITLE), "Untitled"));

                Ht body = new Ht();
                Ht.tag(body, "b", null, maybeString(dict.dict.get(TITLE), "Untitled"));
                Ht.tag(body, "hr", null, "");
                body.append(this.innerHt());
                Ht.tag(body, "hr", null, "");

                // Ask Home for standard footer buttons, and render them.
                Vec fb = terp.newTmp().eval("Home footerButtons").asVec();
                Ht footer = new Ht();
                for (int i = 0; i < fb.vec.size(); i++) {
                    String s = ((Pro) fb.vec.get(i)).asStr().str;
                    footer.append(Ht.entity("nbsp"));
                    footer.append(maybeLink(s, query));
                    footer.append(Ht.entity("nbsp"));
                }
                Ht.tag(body, "tt", null, footer);
                Ht.tag(body, "p", null, "");
                Ht.tag(body, "small", null, "\"DEBUG:\"" + dict.toString());

                return Ht.tag(null, "html", null, head.append(body));

            } catch (Exception ex) {
                StackTraceElement[] arr = ex.getStackTrace();

                Ht pre = new Ht();
                pre.append(ex.toString());
                for (StackTraceElement x : arr) {
                    pre.append("\n***  " + x.toString());
                }

                Ht z = new Ht();
                Ht.tag(z, "pre", null, pre);
                Ht.tag(z, "hr", null, "");
                Ht.tag(z, "small", null, dict.toString());
                return z;
            }
        }
        Ht innerHt() {
            // A crude fallback if 'type' field is unknown.
            InspectorVisitor vis = new InspectorVisitor();
            vis.visitDict(dict);
            return vis.ht;
        }
    }

    public class TerpHandler extends Talk implements HttpHandler {
        public TerpHandler() {
            super();
            System.err.println("Initialized Terp Hander.");
        }
        public void handle(HttpExchange t) throws IOException {
            // beginNewTerp();
            System.err.println("handle().");
            String response = "?";
            String responseType = "text/plain";
            try {
                // Path and body query.
                URI uri = t.getRequestURI();
                String path = uri.getPath();
                InputStream is = t.getRequestBody();
                String bodyQuery = readAll(is);

                // Url query.
                HashMap<String, String> query = new HashMap<String, String>();
                String uriQuery = uri.getQuery();
                uriQuery = uriQuery == null ? "" : uriQuery; // Don't be null.

                String[] parts = (uriQuery + "&" + bodyQuery).split("&");
                for (String part : parts) {
                    String[] kv = part.split("=", 2);
                    if (kv.length == 2)
                        query.put(kv[0], URLDecoder.decode(kv[1], "UTF-8"));
                }

                // // Special Hack while debugging and testing:
                // // Going HOME to '/' actually resets the terp.
                // if (path.equals("/")) {
                // beginNewTerp(Talk.STANDARD_INIT_FILENAME);
                // }

                try {
                    // Create a job.
                    Async.Job job = async.newJob(path, query);
                    async.inQueue.put(job);
                    // Block waiting for a reply.
                    Async.Result reply = job.reply.take();
                    setTerp(reply.terp);

                    Dict dict = reply.renderMe;
                    String raw = query.get("raw");

                    Str type = dict.dict.get(TYPE).asStr();
                    BaseRenderer r;
                    if (raw != null) {
                        r = new BaseRenderer(dict, query);
                    } else if (type.equals(LIST)) {
                        r = new ListRenderer(dict, query);
                    } else if (type.equals(TEXT)) {
                        r = new TextRenderer(dict, query);
                    } else if (type.equals(EDIT)) {
                        r = new EditRenderer(dict, query);
                    } else if (type.equals(DRAW)) {
                        r = new DrawRenderer(dict, query);
                    } else {
                        r = new BaseRenderer(dict, query); // Crude fallback if
                                                           // unknown
                        // type.
                    }

                    response = r.toHt().toString();
                    responseType = "text/html";
                } catch (Exception ex) {
                    ex.printStackTrace();
                    response = "*** ERROR *** " + ex;
                    System.err.println("[1] " + response);
                }
            } catch (Exception ex) {
                response = "*** (Outer handle) ERROR ***" + ex;
                System.err.println("[2] " + response);
            }

            Headers respHeads = t.getResponseHeaders();
            respHeads.set("Content-Type", responseType);
            t.sendResponseHeaders(200, response.length());
            OutputStream os = t.getResponseBody();
            os.write(response.getBytes());
            os.close();
        }
        public String readAll(InputStream is) throws IOException {
            StringBuffer b = new StringBuffer();
            while (true) {
                int c = is.read();
                if (c < 0)
                    break;
                b.append((char) c);
            }
            return b.toString();
        }
    }

    public static class FavIconHandler extends Talk implements HttpHandler {

        public void handle(HttpExchange t) throws IOException {
            Headers respHeads = t.getResponseHeaders();
            respHeads.set("Content-Type", "text/plain");
            t.sendResponseHeaders(400, 0);
            OutputStream os = t.getResponseBody();
            os.close();
        }
    }

    public void run(String[] args) throws IOException {
        HttpServer server = HttpServer.create(new InetSocketAddress(8000), 5);
        server.createContext("/favicon.ico", new FavIconHandler());
        server.createContext("/", new TerpHandler());
        server.setExecutor(null); // creates a default executor
        server.start();
    }

    public static void main(String[] args) throws IOException {
        new WebServer().run(args);
    }
}
