/* Tokens and patterns; keep arrays in order of token numbers */
const T = {
    END:-2, ERR:-1, NAME:0, COMP:1, UNARY:2, AND:3, OR:4, LPAR:5, RPAR:6, STR:7
}
const patterns = [
    /[a-z]+/, /=|!=|~=|!~=/, /!/, /&&/, /\|\|/, /\(/, /\)/, /"([^"]*)"/
]

function* lex(str) {
    while(str = str.trim()) {
        let t = T.ERR, best = undefined;

        for(const i in patterns) {
            const m = str.match(patterns[i]);
            if(m === null || m.index != 0 || (best && m[0].length < best[0].length)) continue;
            t = i;
            best = m;
        }

        if(t == T.ERR) throw "LEXING ERROR";
        yield [t, best[best.length-1]];
        str = str.slice(best[0].length);
    }
    /* Finish with a continuous stream of T.END */
    while(true) yield [T.END, undefined];
}

class Parser {
    constructor(lex) {
        this.lex = lex;
        [this.la, this.value] = lex.next().value;
    }

    /* Expect a token of type t, returns the value */
    expect(t) {
        const v = this.value;
        if(this.la != t) throw (`SYNTAX ERROR, EXPECTED ${t}, GOT ${this.la} ${this.value}`);
        [this.la, this.value] = this.lex.next().value;
        return v;
    }

    /* filter -> END | expr END */
    filter() {
        if(this.la == T.END) return true;
        const e = this.expr();
        this.expect(T.END);
        return e;
    }
    /* expr -> term_and | term_and OR expr */
    expr() {
        const left = this.term_and();
        if(this.la != T.OR) return left;
        this.expect(T.OR);
        return {
            type: "Or",
            left: left,
            right: this.expr(),
        };
    }
    /* term_and -> unary | unary AND term_and */
    term_and() {
        const left = this.unary();
        if(this.la != T.AND) return left;
        this.expect(T.AND);
        return {
            type: "And",
            left: left,
            right: this.term_and(),
        };
    }
    /* unary -> UNARY* atom */
    unary() {
        if(this.la == T.UNARY) return {
            type: "Unary",
            op: this.expect(T.UNARY),
            val: this.unary(),
        };
        return this.atom();
    }
    /* atom -> NAME COMP STR | LPAR expr RPAR */
    atom() {
        if(this.la == T.LPAR) {
            this.expect(T.LPAR);
            const e = this.expr();
            this.expect(T.RPAR);
            return e;
        }

        let e = {
            type: "Atom",
            field: this.expect(T.NAME),
            op: this.expect(T.COMP),
        };
        const str = this.expect(T.STR);

        /* Precompile regular expressions */
        if(e.op == "~=" || e.op == "!~=")
            e.regex = new RegExp(str, "i");
        else
            e.str = str;
        return e;
    }
}

function ev(e, row, fields) {
    switch(e.type) {
    case "Atom":
        const val = row.children[fields[e.field]].textContent.trim();
        if(e.op == "=")   return val == e.str;
        if(e.op == "!=")  return val != e.str;
        if(e.op == "~=")  return e.regex.test(val);
        if(e.op == "!~=") return !e.regex.test(val);
    case "Unary":
        if(e.op == "!") return !ev(e.val, row, fields);
    case "And":
        return ev(e.left, row, fields) && ev(e.right, row, fields);
    case "Or":
        return ev(e.left, row, fields) || ev(e.right, row, fields);
    }
}

function filter_update(input) {
    const t  = document.querySelector(input.parentNode.dataset.target);
    const th = t.querySelectorAll("tr:first-child > th");

    /* Generate the names of fields from the header */
    let fields = {};
    for(let i = 0; i < th.length; i++) {
        const name = th[i].dataset.filter;
        if(name) fields[name] = i;
    }

    /* Parse the filter as an expression */
    const parser = new Parser(lex(input.value));
    const expr = parser.filter();

    /* Evaluate the expression on each row of the table */
    const rows = t.querySelectorAll("tr:not(:first-child)");
    for(const row of rows) {
        const ok = (expr === true) || ev(expr, row, fields);
        row.style.display = ok ? "table-row" : "none";
    }
}
