/**
 * Parses a given response text into a JSON object. If the parsing fails due to invalid JSON format,
 * the original response text is returned.
 *
 * @param {string} responseText - The response text to be parsed into JSON. It is expected to be a valid JSON string.
 * @returns {Object|string} The parsed JSON object if the responseText is valid JSON, otherwise returns the original responseText.
 * @example
 * // returns { key: "value" }
 * parseResponse('{"key": "value"}');
 *
 * @example
 * // returns "Invalid JSON"
 * parseResponse('Invalid JSON');
 */
async function parseResponse(target) {
    if ( target.responseType === 'blob' ) {
        // Get content type of the blob
        const contentType = target.getResponseHeader('content-type');
        if ( contentType.startsWith('application/json') ) {
            // If the blob is JSON, parse it
            const text = await target.response.text();
            try {
                return JSON.parse(text);
            } catch (error) {
                return text;
            }
        }else if ( contentType.startsWith('application/octet-stream') ) {
            // If the blob is an octet stream, return the blob
            return target.response;
        }

        // Otherwise return an ojbect
        return {
            success: true,
            result: target.response,
        };
    }
    const responseText = target.responseText;
    try {
        return JSON.parse(responseText);
    } catch (error) {
        return responseText;
    }
}

/**
 * A function that generates a UUID (Universally Unique Identifier) using the version 4 format, 
 * which are random UUIDs. It uses the cryptographic number generator available in modern browsers.
 *
 * The generated UUID is a 36 character string (32 alphanumeric characters separated by 4 hyphens). 
 * It follows the pattern: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx, where x is any hexadecimal digit 
 * and y is one of 8, 9, A, or B.
 *
 * @returns {string} Returns a new UUID v4 string.
 *
 * @example
 * 
 * let id = this.#uuidv4(); // Generate a new UUID
 * 
 */
function uuidv4(){
    return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
        (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
    );
}


/**
 * Initializes and returns an XMLHttpRequest object configured for a specific API endpoint, method, and headers.
 *
 * @param {string} endpoint - The API endpoint to which the request will be sent. This is appended to the API origin URL.
 * @param {string} APIOrigin - The origin URL of the API. This is prepended to the endpoint.
 * @param {string} authToken - The authorization token used for accessing the API. This is included in the request headers.
 * @param {string} [method='post'] - The HTTP method to be used for the request. Defaults to 'post' if not specified.
 * @param {string} [contentType='application/json;charset=UTF-8'] - The content type of the request. Defaults to
 *                                                                  'application/json;charset=UTF-8' if not specified.
 *
 * @returns {XMLHttpRequest} The initialized XMLHttpRequest object.
 */
function initXhr(endpoint, APIOrigin, authToken, method= "post", contentType = "application/json;charset=UTF-8", responseType = undefined) {
    const xhr = new XMLHttpRequest();
    xhr.open(method, APIOrigin + endpoint, true);
    xhr.setRequestHeader("Authorization", "Bearer " + authToken);
    xhr.setRequestHeader("Content-Type", contentType);
    xhr.responseType = responseType ?? '';
    return xhr;
}

/**
 * Handles an HTTP response by invoking appropriate callback functions and resolving or rejecting a promise.
 * 
 * @param {Function} success_cb - An optional callback function for successful responses. It should take a response object
 *                                as its only argument.
 * @param {Function} error_cb - An optional callback function for error handling. It should take an error object
 *                              as its only argument.
 * @param {Function} resolve_func - A function used to resolve a promise. It should take a response object
 *                                  as its only argument.
 * @param {Function} reject_func - A function used to reject a promise. It should take an error object
 *                                 as its only argument.
 * @param {Object} response - The HTTP response object from the request. Expected to have 'status' and 'responseText'
 *                            properties.
 * 
 * @returns {void} The function does not return a value but will either resolve or reject a promise based on the
 *                 response status.
 */
async function handle_resp(success_cb, error_cb, resolve_func, reject_func, response){
    const resp = await parseResponse(response);
    // error - unauthorized
    if(response.status === 401){
        // if error callback is provided, call it
        if(error_cb && typeof error_cb === 'function')
            error_cb({status: 401, message: 'Unauthorized'})
        // reject promise
        return reject_func({status: 401, message: 'Unauthorized'})
    }
    // error - other
    else if(response.status !== 200){
        // if error callback is provided, call it
        if(error_cb && typeof error_cb === 'function')
            error_cb(resp)
        // reject promise
        return reject_func(resp)
    }
    // success
    else{
        // This is a driver error
        if(resp.success === false && resp.error?.code === 'permission_denied'){
            let perm = await puter.ui.requestPermission({permission: 'driver:puter-image-generation:generate'});
            // try sending again if permission was granted
            if(perm.granted){
                // todo repeat request
            }
        }
        // if success callback is provided, call it
        if(success_cb && typeof success_cb === 'function')
            success_cb(resp);
        // resolve with success
        return resolve_func(resp);
    }
}

/**
 * Handles an error by invoking a specified error callback and then rejecting a promise.
 * 
 * @param {Function} error_cb - An optional callback function that is called if it's provided.
 *                              This function should take an error object as its only argument.
 * @param {Function} reject_func - A function used to reject a promise. It should take an error object
 *                                 as its only argument.
 * @param {Object} error - The error object that is passed to both the error callback and the reject function.
 * 
 * @returns {void} The function does not return a value but will call the reject function with the error.
 */
function handle_error(error_cb, reject_func, error){
    // if error callback is provided, call it
    if(error_cb && typeof error_cb === 'function')
        error_cb(error)
    // reject promise
    return reject_func(error)
}

function setupXhrEventHandlers(xhr, success_cb, error_cb, resolve_func, reject_func) {
    // load: success or error
    xhr.addEventListener('load', function(e){
        return handle_resp(success_cb, error_cb, resolve_func, reject_func, this, xhr);
    });

    // error
    xhr.addEventListener('error', function(e){
        return handle_error(error_cb, reject_func, this);
    })
}

const NOOP = () => {};
class Valid {
    static callback (cb) {
        return (cb && typeof cb === 'function') ? cb : undefined;
    }
}

/**
 * Makes the hybrid promise/callback function for a particular driver method
 * @param {string[]} arg_defs - argument names (for now; definitions eventually)
 * @param {string} driverInterface - name of the interface
 * @param {string} driverName - name of the driver
 * @param {string} driverMethod - name of the method
 */
function make_driver_method(arg_defs, driverInterface, driverName, driverMethod, settings = {}) {
    return async function (...args) {
        let driverArgs = {};
        let options = {};

        // Check if the first argument is an object and use it as named parameters
        if (args.length === 1 && typeof args[0] === 'object' && !Array.isArray(args[0])) {
            driverArgs = { ...args[0] };
            options = {
                success: driverArgs.success,
                error: driverArgs.error,
            };
            // Remove callback functions from driverArgs if they exist
            delete driverArgs.success;
            delete driverArgs.error;
        } else {
            // Handle as individual arguments
            arg_defs.forEach((argName, index) => {
                driverArgs[argName] = args[index];
            });
            options = {
                success: args[arg_defs.length],
                error: args[arg_defs.length + 1],
            };
        }

        // preprocess
        if(settings.preprocess && typeof settings.preprocess === 'function'){
            driverArgs = settings.preprocess(driverArgs);
        }

        return await driverCall(options, driverInterface, driverName, driverMethod, driverArgs, settings);
    };
}

async function driverCall (options, driverInterface, driverName, driverMethod, driverArgs, settings) {
    const tp = new TeePromise();

    driverCall_(
        options,
        tp.resolve.bind(tp),
        tp.reject.bind(tp),
        driverInterface,
        driverName,
        driverMethod,
        driverArgs,
        undefined,
        undefined,
        settings,
    );

    return await tp;
}

// This function encapsulates the logic for sending a driver call request
async function driverCall_(
    options = {},
    resolve_func, reject_func,
    driverInterface, driverName, driverMethod, driverArgs,
    method,
    contentType = 'application/json;charset=UTF-8',
    settings = {},
) {
    // If there is no authToken and the environment is web, try authenticating with Puter
    if(!puter.authToken && puter.env === 'web'){
        try{
            await puter.ui.authenticateWithPuter();
        }catch(e){
            return reject_func({
                error: {
                    code: 'auth_canceled', message: 'Authentication canceled'
                }
            })
        }
    }

    const success_cb = Valid.callback(options.success) ?? NOOP;
    const error_cb = Valid.callback(options.error) ?? NOOP;
    // create xhr object
    const xhr = initXhr('/drivers/call', puter.APIOrigin, puter.authToken, 'POST', contentType);

    if ( settings.responseType ) {
        xhr.responseType = settings.responseType;
    }
    
    // ===============================================
    // TO UNDERSTAND THIS CODE, YOU MUST FIRST
    // UNDERSTAND THE FOLLOWING TEXT:
    //
    // Everything between here and the comment reading
    // "=== END OF STREAMING ===" is ONLY for handling
    // requests with content type "application/x-ndjson"
    // ===============================================
    
    let is_stream = false;
    let signal_stream_update = null;
    let lastLength = 0;
    let response_complete = false;
    const parts_received = [];
    xhr.onreadystatechange = () => {
        if ( xhr.readyState === 2 ) {
            if ( xhr.getResponseHeader("Content-Type") !==
                'application/x-ndjson'
            ) return;
            is_stream = true;
            const Stream = async function* Stream () {
                while ( ! response_complete ) {
                    const tp = new TeePromise();
                    signal_stream_update = tp.resolve.bind(tp);
                    await tp;
                    if ( response_complete ) break;
                    while ( parts_received.length > 0 ) {
                        const value = parts_received.pop();
                        const parts = value.split('\n');
                        for ( const part of parts ) {
                            if ( part.trim() === '' ) continue;
                            yield JSON.parse(part);
                        }
                    }
                }
            }
        
            return resolve_func(Stream());
        }
        if ( xhr.readyState === 4 ) {
            response_complete = true;
            if ( is_stream ) {
                signal_stream_update?.();
            }
        }
    };

    xhr.onprogress = function() {
        if ( ! signal_stream_update ) return;

        const newText = xhr.responseText.slice(lastLength);
        lastLength = xhr.responseText.length; // Update lastLength to the current length
        
        parts_received.push(newText);
        signal_stream_update();
    };
    
    // ========================
    // === END OF STREAMING ===
    // ========================

    // load: success or error
    xhr.addEventListener('load', async function(response){
        if ( is_stream ) {
            return;
        }
        const resp = await parseResponse(response.target);
        // HTTP Error - unauthorized
        if(response.status === 401 || resp?.code === "token_auth_failed"){
            if(resp?.code === "token_auth_failed" && puter.env === 'web'){
                try{
                    puter.resetAuthToken();
                    await puter.ui.authenticateWithPuter();
                }catch(e){
                    return reject_func({
                        error: {
                            code: 'auth_canceled', message: 'Authentication canceled'
                        }
                    })
                }
            }
            // if error callback is provided, call it
            if(error_cb && typeof error_cb === 'function')
                error_cb({status: 401, message: 'Unauthorized'})
            // reject promise
            return reject_func({status: 401, message: 'Unauthorized'})
        }
        // HTTP Error - other
        else if(response.status && response.status !== 200){
            // if error callback is provided, call it
            error_cb(resp)
            // reject promise
            return reject_func(resp)
        }
        // HTTP Success
        else{
            // Driver Error: permission denied
            if(resp.success === false && resp.error?.code === 'permission_denied'){
                let perm = await puter.ui.requestPermission({permission: 'driver:' + driverInterface + ':' + driverMethod});
                // try sending again if permission was granted
                if(perm.granted){
                    // repeat request with permission granted
                    return driverCall_(options, resolve_func, reject_func, driverInterface, driverMethod, driverArgs, method, contentType, settings);
                }else{
                    // if error callback is provided, call it
                    error_cb(resp)
                    // reject promise
                    return reject_func(resp)
                }
            }
            // Driver Error: other
            else if(resp.success === false){
                // if error callback is provided, call it
                error_cb(resp)
                // reject promise
                return reject_func(resp)
            }

            let result = resp.result !== undefined ? resp.result : resp;
            if ( settings.transform ) {
                result = await settings.transform(result);
            }

            // Success: if callback is provided, call it
            if(resolve_func.success)
                success_cb(result);
            // Success: resolve with the result
            return resolve_func(result);
        }
    });

    // error
    xhr.addEventListener('error', function(e){
        return handle_error(error_cb, reject_func, this);
    })

    // send request
    xhr.send(JSON.stringify({
        interface: driverInterface,
        driver: driverName,
        test_mode: settings?.test_mode, 
        method: driverMethod,
        args: driverArgs,
    }));
}

class TeePromise {
    static STATUS_PENDING = {};
    static STATUS_RUNNING = {};
    static STATUS_DONE = {};
    constructor () {
        this.status_ = this.constructor.STATUS_PENDING;
        this.donePromise = new Promise((resolve, reject) => {
            this.doneResolve = resolve;
            this.doneReject = reject;
        });
    }
    get status () {
        return this.status_;
    }
    set status (status) {
        this.status_ = status;
        if ( status === this.constructor.STATUS_DONE ) {
            this.doneResolve();
        }
    }
    resolve (value) {
        this.status_ = this.constructor.STATUS_DONE;
        this.doneResolve(value);
    }
    awaitDone () {
        return this.donePromise;
    }
    then (fn, rfn) {
        return this.donePromise.then(fn, rfn);
    }

    reject (err) {
        this.status_ = this.constructor.STATUS_DONE;
        this.doneReject(err);
    }
    
    /**
     * @deprecated use then() instead
     */
    onComplete(fn) {
        return this.then(fn);
    }
}

async function blob_to_url (blob) {
    const tp = new TeePromise();
    const reader = new FileReader();
    reader.onloadend = () => tp.resolve(reader.result);
    reader.readAsDataURL(blob);
    return await tp;
}

function blobToDataUri(blob) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = function(event) {
            resolve(event.target.result);
        };
        reader.onerror = function(error) {
            reject(error);
        };
        reader.readAsDataURL(blob);
    });
}

function arrayBufferToDataUri(arrayBuffer) {
    return new Promise((resolve, reject) => {
        const blob = new Blob([arrayBuffer]);
        const reader = new FileReader();
        reader.onload = function(event) {
            resolve(event.target.result);
        };
        reader.onerror = function(error) {
            reject(error);
        };
        reader.readAsDataURL(blob);
    });
}

export {parseResponse, uuidv4, handle_resp, handle_error, initXhr, setupXhrEventHandlers, driverCall,
    TeePromise,
    make_driver_method,
    blob_to_url,
    arrayBufferToDataUri,
    blobToDataUri,
};