import { UrlEncoder } from './UrlEncoder.js';
class Tokenizer {
    tokens;
    /**
     * This is set to the next token to be read.
     */
    index = 0;
    size;
    get ended() {
        return this.index === this.size - 1;
    }
    constructor(tokens) {
        this.tokens = tokens;
        this.size = tokens.length;
    }
    /**
     * Reads characters until one of the passed characters (not included).
     * @param chars The list of characters to search for.
     * @returns The string between the current index and the found character or the end of tokens.
     */
    untilChar(...chars) {
        const { tokens, index, size } = this;
        let result = '';
        for (let i = index; i < size; i++) {
            const token = tokens[i];
            this.index = i;
            if (chars.includes(token)) {
                return result;
            }
            result += token;
        }
        return result;
    }
    /**
     * Reads the next token and changes the state.
     * @returns The next token.
     */
    next() {
        const { ended, tokens } = this;
        if (ended) {
            return undefined;
        }
        const next = tokens[this.index];
        this.index += 1;
        return next;
    }
    /**
     * Reads the next character without changing the state.
     */
    getNext() {
        const { ended, tokens, index } = this;
        if (ended) {
            return undefined;
        }
        return tokens[index + 1];
    }
}
var PartType;
(function (PartType) {
    /**
     * Literal string, we do not process these
     */
    PartType[PartType["literal"] = 0] = "literal";
    /**
     * URI template expression
     */
    PartType[PartType["template"] = 1] = "template";
    /**
     * query parameter
     */
    PartType[PartType["param"] = 2] = "param";
})(PartType || (PartType = {}));
var State;
(function (State) {
    /**
     * Processing a literal
     */
    State[State["literal"] = 0] = "literal";
    /**
     * Processing a query parameter
     */
    State[State["param"] = 1] = "param";
    /**
     * Processing a template expression
     */
    State[State["expression"] = 2] = "expression";
})(State || (State = {}));
var DataType;
(function (DataType) {
    // undefined/null
    DataType[DataType["nil"] = 0] = "nil";
    // string
    DataType[DataType["string"] = 1] = "string";
    // object
    DataType[DataType["object"] = 2] = "object";
    // array
    DataType[DataType["array"] = 3] = "array";
})(DataType || (DataType = {}));
export class UrlSearchParams {
    /**
     * A reference to the URL processor's parts.
     */
    parts = [];
    constructor(parts) {
        this.parts = parts;
    }
    /**
     * Iterates over each query parameter, in order.
     */
    *[Symbol.iterator]() {
        for (const part of this.parts) {
            if (part.type === PartType.param) {
                yield part;
            }
        }
    }
    /**
     * Reads parts, in order, that are query parameters.
     */
    list() {
        return this.parts.filter((i) => i.type === PartType.param);
    }
    /**
     * Adds a new query parameter to the list.
     *
     * @param name The name of the parameter.
     * @param value The value of the parameter.
     * @param enabled Whether her parameter is enabled.
     */
    append(name, value = '', enabled = true) {
        const part = {
            type: PartType.param,
            expression: `${name}=${value}`,
            name,
            value,
            enabled,
        };
        const length = this.parts.push(part);
        return length - 1;
    }
    /**
     * Replaces the param part in the parts list.
     *
     * @param index The index of the parameter as returned by the `getParameters()` method.
     * @param param The param to set.
     */
    update(index, param) {
        if (param.type !== PartType.param) {
            throw new Error(`Invalid query parameter definition`);
        }
        param.expression = `${param.name}=${param.value || ''}`;
        let current = 0;
        for (let i = 0, len = this.parts.length; i < len; i++) {
            const part = this.parts[i];
            if (part.type === PartType.param) {
                if (current === index) {
                    this.parts[i] = param;
                    return;
                }
                current += 1;
            }
        }
        throw new Error(`Missing query parameter at position ${index}.`);
    }
    /**
     * Replaces all parameters with the name with the passed value.
     * It insets the param at the first occurrence of the current param in the parts list.
     * If not exists, it adds it as a last.
     *
     * @param name The name of the parameter
     * @param value the value of the parameter
     * @param enabled Whether her parameter is enabled.
     */
    set(name, value, enabled = true) {
        let firstIndex = -1;
        for (let i = this.parts.length - 1; i >= 0; i--) {
            const part = this.parts[i];
            if (part.type === PartType.param) {
                const typed = part;
                if (typed.name === name) {
                    this.parts.splice(i, 1);
                    firstIndex = i;
                }
            }
        }
        const part = {
            type: PartType.param,
            expression: `${name}=${value}`,
            name,
            value,
            enabled,
        };
        let position;
        if (firstIndex === -1) {
            const len = this.parts.push(part);
            position = len - 1;
        }
        else {
            this.parts.splice(firstIndex, 0, part);
            position = firstIndex;
        }
        return position;
    }
    /**
     * @param name The name of the parameter to search for.
     * @returns The reference to the parameter.
     */
    get(name) {
        return this.parts.find((i) => i.type === PartType.param && i.name === name);
    }
    /**
     * Removes a query parameter from the parameters list.
     *
     * @param index The index of the parameter as returned by the `getParameters()` method.
     */
    delete(index) {
        let current = 0;
        for (let i = 0, len = this.parts.length; i < len; i++) {
            const part = this.parts[i];
            if (part.type === PartType.param) {
                if (current === index) {
                    this.parts.splice(i, 1);
                    return;
                }
                current += 1;
            }
        }
        throw new Error(`Missing query parameter at position ${index}.`);
    }
    /**
     * Toggles the enabled state of the parameter.
     *
     * @param index The index of the parameter as returned by the `getParameters()` method.
     * @param enabled The enabled state of the parameter.
     */
    toggle(index, enabled) {
        const params = this.list();
        const param = params[index];
        if (!param) {
            throw new Error(`Missing query parameter at position ${index}.`);
        }
        param.enabled = enabled;
    }
    /**
     * Removes all query parameters.
     */
    clear() {
        for (let i = this.parts.length - 1; i >= 0; i--) {
            const part = this.parts[i];
            if (part.type !== PartType.param) {
                continue;
            }
            this.parts.splice(i, 1);
        }
    }
    /**
     * Transforms the parameters to a key-value pair separated by a comma,
     * ech in a new line.
     *
     * For example `a=b&c=d` is translated to:
     * ```
     * a: b
     * c: d
     * ```
     * Disabled parameters have `#` sign at the beginning of the line.
     */
    toParamsString() {
        const lines = [];
        for (const part of this) {
            const { enabled, name, value } = part;
            if (!name && !value) {
                continue;
            }
            let line = '';
            if (!enabled) {
                line += '# ';
            }
            if (name) {
                line += name;
            }
            line += ': ';
            if (value) {
                line += value;
            }
            lines.push(line);
        }
        return lines.join('\n');
    }
    /**
     * The opposite of `toParamsString()`. Takes the input string
     * and creates a new list of query parameters from it.
     */
    fromParamsString(value) {
        this.clear();
        const lines = value.split('\n');
        lines.forEach((line) => this.processLine(line));
    }
    processLine(line) {
        let text = line.trim();
        const disabled = text.startsWith('#');
        if (disabled) {
            text = text.replace('#', '').trim();
        }
        const parts = text.split(':');
        const [nameRaw, ...values] = parts;
        const name = nameRaw.trim();
        const value = values.join(':').trim();
        this.append(name, value, !disabled);
    }
}
/**
 * A class that parses a string a treats it as a value that may include
 * URI templates and request parameters.
 *
 * It can be used by the UI libraries to manipulate the URL templates and query parameters.
 */
export class UrlProcessor {
    /**
     * The source expression.
     * It is immutable for the entire manipulation process of the URI.
     * Any modification is on the parts level and the `expand()` and `toString()`
     * function only rely on the `parts`.
     */
    expression;
    /**
     * The tokenizer object
     */
    tokens;
    /**
     * An ordered list of parts of the expression.
     */
    parts = [];
    /**
     * A helper class to manipulate query parameters on the parser.
     */
    search;
    constructor(expression) {
        this.expression = expression;
        this.tokens = new Tokenizer(expression);
        this.search = new UrlSearchParams(this.parts);
        this._parse();
    }
    /**
     * Creates an URI leaving the template expressions are they are (without expanding them).
     */
    toString() {
        let result = '';
        this.parts.forEach((part) => {
            if (part.type === PartType.literal) {
                result += part.expression;
            }
            else if (part.type === PartType.template) {
                result += `{${part.expression}}`;
            }
            else {
                const typed = part;
                if (!typed.enabled) {
                    return;
                }
                if (result.includes('?')) {
                    result += '&';
                }
                else {
                    result += '?';
                }
                result += part.expression;
            }
        });
        return result;
    }
    /**
     * Creates a URI with expanded template values.
     *
     * @param map The variables to evaluate.
     * @param opts Processing options.
     */
    expand(map, opts = {}) {
        let result = '';
        for (const part of this.parts) {
            if (part.type === PartType.literal) {
                result += part.expression;
            }
            else if (part.type === PartType.param) {
                const typed = part;
                if (!typed.enabled) {
                    continue;
                }
                if (result.includes('?')) {
                    result += '&';
                }
                else {
                    result += '?';
                }
                result += part.expression;
            }
            else {
                result += this._expandExpression(part, map, opts);
            }
        }
        return result;
    }
    _expandExpression(part, map, opts) {
        const { operator, variables } = part;
        const buffer = [];
        for (const variable of variables) {
            const data = this._getData(map, variable.name);
            if (data.type === DataType.nil && opts.strict) {
                throw new Error(`Missing expansion value for variable "${variable.name}"`);
            }
            if (data.type === DataType.nil && opts.ignoreMissing) {
                buffer.push(part.expression);
                continue;
            }
            if (!data.values.length) {
                if (data.type !== DataType.nil) {
                    // Empty object / array. We still append the separator.
                    buffer.push('');
                }
                continue;
            }
            if (data.type > DataType.string && variable.maxLength) {
                if (opts.strict) {
                    throw new Error(`Invalid expression: Prefix modifier not applicable to variable "${variable.name}"`);
                }
                // we ignore invalid values.
                continue;
            }
            if (operator.named) {
                buffer.push(this._expandNamedExpression(data, operator, variable));
            }
            else {
                buffer.push(this._expandUnNamedExpression(data, operator, variable));
            }
        }
        if (buffer.length) {
            return (operator.prefix || '') + buffer.join(operator.separator);
        }
        // prefix is not prepended for empty expressions
        return '';
    }
    _expandNamedExpression(data, operator, variable) {
        let result = '';
        const separator = (variable.explode && operator.separator) || ',';
        const encodeFunction = operator.reserved ? UrlEncoder.encodeReserved : UrlEncoder.strictEncode;
        let name = '';
        if (data.type !== DataType.object && variable.name) {
            name = encodeFunction(variable.name);
        }
        const hasLength = typeof variable.maxLength === 'number';
        data.values.forEach((item, index) => {
            let value;
            if (hasLength) {
                value = encodeFunction(item.value.substring(0, variable.maxLength));
                if (data.type === DataType.object) {
                    // apply maxLength to keys of objects as well
                    name = encodeFunction(item.name.substring(0, variable.maxLength));
                }
            }
            else {
                // encode value
                value = encodeFunction(item.value);
                if (data.type === DataType.object) {
                    // encode name and cache encoded value
                    name = encodeFunction(item.name);
                }
            }
            if (result) {
                result += separator;
            }
            if (!variable.explode) {
                if (!index) {
                    result += encodeFunction(variable.name) + (operator.emptyNameSeparator || value ? '=' : '');
                }
                if (data.type === DataType.object) {
                    result += `${name},`;
                }
                result += value;
            }
            else {
                result += name + (operator.emptyNameSeparator || value ? '=' : '') + value;
            }
        });
        return result;
    }
    _expandUnNamedExpression(data, operator, variable) {
        let result = '';
        const separator = (variable.explode && operator.separator) || ',';
        const encodeFunction = operator.reserved ? UrlEncoder.encodeReserved : UrlEncoder.strictEncode;
        const hasLength = typeof variable.maxLength === 'number';
        data.values.forEach((item) => {
            let value;
            if (hasLength) {
                value = encodeFunction(item.value.substring(0, variable.maxLength));
            }
            else {
                value = encodeFunction(item.value);
            }
            if (result) {
                result += separator;
            }
            if (data.type === DataType.object) {
                let _name;
                if (hasLength) {
                    _name = encodeFunction(item.name.substring(0, variable.maxLength));
                }
                else {
                    _name = encodeFunction(item.name || '');
                }
                result += _name;
                if (variable.explode) {
                    result += operator.emptyNameSeparator || value ? '=' : '';
                }
                else {
                    result += ',';
                }
            }
            result += value;
        });
        return result;
    }
    _getData(map, name) {
        const result = {
            type: DataType.nil,
            values: [],
        };
        const value = map[name];
        if (value === undefined || value === null) {
            // undefined and null values are to be ignored completely
            return result;
        }
        if (Array.isArray(value)) {
            for (const v of value) {
                // we only allow primitives in the values
                if (this._validExpansionValue(v)) {
                    result.values.push({ value: String(v) });
                }
            }
        }
        else if (String(Object.prototype.toString.call(value)) === '[object Object]') {
            Object.keys(value).forEach((k) => {
                const v = value[k];
                if (this._validExpansionValue(v)) {
                    result.values.push({ name: k, value: v });
                }
            });
            if (result.values.length) {
                result.type = DataType.object;
            }
        }
        else {
            result.type = DataType.string;
            result.values.push({ value: String(value) });
        }
        return result;
    }
    _validExpansionValue(value) {
        if (value === undefined || value === null) {
            // these are ignored completely
            return false;
        }
        // we only allow primitives in the values
        return ['string', 'number', 'boolean'].includes(typeof value);
    }
    /**
     * Creates an ordered list of parts that describe the passed expression.
     *
     * FIXME: Handle error states like unclosed brackets.
     */
    _parse() {
        const { tokens } = this;
        let state = State.literal;
        while (true) {
            let value;
            if (state === State.literal) {
                value = tokens.untilChar('?', '&', '{', '}');
            }
            else if (state === State.param) {
                value = tokens.untilChar('&', '{');
            }
            else {
                value = tokens.untilChar('}');
            }
            const next = tokens.next();
            if (state === State.literal) {
                // when the value is an empty string that means that we entered the expression string
                // and we have a variable or a query parameter as the first character.
                // We don't need to pass a literal in this case.
                if (value !== '') {
                    this._addLiteral(value);
                }
            }
            else if (state === State.param) {
                this._addParam(value);
            }
            else {
                this._addTemplate(value);
            }
            if (next === '?' || next === '&') {
                state = State.param;
            }
            else if (next === '{') {
                state = State.expression;
            }
            else if (next === undefined) {
                break;
            }
            else {
                state = State.literal;
            }
        }
    }
    /**
     * Adds a literal part.
     * Literal parts are not processed in any way. They do not contain query parameters
     * or template expressions.
     *
     * @param expression The literal expression.
     */
    _addLiteral(expression) {
        this.parts.push({
            type: PartType.literal,
            expression,
        });
    }
    /**
     * Adds a part that describes a query parameter
     * @param expression The query param as `name=value` or `name` or `name=`
     */
    _addParam(expression) {
        const parts = expression.split('=');
        const part = {
            type: PartType.param,
            expression,
            name: parts[0],
            value: parts[1] || '',
            enabled: true,
        };
        this.parts.push(part);
    }
    /**
     * Adds a part that is a template expression.
     *
     * @param expression The template expression as defined in RFC 6570
     */
    _addTemplate(expression) {
        const ch = expression[0];
        let operator;
        if (ch === '+') {
            // Reserved character strings (no encoding)
            operator = {
                operator: '+',
                separator: ',',
                reserved: true,
            };
        }
        else if (ch === '#') {
            // Fragment identifiers prefixed by '#'
            operator = {
                operator: '#',
                prefix: '#',
                separator: ',',
                reserved: true,
            };
        }
        else if (ch === '.') {
            // Name labels or extensions prefixed by '.'
            operator = {
                operator: '.',
                prefix: '.',
                separator: '.',
            };
        }
        else if (ch === '/') {
            // Path segments prefixed by '/'
            operator = {
                operator: '/',
                prefix: '/',
                separator: '/',
            };
        }
        else if (ch === ';') {
            // Path parameter name or name=value pairs prefixed by ';'
            operator = {
                operator: ';',
                prefix: ';',
                separator: ';',
                named: true,
            };
        }
        else if (ch === '?') {
            // Query component beginning with '?' and consisting
            // of name=value pairs separated by '&'
            operator = {
                operator: '?',
                prefix: '?',
                separator: '&',
                named: true,
                emptyNameSeparator: true,
            };
        }
        else if (ch === '&') {
            // Continuation of query-style &name=value pairs
            // within a literal query component.
            operator = {
                operator: '&',
                prefix: '&',
                separator: '&',
                named: true,
                emptyNameSeparator: true,
            };
        }
        else {
            // The operator characters equals ("="), comma (","), exclamation ("!"),
            // at sign ("@"), and pipe ("|") are reserved for future extensions.
            // this is level1 simple expression
            operator = {
                separator: ',',
            };
        }
        const part = {
            type: PartType.template,
            expression,
            operator,
            variables: this._readVariables(expression, operator.operator),
        };
        this.parts.push(part);
    }
    _readVariables(expression, operator) {
        let name = expression;
        if (operator) {
            name = name.substring(1);
        }
        return name.split(',').map((item) => {
            let maxLength;
            let varName = item;
            let explode = false;
            if (varName.endsWith('*')) {
                explode = true;
                varName = varName.substring(0, varName.length - 1);
            }
            const lengthIndex = varName.indexOf(':');
            if (lengthIndex >= 0) {
                const len = varName.substring(lengthIndex + 1);
                varName = varName.substring(0, lengthIndex);
                if (len) {
                    const parsed = Number(len);
                    if (Number.isInteger(parsed)) {
                        maxLength = parsed;
                    }
                }
            }
            const result = {
                name: varName,
                explode,
            };
            if (typeof maxLength === 'number') {
                result.maxLength = maxLength;
            }
            return result;
        });
    }
}
//# sourceMappingURL=UrlProcessor.js.map