import { cleanUrl, extractQueryWithoutFragment } from './utils.js';
class And {
    kind;
    args;
    constructor(...args) {
        if (args.length === 0) {
            throw new Error('`And` expects at least one operand');
        }
        this.args = args;
        this.kind = 'and';
    }
}
class Or {
    kind;
    args;
    constructor(...args) {
        if (args.length === 0) {
            throw new Error('`Or` expects at least one operand');
        }
        this.args = args;
        this.kind = 'or';
    }
}
class Not {
    kind;
    expr;
    constructor(expr) {
        this.expr = expr;
        this.kind = 'not';
    }
}
class Id {
    kind;
    pattern;
    params;
    constructor(pattern, params) {
        this.pattern = pattern;
        this.kind = 'id';
        this.params = params ?? {
            cleanUrl: false,
        };
    }
}
class ModuleType {
    kind;
    pattern;
    constructor(pattern) {
        this.pattern = pattern;
        this.kind = 'moduleType';
    }
}
class Code {
    kind;
    pattern;
    constructor(expr) {
        this.pattern = expr;
        this.kind = 'code';
    }
}
class Query {
    kind;
    key;
    pattern;
    constructor(key, pattern) {
        this.pattern = pattern;
        this.key = key;
        this.kind = 'query';
    }
}
class Include {
    kind;
    expr;
    constructor(expr) {
        this.expr = expr;
        this.kind = 'include';
    }
}
class Exclude {
    kind;
    expr;
    constructor(expr) {
        this.expr = expr;
        this.kind = 'exclude';
    }
}
export function and(...args) {
    return new And(...args);
}
export function or(...args) {
    return new Or(...args);
}
export function not(expr) {
    return new Not(expr);
}
export function id(pattern, params) {
    return new Id(pattern, params);
}
export function moduleType(pattern) {
    return new ModuleType(pattern);
}
export function code(pattern) {
    return new Code(pattern);
}
/*
 * There are three kinds of conditions are supported:
 * 1. `boolean`: if the value is `true`, the key must exist and be truthy. if the value is `false`, the key must not exist or be falsy.
 * 2. `string`: the key must exist and be equal to the value.
 * 3. `RegExp`: the key must exist and match the value.
 */
export function query(key, pattern) {
    return new Query(key, pattern);
}
export function include(expr) {
    return new Include(expr);
}
export function exclude(expr) {
    return new Exclude(expr);
}
/**
 * convert a queryObject to FilterExpression like
 * ```js
 *   and(query(k1, v1), query(k2, v2))
 * ```
 * @param queryFilterObject The query filter object needs to be matched.
 * @returns a `And` FilterExpression
 */
export function queries(queryFilter) {
    let arr = Object.entries(queryFilter).map(([key, value]) => {
        return new Query(key, value);
    });
    return and(...arr);
}
export function interpreter(exprs, code, id, moduleType) {
    let arr = [];
    if (Array.isArray(exprs)) {
        arr = exprs;
    }
    else {
        arr = [exprs];
    }
    return interpreterImpl(arr, code, id, moduleType);
}
export function interpreterImpl(expr, code, id, moduleType, ctx = {}) {
    let hasInclude = false;
    for (const e of expr) {
        switch (e.kind) {
            case 'include': {
                hasInclude = true;
                if (exprInterpreter(e.expr, code, id, moduleType, ctx)) {
                    return true;
                }
                break;
            }
            case 'exclude': {
                if (exprInterpreter(e.expr, code, id, moduleType)) {
                    return false;
                }
                break;
            }
        }
    }
    return !hasInclude;
}
export function exprInterpreter(expr, code, id, moduleType, ctx = {}) {
    switch (expr.kind) {
        case 'and': {
            return expr.args.every((e) => exprInterpreter(e, code, id, moduleType, ctx));
        }
        case 'or': {
            return expr.args.some((e) => exprInterpreter(e, code, id, moduleType, ctx));
        }
        case 'not': {
            return !exprInterpreter(expr.expr, code, id, moduleType, ctx);
        }
        case 'id': {
            if (id === undefined) {
                throw new Error('`id` is required for `id` expression');
            }
            if (expr.params.cleanUrl) {
                id = cleanUrl(id);
            }
            return typeof expr.pattern === 'string'
                ? id === expr.pattern
                : expr.pattern.test(id);
        }
        case 'moduleType': {
            if (moduleType === undefined) {
                throw new Error('`moduleType` is required for `moduleType` expression');
            }
            return moduleType === expr.pattern;
        }
        case 'code': {
            if (code === undefined) {
                throw new Error('`code` is required for `code` expression');
            }
            return typeof expr.pattern === 'string'
                ? code.includes(expr.pattern)
                : expr.pattern.test(code);
        }
        case 'query': {
            if (id === undefined) {
                throw new Error('`id` is required for `Query` expression');
            }
            if (!ctx.urlSearchParamsCache) {
                let queryString = extractQueryWithoutFragment(id);
                ctx.urlSearchParamsCache = new URLSearchParams(queryString);
            }
            let urlParams = ctx.urlSearchParamsCache;
            if (typeof expr.pattern === 'boolean') {
                if (expr.pattern) {
                    return urlParams.has(expr.key);
                }
                else {
                    return !urlParams.has(expr.key);
                }
            }
            else if (typeof expr.pattern === 'string') {
                return urlParams.get(expr.key) === expr.pattern;
            }
            else {
                return expr.pattern.test(urlParams.get(expr.key) ?? '');
            }
        }
        default: {
            throw new Error(`Expression ${JSON.stringify(expr)} is not expected.`);
        }
    }
}
