import XMLHttpRequestEventTarget from './XMLHttpRequestEventTarget.js';
import * as PropertySymbol from '../PropertySymbol.js';
import XMLHttpRequestReadyStateEnum from './XMLHttpRequestReadyStateEnum.js';
import Event from '../event/Event.js';
import DOMException from '../exception/DOMException.js';
import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js';
import XMLHttpResponseTypeEnum from './XMLHttpResponseTypeEnum.js';
import ErrorEvent from '../event/events/ErrorEvent.js';
import Fetch from '../fetch/Fetch.js';
import SyncFetch from '../fetch/SyncFetch.js';
import ProgressEvent from '../event/events/ProgressEvent.js';
import NodeTypeEnum from '../nodes/node/NodeTypeEnum.js';
import XMLHttpRequestResponseDataParser from './XMLHttpRequestResponseDataParser.js';
import FetchRequestHeaderUtility from '../fetch/utilities/FetchRequestHeaderUtility.js';
import WindowBrowserContext from '../window/WindowBrowserContext.js';
import BrowserErrorCaptureEnum from '../browser/enums/BrowserErrorCaptureEnum.js';
/**
 * XMLHttpRequest.
 *
 * Based on:
 * https://github.com/mjwwit/node-XMLHttpRequest/blob/master/lib/XMLHttpRequest.js
 */
export default class XMLHttpRequest extends XMLHttpRequestEventTarget {
    // Constants
    static UNSENT = XMLHttpRequestReadyStateEnum.unsent;
    static OPENED = XMLHttpRequestReadyStateEnum.opened;
    static HEADERS_RECEIVED = XMLHttpRequestReadyStateEnum.headersReceived;
    static LOADING = XMLHttpRequestReadyStateEnum.loading;
    static DONE = XMLHttpRequestReadyStateEnum.done;
    // Public properties
    upload = new this[PropertySymbol.window].XMLHttpRequestUpload();
    withCredentials = false;
    // Private properties
    #async = true;
    #abortController = null;
    #aborted = false;
    #request = null;
    #response = null;
    #responseType = '';
    #responseBody = null;
    #accumulatedData = Buffer.from([]);
    #readyState = XMLHttpRequestReadyStateEnum.unsent;
    #overriddenMimeType = null;
    /**
     * Constructor.
     */
    constructor() {
        super();
        if (!this[PropertySymbol.window]) {
            throw new TypeError(`Failed to construct '${this.constructor.name}': '${this.constructor.name}' was constructed outside a Window context.`);
        }
    }
    /**
     * Returns the status.
     *
     * @returns Status.
     */
    get status() {
        return this.#response?.status || 0;
    }
    /**
     * Returns the status text.
     *
     * @returns Status text.
     */
    get statusText() {
        return this.#response?.statusText || '';
    }
    /**
     * Returns the response.
     *
     * @returns Response.
     */
    get response() {
        if (!this.#response) {
            return '';
        }
        return this.#responseBody;
    }
    /**
     * Get the response text.
     *
     * @throws {DOMException} If the response type is not text or empty.
     * @returns The response text.
     */
    get responseText() {
        if (this.responseType !== XMLHttpResponseTypeEnum.text && this.responseType !== '') {
            throw new this[PropertySymbol.window].DOMException(`Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'text' (was '${this.responseType}').`, DOMExceptionNameEnum.invalidStateError);
        }
        // If response is complete, use the final response body
        if (this.#responseBody !== null) {
            return this.#responseBody;
        }
        // If we're in loading state and have accumulated data, process it
        if (this.readyState === XMLHttpRequestReadyStateEnum.headersReceived ||
            this.readyState === XMLHttpRequestReadyStateEnum.loading) {
            return this.#accumulatedData.toString() ?? '';
        }
        return '';
    }
    /**
     * Get the responseXML.
     *
     * @throws {DOMException} If the response type is not text or empty.
     * @returns Response XML.
     */
    get responseXML() {
        if (this.responseType !== XMLHttpResponseTypeEnum.document && this.responseType !== '') {
            throw new this[PropertySymbol.window].DOMException(`Failed to read the 'responseXML' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'document' (was '${this.responseType}').`, DOMExceptionNameEnum.invalidStateError);
        }
        return this.responseType === '' ? null : this.#responseBody;
    }
    /**
     * Returns the response URL.
     *
     * @returns Response URL.
     */
    get responseURL() {
        return this.#response?.url || '';
    }
    /**
     * Returns the ready state.
     *
     * @returns Ready state.
     */
    get readyState() {
        return this.#readyState;
    }
    /**
     * Set response type.
     *
     * @param type Response type.
     * @throws {DOMException} If the state is not unsent or opened.
     * @throws {DOMException} If the request is synchronous.
     */
    set responseType(type) {
        // ResponseType can only be set when the state is unsent or opened.
        if (this.readyState !== XMLHttpRequestReadyStateEnum.opened &&
            this.readyState !== XMLHttpRequestReadyStateEnum.unsent) {
            throw new this[PropertySymbol.window].DOMException(`Failed to set the 'responseType' property on 'XMLHttpRequest': The object's state must be OPENED or UNSENT.`, DOMExceptionNameEnum.invalidStateError);
        }
        // Sync requests can only have empty string or 'text' as response type.
        if (!this.#async) {
            throw new this[PropertySymbol.window].DOMException(`Failed to set the 'responseType' property on 'XMLHttpRequest': The response type cannot be changed for synchronous requests made from a document.`, DOMExceptionNameEnum.invalidStateError);
        }
        this.#responseType = type;
    }
    /**
     * Get response Type.
     *
     * @returns Response type.
     */
    get responseType() {
        return this.#responseType;
    }
    /**
     * Opens the connection.
     *
     * @param method Connection method (eg GET, POST).
     * @param url URL for the connection.
     * @param [async=true] Asynchronous connection.
     * @param [user] Username for basic authentication (optional).
     * @param [password] Password for basic authentication (optional).
     */
    open(method, url, async = true, user, password) {
        const window = this[PropertySymbol.window];
        if (!async && !!this.responseType && this.responseType !== XMLHttpResponseTypeEnum.text) {
            throw new window.DOMException(`Failed to execute 'open' on 'XMLHttpRequest': Synchronous requests from a document must not set a response type.`, DOMExceptionNameEnum.invalidAccessError);
        }
        const headers = new this[PropertySymbol.window].Headers();
        if (user) {
            const authBuffer = Buffer.from(`${user}:${password || ''}`);
            headers.set('Authorization', 'Basic ' + authBuffer.toString('base64'));
        }
        this.#async = async;
        this.#aborted = false;
        this.#response = null;
        this.#responseBody = null;
        this.#accumulatedData = Buffer.from([]);
        this.#abortController = new window.AbortController();
        this.#request = new window.Request(url, {
            method,
            headers,
            signal: this.#abortController.signal,
            credentials: this.withCredentials ? 'include' : 'same-origin'
        });
        this.#readyState = XMLHttpRequestReadyStateEnum.opened;
    }
    /**
     * Sets a header for the request.
     *
     * @param name Header name.
     * @param value Header value.
     * @returns Header added.
     */
    setRequestHeader(name, value) {
        if (this.readyState !== XMLHttpRequestReadyStateEnum.opened) {
            throw new this[PropertySymbol.window].DOMException(`Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.`, DOMExceptionNameEnum.invalidStateError);
        }
        // TODO: Use FetchRequestHeaderUtility.removeForbiddenHeaders() instead.
        if (FetchRequestHeaderUtility.isHeaderForbidden(name)) {
            return false;
        }
        this.#request.headers.set(name, value);
        return true;
    }
    /**
     * Gets a header from the server response.
     *
     * @param header header Name of header to get.
     * @returns string Text of the header or null if it doesn't exist.
     */
    getResponseHeader(header) {
        return this.#response?.headers.get(header) ?? null;
    }
    /**
     * Gets all the response headers.
     *
     * @returns A string with all response headers separated by CR+LF.
     */
    getAllResponseHeaders() {
        if (!this.#response) {
            return '';
        }
        const result = [];
        for (const [name, value] of this.#response?.headers) {
            const lowerName = name.toLowerCase();
            if (lowerName !== 'set-cookie' && lowerName !== 'set-cookie2') {
                result.push(`${name}: ${value}`);
            }
        }
        return result.join('\r\n');
    }
    /**
     * Sends the request to the server.
     *
     * @param body Optional data to send as request body.
     */
    send(body) {
        const window = this[PropertySymbol.window];
        if (this.readyState != XMLHttpRequestReadyStateEnum.opened) {
            throw new this[PropertySymbol.window].DOMException(`Failed to execute 'send' on 'XMLHttpRequest': Connection must be opened before send() is called.`, DOMExceptionNameEnum.invalidStateError);
        }
        // When body is a Document, serialize it to a string.
        if (typeof body === 'object' &&
            body !== null &&
            body[PropertySymbol.nodeType] === NodeTypeEnum.documentNode) {
            body = new window.XMLSerializer().serializeToString(body);
        }
        if (this.#async) {
            this.#sendAsync(body).catch((error) => {
                throw error;
            });
        }
        else {
            this.#sendSync(body);
        }
    }
    /**
     * Aborts a request.
     */
    abort() {
        if (this.#aborted) {
            return;
        }
        this.#aborted = true;
        this.#abortController.abort();
    }
    /**
     * Overrides the MIME type returned by the server.
     * This method must be called before send().
     *
     * @param mimeType The MIME type to use instead of the one specified by the server.
     * @throws {DOMException} If the request state is LOADING or DONE.
     */
    overrideMimeType(mimeType) {
        if (this.readyState === XMLHttpRequestReadyStateEnum.loading ||
            this.readyState === XMLHttpRequestReadyStateEnum.done) {
            throw new this[PropertySymbol.window].DOMException(`Failed to execute 'overrideMimeType' on 'XMLHttpRequest': MIME type cannot be overridden when the request state is LOADING or DONE.`, DOMExceptionNameEnum.invalidStateError);
        }
        this.#overriddenMimeType = mimeType;
    }
    /**
     * Sends the request to the server asynchronously.
     *
     * @param body Optional data to send as request body.
     */
    async #sendAsync(body) {
        const window = this[PropertySymbol.window];
        const browserFrame = new WindowBrowserContext(window).getBrowserFrame();
        if (!browserFrame) {
            return;
        }
        const asyncTaskManager = browserFrame[PropertySymbol.asyncTaskManager];
        const taskID = asyncTaskManager.startTask(() => this.abort());
        this.#readyState = XMLHttpRequestReadyStateEnum.loading;
        this.#dispatchEvent(new Event('readystatechange'));
        this.#dispatchEvent(new Event('loadstart'));
        if (body) {
            this.#request = new window.Request(this.#request.url, {
                method: this.#request.method,
                headers: this.#request.headers,
                signal: this.#abortController.signal,
                credentials: this.#request.credentials,
                body
            });
        }
        this.#abortController.signal.addEventListener('abort', () => {
            this.#aborted = true;
            this.#readyState = XMLHttpRequestReadyStateEnum.unsent;
            this.#dispatchEvent(new Event('abort'));
            this.#dispatchEvent(new Event('loadend'));
            this.#dispatchEvent(new Event('readystatechange'));
            asyncTaskManager.endTask(taskID);
        });
        const onError = (error) => {
            if (error instanceof DOMException && error.name === DOMExceptionNameEnum.abortError) {
                if (this.#aborted) {
                    return;
                }
                this.#readyState = XMLHttpRequestReadyStateEnum.unsent;
                this.#dispatchEvent(new Event('abort'));
            }
            else {
                this.#readyState = XMLHttpRequestReadyStateEnum.done;
                this.#dispatchEvent(new ErrorEvent('error', { error, message: error.message }));
            }
            this.#dispatchEvent(new Event('loadend'));
            this.#dispatchEvent(new Event('readystatechange'));
            asyncTaskManager.endTask(taskID);
        };
        const fetch = new Fetch({
            browserFrame: browserFrame,
            window: window,
            url: this.#request.url,
            init: this.#request
        });
        try {
            this.#response = await fetch.send();
        }
        catch (error) {
            onError(error);
            return;
        }
        this.#readyState = XMLHttpRequestReadyStateEnum.headersReceived;
        this.#dispatchEvent(new Event('readystatechange'));
        const contentLength = this.#response.headers.get('Content-Length');
        const contentLengthNumber = contentLength !== null && !isNaN(Number(contentLength)) ? Number(contentLength) : null;
        let loaded = 0;
        if (this.#response.body) {
            let eventError;
            try {
                for await (const chunk of this.#response.body) {
                    const chunkBuffer = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
                    this.#accumulatedData = Buffer.concat([this.#accumulatedData, chunkBuffer]);
                    loaded += chunk.length;
                    // We need to re-throw the error as we don't want it to be caught by the try/catch.
                    try {
                        this.#dispatchEvent(new ProgressEvent('progress', {
                            lengthComputable: contentLengthNumber !== null,
                            loaded,
                            total: contentLengthNumber !== null ? contentLengthNumber : 0
                        }));
                    }
                    catch (error) {
                        eventError = error;
                        throw error;
                    }
                }
            }
            catch (error) {
                if (error === eventError) {
                    throw error;
                }
                onError(error);
                return;
            }
        }
        this.#responseBody = XMLHttpRequestResponseDataParser.parse({
            window: window,
            responseType: this.#responseType,
            data: this.#accumulatedData,
            contentType: this.#overriddenMimeType ||
                this.#response.headers.get('Content-Type') ||
                this.#request.headers.get('Content-Type')
        });
        this.#readyState = XMLHttpRequestReadyStateEnum.done;
        this.#accumulatedData = Buffer.from([]);
        asyncTaskManager.endTask(taskID);
        this.#dispatchEvent(new Event('readystatechange'));
        this.#dispatchEvent(new Event('load'));
        this.#dispatchEvent(new Event('loadend'));
    }
    /**
     * Sends the request to the server synchronously.
     *
     * @param body Optional data to send as request body.
     */
    #sendSync(body) {
        const window = this[PropertySymbol.window];
        const browserFrame = new WindowBrowserContext(window).getBrowserFrame();
        if (!browserFrame) {
            return;
        }
        if (body) {
            this.#request = new window.Request(this.#request.url, {
                method: this.#request.method,
                headers: this.#request.headers,
                signal: this.#abortController.signal,
                credentials: this.#request.credentials,
                body
            });
        }
        this.#readyState = XMLHttpRequestReadyStateEnum.loading;
        const fetch = new SyncFetch({
            browserFrame,
            window: window,
            url: this.#request.url,
            init: this.#request
        });
        try {
            this.#response = fetch.send();
        }
        catch (error) {
            this.#readyState = XMLHttpRequestReadyStateEnum.done;
            this.#dispatchEvent(new ErrorEvent('error', { error: error, message: error.message }));
            this.#dispatchEvent(new Event('loadend'));
            this.#dispatchEvent(new Event('readystatechange'));
            return;
        }
        this.#readyState = XMLHttpRequestReadyStateEnum.headersReceived;
        this.#responseBody = XMLHttpRequestResponseDataParser.parse({
            window: window,
            responseType: this.#responseType,
            data: this.#response.body,
            contentType: this.#overriddenMimeType ||
                this.#response.headers.get('Content-Type') ||
                this.#request.headers.get('Content-Type')
        });
        this.#readyState = XMLHttpRequestReadyStateEnum.done;
        this.#dispatchEvent(new Event('readystatechange'));
        this.#dispatchEvent(new Event('load'));
        this.#dispatchEvent(new Event('loadend'));
    }
    /**
     * Dispatches an event with try/catch.
     *
     * This is necessary to prevent errors from setting XMLHttpRequest in an invalid state.
     *
     * @param event Event.
     */
    #dispatchEvent(event) {
        const browserFrame = new WindowBrowserContext(this[PropertySymbol.window]).getBrowserFrame();
        if (!browserFrame?.page?.context?.browser?.settings?.errorCapture) {
            return;
        }
        const settings = browserFrame.page.context.browser.settings;
        const errorCapture = settings.errorCapture;
        settings.errorCapture = BrowserErrorCaptureEnum.tryAndCatch;
        this.dispatchEvent(event);
        settings.errorCapture = errorCapture;
    }
}
//# sourceMappingURL=XMLHttpRequest.js.map