const match = str => input => {
    if (input.startsWith(str)) {
        return [[str, input.slice(str.length)]]
    }
    return [];
};

const map = (f, m) => input => {
    return m(input).map(([v, rest]) => [f(v), rest]);
};

const apply = (m1, m2) => input => {
    return m1(input).flatMap(([f, rest]) => m2(rest).map(([v, rest]) => [f(v), rest]));
};

const pure = v => input => [[v, input]];

const liftA = (f, ...ms) => input => {
    if (ms.length <= 0) return []

    let results = map(v => [v], ms[0])(input);
    for (let i = 1; i < ms.length; i++) {
        results = results.flatMap(([vals, rest]) =>
            ms[i](rest).map(([val, rest]) => [[...vals, val], rest])
        );
    }
    return results.map(([vals, rest]) => [f(...vals), rest]);
};

const many1 = (m) => liftA((x, xs) => [x].concat(xs), m, oneOf([
    lazy(() => many1(m)),
    pure([])
]));

const many = (m) => oneOf([ pure([]), many1(m) ]);

const oneOf = ms => input => {
    return ms.flatMap(m => m(input));
};

const takeWhileRegex0 = regex => input => {
    let idx = 0;
    while (idx < input.length && regex.test(input[idx])) {
        idx++;
    }
    return [[input.slice(0, idx), input.slice(idx)]];
};

const takeWhileRegex = regex => input => {
    const result = takeWhileRegex0(regex)(input);
    if (result[0][0].length > 0) return result;
    return [];
};

const spaces = takeWhileRegex0(/\s/);

const digits = takeWhileRegex(/\d/);

const alphas = takeWhileRegex(/[a-zA-Z]/);

const left = (m1, m2) => liftA((a, _) => a, m1, m2);

const right = (m1, m2) => liftA((_, b) => b, m1, m2);

const word = s => left(match(s), spaces);

const end = s => s.length == 0 ? [['', '']] : [];

const lazy = deferred => input => deferred()(input);

const ident = left(alphas, spaces);

const number = oneOf([
    liftA((a, b) => a + b, word("-"), left(digits, spaces)),
    left(digits, spaces),
]);

const basicExpr = oneOf([
    map(n => `lit(${n})`, number),
    map(x => `var(${x})`, ident),
    liftA((lp, v, rp) => v, word("("), lazy(() => expr), word(")")),
]);

const opExpr = oneOf([
  liftA((_a, _b, e) => ["plus", e], word("+"), spaces, lazy(() => expr)),
  liftA((_a, _b, e) => ["minus", e], word("-"), spaces, lazy(() => expr)),
]);

const flatten = (e, es) => {
    return es.reduce((e1, [op, e2]) => `${op}(${e1}, ${e2})`, e);
}

const expr = oneOf([
  basicExpr,
  liftA(flatten, basicExpr, many(opExpr)),
]);

const basicStmt = oneOf([
    liftA((x, _, e) => `assign(${x}, ${e})`, ident, word("="), expr),
    word("noop"),
]);

const stmt = oneOf([
    basicStmt,
    liftA((_if, _lp_, cond, _rp, _lbr1_, s1, _rbr1, _else, _lbr2, s2, _rbr2) => `if(${cond}, ${s1}, ${s2})`,
        word("if"), word("("), expr, word(")"),
        word("{"), lazy(() => stmtSeq), word("}"),
        word("else"), word("{"), lazy(() => stmtSeq), word("}")),
    liftA((_while, _lp_, cond, _rp, _lbr_, s1, _rbr) => `while(${cond}, ${s1})`,
        word("while"), word("("), expr, word(")"),
        word("{"), lazy(() => stmtSeq), word("}")),
]);

const stmtSeq = oneOf([
    liftA((s1, _semi, rest) => `seq(${s1}, ${rest})`, stmt, word(";"), lazy(() => stmtSeq)),
    stmt,
]);

const parseWhole = m => string => {
    const result = left(m, end)(string);
    console.log(result);
    if (result.length > 0) return result[0][0];
    return null;
}

window.parseExpr = parseWhole(expr);
window.parseBasicStmt = parseWhole(basicStmt);
window.parseStmt = parseWhole(stmtSeq);
