java/ch/wlkl/wsm/Formatter.java

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package ch.wlkl.wsm;

import static ch.wlkl.env.Env.*;
import ch.wlkl.wsm.Syntax.SynLex;
import java.util.ArrayList;
import java.util.List;

/**
 *
 * @author walter
 */
interface Formatter {

    /**
     * format ele
     *
     * @param ele the Formattable to format
     * @param fmt the Format to use
     */
    void format(Format fmt, Formattable ele);

    /**
     * recursively format a subelement
     *
     * @param fx index of subelement
     * @param ele the Formattable to format
     */
    void format(int fx, Formattable ele);

    /**
     * format a subelement
     *
     * @param fx index of subelement
     * @param txt the text for the subelement
     */
    void format(int fx, String txt);

    /**
     * format a subelement without data
     *
     * @param fx index of subelement
     */
    void format(int fx);

    void formatOk(Format fmt, Formattable ele);

    Formatter reset();

    void toOut();

    CharSequence text();
     
   static Formatter make() {
        return new FormatterOut();
    }

    static class FormatterOut implements Formatter {

        StringBuilder t = new StringBuilder();
        int ind = 0;
        final int fl0len;
        final String[] indAll = new String[20];
        final String delims;
        final String squash;

        Format format = null;
        String[][] code = null; // the code for all elements
        String[] coEl = null;  // the code for the current eleIx
        boolean chcOk = false;
        int lex = -1, lexNx = -1, // index of lex work in code
                eleIx = -1, // the index of the current element (in work[0])
                preIx = -1;// the index of the previous element (in work[0])
        boolean lexOk = true;   // true if output ok for lex
        int overCx = -1;
        int overInd = -1;
        Formattable subEle = null;
        String subStr = null;

        public FormatterOut() {
            this(0, null, null, null);
        }

        public FormatterOut(int l, String i, String d, String s) {
            fl0len = l < 5 ? 50 : l;
            i = i == null ? "   " : i;
            delims = d == null ? " \t\n" : d;
            squash = delims + (s == null ? ",.:;+*?()[]{}<>" : s);
            indAll[0] = "\n";
            for (int y = 1; y < indAll.length; y++) {
                indAll[y] = indAll[y - 1] + i;
            }
        }

        public Formatter reset() {
            t.setLength(ind = 0);
            return this;
        }

        /**
         * format ele with format fmt - dy if not applicable
         *
         * @param ele the Formattable to format
         * @param fmt the Format to apply
         */
        public void formatOk(Format fmt, Formattable ele) {
            chcOk = true;
            format(fmt, ele);
            if (!chcOk) {
                dy("cannot formatOk ele=", ele, ", fmt=", fmt);
            }
        }

        /**
         * recursively format a subelement to t
         *
         * @param fx index of subelement
         * @param ele the Formattable to format
         */
        @Override
        public void format(int fx, Formattable ele) {
            final Formattable oldEle = subEle;
            final String oldStr = subStr;
            subEle = ele;
            subStr = null;
            formatIx(fx);
            subEle = oldEle;
            subStr = oldStr;
        }

        /**
         * recursively format a subelement to t
         *
         * @param fx index of subelement
         * @param txt the text for subelement
         */
        @Override
        public void format(int fx, String txt) {
            final Formattable oldEle = subEle;
            final String oldStr = subStr;
            subEle = null;
            subStr = txt;
            formatIx(fx);
            subEle = oldEle;
            subStr = oldStr;
        }

        /**
         * format fx without data
         *
         * @param fx index of subelement
         */
        @Override
        public void format(int fx) {
            final Formattable oldEle = subEle;
            final String oldStr = subStr;
            subEle = null;
            subStr = null;
            formatIx(fx);
            subEle = oldEle;
            subStr = oldStr;
        }

        /**
         * output a newline
         */
        public void nl() {
            while (t.length() > 0 && t.charAt(t.length() - 1) == ' ') {
                t.setLength(t.length() - 1);
            }
            t.append(nlStr(ind));
        }

        /**
         * return newline with intendation
         *
         * @param i indentation level
         * @return
         */
        public String nlStr(int i) {
            return indAll[i];
        }

        /**
         * append a String, if necessary after a space (to avoid token joining)
         *
         * @param txt
         * @return
         */
        public FormatterOut app(String txt) {
            if (!txt.equals("") && squash.indexOf(txt.charAt(0)) < 0 && t.length() != 0 && squash.indexOf(t.charAt(t.length() - 1)) < 0) {
                t.append(' ');
            }
            t.append(txt);
            return this;
        }

        /**
         * check whether a part of output is a proper line (neither too long,
         * nor containing \n)
         *
         * @param b begin index
         * @param e end index
         * @return true if a proper line
         */
        public boolean checkInline(int b, int e) {
            if (b >= t.length()) {
                return true;
            }
            if (t.charAt(b) == '\n') {
                for (b++; b < e && t.charAt(++b) == ' '; b++) {
                }
            }
            if (e - b > fl0len) {
                return false;
            }
            int nx = t.indexOf("\n", b);
            return nx >= e || nx < b;
        }

        /**
         * checkInline from b to end
         *
         * @param b begin index
         * @return
         */
        public boolean checkInline(int b) {
            return checkInline(b, t.length());
        }

        /**
         * check the line lengths in region of output
         *
         * @param b begin index
         * @param e end index
         * @return true if all lines are not longer then fl0len
         */
        public boolean checkLinelen(int b, int e) {
            int linC = 0, linM = 0, ovrC = 0, ovrS = 0, nx;
            do {
                linC++;
                if (t.charAt(b) == '\n') {
                    for (b++; b < e && t.charAt(b) == ' '; b++) {
                    }
                }
                nx = t.indexOf("\n", b);
                int a = (nx <= e && nx >= b ? nx : e) - b;
                if (linM < a) {
                    linM = a;
                }
                if (a > fl0len) {
                    ovrC++;
                    ovrS += a - fl0len;
                }
                b = nx;
            } while (nx > 0 && nx < e);
            // say("linC="+linC + " linM="+linM + " ovrC=" + ovrC + " ovrS=" + ovrS);
            return ovrC == 0;
        }

        /**
         * check the line lengths in a end region of output
         *
         * @param b begin index
         * @return true if all lines are not longer then fl0len
         */
        public boolean checkLinelen(int b) {
            return checkLinelen(b, t.length());
        }

        public CharSequence text() {
            return t;
        };
     

        public void toOut() {
            for (String li : t.toString().split("\n")) {
                out(li);
            }
        }

        /**
         * format ele with format fmt
         *
         * @param ele the Formattable to format
         * @param fmt the Format to apply
         */
        @Override
        public void format(Format fmt, Formattable ele) {
            final Format oldFormat = format;
            final String[][] oldCode = code;
            final String[] oldCoEl = coEl;
            final int oldEleIx = eleIx, oldPreIx = preIx;
            debug("enter format fmt=", fmt, ", ele=", ele);
            format = fmt;
            code = format.code();
            coEl = null;
            eleIx = preIx = -99;
            assert code != null && code.length >= 2 : assFail("code fmt=", fmt, ", ele=", ele);
            formatChcLex(ele);
            format = oldFormat;     // restore state variables for recursive calls
            code = oldCode;
            coEl = oldCoEl;
            eleIx = oldEleIx;
            preIx = oldPreIx;
            debug("exit  format fmt=", fmt, ", ele=", ele);
        }

        /**
         * format ele, try different chc and increasing lex variants
         *
         * @param ele
         */
        public void formatChcLex(Formattable ele) {
            if (!chcOk) {
                return;
            }
            final int oldLen = t.length();
            final int oldInd = ind;
            final int oldLex = lex, oldLexNx = lexNx;
            final boolean oldLexOk = lexOk;
            final int oldOverInd = overInd, oldOverCx = overCx;
            int ff = 0;
            lex = 0;
            while (true) { // try different chc and lex
                chcOk = true;
                lexNx = 9;
                lexOk = true;
                overCx = -99;
                overInd = -99;
                formatIx(0);
                int ffNx = format.format(this, ff, ele);
                if (!chcOk && ff > 1) { // try next choice
                    ff = ffNx;
                    lex = 0;
                } else {
                    formatIx(code.length - 1); // last eleIx is unconditional postfix

                    if (overInd >= 0) {
                        ruleOver();
                    }
                    ind = oldInd;
                    if (lexNx >= 9 || (lexOk && checkLinelen(oldLen))) {
                        break;
                    }
                    lex = lexNx; // try next lex
                }
                t.setLength(oldLen);
            }
            lex = oldLex;
            lexNx = oldLexNx;
            lexOk = oldLexOk;
            overInd = oldOverInd;
            overCx = oldOverCx;
        }

        /**
         * append code for elementIx ix, possibly including subEle (at $ clause)
         */
        public void formatIx(int ix) {
            preIx = eleIx;
            eleIx = ix;
            if (eleIx >= code.length)
                outPa("");
            coEl = code[eleIx];
            int y = appCode(0);
            assert y >= coEl.length : assFail("not at end ix=", ix, ", coEl=", coEl);
        }

        /**
         * append code in coEl from index x
         *
         * @param x: current index in coEl
         * @return next index in coEl
         */
        public int appCode(int x) {
            for (; x < coEl.length; x++) { // // handle each clause in abst
                String s = coEl[x];
                char c = s.charAt(0);
                if (c == ':') {
                    x = appLex(x, -1) - 1;
                } else if (c == '|') {
                    if (s.charAt(1) == '(') {
                        x = appPre(x);
                    } else {
                        return x;
                    }
                } else if (c == '$') {
                    int eleStart = t.length();
                    if (subEle != null) {
                        format.format(this, eleIx, subEle);
                    } else if (subStr != null) {
                        app(subStr);
                    }
                    x = appLex(x + 1, eleStart) - 1;
                } else if (c == '%' || c == '\"') {
                    app(s.substring(1)); // append literal
                } else {
                    er("bad code " + c, x);
                }
            }
            return x;
        }

        /**
         * append pre alternatives, i.e. clauses |(d |d ... |)
         *
         * @param x: current index in coEl
         * @return next index in coEl
         */
        public int appPre(int x) {
            assert coEl[x].substring(0, 2).equals("|(") : assFail("bad pre x=", x, ", coEl=", coEl[x]);
            int v = Integer.valueOf(coEl[x].substring(2));
            while (preIx >= v) {
                x = lexUntil(x + 1, "|");
                if (coEl[x].equals("|)")) {
                    return x;
                }
                v = Integer.valueOf(coEl[x].substring(1));
            }
            x = appCode(x + 1);
            return lexUntil(x, "|)");
        }

        /**
         * append pre alternatives i.e. ${ | ... $} clauses
         *
         * @param x: current index in coEl
         * @return next index in coEl
         */
        public int lexUntil(int x, String u) {
            for (; x < coEl.length; x++) {
                if (coEl[x].charAt(0) != '|') {
                } else if (coEl[x].equals(u)) {
                    return x;
                } else if (coEl[x].length() <= 1) {
                } else if (coEl[x].substring(0, 2).equals("|(")) {
                    x = lexUntil(x + 1, "|)");
                } else if (u.equals("|")) {
                    return x;
                }
            }
            assert false: assFail(u, " not found in ", coEl);
            return x;
        }

        /**
         * append lex alternatives (: clauses)
         *
         * @param x: current index in coEl
         * @return next index in coEl after all alternatives
         */
        public int appLex(int x, int eleStart) {
            int v;
            for (;; x++) {
                if (x >= coEl.length || coEl[x].charAt(0) != ':') {
                    return x;
                } else if (lex < (v = Integer.valueOf(coEl[x].substring(1, 2)))) {
                    break;
                }
            }
            if (lexNx > v) {
                lexNx = v;
            }
            String s = coEl[x];
            for (int y = 2; y < s.length(); y++) {
                char c = s.charAt(y);
                if (c == ' ') {
                    t.append(c);
                } else if (c == 'n') {
                    nl();
                } else if (c == '>') {
                    ind++;
                } else if (c == '<') {
                    ind--;
                } else if (c == 'i') {
                    assert eleStart >= 0 : assFail("i not after $");
                    lexOk &= checkInline(eleStart); // after $i check inline                
                } else if (c == 'o') {
                    ruleOver();
                } else {
                    assert false: assFail("bad lex " + c, s, y);
                }
            }
            for (; x < coEl.length && coEl[x].charAt(0) == ':'; x++) {
            }
            return x;
        }

        /**
         * lex 'o' inserts nl at last o if necessary
         */
        public void ruleOver() {
            int laNL = t.lastIndexOf("\n");
            for (laNL++; laNL < t.length() && t.charAt(laNL) == ' '; laNL++) {
            };
            int ex = overCx;
            do { // move overCx before spaces
                overCx--;
            } while (overCx >= 0 && t.charAt(overCx) == ' ');
            if (laNL + 10 < overCx && t.length() - laNL > fl0len) { // insert NL if laNL faraway and laNL to overCX is not very small
                while (ex < t.length() && t.charAt(ex) == ' ') {
                    ex++;
                }
                t.replace(overCx + 1, ex, nlStr(overInd));
            }
            overCx = t.length();  // remember current state for next over
            overInd = ind;
        }

        void er(String e, int x) {
            dy(e, " cx=" + x + ": " + (x >= coEl.length ? " @end" : coEl[x]) + " coEl=", coEl);
        }
    }

    /**
     * create code from String
     *
     * @param s source of format
     * @return formatted code
     */
    public static String[][] toCode(String s, int es) {
        ArrayList<String[]> c = new ArrayList();
        ArrayList<String> ele = new ArrayList();
        int sx = 0;
        while (true) {
            ele.clear();
            sx = toCode(s, sx, ele);
            c.add(ele.toArray(new String[ele.size()]));
            if (sx >= s.length() || s.charAt(sx) != ',') {
                break;
            }
            sx++;
        }
        for (int i = c.size(); i <= es; i++) {
            c.add(new String[]{"$"});
        }
        if (c.size() < es + 2) {
            c.add(new String[0]);
        }

        String[][] abs = c.toArray(new String[c.size()][]);
        if (sx < s.length() && s.charAt(sx) == ':') {
            sx = mergeLex(s, sx + 1, abs);
        }
        assert sx >= s.length() :assFail("not at end", s, sx);
        return abs;
    }

    /**
     * handle source for one eleIx (until , or :)
     *
     * @param s source String
     * @param x start index in s
     * @param ele to add the generated code clauses
     * @return index for next character in s
     */
    public static int toCode(String s, int x, List<String> ele) {
        int bar = -1;
        int barEx = -1;
        int y;
        while (true) {
            char c = ' ';
            for (; x < s.length() && ' ' == (c = s.charAt(x)); x++) {
            }
            if (" ,:".indexOf(c) >= 0) { // at end
                if (bar >= 0) {
                    dy("end in or", s, x);
                }
                return x;
            }
            y = s.indexOf(' ', x + (c == '\'' ? 2 : 1)); // find word, possibly with escape '
            if (y < x) {
                y = s.length();
            }
            String w = s.substring(x, y);
            if (c == '|') {
                if (w.equals("|")) {
                    if (bar >= 0) {
                        ele.set(barEx, "|999");
                    }
                    bar = -1;
                    ele.add("|)");
                } else {
                    if (barEx < 0) {
                        dy("no preceeding | at " + x + ": " + s.substring(x) + " in " + s);
                    }
                    ele.set(barEx, bar < 0 ? "|(" + w.substring(1) : w);
                    int v = Integer.valueOf(w.substring(1));
                    if (v <= bar) {
                        dy("| not increasing");
                    }
                    bar = v;
                    ele.add("|??");
                }
                barEx = ele.size() - 1;
            } else if (c == '$') {
                ele.add("$");
                y = x;
            } else {
                ele.add("%" + (c == '\'' ? w.substring(1) : w));
            }
            x = y + 1;
        }
    }

    /**
     * merge lex syntax with existing code (remove lexx syntax from existing
     * code and merge lex from s
     */
    public static int mergeLex(String s, int x, String[][] abs) {
        ArrayList<String> ele = new ArrayList(), lex = new ArrayList();
        for (int ax = 0; ax < abs.length; ax++) {
            ele.clear();
            x = mergeLex(s, x, abs[ax], ele);
            //   mergeLex(abs[ax], lex, ele);
            abs[ax] = ele.toArray(new String[ele.size()]);
            if (x >= s.length()) {
            } else if (s.charAt(x) == ',') {
                x++;
            } else {
                dy("comma expexted not " + s.charAt(x) + " @" + x + " in " + s);
            }
        }
        if (x < s.length()) {
            dy("too many ,", s, x);
        }
        debug(3, "mergeLex end   " + s + ", abs=", abs);
        return x;
    }

    /**
     * merge lex syntax with existing code for one elementIx (remove lexx syntax
     * from existing code and merge lex from s
     */
    public static int mergeLex(String s, int x, String[] abs, List<String> ele) {
        int ly = 0;
        int y;
        char c;
        for (int ay = 0;; ay++) {
            if (ay < abs.length && "$%|".indexOf(abs[ay].charAt(0)) < 0) {
                continue;
            }
            y = x;
            while (true) { // add lex alternatives
                c = y < s.length() ? s.charAt(y) : ',';
                if (",|%$".indexOf(c) >= 0) {
                    if (y > x) {
                        ele.add(":9" + s.substring(x, y));
                    }
                    x = y;
                    break;
                } else if ("0123456789".indexOf(c) >= 0) {
                    ele.add(":" + c + s.substring(x, y));
                    x = ++y;
                } else {
                    y++;
                }
            }
            if (ay >= abs.length) {
                if (c != ',') {
                    dy("end of abs ", a2r(abs) + " overflow " + c + " in lex", s, y);
                }
                return x;
            } else {
                ele.add(abs[ay]);
                if (abs[ay].charAt(0) == c) {
                    x++;
                } else if (c != ',') {
                    dy("abs[" + ay + "] = " + abs[ay] + " in " + a2r(abs) + " mismatches " + c + " in lex", s, x);
                }
            }
        }
    }

    /**
     * replace existing (abstract) syntax until :, mergeLex afterwards
     */
    public static int mergeSyn(String s, int x, String[][] abs) {
        //      toOut("mergeLex begin " + s + " abs " + a2s(abs));
        ArrayList<String> ele = new ArrayList();
        // replace existing syntax until :
        for (int ax = 0; ax < abs.length && s.charAt(x) != ':'; ax++) {
            if (s.charAt(x) != ',') {
                ele.clear();
                x = toCode(s, x, ele);
                abs[ax] = ele.toArray(new String[ele.size()]);
            }
            if (s.charAt(x) == ',') {
                x++;
            }
        }
        if (s.charAt(x) != ':') {
            dy("colon : expected", s, x);
        }
        return mergeLex(s, x + 1, abs);
    }
}