'use strict';

var util_ts = require('./util/util.cjs');

function mapInner(r, f) {
    return (r.matched)
        ? ({
            matched: true,
            position: r.position,
            value: f(r.value, r.position),
        })
        : r;
}

function mapOuter(r, f) {
    return (r.matched) ? f(r) : r;
}

function ab(pa, pb, join) {
    return (data, i) => mapOuter(pa(data, i), ma => mapInner(pb(data, ma.position), (vb, j) => join(ma.value, vb, data, i, j)));
}

function abc(pa, pb, pc, join) {
    return (data, i) => mapOuter(pa(data, i), ma => mapOuter(pb(data, ma.position), mb => mapInner(pc(data, mb.position), (vc, j) => join(ma.value, mb.value, vc, data, i, j))));
}

function action(
f) {
    return (data, i) => {
        f(data, i);
        return {
            matched: true,
            position: i,
            value: null,
        };
    };
}

function ahead(p) {
    return (data, i) => mapOuter(p(data, i), m1 => ({
        matched: true,
        position: i,
        value: m1.value,
    }));
}

function all(...ps) {
    return (data, i) => {
        const result = [];
        let position = i;
        for (const p of ps) {
            const r1 = p(data, position);
            if (r1.matched) {
                result.push(r1.value);
                position = r1.position;
            }
            else {
                return { matched: false };
            }
        }
        return {
            matched: true,
            position: position,
            value: result,
        };
    };
}

function any(data, i) {
    return (i < data.tokens.length)
        ? {
            matched: true,
            position: i + 1,
            value: data.tokens[i],
        }
        : { matched: false };
}

function chain(
p,
f) {
    return (data, i) => mapOuter(p(data, i), m1 => f(m1.value, data, i, m1.position)(data, m1.position));
}

function chainReduce(
acc,
f) {
    return (data, i) => {
        let loop = true;
        let acc1 = acc;
        let pos = i;
        do {
            const r = f(acc1, data, pos)(data, pos);
            if (r.matched) {
                acc1 = r.value;
                pos = r.position;
            }
            else {
                loop = false;
            }
        } while (loop);
        return {
            matched: true,
            position: pos,
            value: acc1,
        };
    };
}

function condition(cond, pTrue, pFalse) {
    return (data, i) => (cond(data, i))
        ? pTrue(data, i)
        : pFalse(data, i);
}

function decide(p) {
    return (data, i) => mapOuter(p(data, i), m1 => m1.value(data, m1.position));
}

function eitherOr(pa, pb) {
    return (data, i) => {
        const r1 = pa(data, i);
        return (r1.matched)
            ? r1
            : pb(data, i);
    };
}

function emit(value) {
    return (data, i) => ({
        matched: true,
        position: i,
        value: value,
    });
}

function end(data, i) {
    return (i < data.tokens.length)
        ? { matched: false }
        : {
            matched: true,
            position: i,
            value: true,
        };
}

function error(message) {
    return (data, i) => {
        throw new Error((message instanceof Function) ? message(data, i) : message);
    };
}

function fail(
data, i) {
    return { matched: false };
}

function first(...ps) {
    return (data, i) => {
        for (const p of ps) {
            const result = p(data, i);
            if (result.matched) {
                return result;
            }
        }
        return { matched: false };
    };
}

function map(p, mapper) {
    return (data, i) => mapInner(p(data, i), (v, j) => mapper(v, data, i, j));
}

function flatten1(p) {
    return map(p, vs => vs.flatMap(v => v));
}

function flatten(...ps) {
    return flatten1(all(...ps));
}

function last(...ps) {
    return (data, i) => {
        for (let j = ps.length - 1; j >= 0; j--) {
            const result = ps[j](data, i);
            if (result.matched) {
                return result;
            }
        }
        return { matched: false };
    };
}

function left(pa, pb) {
    return ab(pa, pb, va => va);
}

function reduceLeft(
acc,
p,
reducer) {
    return chainReduce(acc, acc => map(p, (v, data, i, j) => reducer(acc, v, data, i, j)));
}

function leftAssoc1(pLeft, pOper) {
    return chain(pLeft, v0 => reduceLeft(v0, pOper, (acc, f) => f(acc)));
}

function leftAssoc2(pLeft, pOper, pRight) {
    return chain(pLeft, v0 => reduceLeft(v0, ab(pOper, pRight, (f, y) => [f, y]), (acc, [f, y]) => f(acc, y)));
}

function longest(...ps) {
    return (data, i) => {
        let match = undefined;
        for (const p of ps) {
            const result = p(data, i);
            if (result.matched && (!match || match.position < result.position)) {
                match = result;
            }
        }
        return match || { matched: false };
    };
}

function make(
f) {
    return (data, i) => ({
        matched: true,
        position: i,
        value: f(data, i),
    });
}

function takeWhile(p, test) {
    return (data, i) => {
        const values = [];
        let success = true;
        do {
            const r = p(data, i);
            if (r.matched && test(r.value, values.length + 1, data, i, r.position)) {
                values.push(r.value);
                i = r.position;
            }
            else {
                success = false;
            }
        } while (success);
        return {
            matched: true,
            position: i,
            value: values,
        };
    };
}

function many(p) {
    return takeWhile(p, () => true);
}

function many1(p) {
    return ab(p, many(p), (head, tail) => [head, ...tail]);
}

function mapR(
p,
mapper) {
    return (data, i) => mapOuter(p(data, i), m => mapper(m, data, i));
}

function middle(pa, pb, pc) {
    return abc(pa, pb, pc, (ra, rb) => rb);
}

function not(p) {
    return (data, i) => {
        const r = p(data, i);
        return (r.matched)
            ? { matched: false }
            : {
                matched: true,
                position: i,
                value: true,
            };
    };
}

function option(p, def) {
    return (data, i) => {
        const r = p(data, i);
        return (r.matched)
            ? r
            : {
                matched: true,
                position: i,
                value: def,
            };
    };
}

function peek(p, f) {
    return (data, i) => {
        const r = p(data, i);
        f(r, data, i);
        return r;
    };
}

function recursive(f) {
    return function (data, i) {
        return f()(data, i);
    };
}

function reduceRight(
p,
acc,
reducer) {
    return map(many(p), (vs, data, i, j) => vs.reduceRight((acc, v) => reducer(v, acc, data, i, j), acc));
}

function right(pa, pb) {
    return ab(pa, pb, (va, vb) => vb);
}

function rightAssoc1(pOper, pRight) {
    return ab(reduceRight(pOper, (y) => y, (f, acc) => y => f(acc(y))), pRight, (f, v) => f(v));
}

function rightAssoc2(pLeft, pOper, pRight) {
    return ab(reduceRight(ab(pLeft, pOper, (x, f) => [x, f]), (y) => y, ([x, f], acc) => y => f(x, acc(y))), pRight, (f, v) => f(v));
}

function satisfy(test) {
    return (data, i) => (i < data.tokens.length && test(data.tokens[i], data, i))
        ? {
            matched: true,
            position: i + 1,
            value: data.tokens[i],
        }
        : { matched: false };
}

function sepBy1(pValue, pSep) {
    return ab(pValue, many(right(pSep, pValue)), (head, tail) => [head, ...tail]);
}

function sepBy(pValue, pSep) {
    return eitherOr(sepBy1(pValue, pSep), emit([]));
}

function skip(...ps) {
    return map(all(...ps), () => null);
}

function start(data, i) {
    return (i !== 0)
        ? { matched: false }
        : {
            matched: true,
            position: i,
            value: true,
        };
}

function takeMinMax(p, min, max) {
    return (data, i) => {
        const values = [];
        let j = i;
        for (let k = 0; k < (max ?? Infinity); k++) {
            const r = p(data, j);
            if (r.matched) {
                values.push(r.value);
                j = r.position;
            }
            else {
                break;
            }
        }
        if (min !== undefined && values.length < min) {
            return { matched: false };
        }
        return {
            matched: true,
            position: j,
            value: values,
        };
    };
}

function takeN(p, n) {
    return (data, i) => {
        const values = [];
        let j = i;
        for (let k = 0; k < n; k++) {
            const r = p(data, j);
            if (r.matched) {
                values.push(r.value);
                j = r.position;
            }
            else {
                return { matched: false };
            }
        }
        return {
            matched: true,
            position: j,
            value: values,
        };
    };
}

function takeUntil(
p,
test) {
    return takeWhile(p, (value, n, data, i, j) => !test(value, n, data, i, j));
}

function takeUntilP(pValue, pTest) {
    return takeWhile(pValue, (value, n, data, i) => !pTest(data, i).matched);
}

function takeWhileP(pValue, pTest) {
    return takeWhile(pValue, (value, n, data, i) => pTest(data, i).matched);
}

function token(
onToken,
onEnd) {
    return (data, i) => {
        let position = i;
        let value = undefined;
        if (i < data.tokens.length) {
            value = onToken(data.tokens[i], data, i);
            if (value !== undefined) {
                position++;
            }
        }
        else {
            onEnd?.(data, i);
        }
        return (value === undefined)
            ? { matched: false }
            : {
                matched: true,
                position: position,
                value: value,
            };
    };
}

function filter(p, test) {
    return (data, i) => mapOuter(p(data, i), m => test(m.value, data, i, m.position)
        ? m
        : { matched: false });
}

function remainingTokensNumber(data, i) {
    return data.tokens.length - i;
}

function parserPosition(data, i, formatToken, contextTokens = 3) {
    const len = data.tokens.length;
    const lowIndex = util_ts.clamp(0, i - contextTokens, len - contextTokens);
    const highIndex = util_ts.clamp(contextTokens, i + 1 + contextTokens, len);
    const tokensSlice = data.tokens.slice(lowIndex, highIndex);
    const lines = [];
    const indexWidth = String(highIndex - 1).length + 1;
    if (i < 0) {
        lines.push(`${String(i).padStart(indexWidth)} >>`);
    }
    if (0 < lowIndex) {
        lines.push('...'.padStart(indexWidth + 6));
    }
    for (let j = 0; j < tokensSlice.length; j++) {
        const index = lowIndex + j;
        lines.push(`${String(index).padStart(indexWidth)} ${(index === i ? '>' : ' ')} ${util_ts.escapeWhitespace(formatToken(tokensSlice[j]))}`);
    }
    if (highIndex < len) {
        lines.push('...'.padStart(indexWidth + 6));
    }
    if (len <= i) {
        lines.push(`${String(i).padStart(indexWidth)} >>`);
    }
    return lines.join('\n');
}

function parse(parser, tokens, options, formatToken = JSON.stringify) {
    const data = { tokens: tokens, options: options };
    const result = parser(data, 0);
    if (!result.matched) {
        throw new Error('No match');
    }
    if (result.position < data.tokens.length) {
        throw new Error(`Partial match. Parsing stopped at:\n${parserPosition(data, result.position, formatToken)}`);
    }
    return result.value;
}

function tryParse(parser, tokens, options) {
    const result = parser({ tokens: tokens, options: options }, 0);
    return (result.matched)
        ? result.value
        : undefined;
}

function match(matcher, tokens, options) {
    const result = matcher({ tokens: tokens, options: options }, 0);
    return result.value;
}

exports.ab = ab;
exports.abc = abc;
exports.action = action;
exports.ahead = ahead;
exports.all = all;
exports.and = all;
exports.any = any;
exports.between = middle;
exports.chain = chain;
exports.chainReduce = chainReduce;
exports.choice = first;
exports.condition = condition;
exports.decide = decide;
exports.discard = skip;
exports.eitherOr = eitherOr;
exports.emit = emit;
exports.end = end;
exports.eof = end;
exports.error = error;
exports.fail = fail;
exports.filter = filter;
exports.first = first;
exports.flatten = flatten;
exports.flatten1 = flatten1;
exports.guard = filter;
exports.last = last;
exports.left = left;
exports.leftAssoc1 = leftAssoc1;
exports.leftAssoc2 = leftAssoc2;
exports.longest = longest;
exports.lookAhead = ahead;
exports.make = make;
exports.many = many;
exports.many1 = many1;
exports.map = map;
exports.map1 = mapR;
exports.mapR = mapR;
exports.match = match;
exports.middle = middle;
exports.not = not;
exports.of = emit;
exports.option = option;
exports.or = first;
exports.otherwise = eitherOr;
exports.parse = parse;
exports.parserPosition = parserPosition;
exports.peek = peek;
exports.recursive = recursive;
exports.reduceLeft = reduceLeft;
exports.reduceRight = reduceRight;
exports.refine = filter;
exports.remainingTokensNumber = remainingTokensNumber;
exports.right = right;
exports.rightAssoc1 = rightAssoc1;
exports.rightAssoc2 = rightAssoc2;
exports.satisfy = satisfy;
exports.sepBy = sepBy;
exports.sepBy1 = sepBy1;
exports.skip = skip;
exports.some = many1;
exports.start = start;
exports.takeMinMax = takeMinMax;
exports.takeN = takeN;
exports.takeUntil = takeUntil;
exports.takeUntilP = takeUntilP;
exports.takeWhile = takeWhile;
exports.takeWhileP = takeWhileP;
exports.token = token;
exports.tryParse = tryParse;
