# The contents of this file are subject to the Common Public Attribution License
# Version 1.0 (the “License”); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
# https://myou.dev/licenses/LICENSE-CPAL. The License is based on the Mozilla
# Public License Version 1.1 but Sections 14 and 15 have been added to cover use
# of software over a computer network and provide for limited attribution for
# the Original Developer. In addition, Exhibit A has been modified to be
# consistent with Exhibit B.
#
# Software distributed under the License is distributed on an “AS IS” basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is Myou Engine.
#
# the Original Developer is the Initial Developer.
#
# The Initial Developer of the Original Code is the Myou Engine developers.
# All portions of the code written by the Myou Engine developers are Copyright
# (c) 2024. All Rights Reserved.
#
# Alternatively, the contents of this file may be used under the terms of the
# GNU Affero General Public License version 3 (the [AGPL-3] License), in which
# case the provisions of [AGPL-3] License are applicable instead of those above.
#
# If you wish to allow use of your version of this file only under the terms of
# the [AGPL-3] License and not to allow others to use your version of this file
# under the CPAL, indicate your decision by deleting the provisions above and
# replace them with the notice and other provisions required by the [AGPL-3]
# License. If you do not delete the provisions above, a recipient may use your
# version of this file under either the CPAL or the [AGPL-3] License.


## A LoadableResource is similar to a JS promise or a Future, except that you can
## store it to load it any number of times, and instead of providing an object,
## you get a pointer, a length, and a function to tell it you're done with it.
## You cannot have more than one callback, but you can obtain the results of many
## with `loadAll` or get a callback when many are done with `allDone`
## 
## TODO IMPORTANT: probably not thread safe, call all functions in the same thread
##                 if you use loadAll and allDone
## 
## TODO: investigate the use of signals instead of callbacks


type LoadableResourceStatus* = enum
    NotStarted
    Started
    Finished
    Error

type Result = tuple[ok: bool, err: string, p: pointer, len: int]

type LoadableResource* = ref object of RootObj
    status: LoadableResourceStatus
    start_func: proc(self: LoadableResource)
    onload_func: proc(ok: bool, err: string, p: pointer, len: int)
    done_func: proc()
    cancel_func: proc()
    str*: proc(): string
    result: ref Result # only to be used with loadAll

proc newLoadableResource*[T: LoadableResource](
        start: proc(self: LoadableResource),
        done: proc() = nil,
        str: proc(): string = nil,
    ): T =
    new(result)
    result.start_func = start
    result.done_func = done
    result.str = str
    if str == nil:
        result.str = proc(): string = ""

proc start*[T: LoadableResource](self: T) =
    self.status = Started
    self.start_func(self)

proc `onload=`*[T: LoadableResource](self: T, onload_func: proc(ok: bool, err: string, p: pointer, len: int)) =
    self.onload_func = onload_func

proc onload*[T: LoadableResource](self: T, ok: bool, err: string, p: pointer, len: int) =
    if self.status == Started:
        self.status = if ok: Finished else: Error
        if self.result != nil:
            self.result[] = (ok, err, p, len)
        if self.onload_func != nil:
            self.onload_func(ok, err, p, len)
    else: # cancelled
        self.status = NotStarted
        if self.done_func != nil:
            self.done_func()

# TODO: check if we can always use destructors instead of calling this
proc done*[T: LoadableResource](self: T) =
    if self.status == Started:
        self.cancel()
    else:
        self.status = NotStarted
        if self.done_func != nil:
            self.done_func()

proc cancel*[T: LoadableResource](self: T) =
    if self.status != Started:
        return
    if self.cancel_func != nil:
        # self.`onload=`(proc(ok, err, p, len: auto) =
        #     self.done())
        self.onload_func = proc(ok: bool, err: string, p: pointer, len: int) =
            self.done()
    else:
        self.cancel_func()
        self.status = NotStarted

# proc allDone*(list: seq[LoadableResource], done: proc()) =


# proc loadAll*(list: seq[LoadableResource], onload: proc(all_ok: bool, result: seq[Result])) =
#     for i,res in list:
#         if not defined(release):
#             assert res.status == NotStarted, "All resources in the list must be not started"
#             assert res.onload_func == nil, "All resources in the list must not have onload"
#         new(res.result)
#         res.start()
#     list.allDone, proc() =
#         var results = newSeqOfCap[Result](list.len)
#         for res in list:
#             results.add res.result[]
#             res.result = nil


type Fetch* = ref object of LoadableResource
    custom: pointer
    auto_done: bool

when not defined(onlyLocalFiles):
    when defined(emscripten):
        type
            FetchAttribute = enum
                LoadToMemory = 1
                StreamData = 2
                PersistFile = 4
                Append = 8
                Replace = 16
                NoDownload = 32
                Synchronous = 64
                Waitable = 128
            emscripten_fetch_attr_t {.importc,nodecl,header:"emscripten/fetch.h".} = object
                requestMethod: array[32, char]
                userData: pointer
                onsuccess: proc(fetch: ptr emscripten_fetch_t) {.cdecl.}
                onerror: proc(fetch: ptr emscripten_fetch_t) {.cdecl.}
                onprogress: proc(fetch: ptr emscripten_fetch_t) {.cdecl.}
                onreadystatechange: proc(fetch: ptr emscripten_fetch_t) {.cdecl.}
                attributes: FetchAttribute # uint32
                timeoutMSecs: uint32
                withCredentials: bool # actually int32
                destinationPath: cstring
                userName: cstring
                password: cstring
                requestHeaders: ptr UncheckedArray[cstring]
                overriddenMimeType: cstring
                requestData: cstring
                requestDataSize: uint
            emscripten_fetch_t {.importc,nodecl.} = object
                id: uint32
                userData: pointer
                url: cstring
                data: pointer
                numBytes: uint64
                dataOffset: uint64
                totalBytes: uint64
                readyState: uint16
                status: uint16
                statusText: array[64, char]
            EmscriptenResult = enum
                TimedOut = -8
                NoData = -7
                Failed = -6
                InvalidParam = -5
                UnknownTarget = -4
                InvalidTarget = -3
                FailedNotDeferred = -2
                NotSupported = -1
                Success = 0
                Deferred = 1
        const EMSCRIPTEN_RESULT_STRINGS = [
                "TimedOut","NoData","Failed","InvalidParam",
                "UnknownTarget","InvalidTarget","FailedNotDeferred",
                "NotSupported","Success","Deferred"]
        proc emscripten_fetch_attr_init(fetch_attr: var emscripten_fetch_attr_t)  {.importc,nodecl.}
        proc emscripten_fetch(fetch_attr: var emscripten_fetch_attr_t, url: cstring): ptr emscripten_fetch_t {.importc,nodecl.}
        proc emscripten_fetch_wait(fetch: ptr emscripten_fetch_t, timeoutMSecs: float): EmscriptenResult  {.importc,nodecl.}
        proc emscripten_fetch_close(fetch: ptr emscripten_fetch_t): EmscriptenResult  {.importc,nodecl.}
        proc emscripten_fetch_get_response_headers_length(fetch: ptr emscripten_fetch_t): uint  {.importc,nodecl.}
        proc emscripten_fetch_get_response_headers(fetch: ptr emscripten_fetch_t, dst: cstring , dstSizeBytes: uint): uint  {.importc,nodecl.}
        proc emscripten_fetch_unpack_response_headers(headersString: cstring): ptr UncheckedArray[cstring] {.importc,nodecl.}
        proc emscripten_fetch_free_unpacked_response_headers(unpackedHeaders: ptr UncheckedArray[cstring]) {.importc,nodecl.}

    else:
        import std/httpclient

import std/memfiles
import std/strutils
import std/strformat

func escapeUTF8*(s: string): string =
    result = newStringOfCap(s.len + 5)
    for i,c in s:
        if c > ' ' and c <= '~':
            result &= c
        else:
            result &= '%' & c.byte.toHex

proc loadUri*(
        uri: string, 
        onload_func: proc(ok: bool, err: string, data: pointer, len: int) = nil,
        range = (-1,-1),
        auto_start = true,
        auto_done = true,
    ): Fetch {.discardable.} =

    echo "fetching ", uri

    var start_func: proc(self: LoadableResource)
    var done_func: proc()
    var self: Fetch
    var uri = uri
    when not defined(onlyLocalFiles):
        when defined(emscripten):
            const is_remote = true
        else:
            let is_remote = uri.startswith("http://") or uri.startswith("https://")
    
        if is_remote:
            uri = uri.escapeUTF8
            when defined(emscripten):
                start_func = proc(self: LoadableResource) =
                    var attr: emscripten_fetch_attr_t
                    emscripten_fetch_attr_init(attr)
                    attr.userData = cast[pointer](self)
                    attr.requestMethod.addr.copyMem("GET".cstring, 4)
                    attr.attributes = LoadToMemory
                    attr.onsuccess = proc(fetch: ptr emscripten_fetch_t) {.cdecl.} =
                        var self = cast[Fetch](fetch.userData)
                        self.custom = fetch.pointer
                        self.onload(true, "", fetch.data, fetch.numBytes.int)
                        if self.auto_done:
                            self.done()
                    attr.onerror = proc(fetch: ptr emscripten_fetch_t) {.cdecl.} =
                        var self = cast[Fetch](fetch.userData)
                        let err_msg = $fetch.statusText.addr.cstring
                        let err = emscripten_fetch_close(fetch)
                        # self.onload(false, &"Error: {err_msg} ({EMSCRIPTEN_RESULT_STRINGS[err.int + 8]})", nil, 0)
                        self.onload(false, &"Error fetching {fetch.url}: {err_msg}", nil, 0)
                        if self.auto_done:
                            self.done()
                    discard emscripten_fetch(attr, uri.cstring)
                done_func = proc() =
                    if self.custom != nil:
                        discard emscripten_fetch_close(cast[ptr emscripten_fetch_t](self.custom))
                        self.custom = nil
            else:
                var client = newHttpClient()
                var response: string
                start_func = proc(self: LoadableResource) =
                    let self = cast[Fetch](self)
                    var ok = false
                    try:
                        response = client.getContent(uri)
                        ok = true
                    except:
                        self.onload(false, &"Error fetching {uri}: {getCurrentExceptionMsg()}", nil, 0)
                    if ok:
                        let p = response[0].addr
                        self.onload(true, "", p, response.len)
                    if self.auto_done:
                        self.done()
                done_func = proc() =
                    response = ""

    if not is_remote:
        start_func = proc(self: LoadableResource) =
            try:
                var memfile = memfiles.open(uri, mode=fmRead)
                self.done_func = proc() =
                    memfile.close()
                self.onload(true, "", memfile.mem, memfile.size)
                # TODO!!!! check whether these objects are freed
                # and if we have to add a destructor, or what
            except OSError:
                self.onload(false, "Could not open file: " & uri, nil, 0)
            if cast[Fetch](self).auto_done:
                self.done()
    proc str(): string = uri
    self = newLoadableResource[Fetch](start_func, done_func, str)
    self.onload_func = onload_func
    self.auto_done = auto_done
    if auto_start:
        start(self)
    return self