# **************************************************************************
#                                  _   _ ____  _
#  Project                     ___| | | |  _ \| |
#                             / __| | | | |_) | |
#                            | (__| |_| |  _ <| |___
#                             \___|\___/|_| \_\_____|
#
# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at https://curl.se/docs/copyright.html.
#
# You may opt to use, copy, modify, merge, publish, distribute and/or sell
# copies of the Software, and permit persons to whom the Software is
# furnished to do so, under the terms of the COPYING file.
#
# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
# KIND, either express or implied.
#
# SPDX-License-Identifier: curl
#
# **************************************************************************

"""
HTTP/2 server push
"""

import argparse
import sys
import ctypes as ct

import libcurl as lcurl
from curl_utils import *  # noqa
from utils import debug_cb

if not hasattr(lcurl, "CURLPIPE_MULTIPLEX"):
    print("too old libcurl, cannot do HTTP/2 server push!", file=sys.stderr)
    sys.exit(1)
# endif

verbose: bool = True


class transfer(ct.Structure):
    _fields_ = [
    ("idx",       ct.c_int),
    ("easy",      ct.POINTER(lcurl.CURL)),
    char filename[128];
    FILE *out;
    ("recv_size", lcurl.off_t),
    ("fail_at",   lcurl.off_t),
    ("pause_at",  lcurl.off_t),
    ("abort_at",  lcurl.off_t),
    ("started",   ct.c_bool),
    ("paused",    ct.c_bool),
    ("resumed",   ct.c_bool),
    ("done",      ct.c_int),
]


transfer_count: int = 1
transfers: ct.POINTER(transfer)
forbid_reuse: bool = False


def get_transfer_for_easy(easy: ct.POINTER(lcurl.CURL)) -> ct.POINTER(transfer):
    global transfer_count, transfers
    for i in range(transfer_count):
        if easy == transfers[i].easy:
            return &transfers[i]
    return None


size_t my_write_cb(char *buf, size_t nitems, size_t buflen, void *userdata):

    t: transfer = ct.cast(userdata, ct.POINTER(transfer)).contents
    blen = nitems * buflen

    print("[t-%d] RECV %ld bytes, total=%ld, pause_at=%ld" %
            (t->idx, blen, (long)t->recv_size, (long)t->pause_at),
            file=sys.stderr);

    if not t->out:
        curl_msnprintf(t->filename, sizeof(t->filename)-1, "download_%u.data" % t->idx)
        try:
            t->out = fopen(t->filename, "wb")
        except:
            t->out = None
            return 0

    if (not t->resumed and
        t->recv_size < t->pause_at and
        ((t->recv_size + lcurl.off_t(blen).value) >= t->pause_at)):
        print("[t-%d] PAUSE" % t->idx, file=sys.stderr)
        t->paused = True
        return lcurl.CURL_WRITEFUNC_PAUSE

    nwritten: size_t = fwrite(buf, nitems, buflen, t->out);
    if nwritten < blen:
        print("[t-%d] write failure" % t->idx, file=sys.stderr)
        return 0

    t->recv_size += lcurl.off_t(nwritten).value
    if t->fail_at > 0 and t->recv_size >= t->fail_at:
        print("[t-%d] FAIL by write callback at %ld bytes" %
              (t->idx, (long)t->recv_size), file=sys.stderr)
        return lcurl.CURL_WRITEFUNC_ERROR

    return (size_t) nwritten


@lcurl.progress_callback
def my_progress_cb(clientp, dltotal, dlnow, ultotal, ulnow):
    t: transfer = ct.cast(clientp, ct.POINTER(transfer)).contents
    if t.abort_at > 0 and dlnow >= t.abort_at:
        print("[t-%d] ABORT by progress_cb at %ld bytes" %
              (t.idx, dlnow), file=sys.stderr)
        return 1
    return 0


def setup(hnd: ct.POINTER(lcurl.CURL), const char *url, struct transfer * t, http_version: int) -> int:

    lcurl.easy_setopt(hnd, lcurl.CURLOPT_URL, url);
    lcurl.easy_setopt(hnd, lcurl.CURLOPT_HTTP_VERSION, http_version)
    lcurl.easy_setopt(hnd, lcurl.CURLOPT_SSL_VERIFYPEER, 0)
    lcurl.easy_setopt(hnd, lcurl.CURLOPT_SSL_VERIFYHOST, 0)
    lcurl.easy_setopt(hnd, lcurl.CURLOPT_BUFFERSIZE, 128 * 1024)
    lcurl.easy_setopt(hnd, lcurl.CURLOPT_WRITEFUNCTION, my_write_cb)
    lcurl.easy_setopt(hnd, lcurl.CURLOPT_WRITEDATA, t);
    lcurl.easy_setopt(hnd, lcurl.CURLOPT_NOPROGRESS, 0)
    lcurl.easy_setopt(hnd, lcurl.CURLOPT_XFERINFOFUNCTION, my_progress_cb)
    lcurl.easy_setopt(hnd, lcurl.CURLOPT_XFERINFODATA, t);
    if forbid_reuse:
        lcurl.easy_setopt(hnd, lcurl.CURLOPT_FORBID_REUSE, 1)

    # please be verbose
    if verbose:
        lcurl.easy_setopt(hnd, lcurl.CURLOPT_VERBOSE, 1)
        lcurl.easy_setopt(hnd, lcurl.CURLOPT_DEBUGFUNCTION, debug_cb)

    if lcurl.CURLPIPE_MULTIPLEX > 0:
        # wait for pipe connection to confirm
        lcurl.easy_setopt(hnd, lcurl.CURLOPT_PIPEWAIT, 1)

    return 0  # all is good


def usage(msg = None):
    if msg is not None: print("%s" % msg, file=sys.stderr)
    print("usage: [options] url\n"
          "  download a url with following options:\n"
          "  -a         abort paused transfer\n"
          "  -m number  max parallel downloads\n"
          "  -n number  total downloads\n"
          "  -A number  abort transfer after `number` response bytes\n"
          "  -F number  fail writing response after `number` response bytes\n"
          "  -P number  pause transfer after `number` response bytes\n"
          "  -V http_version (http/1.1, h2, h3) http version to use",
          file=sys.stderr)


def main(argv=sys.argv[1:]) -> int:
    #
    # Download a file over HTTP/2, take care of server push.
    #
    app_name = sys.argv[0].rpartition("/")[2].rpartition("\\")[2]

    if is_windows:
        print("Not supported with this compiler.", file=sys.stderr)
        return 1

    global transfer_count, transfers

    m: ct.POINTER(lcurl.CURLMsg)
    const char *url;
    size_t i, n, max_parallel = 1
    size_t active_transfers;
    size_t pause_offset = 0
    size_t abort_offset = 0
    size_t fail_offset  = 0
    abort_paused: bool = False
    struct transfer * t;
    int ch;

    parser = argparse.ArgumentParser(prog=f"python {app_name}", add_help=False)
    parser.add_argument("-h", action=usage)  # return 2
    parser.add_argument("-V", dest="http_proto")
    parser.add_argument("url")
    args = parser.parse_args(argv)

    http_version: int = lcurl.CURL_HTTP_VERSION_2_0
    while((ch = getopt(argc, argv, "afhm:n:A:F:P:V:")) != -1)
        switch(ch) {
        case "h":
            usage()
            return 2
        case "a":
            abort_paused = True
            break;
        case "f":
            forbid_reuse = True
            break;
        case "m":
            max_parallel = (size_t)strtol(optarg, NULL, 10);
            break;
        case "n":
            transfer_count = (size_t)strtol(optarg, NULL, 10);
            break;
        case "A":
            abort_offset = (size_t)strtol(optarg, NULL, 10);
            break;
        case "F":
            fail_offset = (size_t)strtol(optarg, NULL, 10);
            break;
        case "P":
            pause_offset = (size_t)strtol(optarg, NULL, 10);
            break;
        case "V":
            if optarg == "http/1.1":
                http_version = lcurl.CURL_HTTP_VERSION_1_1
            elif optarg == "h2":
                http_version = lcurl.CURL_HTTP_VERSION_2_0
            elif optarg == "h3":
                http_version = lcurl.CURL_HTTP_VERSION_3ONLY
            else:
                usage("invalid http version")
                return 1
            break
        default:
            usage("invalid option")
            return 1

    argc -= optind
    argv += optind

    lcurl.global_init(lcurl.CURL_GLOBAL_DEFAULT)
    lcurl.global_trace(b"ids,time,http/2,http/3")

    if argc != 1:
        usage("not enough arguments")
        return 2

    url: str = args.url

    transfers = calloc(transfer_count, sizeof(*transfers));
    if not transfers:
        print("error allocating transfer structs", file=sys.stderr)
        return 1

    multi_handle: ct.POINTER(lcurl.CURLM) = lcurl.multi_init()
    lcurl.multi_setopt(multi_handle, lcurl.CURLMOPT_PIPELINING,
                                     lcurl.CURLPIPE_MULTIPLEX)

    active_transfers = 0

    for i in range(transfer_count):
        t = &transfers[i];
        t->idx      = (int)i;
        t->abort_at = lcurl.off_t(abort_offset).value
        t->fail_at  = lcurl.off_t(fail_offset).value
        t->pause_at = lcurl.off_t(pause_offset).value

    n = min(max_parallel, transfer_count)
    for i in range(n):
        t = &transfers[i];
        t->easy = lcurl.easy_init()
        if not t->easy or setup(t->easy, url, t, http_version):
            print("[t-%d] FAILED setup" % (int)i, file=sys.stderr)
            return 1
        lcurl.multi_add_handle(multi_handle, t->easy);
        t->started = True
        active_transfers += 1
        print("[t-%d] STARTED" % t->idx, file=sys.stderr)

    do {
        still_running = ct.c_int()  # keep number of running handles
        mc: lcurl.CURLMcode = lcurl.multi_perform(multi_handle, ct.byref(still_running))
        # wait for activity, timeout or "nothing"
        if still_running.value: mc = lcurl.multi_poll(multi_handle, None, 0, 1000, None)
        if mc: break

        do {
          int msgq = 0;
          m = lcurl.multi_info_read(multi_handle, &msgq);
          if m and m->msg == lcurl.CURLMSG_DONE:
              e: ct.POINTER(lcurl.CURL) = m->easy_handle
              active_transfers -= 1
              lcurl.multi_remove_handle(multi_handle, e)
              t = get_transfer_for_easy(e);
              if t:
                  t->done = 1
                  print("[t-%d] FINISHED" % t->idx, file=sys.stderr)
              else:
                  lcurl.easy_cleanup(e)
                  print("unknown FINISHED???", file=sys.stderr)

          # nothing happening, maintenance
          if abort_paused:
              # abort paused transfers
              for i in range(transfer_count):
                  t = &transfers[i];
                  if not t->done and t->paused and t->easy:
                      lcurl.multi_remove_handle(multi_handle, t->easy)
                      t->done = 1
                      active_transfers -= 1
                      print("[t-%d] ABORTED" % t->idx, file=sys.stderr)
          else:
              # resume one paused transfer
              for i in range(transfer_count):
                  t = &transfers[i];
                  if not t->done and t->paused:
                      t->resumed = True
                      t->paused  = False
                      lcurl.easy_pause(t->easy, lcurl.CURLPAUSE_CONT)
                      print("[t-%d] RESUMED" % t->idx, file=sys.stderr)
                      break;

          while active_transfers < max_parallel:
              for i in range(transfer_count):
                  t = &transfers[i];
                  if not t->started:
                      t->easy = lcurl.easy_init()
                      if not t->easy or setup(t->easy, url, t, http_version):
                          print("[t-%d] FAILED setup" % (int)i, file=sys.stderr)
                          return 1
                      lcurl.multi_add_handle(multi_handle, t->easy);
                      t->started = True
                      active_transfers += 1
                      print("[t-%d] STARTED" % t->idx, file=sys.stderr)
                      break
              # all started
              if(i == transfer_count)
                  break;
        } while(m);

    } while(active_transfers);  # as long as we have transfers going

    for i in range(transfer_count):
        t = &transfers[i];
        if t->out:
            fclose(t->out);
            t->out = NULL;
        if t->easy:
            lcurl.easy_cleanup(t->easy);
            t->easy = NULL;
    free(transfers);

    lcurl.multi_cleanup(multi_handle)

    return 0


if __name__ == "__main__":
    sys.exit(main())
