// -*- mode: c++; c-basic-offset: 4 -*-
/*
 * progressbar.{cc,hh} -- element displays a progress bar on stderr
 * Eddie Kohler
 *
 * Copyright (c) 2001 International Computer Science Institute
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, subject to the conditions
 * listed in the Click LICENSE file. These conditions include: you must
 * preserve this copyright notice, and you cannot mention the copyright
 * holders in advertising related to the Software without their permission.
 * The Software is provided WITHOUT ANY WARRANTY, EXPRESS OR IMPLIED. This
 * notice is a summary of the Click LICENSE file; the license in that file is
 * legally binding.
 */

#include <click/config.h>
#include "progressbar.hh"
#include <click/confparse.hh>
#include <click/router.hh>
#include <click/error.hh>
#include <click/straccum.hh>
#include <click/handlercall.hh>
#ifdef HAVE_TERMIO_H
# include <termio.h>
#endif
#include <termios.h>
#include <unistd.h>
#include <sys/ioctl.h>
CLICK_DECLS

ProgressBar::ProgressBar()
    : _status(ST_FIRST), _timer(this)
{
}

ProgressBar::~ProgressBar()
{
}

int
ProgressBar::configure(Vector<String> &, ErrorHandler *)
{
    return 0;
}

int
ProgressBar::initialize(ErrorHandler *errh)
{
    _interval = 250;
    _delay_ms = 0;
    _active = true;
    _size = -1;
    String position_str, size_str;
    bool check_stdout = false, have_size = false;

    if (cp_va_kparse(configuration(), this, errh,
		     "POSHANDLER", cpkP+cpkM, cpArgument, &position_str,
		     "SIZEHANDLER", cpkP, cpArgument, &size_str,
		     "UPDATE", 0, cpSecondsAsMilli, &_interval,
		     "BANNER", 0, cpString, &_banner,
		     "ACTIVE", 0, cpBool, &_active,
		     "DELAY", 0, cpSecondsAsMilli, &_delay_ms,
		     "CHECK_STDOUT", 0, cpBool, &check_stdout,
		     "FIXED_SIZE", cpkC, &have_size, cpDouble, &_size,
		     cpEnd) < 0)
	return -1;

    Vector<String> words;
    cp_spacevec(size_str, words);
    _first_pos_h = words.size();
    cp_spacevec(position_str, words);
    _es.assign(words.size(), 0);
    _hs.assign(words.size(), 0);

    for (int i = 0; i < words.size(); i++)
	if (!cp_handler(words[i], Handler::OP_READ, &_es[i], &_hs[i], this, errh))
	    return -1;

    if (!isatty(STDERR_FILENO) || (check_stdout && isatty(STDOUT_FILENO)))
	_status = ST_DEAD;
    else
	_status = ST_FIRST;

    _have_size = have_size;
    _timer.initialize(this);
    if (_active && _status != ST_DEAD)
	_timer.schedule_now();
    return 0;
}

void
ProgressBar::cleanup(CleanupStage)
{
    if (_status == ST_MIDDLE) {
	_status = ST_DONE;
	run_timer(&_timer);
    }
}


// Code based on the progress bar in the OpenSSH project's B<scp> program. Its
// authors are listed as Timo Rinne, Tatu Ylonen, Theo de Raadt, and Aaron
// Campbell. Under a BSD-like copyright.

static bool
foregroundproc(int fd)
{
    static pid_t pgrp = -1;
    int ctty_pgrp;

    if (pgrp == -1)
	pgrp = getpgrp();

#ifdef HAVE_TCGETPGRP
    return ((ctty_pgrp = tcgetpgrp(fd)) != -1 &&
	    ctty_pgrp == pgrp);
#else
    return ((ioctl(fd, TIOCGPGRP, &ctty_pgrp) != -1 &&
	     ctty_pgrp == pgrp));
#endif
}

static int
getttywidth()
{
    // set TTY width (from openssh scp)
    struct winsize winsize;
    if (ioctl(STDERR_FILENO, TIOCGWINSZ, &winsize) != -1 && winsize.ws_col)
	return (winsize.ws_col ? winsize.ws_col : 80);
    else
	return 80;
}

static const char bar[] =
"************************************************************"
"************************************************************"
"************************************************************"
"************************************************************";
static const char bad_bar[] =
"------------------------------------------------------------"
"------------------------------------------------------------"
"------------------------------------------------------------"
"------------------------------------------------------------";
static const int max_bar_length = 240;
static const char prefixes[] = " KMGTP";

#define STALLTIME	5

bool
ProgressBar::get_value(int first, int last, double *value)
{
    *value = 0;
    bool all_known = true;
    for (int i = first; i < last; i++) {
	String s = cp_uncomment(_hs[i]->call_read(_es[i]));
	double this_value;
	bool ok = cp_double(s, &this_value);
	if (ok)
	    *value += this_value;
	else
	    all_known = false;
    }
    return (first == last ? false : all_known);
}

void
ProgressBar::run_timer(Timer *)
{
    // check _active
    if (!_active || _status == ST_DEAD)
	return;

    // get size on first time through
    if (_status == ST_FIRST || _status == ST_FIRSTDONE) {
	if (!_have_size)
	    _have_size = get_value(0, _first_pos_h, &_size);
	_last_pos = 0;
	_last_time = _start_time = Timestamp::now();
	_stall_time = Timestamp();
	_delay_time = _start_time + Timestamp::make_msec(_delay_ms);
	if (_status == ST_FIRST)
	    _status = ST_MIDDLE;
    }

    // exit if not in foreground
    if (!foregroundproc(STDERR_FILENO)) {
	_timer.reschedule_after_msec(_interval);
	return;
    }

    // get current time
    Timestamp now = Timestamp::now();

    // exit if wait time not passed
    if (now < _delay_time) {
	_timer.reschedule_at(_delay_time);
	return;
    }

    // get position
    double pos = 0;
    bool have_pos = get_value(_first_pos_h, _es.size(), &pos);

    // measure how far along we are
    double thermpos;
    if (!have_pos)
	thermpos = -1;
    else if (!_have_size) {
	thermpos = ((int)(pos / 100000)) % 200;
	if (thermpos > 100) thermpos = 200 - thermpos;
    } else if (_size > 0) {
	thermpos = (int)(100 * pos / _size);
	if (thermpos < 0) thermpos = 0;
	else if (thermpos > 100) thermpos = 100;
    } else
	thermpos = 100;

    // start sa, print percentage
    StringAccum sa;
    sa << "\r";
    if (_banner)
	sa << _banner << ' ';
    if (have_pos && _have_size)
	sa.snprintf(6, "%3d%% ", (int)thermpos);
    else if (_have_size)
	sa << "  -% ";

    // print the bar
    int barlength = getttywidth() - (sa.length() + 25);
    if (_have_size && _first_pos_h == 0)
	barlength += 8;
    barlength = (barlength <= max_bar_length ? barlength : max_bar_length);
    if (barlength > 0) {
	if (thermpos < 0 || (!_have_size && _status >= ST_DONE))
	    sa.snprintf(barlength + 10, "|%.*s|", barlength, bad_bar);
	else if (!_have_size && barlength > 3) {
	    int barchar = (int)((barlength - 3) * thermpos / 100);
	    sa.snprintf(barlength + 10, "|%*s***%*s|", barchar, "", barlength - barchar - 3, "");
	} else if (!_have_size) {
	    int barchar = (int)((barlength - 1) * thermpos / 100);
	    sa.snprintf(barlength + 10, "|%*s*%*s|", barchar, "", barlength - barchar - 1, "");
	} else {
	    int barchar = (int)(barlength * thermpos / 100);
	    sa.snprintf(barlength + 10, "|%.*s%*s|", barchar, bar, barlength - barchar, "");
	}
    }

    // print position
    if (_have_size && _first_pos_h == 0)
	// don't print position, it's relevant only as a fraction of _size
	sa << " ";
    else if (have_pos) {
	int which_pfx = 0;
	double abbrevpos = pos;
	while (abbrevpos >= 100000 && which_pfx < (int)(sizeof(prefixes))) {
	    which_pfx++;
	    abbrevpos /= 1024;
	}
	sa.snprintf(30, " %5lu%c%c ", (unsigned long)abbrevpos, prefixes[which_pfx], (prefixes[which_pfx] == ' ' ? ' ' : 'B'));
    } else
	sa << " -----   ";

    // check wait time
    Timestamp wait = now - _last_time;
    if (pos > _last_pos) {
	_last_time = now;
	_last_pos = pos;
	if (wait.sec() >= STALLTIME)
	    _stall_time += wait;
	wait.set_sec(0);
    }

    // check elapsed time
    double elapsed = (now - _start_time - _stall_time).doubleval();

    // collect time
    if (_status < ST_DONE
	&& (!have_pos || !_have_size || elapsed <= 0.0 || pos > _size))
	sa << "   --:-- ETA";
    else if (wait.sec() >= STALLTIME)
	sa << " - stalled -";
    else {
	int time_remaining;
	bool eta;
	if (_status >= ST_DONE || !have_pos || pos < 1e-6) {
	    time_remaining = (int)elapsed;
	    eta = false;
	} else {
	    time_remaining = (int)(_size / (pos / elapsed) - elapsed);
	    eta = true;
	}

	int hr = time_remaining / 3600;
	if (hr)
	    sa.snprintf(12, "%2d:", hr);
	else
	    sa << "   ";
	int sec = time_remaining % 3600;
	sa.snprintf(12, "%02d:%02d%s", sec / 60, sec % 60,
		    (eta ? " ETA" : "    "));
    }

    // add \n if appropriate
    if (_status >= ST_DONE)
	sa << '\n';

    // write data
    int fd = STDERR_FILENO;
    int buflen = sa.length();
    int bufpos = 0;
    const char *data = sa.data();
    while (bufpos < buflen) {
	ssize_t got = write(fd, data + bufpos, buflen - bufpos);
	if (got > 0)
	    bufpos += got;
	else if (errno != EINTR && errno != EAGAIN
#ifdef EWOULDBLOCK
		 && errno != EWOULDBLOCK
#endif
		 )
	    break;
    }

    if (_status < ST_DONE)
	_timer.reschedule_after_msec(_interval);
    else
	_active = false;
}

void
ProgressBar::complete(bool is_full)
{
    if (_status < ST_DONE && _active) {
	if (is_full) {
	    _have_size = true;
	    (void) get_value(_first_pos_h, _es.size(), &_size);
	}
	_status = (_status == ST_FIRST ? ST_FIRSTDONE : ST_DONE);
	_timer.unschedule();
	run_timer(&_timer);
    }
}


enum { H_MARK_STOPPED, H_MARK_DONE, H_ACTIVE,
       H_POSHANDLER, H_SIZEHANDLER, H_RESET };

String
ProgressBar::read_handler(Element *e, void *thunk)
{
    ProgressBar *pb = static_cast<ProgressBar *>(e);
    switch ((intptr_t)thunk) {
      case H_POSHANDLER:
      case H_SIZEHANDLER: {
	  bool is_pos = ((intptr_t)thunk == H_POSHANDLER);
	  StringAccum sa;
	  for (int i = (is_pos ? pb->_first_pos_h : 0); i < (is_pos ? pb->_es.size() : pb->_first_pos_h); i++) {
	      if (sa.length()) sa << ' ';
	      sa << pb->_hs[i]->unparse_name(pb->_es[i]);
	  }
	  return sa.take_string();
      }
      default:
	return "<error>";
    }
}

int
ProgressBar::write_handler(const String &in_str, Element *e, void *thunk, ErrorHandler *errh)
{
    ProgressBar *pb = static_cast<ProgressBar *>(e);
    String str = cp_uncomment(in_str);
    switch ((intptr_t)thunk) {
      case H_MARK_STOPPED:
	pb->complete(false);
	return 0;
      case H_MARK_DONE:
	pb->complete(true);
	return 0;
      case H_POSHANDLER:
      case H_SIZEHANDLER: {
	  Vector<String> words;
	  cp_spacevec(str, words);
	  bool is_pos = ((intptr_t)thunk == H_POSHANDLER);
	  int total = (is_pos ? pb->_first_pos_h + words.size() : pb->_es.size() - pb->_first_pos_h + words.size());
	  int offset = (is_pos ? pb->_first_pos_h : 0);

	  Vector<Element*> es(total, 0);
	  Vector<const Handler*> hs(total, 0);

	  for (int i = 0; i < words.size(); i++)
	      if (!cp_handler(words[i], Handler::OP_READ, &es[i+offset], &hs[i+offset], pb, errh))
		  return -1;

	  offset = (is_pos ? 0 : words.size() - pb->_first_pos_h);
	  for (int i = (is_pos ? 0 : pb->_first_pos_h); i < (is_pos ? pb->_first_pos_h : pb->_es.size()); i++)
	      es[i + offset] = pb->_es[i], hs[i + offset] = pb->_hs[i];

	  es.swap(pb->_es);
	  hs.swap(pb->_hs);
	  if (!is_pos) {
	      pb->_have_size = false;
	      pb->_first_pos_h = words.size();
	  }
	  return 0;
      }
      case H_ACTIVE:
	if (cp_bool(str, &pb->_active)) {
	    if (pb->_active && !pb->_timer.scheduled())
		pb->_timer.schedule_now();
	    return 0;
	} else
	    return errh->error("'active' should be bool (active setting)");
      case H_RESET:
	pb->_have_size = false;
	pb->_status = ST_FIRST;
	pb->_active = true;
	pb->_timer.schedule_now();
	return 0;
      default:
	return errh->error("internal");
    }
}

void
ProgressBar::add_handlers()
{
    add_write_handler("mark_stopped", write_handler, (void *)H_MARK_STOPPED);
    add_write_handler("mark_done", write_handler, (void *)H_MARK_DONE);
    add_data_handlers("active", Handler::OP_READ | Handler::CHECKBOX, &_active);
    add_write_handler("active", write_handler, (void *)H_ACTIVE);
    add_data_handlers("banner", Handler::OP_READ | Handler::OP_WRITE | Handler::RAW, &_banner);
    add_read_handler("poshandler", read_handler, (void *)H_POSHANDLER);
    add_write_handler("poshandler", write_handler, (void *)H_POSHANDLER);
    add_read_handler("sizehandler", read_handler, (void *)H_SIZEHANDLER);
    add_write_handler("sizehandler", write_handler, (void *)H_SIZEHANDLER);
    add_data_handlers("pos", Handler::OP_READ, &_last_pos);
    add_data_handlers("size", Handler::OP_READ | Handler::OP_WRITE, &_size);
    add_write_handler("reset", write_handler, (void *)H_RESET, Handler::BUTTON);
}

CLICK_ENDDECLS
ELEMENT_REQUIRES(userlevel)
EXPORT_ELEMENT(ProgressBar)
