/** SD-UI Backend control and classes.
 */
(function () { "use strict";
    const RETRY_DELAY_IF_BUFFER_IS_EMPTY = 1000 // ms
    const RETRY_DELAY_IF_SERVER_IS_BUSY = 30 * 1000 // ms, status_code 503, already a task running
    const RETRY_DELAY_ON_ERROR = 4000 // ms
    const TASK_STATE_SERVER_UPDATE_DELAY = 1500 // ms
    const SERVER_STATE_VALIDITY_DURATION = 90 * 1000 // ms - 90 seconds to allow ping to timeout more than once before killing tasks.
    const HEALTH_PING_INTERVAL = 5000 // ms
    const IDLE_COOLDOWN = 2500 // ms
    const CONCURRENT_TASK_INTERVAL = 100 // ms

    /** Connects to an endpoint and resumes connection after reaching end of stream until all data is received.
     * Allows closing the connection while the server buffers more data.
     */
    class ChunkedStreamReader {
        #bufferedString = '' // Data received waiting to be read.
        #url
        #fetchOptions
        #response

        constructor(url, initialContent='', options={}) {
            if (typeof url !== 'string' && !(url instanceof String)) {
                throw new Error('url is not a string.')
            }
            if (typeof initialContent !== 'undefined' && typeof initialContent !== 'string') {
                throw new Error('initialContent is not a string.')
            }
            this.#bufferedString = initialContent
            this.#url = url
            this.#fetchOptions = Object.assign({
                headers: {
                    'Content-Type': 'application/json'
                }
            }, options)
            this.onNext = undefined
        }

        get url() {
            if (this.#response.redirected) {
                return this.#response.url
            }
            return this.#url
        }
        get bufferedString() {
            return this.#bufferedString
        }
        get status() {
            this.#response?.status
        }
        get statusText() {
            this.#response?.statusText
        }

        parse(value) {
            if (typeof value === 'undefined') {
                return
            }
            if (!isArrayOrTypedArray(value)) {
                return [value]
            }
            if (value.length === 0) {
                return value
            }
            if (typeof this.textDecoder === 'undefined') {
                this.textDecoder = new TextDecoder()
            }
            return [this.textDecoder.decode(value)]
        }
        onComplete(value) {
            return value
        }
        onError(response) {
            throw new Error(response.statusText)
        }
        onNext({value, done}, response) {
            return {value, done}
        }

        async *[Symbol.asyncIterator]() {
            return this.open()
        }
        async *open() {
            let value = undefined
            let done = undefined
            do {
                if (this.#response) {
                    await asyncDelay(RETRY_DELAY_IF_BUFFER_IS_EMPTY)
                }
                this.#response = await fetch(this.#url, this.#fetchOptions)
                if (!this.#response.ok) {
                    if (this.#response.status === 425) {
                        continue
                    }
                    // Request status indicate failure
                    console.warn('Stream %o stopped unexpectedly.', this.#response)
                    value = await Promise.resolve(this.onError(this.#response))
                    if (typeof value === 'boolean' && value) {
                        continue
                    }
                    return value
                }
                const reader = this.#response.body.getReader()
                done = false
                do {
                    const readState = await reader.read()
                    value = this.parse(readState.value)
                    if (value) {
                        for(let sVal of value) {
                            ({value: sVal, done} = await Promise.resolve(this.onNext({value:sVal, done:readState.done})))
                            yield sVal
                            if (done) {
                                return this.onComplete(sVal)
                            }
                        }
                    }
                    if (done) {
                        return
                    }
                } while(value && !done)
            } while (!done && (this.#response.ok || this.#response.status === 425))
        }
        *readStreamAsJSON(jsonStr, throwOnError) {
            if (typeof jsonStr !== 'string') {
                throw new Error('jsonStr is not a string.')
            }
            do {
                if (this.#bufferedString.length > 0) {
                    // Append new data when required
                    if (jsonStr.length > 0) {
                        jsonStr = this.#bufferedString + jsonStr
                    } else {
                        jsonStr = this.#bufferedString
                    }
                    this.#bufferedString = ''
                }
                if (!jsonStr) {
                    return
                }
                // Find next delimiter
                let lastChunkIdx = jsonStr.indexOf('}{')
                if (lastChunkIdx >= 0) {
                    this.#bufferedString = jsonStr.substring(0, lastChunkIdx + 1)
                    jsonStr = jsonStr.substring(lastChunkIdx + 1)
                } else {
                    this.#bufferedString = jsonStr
                    jsonStr = ''
                }
                if (this.#bufferedString.length <= 0) {
                    return
                }
                // hack for a middleman buffering all the streaming updates, and unleashing them on the poor browser in one shot.
                // this results in having to parse JSON like {"step": 1}{"step": 2}{"step": 3}{"ste...
                // which is obviously invalid and can happen at any point while rendering.
                // So we need to extract only the next {} section
                try { // Try to parse
                    const jsonObj = JSON.parse(this.#bufferedString)
                    this.#bufferedString = jsonStr
                    jsonStr = ''
                    yield jsonObj
                } catch (e) {
                    if (throwOnError) {
                        console.error(`Parsing: "${this.#bufferedString}", Buffer: "${jsonStr}"`)
                    }
                    this.#bufferedString += jsonStr
                    if (e instanceof SyntaxError && !throwOnError) {
                        return
                    }
                    throw e
                }
            } while (this.#bufferedString.length > 0 && this.#bufferedString.indexOf('}') >= 0)
        }
    }

    const EVENT_IDLE = 'idle'
    const EVENT_STATUS_CHANGED = 'statusChange'
    const EVENT_UNHANDLED_REJECTION = 'unhandledRejection'
    const EVENT_TASK_QUEUED = 'taskQueued'
    const EVENT_TASK_START = 'taskStart'
    const EVENT_TASK_END = 'taskEnd'
    const EVENT_TASK_ERROR = 'task_error'
    const EVENT_UNEXPECTED_RESPONSE = 'unexpectedResponse'
    const EVENTS_TYPES = [
        EVENT_IDLE,
        EVENT_STATUS_CHANGED,
        EVENT_UNHANDLED_REJECTION,

        EVENT_TASK_QUEUED,
        EVENT_TASK_START,
        EVENT_TASK_END,
        EVENT_TASK_ERROR,

        EVENT_UNEXPECTED_RESPONSE,
    ]
    Object.freeze(EVENTS_TYPES)
    const eventSource = new GenericEventSource(EVENTS_TYPES)

    function setServerStatus(msgType, msg) {
        return eventSource.fireEvent(EVENT_STATUS_CHANGED, {type: msgType, message: msg})
    }

    const ServerStates = {
        init: 'Init',
        loadingModel: 'LoadingModel',
        online: 'Online',
        rendering: 'Rendering',
        unavailable: 'Unavailable',
    }
    Object.freeze(ServerStates)

    let sessionId = Date.now()
    let serverState = {'status': ServerStates.unavailable, 'time': Date.now()}

    async function healthCheck() {
        if (Date.now() < serverState.time + (HEALTH_PING_INTERVAL / 2) && isServerAvailable()) {
            // Ping confirmed online less than half of HEALTH_PING_INTERVAL ago.
            return true
        }
        if (Date.now() >= serverState.time + SERVER_STATE_VALIDITY_DURATION) {
            console.warn('WARNING! SERVER_STATE_VALIDITY_DURATION has elapsed since the last Ping completed.')
        }
        try {
            let res = undefined
            if (typeof sessionId !== 'undefined') {
                res = await fetch('/ping?session_id=' + sessionId)
            } else {
                res = await fetch('/ping')
            }
            serverState = await res.json()
            if (typeof serverState !== 'object' || typeof serverState.status !== 'string') {
                console.error(`Server reply didn't contain a state value.`)
                serverState = {'status': ServerStates.unavailable, 'time': Date.now()}
                setServerStatus('error', 'offline')
                return false
            }
            // Set status
            switch(serverState.status) {
                case ServerStates.init:
                    // Wait for init to complete before updating status.
                    break
                case ServerStates.online:
                    setServerStatus('online', 'ready')
                    break
                case ServerStates.loadingModel:
                    setServerStatus('busy', 'loading..')
                    break
                case ServerStates.rendering:
                    setServerStatus('busy', 'rendering..')
                    break
                default: // Unavailable
                    console.error('Ping received an unexpected server status. Status: %s', serverState.status)
                    setServerStatus('error', serverState.status.toLowerCase())
                    break
            }
            serverState.time = Date.now()
            return true
        } catch (e) {
            console.error(e)
            serverState = {'status': ServerStates.unavailable, 'time': Date.now()}
            setServerStatus('error', 'offline')
        }
        return false
    }

    function isServerAvailable() {
        if (typeof serverState !== 'object') {
            console.error('serverState not set to a value. Connection to server could be lost...')
            return false
        }
        if (Date.now() >= serverState.time + SERVER_STATE_VALIDITY_DURATION) {
            console.warn('SERVER_STATE_VALIDITY_DURATION elapsed. Connection to server could be lost...')
            return false
        }
        switch (serverState.status) {
            case ServerStates.loadingModel:
            case ServerStates.rendering:
            case ServerStates.online:
                return true
            default:
                console.warn('Unexpected server status. Server could be unavailable... Status: %s', serverState.status)
                return false
        }
    }

    async function waitUntil(isReadyFn, delay, timeout) {
        if (typeof delay === 'number') {
            const msDelay = delay
            delay = () => asyncDelay(msDelay)
        }
        if (typeof delay !== 'function') {
            throw new Error('delay is not a number or a function.')
        }
        if (typeof timeout !== 'undefined' && typeof timeout !== 'number') {
            throw new Error('timeout is not a number.')
        }
        if (typeof timeout === 'undefined' || timeout < 0) {
            timeout = Number.MAX_SAFE_INTEGER
        }
        timeout = Date.now() + timeout
        while (timeout > Date.now()
            && Date.now() < serverState.time + SERVER_STATE_VALIDITY_DURATION
            && !Boolean(await Promise.resolve(isReadyFn()))
        ) {
            await delay()
            if (!isServerAvailable()) { // Can fail if ping got frozen/suspended...
                if (await healthCheck() && isServerAvailable()) { // Force a recheck of server status before failure...
                    continue // Continue waiting if last healthCheck confirmed the server is still alive.
                }
                throw new Error('Connection with server lost.')
            }
        }
        if (Date.now() >= serverState.time + SERVER_STATE_VALIDITY_DURATION) {
            console.warn('SERVER_STATE_VALIDITY_DURATION elapsed. Released waitUntil on stale server state.')
        }
    }

    const TaskStatus = {
        init: 'init',
        pending: 'pending', // Queued locally, not yet posted to server
        waiting: 'waiting', // Waiting to run on server
        processing: 'processing',
        stopped: 'stopped',
        completed: 'completed',
        failed: 'failed',
    }
    Object.freeze(TaskStatus)

    const TASK_STATUS_ORDER = [
        TaskStatus.init,
        TaskStatus.pending,
        TaskStatus.waiting,
        TaskStatus.processing,
        //Don't add status that are final.
    ]

    const task_queue = new Map()
    const concurrent_generators = new Map()
    const weak_results = new WeakMap()

    class Task {
        // Private properties...
        _reqBody = {} // request body of this task.
        #reader = undefined
        #status = TaskStatus.init
        #id = undefined
        #exception = undefined

        constructor(options={}) {
            this._reqBody = Object.assign({}, options)
            if (typeof this._reqBody.session_id === 'undefined') {
                this._reqBody.session_id = sessionId
            } else if (this._reqBody.session_id !== SD.sessionId && String(this._reqBody.session_id) !== String(SD.sessionId)) {
                throw new Error('Use SD.sessionId to set the request session_id.')
            }
            this._reqBody.session_id = String(this._reqBody.session_id)
        }

        get id() {
            return this.#id
        }
        _setId(id) {
            if (typeof this.#id !== 'undefined') {
                throw new Error('The task ID can only be set once.')
            }
            this.#id = id
        }

        get exception() {
            return this.#exception
        }
        async abort(exception) {
            if (this.isCompleted || this.isStopped || this.hasFailed) {
                return
            }
            if (typeof exception !== 'undefined') {
                if (typeof exception === 'string') {
                    exception = new Error(exception)
                }
                if (typeof exception !== 'object') {
                    throw new Error('exception is not an object.')
                }
                if (!(exception instanceof Error)) {
                    throw new Error('exception is not an Error or a string.')
                }
            }
            const res = await fetch('/image/stop?task=' + this.id)
            if (!res.ok) {
                console.log('Stop response:', res)
                throw new Error(res.statusText)
            }
            task_queue.delete(this)
            this.#exception = exception
            this.#status = (exception ? TaskStatus.failed : TaskStatus.stopped)
        }

        get reqBody() {
            if (this.#status === TaskStatus.init) {
                return this._reqBody
            }
            console.warn('Task reqBody cannot be changed after the init state.')
            return Object.assign({}, this._reqBody)
        }

        get isPending() {
            return TASK_STATUS_ORDER.indexOf(this.#status) >= 0
        }
        get isCompleted() {
            return this.#status === TaskStatus.completed
        }
        get hasFailed() {
            return this.#status === TaskStatus.failed
        }
        get isStopped() {
            return this.#status === TaskStatus.stopped
        }
        get status() {
            return this.#status
        }
        _setStatus(status) {
            if (status === this.#status) {
                return
            }
            const currentIdx = TASK_STATUS_ORDER.indexOf(this.#status)
            if (currentIdx < 0) {
                throw Error(`The task status ${this.#status} is final and can't be changed.`)
            }
            const newIdx = TASK_STATUS_ORDER.indexOf(status)
            if (newIdx >= 0 && newIdx < currentIdx) {
                throw Error(`The task status ${status} can't replace ${this.#status}.`)
            }
            this.#status = status
        }

        /** Send current task to server.
         * @param {*} [timeout=-1] Optional timeout value in ms
         * @returns the response from the render request.
         * @memberof Task
         */
        async post(url, timeout=-1) {
            if(this.status !== TaskStatus.init && this.status !== TaskStatus.pending) {
                throw new Error(`Task status ${this.status} is not valid for post.`)
            }
            this._setStatus(TaskStatus.pending)
            Object.freeze(this._reqBody)

            const abortSignal = (timeout >= 0 ? AbortSignal.timeout(timeout) : undefined)
            let res = undefined
            try {
                this.checkReqBody()
                do {
                    abortSignal?.throwIfAborted()
                    res = await fetch(url, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json'
                        },
                        body: JSON.stringify(this._reqBody),
                        signal: abortSignal
                    })
                    // status_code 503, already a task running.
                } while (res.status === 503 && await asyncDelay(RETRY_DELAY_IF_SERVER_IS_BUSY))
            } catch (err) {
                this.abort(err)
                throw err
            }
            if (!res.ok) {
                const err = new Error(`Unexpected response HTTP${res.status}. Details: ${res.statusText}`)
                this.abort(err)
                throw err
            }
            return await res.json()
        }

        static getReader(url) {
            const reader = new ChunkedStreamReader(url)
            const parseToString = reader.parse
            reader.parse = function(value) {
                value = parseToString.call(this, value)
                if (!value || value.length <= 0) {
                    return
                }
                return reader.readStreamAsJSON(value.join(''))
            }
            reader.onNext = function({done, value}) {
                // By default is completed when the return value has a status defined.
                if (typeof value === 'object' && 'status' in value) {
                    done = true
                }
                return {done, value}
            }
            return reader
        }
        _setReader(reader) {
            if (typeof this.#reader !== 'undefined') {
                throw new Error('The task reader can only be set once.')
            }
            this.#reader = reader
        }
        get reader() {
            if (this.#reader) {
                return this.#reader
            }
            if (!this.streamUrl) {
                throw new Error('The task has no stream Url defined.')
            }
            this.#reader = Task.getReader(this.streamUrl)
            const task = this
            const onNext = this.#reader.onNext
            this.#reader.onNext = function({done, value}) {
                if (value && typeof value === 'object') {
                    if (task.status === TaskStatus.init
                        || task.status === TaskStatus.pending
                        || task.status === TaskStatus.waiting
                    ) {
                        task._setStatus(TaskStatus.processing)
                    }
                    if ('step' in value && 'total_steps' in value) {
                        task.step = value.step
                        task.total_steps = value.total_steps
                    }
                }
                return onNext.call(this, {done, value})
            }
            this.#reader.onComplete = function(value) {
                task.result = value
                if (task.isPending) {
                    task._setStatus(TaskStatus.completed)
                }
                return value
            }
            this.#reader.onError = function(response) {
                const err = new Error(response.statusText)
                task.abort(err)
                throw err
            }
            return this.#reader
        }

        async waitUntil({timeout=-1, callback, status, signal}) {
            const currentIdx = TASK_STATUS_ORDER.indexOf(this.#status)
            if (currentIdx <= 0) {
                return false
            }
            const stIdx = (status ? TASK_STATUS_ORDER.indexOf(status) : currentIdx + 1)
            if (stIdx >= 0 && stIdx <= currentIdx) {
                return true
            }
            if (stIdx < 0 && currentIdx < 0) {
                return this.#status === (status || TaskStatus.completed)
            }
            if (signal?.aborted) {
                return false
            }
            const task = this
            switch(this.#status) {
                case TaskStatus.pending:
                case TaskStatus.waiting:
                    // Wait for server status to include this task.
                    await waitUntil(
                        async () => {
                            if (task.#id && typeof serverState.tasks === 'object' && Object.keys(serverState.tasks).includes(String(task.#id))) {
                                return true
                            }
                            if (await Promise.resolve(callback?.call(task)) || signal?.aborted) {
                                return true
                            }
                        },
                        TASK_STATE_SERVER_UPDATE_DELAY,
                        timeout,
                    )
                    if (this.#id && typeof serverState.tasks === 'object' && Object.keys(serverState.tasks).includes(String(task.#id))) {
                        this._setStatus(TaskStatus.waiting)
                    }
                    if (await Promise.resolve(callback?.call(this)) || signal?.aborted) {
                        return false
                    }
                    if (stIdx >= 0 && stIdx <= TASK_STATUS_ORDER.indexOf(TaskStatus.waiting)) {
                        return true
                    }
                    // Wait for task to start on server.
                    await waitUntil(
                        async () => {
                            if (typeof serverState.tasks !== 'object' || serverState.tasks[String(task.#id)] !== 'pending') {
                                return true
                            }
                            if (await Promise.resolve(callback?.call(task)) || signal?.aborted) {
                                return true
                            }
                        },
                        TASK_STATE_SERVER_UPDATE_DELAY,
                        timeout,
                    )
                    const state = (typeof serverState.tasks === 'object' ? serverState.tasks[String(task.#id)] : undefined)
                    if (state === 'running' || state === 'buffer' || state === 'completed') {
                        this._setStatus(TaskStatus.processing)
                    }
                    if (await Promise.resolve(callback?.call(task)) || signal?.aborted) {
                        return false
                    }
                    if (stIdx >= 0 && stIdx <= TASK_STATUS_ORDER.indexOf(TaskStatus.processing)) {
                        return true
                    }
                case TaskStatus.processing:
                    await waitUntil(
                        async () => {
                            if (typeof serverState.tasks !== 'object' || serverState.tasks[String(task.#id)] !== 'running') {
                                return true
                            }
                            if (await Promise.resolve(callback?.call(task)) || signal?.aborted) {
                                return true
                            }
                        },
                        TASK_STATE_SERVER_UPDATE_DELAY,
                        timeout,
                    )
                    await Promise.resolve(callback?.call(this))
                default:
                    return this.#status === (status || TaskStatus.completed)
            }
        }

        async enqueue(promiseGenerator, ...args) {
            if (this.status !== TaskStatus.init) {
                throw new Error(`Task is in an invalid status ${this.status} to add to queue.`)
            }
            this._setStatus(TaskStatus.pending)
            task_queue.set(this, promiseGenerator)
            await eventSource.fireEvent(EVENT_TASK_QUEUED, {task:this})
            await Task.enqueue(promiseGenerator, ...args)
            await this.waitUntil({status: TaskStatus.completed})
            if (this.exception) {
                throw this.exception
            }
            return this.result
        }
        static async enqueue(promiseGenerator, ...args) {
            if (typeof promiseGenerator === 'undefined') {
                throw new Error('To enqueue a concurrent task, a *Promise Generator is needed but undefined was found.')
            }
            //if (Symbol.asyncIterator in result || Symbol.iterator in result) {
                //concurrent_generators.set(result, Promise.resolve(args))
            if (typeof promiseGenerator === 'function') {
                concurrent_generators.set(asGenerator({callback: promiseGenerator}), Promise.resolve(args))
            } else {
                concurrent_generators.set(promiseGenerator, Promise.resolve(args))
            }
            await waitUntil(() => !concurrent_generators.has(promiseGenerator), CONCURRENT_TASK_INTERVAL)
            return weak_results.get(promiseGenerator)
        }
        static enqueueNew(task, classCtor, progressCallback) {
            if (task.status !== TaskStatus.init) {
                throw new Error('Task has an invalid status to add to queue.')
            }
            if (!(task instanceof classCtor)) {
                throw new Error('Task is not a instance of classCtor.')
            }
            let promiseGenerator = undefined
            if (typeof progressCallback === 'undefined') {
                promiseGenerator = classCtor.start(task)
            } else if (typeof progressCallback === 'function') {
                promiseGenerator = classCtor.start(task, progressCallback)
            } else {
                throw new Error('progressCallback is not a function.')
            }
            return Task.prototype.enqueue.call(task, promiseGenerator)
        }

        static async run(promiseGenerator, {callback, signal, timeout=-1}={}) {
            let value = undefined
            let done = undefined
            if (timeout < 0) {
                timeout = Number.MAX_SAFE_INTEGER
            }
            timeout = Date.now() + timeout
            do {
                ({value, done} = await Promise.resolve(promiseGenerator.next(value)))
                if (value instanceof Promise) {
                    value = await value
                }
                if (callback) {
                    ({value, done} = await Promise.resolve(callback.call(promiseGenerator, {value, done})))
                }
                if (value instanceof Promise) {
                    value = await value
                }
            } while(!done && !signal?.aborted && timeout > Date.now())
            return value
        }
        static async *asGenerator({callback, generator, signal, timeout=-1}={}) {
            let value = undefined
            let done = undefined
            if (timeout < 0) {
                timeout = Number.MAX_SAFE_INTEGER
            }
            timeout = Date.now() + timeout
            do {
                ({value, done} = await Promise.resolve(generator.next(value)))
                if (value instanceof Promise) {
                    value = await value
                }
                if (callback) {
                    ({value, done} = await Promise.resolve(callback.call(generator, {value, done})))
                    if (value instanceof Promise) {
                        value = await value
                    }
                }
                value = yield value
            } while(!done && !signal?.aborted && timeout > Date.now())
            return value
        }
    }

    const TASK_REQUIRED = {
        "session_id": 'string',
        "prompt": 'string',
        "negative_prompt": 'string',
        "width": 'number',
        "height": 'number',
        "seed": 'number',

        "sampler_name": 'string',
        "use_stable_diffusion_model": 'string',
        "num_inference_steps": 'number',
        "guidance_scale": 'number',

        "num_outputs": 'number',
        "stream_progress_updates": 'boolean',
        "stream_image_progress": 'boolean',
        "show_only_filtered_image": 'boolean',
        "output_format": 'string',
        "output_quality": 'number',
    }
    const TASK_DEFAULTS = {
        "sampler_name": "plms",
        "use_stable_diffusion_model": "sd-v1-4",
        "num_inference_steps": 50,
        "guidance_scale": 7.5,
        "negative_prompt": "",

        "num_outputs": 1,
        "stream_progress_updates": true,
        "stream_image_progress": true,
        "show_only_filtered_image": true,
        "block_nsfw": false,
        "output_format": "png",
        "output_quality": 75,
    }
    const TASK_OPTIONAL = {
        "device": 'string',
        "init_image": 'string',
        "mask": 'string',
        "save_to_disk_path": 'string',
        "use_face_correction": 'string',
        "use_upscale": 'string',
        "use_vae_model": 'string',
        "use_hypernetwork_model": 'string',
        "hypernetwork_strength": 'number',
    }

    // Higer values will result in...
    // pytorch_lightning/utilities/seed.py:60: UserWarning: X is not in bounds, numpy accepts from 0 to 4294967295
    const MAX_SEED_VALUE = 4294967295

    class RenderTask extends Task {
        constructor(options={}) {
            super(options)
            if (typeof this._reqBody.seed === 'undefined') {
                this._reqBody.seed = Math.floor(Math.random() * (MAX_SEED_VALUE + 1))
            }
            if (typeof typeof this._reqBody.seed === 'number' && (this._reqBody.seed > MAX_SEED_VALUE || this._reqBody.seed < 0)) {
                throw new Error(`seed must be in range 0 to ${MAX_SEED_VALUE}.`)
            }

            if ('use_cpu' in this._reqBody) {
                if (this._reqBody.use_cpu) {
                    this._reqBody.device = 'cpu'
                }
                delete this._reqBody.use_cpu
            }
            if (this._reqBody.init_image) {
                if (typeof this._reqBody.prompt_strength === 'undefined') {
                    this._reqBody.prompt_strength = 0.8
                } else if (typeof this._reqBody.prompt_strength !== 'number') {
                    throw new Error(`prompt_strength need to be of type number but ${typeof this._reqBody.prompt_strength} was found.`)
                }
            }
            if ('modifiers' in this._reqBody) {
                if (Array.isArray(this._reqBody.modifiers) && this._reqBody.modifiers.length > 0) {
                    this._reqBody.modifiers = this._reqBody.modifiers.filter((val) => val.trim())
                    if (this._reqBody.modifiers.length > 0) {
                        this._reqBody.prompt = `${this._reqBody.prompt}, ${this._reqBody.modifiers.join(', ')}`
                    }
                }
                if (typeof this._reqBody.modifiers === 'string' && this._reqBody.modifiers.length > 0) {
                    this._reqBody.modifiers = this._reqBody.modifiers.trim()
                    if (this._reqBody.modifiers.length > 0) {
                        this._reqBody.prompt = `${this._reqBody.prompt}, ${this._reqBody.modifiers}`
                    }
                }
                delete this._reqBody.modifiers
            }
            this.checkReqBody()
        }

        checkReqBody() {
            for (const key in TASK_DEFAULTS) {
                if (typeof this._reqBody[key] === 'undefined') {
                    this._reqBody[key] = TASK_DEFAULTS[key]
                }
            }
            for (const key in TASK_REQUIRED) {
                if (typeof this._reqBody[key] !== TASK_REQUIRED[key]) {
                    throw new Error(`${key} need to be of type ${TASK_REQUIRED[key]} but ${typeof this._reqBody[key]} was found.`)
                }
            }
            for (const key in this._reqBody) {
                if (key in TASK_REQUIRED) {
                    continue
                }
                if (key in TASK_OPTIONAL) {
                    if (typeof this._reqBody[key] == "undefined") {
                        delete this._reqBody[key]
                        console.warn(`reqBody[${key}] was set to undefined. Removing optional key without value...`)
                        continue
                    }
                    if (typeof this._reqBody[key] !== TASK_OPTIONAL[key]) {
                        throw new Error(`${key} need to be of type ${TASK_OPTIONAL[key]} but ${typeof this._reqBody[key]} was found.`)
                    }
                }
            }
        }

        /** Send current task to server.
         * @param {*} [timeout=-1] Optional timeout value in ms
         * @returns the response from the render request.
         * @memberof Task
         */
        async post(timeout=-1) {
            performance.mark('make-render-request')
            if (performance.getEntriesByName('click-makeImage', 'mark').length > 0) {
                performance.measure('diff', 'click-makeImage', 'make-render-request')
                console.log('delay between clicking and making the server request:', performance.getEntriesByName('diff', 'measure')[0].duration + ' ms')
            }

            let jsonResponse = await super.post('/render', timeout)
            if (typeof jsonResponse?.task !== 'number') {
                console.warn('Endpoint error response: ', jsonResponse)
                const event = Object.assign({task:this}, jsonResponse)
                await eventSource.fireEvent(EVENT_UNEXPECTED_RESPONSE, event)
                if ('continueWith' in event) {
                    jsonResponse = await Promise.resolve(event.continueWith)
                }
                if (typeof jsonResponse?.task !== 'number') {
                    const err = new Error(jsonResponse?.detail || 'Endpoint response does not contains a task ID.')
                    this.abort(err)
                    throw err
                }
            }
            this._setId(jsonResponse.task)
            if (jsonResponse.stream) {
                this.streamUrl = jsonResponse.stream
            }
            this._setStatus(TaskStatus.waiting)
            return jsonResponse
        }

        enqueue(progressCallback) {
            return Task.enqueueNew(this, RenderTask, progressCallback)
        }
        *start(progressCallback) {
            if (typeof progressCallback !== 'undefined' && typeof progressCallback !== 'function') {
                throw new Error('progressCallback is not a function. progressCallback type: ' + typeof progressCallback)
            }
            if (this.isStopped) {
                return
            }

            this._setStatus(TaskStatus.pending)
            progressCallback?.call(this, {reqBody: this._reqBody})
            Object.freeze(this._reqBody)

            // Post task request to backend
            let renderRequest = undefined
            try {
                renderRequest = yield this.post()
                yield progressCallback?.call(this, {renderResponse: renderRequest})
            } catch (e) {
                yield progressCallback?.call(this, { detail: e.message })
                throw e
            }
            try { // Wait for task to start on server.
                yield this.waitUntil({
                    callback: function() { return progressCallback?.call(this, {}) },
                    status: TaskStatus.processing,
                })
            } catch (e) {
                this.abort(err)
                throw e
            }
            // Update class status and callback.
            const taskState = (typeof serverState.tasks === 'object' ? serverState.tasks[String(this.id)] : undefined)
            switch(taskState) {
                case 'pending': // Session has pending tasks.
                    console.error('Server %o render request %o is still waiting.', serverState, renderRequest)
                    //Only update status if not already set by waitUntil
                    if (this.status === TaskStatus.init
                        || this.status === TaskStatus.pending
                    ) {
                        // Set status as Waiting in backend.
                        this._setStatus(TaskStatus.waiting)
                    }
                    break
                case 'running':
                case 'buffer':
                    // Normal expected messages.
                    this._setStatus(TaskStatus.processing)
                    break
                case 'completed':
                    if (this.isPending) {
                        // Set state to processing until we read the reply
                        this._setStatus(TaskStatus.processing)
                    }
                    console.warn('Server %o render request %o completed unexpectedly', serverState, renderRequest)
                    break // Continue anyway to try to read cached result.
                case 'error':
                    this._setStatus(TaskStatus.failed)
                    console.error('Server %o render request %o has failed', serverState, renderRequest)
                    break // Still valid, Update UI with error message
                case 'stopped':
                    this._setStatus(TaskStatus.stopped)
                    console.log('Server %o render request %o was stopped', serverState, renderRequest)
                    return false
                default:
                    if (!progressCallback) {
                        const err = new Error('Unexpected server task state: ' + taskState || 'Undefined')
                        this.abort(err)
                        throw err
                    }
                    const response = yield progressCallback.call(this, {})
                    if (response instanceof Error) {
                        this.abort(response)
                        throw response
                    }
                    if (!response) {
                        return false
                    }
            }

            // Task started!
            // Open the reader.
            const reader = this.reader
            const task = this
            reader.onError = function(response) {
                if (progressCallback) {
                    task.abort(new Error(response.statusText))
                    return progressCallback.call(task, { response, reader })
                }
                return Task.prototype.onError.call(task, response)
            }
            yield progressCallback?.call(this, { reader })

            //Start streaming the results.
            const streamGenerator = reader.open()
            let value = undefined
            let done = undefined
            yield progressCallback?.call(this, { stream: streamGenerator })
            do {
                ({value, done} = yield streamGenerator.next())
                if (typeof value !== 'object') {
                    continue
                }
                yield progressCallback?.call(this, { update: value })
            } while(!done)
            return value
        }
        static start(task, progressCallback) {
            if (typeof task !== 'object') {
                throw new Error ('task is not an object. task type: ' + typeof task)
            }
            if (!(task instanceof Task)) {
                if (task.reqBody) {
                    task = new RenderTask(task.reqBody)
                } else {
                    task = new RenderTask(task)
                }
            }
            return task.start(progressCallback)
        }
        static run(task, progressCallback) {
            const promiseGenerator = RenderTask.start(task, progressCallback)
            return Task.run(promiseGenerator)
        }
    }
    class FilterTask extends Task {
        constructor(options={}) {
        }
        /** Send current task to server.
         * @param {*} [timeout=-1] Optional timeout value in ms
         * @returns the response from the render request.
         * @memberof Task
         */
        async post(timeout=-1) {
            let jsonResponse = await super.post('/filter', timeout)
            //this._setId(jsonResponse.task)
            this._setStatus(TaskStatus.waiting)
        }
        enqueue(progressCallback) {
            return Task.enqueueNew(this, FilterTask, progressCallback)
        }
        *start(progressCallback) {
            if (typeof progressCallback !== 'undefined' && typeof progressCallback !== 'function') {
                throw new Error('progressCallback is not a function. progressCallback type: ' + typeof progressCallback)
            }
            if (this.isStopped) {
                return
            }
        }
        static start(task, progressCallback) {
            if (typeof task !== 'object') {
                throw new Error ('task is not an object. task type: ' + typeof task)
            }
            if (!(task instanceof Task)) {
                if (task.reqBody) {
                    task = new FilterTask(task.reqBody)
                } else {
                    task = new FilterTask(task)
                }
            }
            return task.start(progressCallback)
        }
        static run(task, progressCallback) {
            const promiseGenerator = FilterTask.start(task, progressCallback)
            return Task.run(promiseGenerator)
        }
    }

    const getSystemInfo = debounce(async function() {
        let systemInfo = {
            devices: {
                all: {},
                active: {},
            },
            hosts: []
        }
        try {
            const res = await fetch('/get/system_info')
            if (!res.ok) {
                console.error('Invalid response fetching devices', res.statusText)
                return systemInfo
            }
            systemInfo = await res.json()
        } catch (e) {
            console.error('error fetching system info', e)
        }
        return systemInfo
    }, 250, true)
    async function getDevices() {
        let systemInfo = getSystemInfo()
        return systemInfo.devices
    }
    async function getHosts() {
        let systemInfo = getSystemInfo()
        return systemInfo.hosts
    }

    async function getModels() {
        let models = {
            'stable-diffusion': [],
            'vae': [],
        }
        try {
            const res = await fetch('/get/models')
            if (!res.ok) {
                console.error('Invalid response fetching models', res.statusText)
                return models
            }
            models = await res.json()
            console.log('get models response', models)
        } catch (e) {
            console.log('get models error', e)
        }
        return models
    }

    function getServerCapacity() {
        let activeDevicesCount = Object.keys(serverState?.devices?.active || {}).length
        if (typeof window === "object" && window.document.visibilityState === 'hidden') {
            activeDevicesCount = 1 + activeDevicesCount
        }
        return activeDevicesCount
    }

    let idleEventPromise = undefined
    function continueTasks() {
        if (typeof navigator?.scheduling?.isInputPending === 'function') {
            const inputPendingOptions = {
                // Report mouse/pointer move events when queue is empty.
                // Delay idle after mouse moves stops.
                includeContinuous: Boolean(task_queue.size <= 0 && concurrent_generators.size <= 0)
            }
            if (navigator.scheduling.isInputPending(inputPendingOptions)) {
                // Browser/User still active.
                return asyncDelay(CONCURRENT_TASK_INTERVAL)
            }
        }
        const serverCapacity = getServerCapacity()
        if (task_queue.size <= 0 && concurrent_generators.size <= 0) {
            if (!idleEventPromise?.isPending) {
                idleEventPromise = makeQuerablePromise(eventSource.fireEvent(EVENT_IDLE, {capacity: serverCapacity, idle: true}))
            }
            // Calling idle could result in task being added to queue.
            // if (task_queue.size <= 0 && concurrent_generators.size <= 0) {
            //     return asyncDelay(IDLE_COOLDOWN).then(() => idleEventPromise)
            // }
        }
        if (task_queue.size < serverCapacity) {
            if (!idleEventPromise?.isPending) {
                idleEventPromise = makeQuerablePromise(eventSource.fireEvent(EVENT_IDLE, {capacity: serverCapacity - task_queue.size}))
            }
        }
        const completedTasks = []
        for (let [generator, promise] of concurrent_generators.entries()) {
            if (promise.isPending) {
                continue
            }
            let value = promise.resolvedValue?.value || promise.resolvedValue
            if (promise.isRejected) {
                console.error(promise.rejectReason)
                const event = {generator, reason: promise.rejectReason}
                eventSource.fireEvent(EVENT_UNHANDLED_REJECTION, event)
                if ('continueWith' in event) {
                    value = Promise.resolve(event.continueWith)
                } else {
                    concurrent_generators.delete(generator)
                    completedTasks.push({generator, promise})
                    continue
                }
            }
            if (value instanceof Promise) {
                promise = makeQuerablePromise(value.then((val) => ({done: promise.resolvedValue?.done, value: val})))
                concurrent_generators.set(generator, promise)
                continue
            }
            weak_results.set(generator, value)
            if (promise.resolvedValue?.done) {
                concurrent_generators.delete(generator)
                completedTasks.push({generator, promise})
                continue
            }

            promise = generator.next(value)
            if (!(promise instanceof Promise)) {
                promise = Promise.resolve(promise)
            }
            promise = makeQuerablePromise(promise)
            concurrent_generators.set(generator, promise)
        }

        for (let [task, generator] of task_queue.entries()) {
            const cTsk = completedTasks.find((item) => item.generator === generator)
            if (cTsk?.promise?.rejectReason || task.hasFailed) {
                eventSource.fireEvent(EVENT_TASK_ERROR, {task, generator, reason: cTsk?.promise?.rejectReason || task.exception })
                task_queue.delete(task)
                continue
            }
            if (task.isCompleted || task.isStopped || cTsk) {
                const eventEndArgs = {task, generator}
                if (task.isStopped) {
                    eventEndArgs.stopped = true
                }
                eventSource.fireEvent(EVENT_TASK_END, eventEndArgs)
                task_queue.delete(task)
                continue
            }
            if (concurrent_generators.size > serverCapacity) {
                break
            }
            if (!generator) {
                if (typeof task.start === 'function') {
                    generator = task.start()
                }
            } else if (concurrent_generators.has(generator)) {
                continue
            }
            const event = {task, generator};
            const beforeStart = eventSource.fireEvent(EVENT_TASK_START, event) // optional beforeStart promise to wait on before starting task.
            const promise = makeQuerablePromise(beforeStart.then(() => Promise.resolve(event.beforeStart)))
            concurrent_generators.set(event.generator, promise)
            task_queue.set(task, event.generator)
        }
        const promises = Array.from(concurrent_generators.values())
        if (promises.length <= 0) {
            return asyncDelay(CONCURRENT_TASK_INTERVAL)
        }
        return Promise.race(promises).finally(continueTasks)
    }
    let taskPromise = undefined
    function startCheck() {
        if (taskPromise?.isPending) {
            return
        }
        do {
            if (taskPromise?.resolvedValue instanceof Promise) {
                taskPromise = makeQuerablePromise(taskPromise.resolvedValue)
                continue
            }
            if (typeof navigator?.scheduling?.isInputPending === 'function' && navigator.scheduling.isInputPending()) {
                return
            }
            const continuePromise = continueTasks().catch(async function(err) {
                console.error(err)
                await eventSource.fireEvent(EVENT_UNHANDLED_REJECTION, {reason: err})
                await asyncDelay(RETRY_DELAY_ON_ERROR)
            })
            taskPromise = makeQuerablePromise(continuePromise)
        } while(taskPromise?.isResolved)
    }

    const SD = {
        ChunkedStreamReader,
        ServerStates,
        TaskStatus,
        Task,
        RenderTask,
        FilterTask,

        Events: EVENTS_TYPES,
        init: async function(options={}) {
            if ('events' in options) {
                for (const key in options.events) {
                    eventSource.addEventListener(key, options.events[key])
                }
            }
            await healthCheck()
            setInterval(healthCheck, HEALTH_PING_INTERVAL)
            setInterval(startCheck, CONCURRENT_TASK_INTERVAL)
        },

        /** Add a new event listener
         */
        addEventListener: (...args) => eventSource.addEventListener(...args),
        /** Remove the event listener
         */
        removeEventListener: (...args) => eventSource.removeEventListener(...args),

        isServerAvailable,
        getServerCapacity,

        getSystemInfo,
        getDevices,
        getHosts,

        getModels,

        render: (...args) => RenderTask.run(...args),
        filter: (...args) => FilterTask.run(...args),
        waitUntil,
    };

    Object.defineProperties(SD, {
        serverState: {
            configurable: false,
            get: () => serverState,
        },
        isAvailable: {
            configurable: false,
            get: () => isServerAvailable(),
        },
        serverCapacity: {
            configurable: false,
            get: () => getServerCapacity(),
        },
        sessionId: {
            configurable: false,
            get: () => sessionId,
            set: (val) => {
                if (typeof val === 'undefined') {
                    throw new Error("Can't set sessionId to undefined.")
                }
                sessionId = val
            },
        },
        MAX_SEED_VALUE: {
            configurable: false,
            get: () => MAX_SEED_VALUE,
        },
        activeTasks: {
            configurable: false,
            get: () => task_queue,
        },
    })
    Object.defineProperties(getGlobal(), {
        SD: {
            configurable: false,
            get: () => SD,
        },
        sessionId: { //TODO Remove in the future in favor of SD.sessionId
            configurable: false,
            get: () => {
                console.warn('Deprecated window.sessionId has been replaced with SD.sessionId.')
                console.trace()
                return SD.sessionId
            },
            set: (val) => {
                console.warn('Deprecated window.sessionId has been replaced with SD.sessionId.')
                console.trace()
                SD.sessionId = val
            }
        }
    })
})()