/***************************************************************************
 *                                  _   _ ____  _
 *  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
 *
 ***************************************************************************/
#include "tool_setup.h"

#include "tool_operate.h"
#include "tool_progress.h"

/* The point of this function would be to return a string of the input data,
   but never longer than 5 columns (+ one zero byte).
   Add suffix k, M, G when suitable...

   Unit test @1622
*/
UNITTEST char *max5data(curl_off_t bytes, char *max5, size_t mlen)
{
  /* a signed 64-bit value is 8192 petabytes maximum */
  const char unit[] = { 'k', 'M', 'G', 'T', 'P', 'E', 0 };
  int k = 0;
  if(bytes < 100000) {
    curl_msnprintf(max5, mlen, "%5" CURL_FORMAT_CURL_OFF_T, bytes);
    return max5;
  }

  do {
    curl_off_t nbytes = bytes / 1024;
    if(nbytes < 100) {
      /* display with a decimal */
      curl_msnprintf(max5, mlen, "%2" CURL_FORMAT_CURL_OFF_T ".%"
                     CURL_FORMAT_CURL_OFF_T "%c", bytes / 1024,
                     (bytes % 1024) * 10 / 1024, unit[k]);
      break;
    }
    else if(nbytes < 10000) {
      /* no decimals */
      curl_msnprintf(max5, mlen, "%4" CURL_FORMAT_CURL_OFF_T "%c", nbytes,
                     unit[k]);
      break;
    }
    bytes = nbytes;
    k++;
    DEBUGASSERT(unit[k]);
  } while(unit[k]);
  return max5;
}

int xferinfo_cb(void *clientp,
                curl_off_t dltotal,
                curl_off_t dlnow,
                curl_off_t ultotal,
                curl_off_t ulnow)
{
  struct per_transfer *per = clientp;
  struct OperationConfig *config = per->config;
  per->dltotal = dltotal;
  per->dlnow = dlnow;
  per->ultotal = ultotal;
  per->ulnow = ulnow;

  if(per->abort)
    return 1;

  if(config->readbusy) {
    config->readbusy = FALSE;
    curl_easy_pause(per->curl, CURLPAUSE_CONT);
  }

  return 0;
}

/* Provide a time string that is 8 letters long (plus the zero byte)

   Unit test @1622
 */
UNITTEST void timebuf(char *r, size_t rlen, curl_off_t seconds)
{
  curl_off_t h;
  if(seconds <= 0) {
    curlx_strcopy(r, rlen, "        ", 8);
    return;
  }
  h = seconds / 3600;
  if(h <= 99) {
    curl_off_t m = (seconds - (h * 3600)) / 60;
    curl_off_t s = (seconds - (h * 3600)) - (m * 60);
    curl_msnprintf(r, rlen, "%02" FMT_OFF_T ":%02" FMT_OFF_T ":"
                   "%02" FMT_OFF_T, h, m, s);
  }
  else {
    curl_off_t d = seconds / 86400;
    h = (seconds - (d * 86400)) / 3600;
    if(d <= 999)
      curl_msnprintf(r, rlen, "%3" FMT_OFF_T "d %02" FMT_OFF_T "h", d, h);
    else { /* more than 999 days */
      curl_off_t m = d / 30;
      if(m <= 999) {
        curl_msnprintf(r, rlen, "%3" FMT_OFF_T "m %02" FMT_OFF_T "d", m, d%30);
      }
      else { /* more than 999 months */
        curl_off_t y = d / 365;
        if(y <= 99999)
          curl_msnprintf(r, rlen, "%7" FMT_OFF_T "y", y);
        else
          curlx_strcopy(r, rlen, " >99999y", 8);
      }
    }
  }
}

static curl_off_t all_dltotal = 0;
static curl_off_t all_ultotal = 0;
static curl_off_t all_dlalready = 0;
static curl_off_t all_ulalready = 0;

struct speedcount {
  curl_off_t dl;
  curl_off_t ul;
  struct curltime stamp;
};
#define SPEEDCNT 10
static unsigned int speedindex;
static bool indexwrapped;
static struct speedcount speedstore[SPEEDCNT];

static void add_offt(curl_off_t *val, curl_off_t add)
{
  if(CURL_OFF_T_MAX - *val < add)
    /* maxed out! */
    *val = CURL_OFF_T_MAX;
  else
    *val += add;
}

/*
  |DL% UL%  Dled  Uled  Xfers  Live Total     Current  Left    Speed
  |  6 --   9.9G     0     2     2   0:00:40  0:00:02  0:00:37 4087M
*/
bool progress_meter(CURLM *multi, struct curltime *start, bool final)
{
  static struct curltime stamp;
  static bool header = FALSE;
  struct curltime now;
  timediff_t diff;

  if(global->noprogress || global->silent)
    return FALSE;

  now = curlx_now();
  diff = curlx_timediff_ms(now, stamp);

  if(!header) {
    header = TRUE;
    fputs("DL% UL%  Dled  Uled  Xfers  Live "
          "Total     Current  Left    Speed\n",
          tool_stderr);
  }
  if(final || (diff > 500)) {
    char time_left[9];
    char time_total[9];
    char time_spent[9];
    char buffer[3][6];
    curl_off_t spent = curlx_timediff_ms(now, *start) / 1000;
    char dlpercen[4] = "--";
    char ulpercen[4] = "--";
    struct per_transfer *per;
    curl_off_t all_dlnow = 0;
    curl_off_t all_ulnow = 0;
    curl_off_t xfers_added = 0;
    curl_off_t xfers_running = 0;
    bool dlknown = TRUE;
    bool ulknown = TRUE;
    curl_off_t speed = 0;
    unsigned int i;
    stamp = now;

    /* first add the amounts of the already completed transfers */
    add_offt(&all_dlnow, all_dlalready);
    add_offt(&all_ulnow, all_ulalready);

    for(per = transfers; per; per = per->next) {
      add_offt(&all_dlnow, per->dlnow);
      add_offt(&all_ulnow, per->ulnow);
      if(!per->dltotal)
        dlknown = FALSE;
      else if(!per->dltotal_added) {
        /* only add this amount once */
        add_offt(&all_dltotal, per->dltotal);
        per->dltotal_added = TRUE;
      }
      if(!per->ultotal)
        ulknown = FALSE;
      else if(!per->ultotal_added) {
        /* only add this amount once */
        add_offt(&all_ultotal, per->ultotal);
        per->ultotal_added = TRUE;
      }
    }
    if(dlknown && all_dltotal)
      curl_msnprintf(dlpercen, sizeof(dlpercen), "%3" CURL_FORMAT_CURL_OFF_T,
                     all_dlnow < (CURL_OFF_T_MAX / 100) ?
                     (all_dlnow * 100 / all_dltotal) :
                     (all_dlnow / (all_dltotal / 100)));

    if(ulknown && all_ultotal)
      curl_msnprintf(ulpercen, sizeof(ulpercen), "%3" CURL_FORMAT_CURL_OFF_T,
                     all_ulnow < (CURL_OFF_T_MAX / 100) ?
                     (all_ulnow * 100 / all_ultotal) :
                     (all_ulnow / (all_ultotal / 100)));

    /* get the transfer speed, the higher of the two */

    i = speedindex;
    speedstore[i].dl = all_dlnow;
    speedstore[i].ul = all_ulnow;
    speedstore[i].stamp = now;
    if(++speedindex >= SPEEDCNT) {
      indexwrapped = TRUE;
      speedindex = 0;
    }

    {
      timediff_t deltams;
      curl_off_t dl;
      curl_off_t ul;
      curl_off_t dls;
      curl_off_t uls;
      if(indexwrapped) {
        /* 'speedindex' is the oldest stored data */
        deltams = curlx_timediff_ms(now, speedstore[speedindex].stamp);
        dl = all_dlnow - speedstore[speedindex].dl;
        ul = all_ulnow - speedstore[speedindex].ul;
      }
      else {
        /* since the beginning */
        deltams = curlx_timediff_ms(now, *start);
        dl = all_dlnow;
        ul = all_ulnow;
      }
      if(!deltams) /* no division by zero please */
        deltams++;
      dls = (curl_off_t)((double)dl / ((double)deltams / 1000.0));
      uls = (curl_off_t)((double)ul / ((double)deltams / 1000.0));
      speed = dls > uls ? dls : uls;
    }

    if(dlknown && speed) {
      curl_off_t est = all_dltotal / speed;
      curl_off_t left = (all_dltotal - all_dlnow) / speed;
      timebuf(time_left, sizeof(time_left), left);
      timebuf(time_total, sizeof(time_total), est);
    }
    else {
      timebuf(time_left, sizeof(time_left), 0);
      timebuf(time_total, sizeof(time_total), 0);
    }
    timebuf(time_spent, sizeof(time_spent), spent);

    (void)curl_multi_get_offt(multi, CURLMINFO_XFERS_ADDED, &xfers_added);
    (void)curl_multi_get_offt(multi, CURLMINFO_XFERS_RUNNING, &xfers_running);
    curl_mfprintf(tool_stderr,
                  "\r"
                  "%-3s " /* percent downloaded */
                  "%-3s " /* percent uploaded */
                  "%s " /* Dled */
                  "%s " /* Uled */
                  "%5" CURL_FORMAT_CURL_OFF_T " " /* Xfers */
                  "%5" CURL_FORMAT_CURL_OFF_T " " /* Live */
                  " %s "  /* Total time */
                  "%s "  /* Current time */
                  "%s "  /* Time left */
                  "%s "  /* Speed */
                  "%5s" /* final newline */,

                  dlpercen,  /* 3 letters */
                  ulpercen,  /* 3 letters */
                  max5data(all_dlnow, buffer[0], sizeof(buffer[0])),
                  max5data(all_ulnow, buffer[1], sizeof(buffer[1])),
                  xfers_added,
                  xfers_running,
                  time_total,
                  time_spent,
                  time_left,
                  max5data(speed, buffer[2], sizeof(buffer[2])), /* speed */
                  final ? "\n" : "");
    return TRUE;
  }
  return FALSE;
}

void progress_finalize(struct per_transfer *per)
{
  /* get the numbers before this transfer goes away */
  add_offt(&all_dlalready, per->dlnow);
  add_offt(&all_ulalready, per->ulnow);
  if(!per->dltotal_added) {
    add_offt(&all_dltotal, per->dltotal);
    per->dltotal_added = TRUE;
  }
  if(!per->ultotal_added) {
    add_offt(&all_ultotal, per->ultotal);
    per->ultotal_added = TRUE;
  }
}
