// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const CHAR_MINUS = '-'.charCodeAt(0);
const CHAR_0 = '0'.charCodeAt(0);
const CHAR_9 = '9'.charCodeAt(0);
const CHAR_A = 'A'.charCodeAt(0);
const CHAR_Z = 'Z'.charCodeAt(0);
const CHAR_LOWER_A = 'a'.charCodeAt(0);
const CHAR_LOWER_Z = 'z'.charCodeAt(0);
const CHAR_DQUOTE = '"'.charCodeAt(0);
const CHAR_COLON = ':'.charCodeAt(0);
const CHAR_QUESTION_MARK = '?'.charCodeAt(0);
const CHAR_STAR = '*'.charCodeAt(0);
const CHAR_UNDERSCORE = '_'.charCodeAt(0);
const CHAR_DOT = '.'.charCodeAt(0);
const CHAR_BACKSLASH = '\\'.charCodeAt(0);
const CHAR_SLASH = '/'.charCodeAt(0);
const CHAR_PLUS = '+'.charCodeAt(0);
const CHAR_EQUALS = '='.charCodeAt(0);
const CHAR_EXCLAMATION = '!'.charCodeAt(0);
const CHAR_HASH = '#'.charCodeAt(0);
const CHAR_DOLLAR = '$'.charCodeAt(0);
const CHAR_PERCENT = '%'.charCodeAt(0);
const CHAR_AND = '&'.charCodeAt(0);
const CHAR_SQUOTE = '\''.charCodeAt(0);
const CHAR_HAT = '^'.charCodeAt(0);
const CHAR_BACKTICK = '`'.charCodeAt(0);
const CHAR_PIPE = '|'.charCodeAt(0);
const CHAR_TILDE = '~'.charCodeAt(0);
// ASCII printable range.
const CHAR_MIN_ASCII_PRINTABLE = 0x20;
const CHAR_MAX_ASCII_PRINTABLE = 0x7e;
/**
 * Note: structured headers operates over ASCII, not unicode, so these are
 * all indeed supposed to return false on things outside 32-127 range regardless
 * of them being other kinds of digits or letters.
 **/
function isDigit(charCode) {
    // DIGIT = %x30-39 ; 0-9 (from RFC 5234)
    if (charCode === undefined) {
        return false;
    }
    return charCode >= CHAR_0 && charCode <= CHAR_9;
}
function isAlpha(charCode) {
    // ALPHA = %x41-5A / %x61-7A   ; A-Z / a-z (from RFC 5234)
    if (charCode === undefined) {
        return false;
    }
    return (charCode >= CHAR_A && charCode <= CHAR_Z) || (charCode >= CHAR_LOWER_A && charCode <= CHAR_LOWER_Z);
}
function isLcAlpha(charCode) {
    // lcalpha = %x61-7A ; a-z
    if (charCode === undefined) {
        return false;
    }
    return (charCode >= CHAR_LOWER_A && charCode <= CHAR_LOWER_Z);
}
function isTChar(charCode) {
    if (charCode === undefined) {
        return false;
    }
    // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
    // "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA (from RFC 7230)
    if (isDigit(charCode) || isAlpha(charCode)) {
        return true;
    }
    switch (charCode) {
        case CHAR_EXCLAMATION:
        case CHAR_HASH:
        case CHAR_DOLLAR:
        case CHAR_PERCENT:
        case CHAR_AND:
        case CHAR_SQUOTE:
        case CHAR_STAR:
        case CHAR_PLUS:
        case CHAR_MINUS:
        case CHAR_DOT:
        case CHAR_HAT:
        case CHAR_UNDERSCORE:
        case CHAR_BACKTICK:
        case CHAR_PIPE:
        case CHAR_TILDE:
            return true;
        default:
            return false;
    }
}
class Input {
    data;
    pos;
    constructor(input) {
        this.data = input;
        this.pos = 0;
        // 4.2 step 2 is to discard any leading SP characters.
        this.skipSP();
    }
    peek() {
        return this.data[this.pos];
    }
    peekCharCode() {
        return (this.pos < this.data.length ? this.data.charCodeAt(this.pos) : undefined);
    }
    eat() {
        ++this.pos;
    }
    // Matches SP*.
    // SP = %x20, from RFC 5234
    skipSP() {
        while (this.data[this.pos] === ' ') {
            ++this.pos;
        }
    }
    // Matches OWS
    // OWS = *( SP / HTAB ) , from RFC 7230
    skipOWS() {
        while (this.data[this.pos] === ' ' || this.data[this.pos] === '\t') {
            ++this.pos;
        }
    }
    atEnd() {
        return (this.pos === this.data.length);
    }
    // 4.2 steps 6,7 --- checks for trailing characters.
    allParsed() {
        this.skipSP();
        return (this.pos === this.data.length);
    }
}
function makeError() {
    return { kind: 0 /* ResultKind.ERROR */ };
}
/** 4.2.1. Parsing a list **/
function parseListInternal(input) {
    const result = { kind: 11 /* ResultKind.LIST */, items: [] };
    while (!input.atEnd()) {
        const piece = parseItemOrInnerList(input);
        if (piece.kind === 0 /* ResultKind.ERROR */) {
            return piece;
        }
        result.items.push(piece);
        input.skipOWS();
        if (input.atEnd()) {
            return result;
        }
        if (input.peek() !== ',') {
            return makeError();
        }
        input.eat();
        input.skipOWS();
        // "If input_string is empty, there is a trailing comma; fail parsing."
        if (input.atEnd()) {
            return makeError();
        }
    }
    return result; // this case corresponds to an empty list.
}
/** 4.2.1.1.  Parsing an Item or Inner List **/
function parseItemOrInnerList(input) {
    if (input.peek() === '(') {
        return parseInnerList(input);
    }
    return parseItemInternal(input);
}
/** 4.2.1.2.  Parsing an Inner List **/
function parseInnerList(input) {
    if (input.peek() !== '(') {
        return makeError();
    }
    input.eat();
    const items = [];
    while (!input.atEnd()) {
        input.skipSP();
        if (input.peek() === ')') {
            input.eat();
            const params = parseParameters(input);
            if (params.kind === 0 /* ResultKind.ERROR */) {
                return params;
            }
            return {
                kind: 12 /* ResultKind.INNER_LIST */,
                items,
                parameters: params,
            };
        }
        const item = parseItemInternal(input);
        if (item.kind === 0 /* ResultKind.ERROR */) {
            return item;
        }
        items.push(item);
        if (input.peek() !== ' ' && input.peek() !== ')') {
            return makeError();
        }
    }
    // Didn't see ), so error.
    return makeError();
}
/** 4.2.3.  Parsing an Item **/
function parseItemInternal(input) {
    const bareItem = parseBareItem(input);
    if (bareItem.kind === 0 /* ResultKind.ERROR */) {
        return bareItem;
    }
    const params = parseParameters(input);
    if (params.kind === 0 /* ResultKind.ERROR */) {
        return params;
    }
    return { kind: 4 /* ResultKind.ITEM */, value: bareItem, parameters: params };
}
/** 4.2.3.1.  Parsing a Bare Item **/
function parseBareItem(input) {
    const upcoming = input.peekCharCode();
    if (upcoming === CHAR_MINUS || isDigit(upcoming)) {
        return parseIntegerOrDecimal(input);
    }
    if (upcoming === CHAR_DQUOTE) {
        return parseString(input);
    }
    if (upcoming === CHAR_COLON) {
        return parseByteSequence(input);
    }
    if (upcoming === CHAR_QUESTION_MARK) {
        return parseBoolean(input);
    }
    if (upcoming === CHAR_STAR || isAlpha(upcoming)) {
        return parseToken(input);
    }
    return makeError();
}
/** 4.2.3.2.  Parsing Parameters **/
function parseParameters(input) {
    // The main noteworthy thing here is handling of duplicates and ordering:
    //
    // "Note that Parameters are ordered as serialized"
    //
    // "If parameters already contains a name param_name (comparing
    // character-for-character), overwrite its value."
    //
    // "Note that when duplicate Parameter keys are encountered, this has the
    // effect of ignoring all but the last instance."
    const items = new Map();
    while (!input.atEnd()) {
        if (input.peek() !== ';') {
            break;
        }
        input.eat();
        input.skipSP();
        const paramName = parseKey(input);
        if (paramName.kind === 0 /* ResultKind.ERROR */) {
            return paramName;
        }
        let paramValue = { kind: 10 /* ResultKind.BOOLEAN */, value: true };
        if (input.peek() === '=') {
            input.eat();
            const parsedParamValue = parseBareItem(input);
            if (parsedParamValue.kind === 0 /* ResultKind.ERROR */) {
                return parsedParamValue;
            }
            paramValue = parsedParamValue;
        }
        // Delete any previous occurrence of duplicates to get the ordering right.
        if (items.has(paramName.value)) {
            items.delete(paramName.value);
        }
        items.set(paramName.value, { kind: 2 /* ResultKind.PARAMETER */, name: paramName, value: paramValue });
    }
    return { kind: 3 /* ResultKind.PARAMETERS */, items: [...items.values()] };
}
/** 4.2.3.3.  Parsing a Key **/
function parseKey(input) {
    let outputString = '';
    const first = input.peekCharCode();
    if (first !== CHAR_STAR && !isLcAlpha(first)) {
        return makeError();
    }
    while (!input.atEnd()) {
        const upcoming = input.peekCharCode();
        if (!isLcAlpha(upcoming) && !isDigit(upcoming) && upcoming !== CHAR_UNDERSCORE && upcoming !== CHAR_MINUS &&
            upcoming !== CHAR_DOT && upcoming !== CHAR_STAR) {
            break;
        }
        outputString += input.peek();
        input.eat();
    }
    return { kind: 1 /* ResultKind.PARAM_NAME */, value: outputString };
}
/** 4.2.4.  Parsing an Integer or Decimal **/
function parseIntegerOrDecimal(input) {
    let resultKind = 5 /* ResultKind.INTEGER */;
    let sign = 1;
    let inputNumber = '';
    if (input.peek() === '-') {
        input.eat();
        sign = -1;
    }
    // This case includes end of input.
    if (!isDigit(input.peekCharCode())) {
        return makeError();
    }
    while (!input.atEnd()) {
        const char = input.peekCharCode();
        if (char !== undefined && isDigit(char)) {
            input.eat();
            inputNumber += String.fromCodePoint(char);
        }
        else if (char === CHAR_DOT && resultKind === 5 /* ResultKind.INTEGER */) {
            input.eat();
            if (inputNumber.length > 12) {
                return makeError();
            }
            inputNumber += '.';
            resultKind = 6 /* ResultKind.DECIMAL */;
        }
        else {
            break;
        }
        if (resultKind === 5 /* ResultKind.INTEGER */ && inputNumber.length > 15) {
            return makeError();
        }
        if (resultKind === 6 /* ResultKind.DECIMAL */ && inputNumber.length > 16) {
            return makeError();
        }
    }
    if (resultKind === 5 /* ResultKind.INTEGER */) {
        const num = sign * Number.parseInt(inputNumber, 10);
        if (num < -999999999999999 || num > 999999999999999) {
            return makeError();
        }
        return { kind: 5 /* ResultKind.INTEGER */, value: num };
    }
    const afterDot = inputNumber.length - 1 - inputNumber.indexOf('.');
    if (afterDot > 3 || afterDot === 0) {
        return makeError();
    }
    return { kind: 6 /* ResultKind.DECIMAL */, value: sign * Number.parseFloat(inputNumber) };
}
/** 4.2.5.  Parsing a String **/
function parseString(input) {
    let outputString = '';
    if (input.peek() !== '"') {
        return makeError();
    }
    input.eat();
    while (!input.atEnd()) {
        const char = input.peekCharCode();
        // can't happen due to atEnd(), but help the typechecker out.
        if (char === undefined) {
            return makeError();
        }
        input.eat();
        if (char === CHAR_BACKSLASH) {
            if (input.atEnd()) {
                return makeError();
            }
            const nextChar = input.peekCharCode();
            input.eat();
            if (nextChar !== CHAR_BACKSLASH && nextChar !== CHAR_DQUOTE) {
                return makeError();
            }
            outputString += String.fromCodePoint(nextChar);
        }
        else if (char === CHAR_DQUOTE) {
            return { kind: 7 /* ResultKind.STRING */, value: outputString };
        }
        else if (char < CHAR_MIN_ASCII_PRINTABLE || char > CHAR_MAX_ASCII_PRINTABLE) {
            return makeError();
        }
        else {
            outputString += String.fromCodePoint(char);
        }
    }
    // No closing quote.
    return makeError();
}
/** 4.2.6.  Parsing a Token **/
function parseToken(input) {
    const first = input.peekCharCode();
    if (first !== CHAR_STAR && !isAlpha(first)) {
        return makeError();
    }
    let outputString = '';
    while (!input.atEnd()) {
        const upcoming = input.peekCharCode();
        if (upcoming === undefined || !isTChar(upcoming) && upcoming !== CHAR_COLON && upcoming !== CHAR_SLASH) {
            break;
        }
        input.eat();
        outputString += String.fromCodePoint(upcoming);
    }
    return { kind: 8 /* ResultKind.TOKEN */, value: outputString };
}
/** 4.2.7.  Parsing a Byte Sequence **/
function parseByteSequence(input) {
    let outputString = '';
    if (input.peek() !== ':') {
        return makeError();
    }
    input.eat();
    while (!input.atEnd()) {
        const char = input.peekCharCode();
        // can't happen due to atEnd(), but help the typechecker out.
        if (char === undefined) {
            return makeError();
        }
        input.eat();
        if (char === CHAR_COLON) {
            return { kind: 9 /* ResultKind.BINARY */, value: outputString };
        }
        if (isDigit(char) || isAlpha(char) || char === CHAR_PLUS || char === CHAR_SLASH || char === CHAR_EQUALS) {
            outputString += String.fromCodePoint(char);
        }
        else {
            return makeError();
        }
    }
    // No closing :
    return makeError();
}
/** 4.2.8.  Parsing a Boolean **/
function parseBoolean(input) {
    if (input.peek() !== '?') {
        return makeError();
    }
    input.eat();
    if (input.peek() === '0') {
        input.eat();
        return { kind: 10 /* ResultKind.BOOLEAN */, value: false };
    }
    if (input.peek() === '1') {
        input.eat();
        return { kind: 10 /* ResultKind.BOOLEAN */, value: true };
    }
    return makeError();
}
export function parseItem(input) {
    const i = new Input(input);
    const result = parseItemInternal(i);
    if (!i.allParsed()) {
        return makeError();
    }
    return result;
}
export function parseList(input) {
    // No need to look for trailing stuff here since parseListInternal does it already.
    return parseListInternal(new Input(input));
}
/** 4.1.3.  Serializing an Item **/
export function serializeItem(input) {
    const bareItemVal = serializeBareItem(input.value);
    if (bareItemVal.kind === 0 /* ResultKind.ERROR */) {
        return bareItemVal;
    }
    const paramVal = serializeParameters(input.parameters);
    if (paramVal.kind === 0 /* ResultKind.ERROR */) {
        return paramVal;
    }
    return { kind: 13 /* ResultKind.SERIALIZATION_RESULT */, value: bareItemVal.value + paramVal.value };
}
/** 4.1.1.  Serializing a List **/
export function serializeList(input) {
    const outputPieces = [];
    for (let i = 0; i < input.items.length; ++i) {
        const item = input.items[i];
        if (item.kind === 12 /* ResultKind.INNER_LIST */) {
            const itemResult = serializeInnerList(item);
            if (itemResult.kind === 0 /* ResultKind.ERROR */) {
                return itemResult;
            }
            outputPieces.push(itemResult.value);
        }
        else {
            const itemResult = serializeItem(item);
            if (itemResult.kind === 0 /* ResultKind.ERROR */) {
                return itemResult;
            }
            outputPieces.push(itemResult.value);
        }
    }
    const output = outputPieces.join(', ');
    return { kind: 13 /* ResultKind.SERIALIZATION_RESULT */, value: output };
}
/** 4.1.1.1.  Serializing an Inner List **/
function serializeInnerList(input) {
    const outputPieces = [];
    for (let i = 0; i < input.items.length; ++i) {
        const itemResult = serializeItem(input.items[i]);
        if (itemResult.kind === 0 /* ResultKind.ERROR */) {
            return itemResult;
        }
        outputPieces.push(itemResult.value);
    }
    let output = '(' + outputPieces.join(' ') + ')';
    const paramResult = serializeParameters(input.parameters);
    if (paramResult.kind === 0 /* ResultKind.ERROR */) {
        return paramResult;
    }
    output += paramResult.value;
    return { kind: 13 /* ResultKind.SERIALIZATION_RESULT */, value: output };
}
/** 4.1.1.2.  Serializing Parameters **/
function serializeParameters(input) {
    let output = '';
    for (const item of input.items) {
        output += ';';
        const nameResult = serializeKey(item.name);
        if (nameResult.kind === 0 /* ResultKind.ERROR */) {
            return nameResult;
        }
        output += nameResult.value;
        const itemVal = item.value;
        if (itemVal.kind !== 10 /* ResultKind.BOOLEAN */ || !itemVal.value) {
            output += '=';
            const itemValResult = serializeBareItem(itemVal);
            if (itemValResult.kind === 0 /* ResultKind.ERROR */) {
                return itemValResult;
            }
            output += itemValResult.value;
        }
    }
    return { kind: 13 /* ResultKind.SERIALIZATION_RESULT */, value: output };
}
/** 4.1.1.3.  Serializing a Key **/
function serializeKey(input) {
    if (input.value.length === 0) {
        return makeError();
    }
    const firstChar = input.value.charCodeAt(0);
    if (!isLcAlpha(firstChar) && firstChar !== CHAR_STAR) {
        return makeError();
    }
    for (let i = 1; i < input.value.length; ++i) {
        const char = input.value.charCodeAt(i);
        if (!isLcAlpha(char) && !isDigit(char) && char !== CHAR_UNDERSCORE && char !== CHAR_MINUS && char !== CHAR_DOT &&
            char !== CHAR_STAR) {
            return makeError();
        }
    }
    return { kind: 13 /* ResultKind.SERIALIZATION_RESULT */, value: input.value };
}
/** 4.1.3.1.  Serializing a Bare Item **/
function serializeBareItem(input) {
    if (input.kind === 5 /* ResultKind.INTEGER */) {
        return serializeInteger(input);
    }
    if (input.kind === 6 /* ResultKind.DECIMAL */) {
        return serializeDecimal(input);
    }
    if (input.kind === 7 /* ResultKind.STRING */) {
        return serializeString(input);
    }
    if (input.kind === 8 /* ResultKind.TOKEN */) {
        return serializeToken(input);
    }
    if (input.kind === 10 /* ResultKind.BOOLEAN */) {
        return serializeBoolean(input);
    }
    if (input.kind === 9 /* ResultKind.BINARY */) {
        return serializeByteSequence(input);
    }
    return makeError();
}
/** 4.1.4.  Serializing an Integer **/
function serializeInteger(input) {
    if (input.value < -999999999999999 || input.value > 999999999999999 || !Number.isInteger(input.value)) {
        return makeError();
    }
    return { kind: 13 /* ResultKind.SERIALIZATION_RESULT */, value: input.value.toString(10) };
}
/** 4.1.5.  Serializing a Decimal **/
function serializeDecimal(_input) {
    throw new Error('Unimplemented');
}
/** 4.1.6.  Serializing a String **/
function serializeString(input) {
    // Only printable ASCII strings are supported by the spec.
    for (let i = 0; i < input.value.length; ++i) {
        const char = input.value.charCodeAt(i);
        if (char < CHAR_MIN_ASCII_PRINTABLE || char > CHAR_MAX_ASCII_PRINTABLE) {
            return makeError();
        }
    }
    let output = '"';
    for (let i = 0; i < input.value.length; ++i) {
        const charStr = input.value[i];
        if (charStr === '"' || charStr === '\\') {
            output += '\\';
        }
        output += charStr;
    }
    output += '"';
    return { kind: 13 /* ResultKind.SERIALIZATION_RESULT */, value: output };
}
/** 4.1.7.  Serializing a Token **/
function serializeToken(input) {
    if (input.value.length === 0) {
        return makeError();
    }
    const firstChar = input.value.charCodeAt(0);
    if (!isAlpha(firstChar) && firstChar !== CHAR_STAR) {
        return makeError();
    }
    for (let i = 1; i < input.value.length; ++i) {
        const char = input.value.charCodeAt(i);
        if (!isTChar(char) && char !== CHAR_COLON && char !== CHAR_SLASH) {
            return makeError();
        }
    }
    return { kind: 13 /* ResultKind.SERIALIZATION_RESULT */, value: input.value };
}
/** 4.1.8.  Serializing a Byte Sequence **/
function serializeByteSequence(_input) {
    throw new Error('Unimplemented');
}
/** 4.1.9.  Serializing a Boolean **/
function serializeBoolean(input) {
    return { kind: 13 /* ResultKind.SERIALIZATION_RESULT */, value: input.value ? '?1' : '?0' };
}
//# sourceMappingURL=StructuredHeaders.js.map