package net.yak.stalk;

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 net.yak.stalk.Pro.Assoc;
import net.yak.stalk.Pro.Dict;
import net.yak.stalk.Pro.Undefined;
import net.yak.stalk.Pro.Num;
import net.yak.stalk.Pro.Str;
import net.yak.stalk.Pro.Vec;
import net.yak.stalk.Talk.Terp;
import net.yak.stalk.Talk.Visitor;

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

    public WebServer() {
        super();
        beginNewTerp();
    }
    void beginNewTerp() {
        Terp t = new Terp(Talk.STANDARD_INIT_FILENAME);
        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.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 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;
        }
    }

    public static class InspectorVisitor extends Visitor {
        Ht ht;

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

        void visitPro(Pro a) {
            ht.append("<Pro>:" + a);
        }
        void visitCls(Cls a) {
            ht.append("<Cls>:" + a);
        }
        void visitNum(Num a) {
            Ht.tag(ht, "span", new String[] { "style", "color=#555555" }, "" + a.num);
        }
        void visitStr(Str a) {
            Ht.tag(ht, "span", new String[] { "style", "color: #338033; font-family: courier" }, a.str);
        }
        void visitUndefined(Undefined a) {
            ht.append("Nil");
        }
        void visitVec(Vec a) {
            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);
        }
        void visitDict(Dict a) {
            Ht defs = new Ht();
            Assoc[] assocs = a.sortedAssocs();
            for (int i = 0; i < assocs.length; i++) {
                InspectorVisitor keyVisitor = new InspectorVisitor();
                assocs[i].key.visit(keyVisitor);
                InspectorVisitor valueVisitor = new InspectorVisitor();
                assocs[i].value.visit(valueVisitor);
                Ht.tag(defs, "dt", null, keyVisitor.ht.append(" {"));
                Ht.tag(defs, "dd", null, valueVisitor.ht.append(" }"));
            }
            Ht.tag(ht, "dl", null, defs);
        }
        void visitAssoc(Assoc a) {
            ht.append("<Assoc>:" + a);
        }
        void visitUsr(Usr a) {
            ht.append("<Usr>:" + a);
        }
    }

    Ht maybeLink(String s) {
        Matcher m = LINK_P.matcher(s);
        if (m.matches()) {
            String link = m.group(1);
            String text = m.group(2);
            String[] params = new String[] { "href", link };
            return Ht.tag(null, "a", params, text);
        } else {
            return new Ht(s);
        }
    }
    Ht maybeLink(Pro x) {
        return maybeLink(maybeString(x, null));
    }
    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 EditDictRenderer {
        Dict dict;

        public EditDictRenderer(Dict dict) {
            this.dict = dict;
        }
        public Ht toHt() {
            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, "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, "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", action }, form);
            return page;
        }
    }

    public class TextDictRenderer {
        Dict dict;

        public TextDictRenderer(Dict dict) {
            this.dict = dict;
        }
        public Ht toHt() {
            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 ListDictRenderer {
        Dict dict;

        public ListDictRenderer(Dict dict) {
            this.dict = dict;
        }
        public Ht toHt() {
            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));
                }
                Ht.tag(rows, "tr", null, cols);
            }
            Ht page = new Ht();
            Ht.tag(page, "table", TABLE_PARAMS, rows);
            return page;
        }
    }

    public class DictRenderer {
        Dict  dict;

        public DictRenderer(Dict dict) {
            this.dict = dict;
        }
        public Ht toHt() {
            try {
                Str type = dict.dict.get(TYPE).asStr();
                Ht inner;
                if (type.equals(LIST)) {
                    inner = new ListDictRenderer(dict).toHt();
                } else if (type.equals(TEXT)) {
                    inner = new TextDictRenderer(dict).toHt();
                } else if (type.equals(EDIT)) {
                    inner = new EditDictRenderer(dict).toHt();
                } else {
                    inner = crudeFallbackToHt();
                }
                
                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(inner);
                Ht.tag(body, "hr", null, "");
                Ht.tag(body, "small", null, 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, "p", null, dict.toString());
                return z;
            }
        }
        Ht crudeFallbackToHt() {
            InspectorVisitor vis = new InspectorVisitor();
            vis.visitDict(dict);
            vis.ht.append(dict.toString());
            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 {
                URI uri = t.getRequestURI();
                String path = uri.getPath();
                InputStream is = t.getRequestBody();
                String bodyQuery = readAll(is);

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

                try {
                    Dict dict = terp.handleUrl(path, query);
                    response = new DictRenderer(dict).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);
    }
}
