// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { HeaderName } from '../constants';
import { AzFuncSystemError } from '../errors';

const space = ' ';
// See "LEXICAL TOKENS" section for definition of ctl chars and quoted string: https://www.w3.org/Protocols/rfc822/3_Lexical.html
const ctlChars = '\\u0000-\\u001F\\u007F';
const quotedString = '(?:[^"\\\\]|\\\\.)*';
// General description of content type header, including defintion of tspecials and token https://www.w3.org/Protocols/rfc1341/4_Content-Type.html
const tspecials = '\\(\\)<>@,;:\\\\"\\/\\[\\]\\?\\.=';
const token = `[^${space}${ctlChars}${tspecials}]+`;

const start = '^\\s*'; // allows leading whitespace
const end = '\\s*(.*)$'; // gets the rest of the string except leading whitespace
const semicolonEnd = `\\s*;?${end}`; // allows optional semicolon and otherwise gets the rest of the string

/**
 * @param data a full header, e.g. "Content-Type: text/html; charset=UTF-8"
 * @param headerName the header name, e.g. "Content-Type"
 * @returns the header value, e.g. "text/html; charset=UTF-8" or null if not found
 */
export function getHeaderValue(data: string, headerName: string): string | null {
    const match = new RegExp(`${start}${headerName}\\s*:${end}`, 'i').exec(data);
    if (match) {
        return match[1].trim();
    }
    return null;
}

/**
 * @param data a content type, e.g. "text/html; charset=UTF-8"
 * @returns an array containing the media type (e.g. text/html) and an object with the parameters
 */
export function parseContentType(data: string): [string, HeaderParams] {
    const match = new RegExp(`${start}(${token}\\/${token})${semicolonEnd}`, 'i').exec(data);
    if (!match) {
        throw new AzFuncSystemError(`${HeaderName.contentType} must begin with format "type/subtype".`);
    } else {
        return [match[1], parseHeaderParams(match[2])];
    }
}

/**
 * @param data a content disposition, e.g. "form-data; name=myfile; filename=test.txt"
 * @returns an array containing the disposition (e.g. form-data) and an object with the parameters
 */
export function parseContentDisposition(data: string): [string, HeaderParams] {
    const match = new RegExp(`${start}(${token})${semicolonEnd}`, 'i').exec(data);
    if (!match) {
        throw new AzFuncSystemError(`${HeaderName.contentDisposition} must begin with disposition type.`);
    } else {
        return [match[1], parseHeaderParams(match[2])];
    }
}

function parseHeaderParams(data: string): HeaderParams {
    const result = new HeaderParams();
    while (data) {
        // try to find an unquoted name=value pair first
        const regexp = new RegExp(`${start}(${token})=(${token})${semicolonEnd}`, 'i');
        let match = regexp.exec(data);
        // if that didn't work, try to find a quoted name="value" pair instead
        if (!match) {
            const quotedPartsRegexp = new RegExp(`${start}(${token})="(${quotedString})"${semicolonEnd}`, 'i');
            match = quotedPartsRegexp.exec(data);
        }

        if (match) {
            result.add(match[1], match[2].replace(/\\"/g, '"')); // un-escape any quotes
            data = match[3];
        } else {
            break;
        }
    }
    return result;
}

export class HeaderParams {
    #params: { [name: string]: string } = {};
    get(name: string): string {
        const result = this.#params[name.toLowerCase()];
        if (result === undefined) {
            throw new AzFuncSystemError(`Failed to find parameter with name "${name}".`);
        } else {
            return result;
        }
    }

    has(name: string): boolean {
        return this.#params[name.toLowerCase()] !== undefined;
    }

    add(name: string, value: string): void {
        this.#params[name.toLowerCase()] = value;
    }
}
