/* ui.cpp -- User Interface

   This file is part of the UPX executable compressor.

   Copyright (C) 1996-2025 Markus Franz Xaver Johannes Oberhumer
   Copyright (C) 1996-2025 Laszlo Molnar
   All Rights Reserved.

   UPX and the UCL library are free software; you can redistribute them
   and/or modify them under the terms of the GNU General Public License as
   published by the Free Software Foundation; either version 2 of
   the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; see the file COPYING.
   If not, write to the Free Software Foundation, Inc.,
   59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

   Markus F.X.J. Oberhumer              Laszlo Molnar
   <markus@oberhumer.com>               <ezerotven+github@gmail.com>
 */

// INFO: not thread-safe; instantiated and used by class Packer, and the
// static (global) variables are also updated in work.cpp

#include "conf.h"
#include "file.h"
#include "packer.h"
#include "ui.h"
#include "console/screen.h"

#if 1 && (USE_SCREEN)
#define UI_USE_SCREEN 1
#endif

enum {
    M_QUIET,    // nothing at all '-qqq'
    M_INFO,     // print a one line info after compression '-qq'
    M_MSG,      // print "compressing", then "\r" and M_INFO
    M_CB_TERM,  // 1 line callback using stdout
    M_CB_SCREEN // 2 line callback using screen
};

struct UiPacker::State final {
    int mode;

    unsigned u_len;
    unsigned step;
    unsigned next_update;

    int pass;
    int total_passes;

    // message stuff
    char msg_buf[1 + 79 + 1];
    int pos;               // last progress bar position
    unsigned spin_counter; // for spinner

    int bar_pos;
    int bar_len;
    int pass_digits; // number of digits needed to print total_passes

#if (UI_USE_SCREEN)
    screen_t *screen;
    int screen_init_done;
    int b_cx, b_cy;
    int s_cx, s_cy;
    int s_fg, s_bg;
    int c_fg;
    int scroll_up;
    int cursor_shape;
#else
    void *screen;
#endif
};

// static
unsigned UiPacker::total_files = 0;
unsigned UiPacker::total_files_done = 0;
upx_uint64_t UiPacker::total_c_len = 0;
upx_uint64_t UiPacker::total_u_len = 0;
upx_uint64_t UiPacker::total_fc_len = 0;
upx_uint64_t UiPacker::total_fu_len = 0;
unsigned UiPacker::update_c_len = 0;
unsigned UiPacker::update_u_len = 0;
unsigned UiPacker::update_fc_len = 0;
unsigned UiPacker::update_fu_len = 0;

/*************************************************************************
// constants
**************************************************************************/

static const char header_line1[] = "        File size         Ratio      Format      Name\n";
static const char header_line2[] = "   --------------------   ------   -----------   -----------\n";

static const char progress_filler[4 + 1] = ".*[]";

static void init_global_constants(void) noexcept {
#if 0 && (ACC_OS_DOS16 || ACC_OS_DOS32)
    // FIXME: should test codepage here

#if 1 && (ACC_OS_DOS32) && defined(__DJGPP__)
    /* check for Windows NT/2000/XP */
    if (_get_dos_version(1) == 0x0532)
        return;
#endif

    char *p;
    for (p = header_line2; *p; p++)
        if (*p == '-')
            *p = '\xc4';

    //strcpy(progress_filler, "\x07\xb0[]");
    //strcpy(progress_filler, "\x07\xb1[]");
    strcpy(progress_filler, "\xf9\xfe[]");
#endif
}

/*************************************************************************
//
**************************************************************************/

static const char *mkline(upx_uint64_t fu_len, upx_uint64_t fc_len, upx_uint64_t u_len,
                          upx_uint64_t c_len, const char *format_name, const char *filename,
                          bool decompress = false) {
    static char buf[2048]; // static! // TODO later: check if affected by WITH_THREADS
    char r[7 + 1];
    char fn[15 + 1];
    const char *f;

    // Large ratios can happen because of overlays that are
    // appended after a program is packed.
    unsigned ratio = get_ratio(fu_len, fc_len);
    if (ratio >= 1000 * 1000)
        strcpy(r, "overlay");
    else
        upx_safe_snprintf(r, sizeof(r), "%3u.%02u%%", ratio / 10000, (ratio % 10000) / 100);
    if (decompress)
        f = "%10lld <-%10lld  %7s %15s %s";
    else
        f = "%10lld ->%10lld  %7s %15s %s";
    center_string(fn, sizeof(fn), format_name);
    assert(strlen(fn) == 15);
    upx_safe_snprintf(buf, sizeof(buf), f, (long long) fu_len, (long long) fc_len, r, fn, filename);
    UNUSED(u_len);
    UNUSED(c_len);
    return buf;
}

/*************************************************************************
//
**************************************************************************/

UiPacker::UiPacker(const PackerBase *pb_) : pb(pb_) {
    static upx_std_once_flag init_done;
    upx_std_call_once(init_done, init_global_constants);

    cb.reset();

    s = new State;
    mem_clear(s);
    s->msg_buf[0] = '\r';

#if defined(UI_USE_SCREEN)
    // FIXME - ugly hack
    s->screen = sobject_get_screen();
#endif

    if (opt->verbose < 0)
        s->mode = M_QUIET;
    else if (opt->verbose == 0 || !acc_isatty(STDOUT_FILENO))
        s->mode = M_INFO;
    else if (opt->verbose == 1 || opt->no_progress)
        s->mode = M_MSG;
    else if (s->screen == nullptr)
        s->mode = M_CB_TERM;
    else
        s->mode = M_CB_SCREEN;
}

UiPacker::~UiPacker() noexcept {
    cb.reset();
    // owner
    upx::owner_delete(s);
}

/*************************************************************************
// start callback
**************************************************************************/

void UiPacker::printInfo(int nl) {
    if (opt->all_methods && s->total_passes > 1)
        con_fprintf(stdout, "Compressing %s [%s]%s", pb->fi->getName(), pb->getName(),
                    nl ? "\n" : "");
    else {
        char method_name[32 + 1];
        set_method_name(method_name, sizeof(method_name), pb->ph.method, pb->ph.level);
        con_fprintf(stdout, "Compressing %s [%s, %s]%s", pb->fi->getName(), pb->getName(),
                    method_name, nl ? "\n" : "");
    }
}

void UiPacker::startCallback(unsigned u_len, unsigned step, int pass, int total_passes) {
    s->u_len = u_len;
    s->step = step;
    s->next_update = step;

    s->pass = pass;
    s->total_passes = total_passes;
    // printf("startCallback %d %d\n", s->pass, s->total_passes);

    s->bar_len = 64;
    s->pos = -2;
    s->spin_counter = 0;
    s->bar_pos = 1; // because of the leading '\r'
    s->pass_digits = 0;

    cb.reset();

    if (s->pass < 0) // no callback wanted
        return;

    if (s->mode <= M_INFO)
        return;
    if (s->mode == M_MSG) {
        if (pass <= 1) {
            printInfo(0);
            fflush(stdout);
            printSetNl(2);
        }
        return;
    }

    cb.nprogress = progress_callback;
    cb.user = this; // parameter for static function UiPacker::progress_callback()

    if (s->mode == M_CB_TERM) {
        const char *fname = fn_basename(pb->fi->getName());
        int l = (int) strlen(fname);
        if (l > 0 && l <= 30) {
            strcpy(&s->msg_buf[s->bar_pos], fname);
            s->bar_pos += l;
            s->msg_buf[s->bar_pos++] = ' ';
            s->msg_buf[s->bar_pos++] = ' ';
            s->bar_len -= l + 2;
        }
    }

    // set pass
    if (total_passes > 1) {
        int buflen, l;
        do {
            s->pass_digits++;
            total_passes /= 10;
        } while (total_passes > 0);
        buflen = sizeof(s->msg_buf) - s->bar_pos;
        l = upx_safe_snprintf(&s->msg_buf[s->bar_pos], buflen, "%*d/%*d  ", s->pass_digits, s->pass,
                              s->pass_digits, s->total_passes);
        if (l > 0 && s->bar_len - l > 10) {
            s->bar_len -= l;
            s->bar_pos += l;
        }
    }

#if (UI_USE_SCREEN)
    if (s->mode == M_CB_SCREEN) {
        if (!s->screen_init_done) {
            s->screen_init_done = 1;
            if (s->screen->hideCursor)
                s->cursor_shape = s->screen->hideCursor(s->screen);
            s->s_fg = s->screen->getFg(s->screen);
            s->s_bg = s->screen->getBg(s->screen);
            s->screen->getCursor(s->screen, &s->s_cx, &s->s_cy);
            s->scroll_up = s->screen->getScrollCounter(s->screen);
            printInfo(1);
            s->screen->getCursor(s->screen, &s->b_cx, &s->b_cy);
            s->scroll_up = s->screen->getScrollCounter(s->screen) - s->scroll_up;
        }
    }
#endif /* UI_USE_SCREEN */
}

// may only get called directly after startCallback()
void UiPacker::firstCallback() {
    if (s->pos == -2)
        doCallback(0, 0);
}

// make sure we reach 100% in the progress bar
void UiPacker::finalCallback(unsigned u_len, unsigned c_len) {
    s->next_update = u_len;
    doCallback(u_len, c_len);
}

/*************************************************************************
// end callback
**************************************************************************/

void UiPacker::endCallback() {
    bool done = (s->total_passes <= 0 || s->pass >= s->total_passes);
    endCallback(done);
}

void UiPacker::endCallback(bool done) {
    if (s->pass < 0) // no callback wanted
        return;

    if (s->mode == M_CB_TERM) {
        if (done)
            printClearLine(stdout);
        else
            printSetNl(2);
    }

// restore screen
#if (UI_USE_SCREEN)
    if (s->mode == M_CB_SCREEN) {
        if (done) {
            int cx, cy, sy;
            assert(s->screen_init_done);
            s->screen_init_done = 0;
            assert(s->s_cx == 0 && s->b_cx == 0);
            s->screen->getCursor(s->screen, &cx, &cy);
            sy = UPX_MAX(0, s->s_cy - s->scroll_up);
            while (cy >= sy)
                s->screen->clearLine(s->screen, cy--);
            s->screen->setCursor(s->screen, s->s_cx, sy);
            s->screen->setFg(s->screen, s->s_fg);
            s->screen->setBg(s->screen, s->s_bg);
            if (s->cursor_shape > 0)
                s->screen->setCursorShape(s->screen, s->cursor_shape);
        } else {
            // not needed:
            //   s->screen->clearLine(s->screen, s->b_cy);
            //   s->screen->setCursor(s->screen, s->b_cx, s->b_cy);
        }
    }
#endif /* UI_USE_SCREEN */

    cb.reset();
#if 0
    printf("callback: pass %d, step %6d, updates %6d\n",
           s->pass, s->step, s->spin_counter);
#endif
}

/*************************************************************************
// the callback
**************************************************************************/

/*static*/
void __acc_cdecl UiPacker::progress_callback(upx_callback_t *cb, unsigned isize, unsigned osize) {
    // printf("%6d %6d %d\n", isize, osize, state);
    UiPacker *self = (UiPacker *) cb->user;
    self->doCallback(isize, osize);
}

void UiPacker::doCallback(unsigned isize, unsigned osize) {
    int i;
    static const char spinner[] = "|/-\\";

    if (s->pass < 0) // no callback wanted
        return;

    if (s->u_len == 0 || isize > s->u_len)
        return;
    // check if we should update the display
    if (s->step > 0 && isize > 0 && isize < s->u_len) {
        if (isize < s->next_update)
            return;
        s->next_update += s->step;
    }

    // compute progress position
    int pos = -1;
    if (isize >= s->u_len)
        pos = s->bar_len;
    else if (isize > 0) {
        pos = get_ratio(s->u_len, isize) * s->bar_len / 1000000;
        assert(pos >= 0);
        assert(pos <= s->bar_len);
    }

#if 0
    printf("%6d %6d %6d %6d %3d %3d\n", isize, osize, s->step, s->next_update, pos, s->pos);
    return;
#endif

    if (pos < s->pos)
        return;
    if (pos < 0 && pos == s->pos)
        return;

    // fill the progress bar
    char *m = &s->msg_buf[s->bar_pos];
    *m++ = progress_filler[2];
    for (i = 0; i < s->bar_len; i++)
        *m++ = progress_filler[i <= pos ? 1 : 0];
    *m++ = progress_filler[3];

    // compute current compression ratio
    unsigned ratio = 1000000;
    if (osize > 0)
        ratio = get_ratio(isize, osize);

    int buflen = (int) (&s->msg_buf[sizeof(s->msg_buf)] - m);
    upx_safe_snprintf(m, buflen, "  %3d.%1d%%  %c ", ratio / 10000, (ratio % 10000) / 1000,
                      spinner[s->spin_counter & 3]);
    assert(strlen(s->msg_buf) < 1 + 80);

    s->pos = pos;
    s->spin_counter++;

    if (s->mode == M_CB_TERM) {
        const char *msg = &s->msg_buf[0];
        int fg = con_fg(stdout, FG_CYAN);
        con_fprintf(stdout, "%s", msg); // avoid backslash interpretation
        (void) con_fg(stdout, fg);
        fflush(stdout);
        printSetNl(1);
        UNUSED(fg);
        return;
    }

#if (UI_USE_SCREEN)
    if (s->mode == M_CB_SCREEN) {
        const char *msg = &s->msg_buf[1];
#if 0
        s->screen->putString(s->screen,msg,s->b_cx,s->b_cy);
#else
        // FIXME: this doesn't honor '--mono' etc.
        int attr = FG_CYAN | s->s_bg;
        s->screen->putStringAttr(s->screen, msg, attr, s->b_cx, s->b_cy);
#endif
        s->screen->refresh(s->screen);
    }
#endif /* UI_USE_SCREEN */
}

/*************************************************************************
// pack
**************************************************************************/

void UiPacker::uiPackStart(const OutputFile *fo) {
    total_files++;
    UNUSED(fo);
}

void UiPacker::uiPackEnd(const OutputFile *fo) {
    uiUpdate(fo->st_size());

    if (s->mode == M_QUIET)
        return;
    if (s->mode == M_MSG) {
        // We must put this here and not in endCallback() as we may
        // have multiple passes.
        printClearLine(stdout);
    }

    const char *name = pb->fi->getName();
    if (opt->output_name)
        name = opt->output_name;
    else if (opt->to_stdout)
        name = "<stdout>";
    con_fprintf(stdout, "%s\n",
                mkline(pb->ph.u_file_size, fo->st_size(), pb->ph.u_len, pb->ph.c_len, pb->getName(),
                       fn_basename(name)));
    printSetNl(0);
}

/*static*/ void UiPacker::uiPackTotal() {
    uiListTotal();
    uiFooter("Packed");
}

/*************************************************************************
// unpack
**************************************************************************/

void UiPacker::uiUnpackStart(const OutputFile *fo) {
    total_files++;
    UNUSED(fo);
}

void UiPacker::uiUnpackEnd(const OutputFile *fo) {
    uiUpdate(-1, fo->getBytesWritten());

    if (s->mode == M_QUIET)
        return;

    const char *name = pb->fi->getName();
    if (opt->output_name)
        name = opt->output_name;
    else if (opt->to_stdout)
        name = "<stdout>";
    con_fprintf(stdout, "%s\n",
                mkline(fo->getBytesWritten(), pb->file_size, pb->ph.u_len, pb->ph.c_len,
                       pb->getName(), fn_basename(name), true));
    printSetNl(0);
}

/*static*/ void UiPacker::uiUnpackTotal() {
    uiListTotal(true);
    uiFooter("Unpacked");
}

/*************************************************************************
// list
**************************************************************************/

void UiPacker::uiListStart() { total_files++; }

void UiPacker::uiList() {
    const char *name = pb->fi->getName();
    con_fprintf(
        stdout, "%s\n",
        mkline(pb->ph.u_file_size, pb->file_size, pb->ph.u_len, pb->ph.c_len, pb->getName(), name));
    printSetNl(0);
}

void UiPacker::uiListEnd() { uiUpdate(); }

/*static*/ void UiPacker::uiListTotal(bool decompress) {
    if (opt->verbose >= 1 && total_files >= 2) {
        char name[32];
        upx_safe_snprintf(name, sizeof(name), "[ %u file%s ]", total_files_done,
                          total_files_done == 1 ? "" : "s");
        con_fprintf(
            stdout, "%s%s\n", header_line2,
            mkline(total_fu_len, total_fc_len, total_u_len, total_c_len, "", name, decompress));
        printSetNl(0);
    }
}

/*************************************************************************
// test
**************************************************************************/

void UiPacker::uiTestStart() {
    total_files++;

    if (opt->verbose >= 1) {
        con_fprintf(stdout, "testing %s ", pb->fi->getName());
        fflush(stdout);
        printSetNl(1);
    }
}

void UiPacker::uiTestEnd() {
    if (opt->verbose >= 1) {
        con_fprintf(stdout, "[OK]\n");
        fflush(stdout);
        printSetNl(0);
    }
    uiUpdate();
}

/*static*/ void UiPacker::uiTestTotal() { uiFooter("Tested"); }

/*************************************************************************
// info
**************************************************************************/

bool UiPacker::uiFileInfoStart() {
    total_files++;

    int fg = con_fg(stdout, FG_CYAN);
    con_fprintf(stdout, "%s [%s, %s]\n", pb->fi->getName(), pb->getFullName(opt), pb->getName());
    fg = con_fg(stdout, fg);
    UNUSED(fg);
    if (pb->ph.c_len > 0) {
        con_fprintf(stdout, "  %8llu bytes", pb->file_size_u);
        con_fprintf(stdout, ", compressed by UPX %d, method %d, level %d, filter 0x%02x/0x%02x\n",
                    pb->ph.version, pb->ph.method, pb->ph.level, pb->ph.filter, pb->ph.filter_cto);
        return false;
    } else {
        con_fprintf(stdout, "  %8llu bytes", pb->file_size_u);
        con_fprintf(stdout, ", not compressed by UPX\n");
        return true;
    }
}

void UiPacker::uiFileInfoEnd() { uiUpdate(); }

/*static*/ void UiPacker::uiFileInfoTotal() {}

/*************************************************************************
// util
**************************************************************************/

/*static*/ void UiPacker::uiHeader() {
    static upx_std_atomic(bool) done;
    if (done)
        return;
    done = true;
    if (opt->cmd == CMD_TEST || opt->cmd == CMD_FILEINFO)
        return;
    if (opt->verbose >= 1) {
        con_fprintf(stdout, "%s%s", header_line1, header_line2);
    }
}

/*static*/ void UiPacker::uiFooter(const char *t) {
    static upx_std_atomic(bool) done;
    if (done)
        return;
    done = true;
    if (opt->verbose >= 1) {
        assert(total_files >= total_files_done);
        unsigned n1 = total_files;
        unsigned n2 = total_files_done;
        unsigned n3 = total_files - total_files_done;
        if (n3 == 0)
            con_fprintf(stdout, "\n%s %u file%s.\n", t, n1, n1 == 1 ? "" : "s");
        else
            con_fprintf(stdout, "\n%s %u file%s: %u ok, %u error%s.\n", t, n1, n1 == 1 ? "" : "s",
                        n2, n3, n3 == 1 ? "" : "s");
    }
}

void UiPacker::uiUpdate(upx_off_t fc_len, upx_off_t fu_len) {
    update_fc_len = (fc_len >= 0) ? fc_len : pb->file_size_u;
    update_fu_len = (fu_len >= 0) ? fu_len : pb->ph.u_file_size;
    update_c_len = pb->ph.c_len;
    update_u_len = pb->ph.u_len;
}

/*static*/ void UiPacker::uiConfirmUpdate() {
    total_files_done++;
    total_fc_len += update_fc_len;
    total_fu_len += update_fu_len;
    total_c_len += update_c_len;
    total_u_len += update_u_len;
}

/* vim:set ts=4 sw=4 et: */
