/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) Daniel Stenberg (2002)
 * Copyright (C) William Wilgus (2026)
 *
 * This program is free software; you can redistribute it and/or
 * modify it 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 software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/
#include "stdarg.h"
#include "string.h"
#include "rbunicode.h"
#include "stdio.h"
#include "kernel.h"
#include "screen_access.h"
#include "lang.h"
#include "settings.h"
#include "talk.h"
#include "splash.h"
#include "viewport.h"
#include "strptokspn_r.h"
#include "scrollbar.h"
#include "font.h"
#ifndef BOOTLOADER
#include "misc.h" /* get_current_activity */
#endif

static long progress_next_tick, talked_tick;

#define MAXLINES  (LCD_HEIGHT/6)


#ifndef BOOTLOADER
#if MEMORYSIZE > 8
    #define MAXBUFFER MAX(1024, (LCD_WIDTH/SYSFONT_WIDTH) * MAXLINES + 1)
#else
    #define MAXBUFFER MAX(512, (LCD_WIDTH/SYSFONT_WIDTH) * MAXLINES + 1)
#endif
#else
    #define MAXBUFFER (512)
#endif

#define RECT_SPACING 3
#define SPLASH_MEMORY_INTERVAL (HZ)

/* splash_internal does the actual drawing of the splash box and text
 * each call to splash remembers the max size and will not shrink the splash box
 * until it is reset by an activity change or \f at the beginning of a string
 *
 * splash supports several escape sequences ("\n\t\f\r\v\b")
 * '\n' (new line) indicates the end of a line of text and starts a new one
 *  ("\n\n" will end a line and create a blank line)
 *  '\n' as the first character of the text will create a blank line
 *
 * '\t' (tab) starts a new line and adds several spaces to the beginning of the
 *  new line it also left/right justifies the splash text box
 *  (RTL languages will right justify)
 * '\t' as the last character of the text will left/right justify
 *  without adding any tabs to the text output
 *
 * '\f' (form feed) as the first character of the text will reset the text box
 *  width and height
 *
 * Others:
 * '\f', \r', '\v' '\b' will be treated as a breaking space character
 *  they will not be displayed as output and multiple sequences will be
 *  treated as a single character
 *  eg. "\b\v\f\r" will break the line but only once and will not show as a space
 *  where as "\n\n" will break a line and add a blank line
 *  "\t\t" would break a line and add a total of 4 spaces to the next line
*/
static bool splash_internal(struct screen * screen, const char *fmt, va_list ap,
                            struct viewport *vp, int addl_lines)
{
    static const char matchstr[] = "\r\n\f\v\t\b";
    static int max_width[NB_SCREENS] = {0};
    static int max_height[NB_SCREENS] = {0};
#ifndef BOOTLOADER
    static char splash_buf[MAXBUFFER] = {0};
    static enum current_activity last_act = ACTIVITY_UNKNOWN;
    enum current_activity act = get_current_activity();

    if (last_act != act) /* changed activities reset width / height */
    {
        FOR_NB_SCREENS(i)
        {
            max_width[i] = 0;
            max_height[i] = 0;
        }
        last_act = act;
    }
#else /*def BOOTLOADER*/
    char splash_buf[MAXBUFFER] = {0}; /* on the stack for the bootloader */
#endif

    struct splash_lines {
        const char *str;
        uint16_t x;
        uint16_t len;
    } lines[MAXLINES];
    memset(lines, 0, sizeof(lines));

    char *buf = splash_buf;
    const char *next, *store;

    bool has_tabs = false;
    int line = 0;
    int x = 0, w = 0;
    int y;
    int space_w, chr_h;
    int width = vp->width - RECT_SPACING*2;
    int height = vp->height;

    /* prevent screen artifacts by keeping the max width / height seen */
    int maxw = max_width[screen->screen_type];
    int maxh = max_height[screen->screen_type];
    int fontnum = vp->font;

    size_t next_len;

    font_getstringsize(" ", &space_w, &chr_h, fontnum);
    y = (addl_lines * chr_h);

    int res = vsnprintf(splash_buf, sizeof(splash_buf), fmt, ap);
    va_end(ap);

    if (res <= 0 || width < space_w || height < chr_h)
    {
#ifdef SIMULATOR
        if (res <= 0)
            printf("ERROR: %s %d\n", __func__, res);
        else
            printf("WARNING: %s vp too small\n", __func__);
#endif
        return false; /* nothing to display */
    }

    const char *lastbreak = splash_buf;
    if (*lastbreak == '\f') /* only as first character - Reset splash box size */
    {
        maxw = 0;
        maxh = 0;
    }

    while(true) /* break splash string into display lines, doing proper word wrap */
    {
#ifndef BOOTLOADER
        while (*lastbreak != '\0') /* handle escape chars*/
        {
            switch (*lastbreak) /* all chars in matchstr */
            {
                case '\t': /*fallthrough*/
                    has_tabs = true; /* left justify (right justify for RTL)*/
                case '\n':
                {
                    if (lines[line].len > 0 || *lastbreak != '\t')
                    {
                        x += w;
                        if (x > maxw)
                            maxw = x;

                        x = 0;
                        y += chr_h;
                        if (y >= height || line >= MAXLINES-1)
                        {
                            break;  /* out of lines */
                        }
                        line++;
                    }
                    /* /t as last char left justifies only */
                    if (lastbreak[0] == '\t' && lastbreak[1] != '\0')
                    {
                        /* add offset for tab */
                        lines[line].x += space_w * 2;
                        x += space_w * 2;
                    }
                    break;
                }
                case '\b': /*fallthrough*/
                case '\f': /*fallthrough*/
                case '\v': /*fallthrough*/
                case '\r':
                    break; /* acts the same as a space character */
                default: /* No valid break chars */
                    lastbreak = " "; /* exit */
                    break;
            }
            lastbreak++;
        }
#endif
        if (lines[line].len > 0)
        {
            x += w;
            if (x > maxw)
                maxw = x;
            x = 0;
            y += chr_h;
            if (y >= height || line >= MAXLINES-1)
            {
                if (y > maxh)
                    maxh = y;
                break;  /* out of lines */
            }
            line++;
        }

        next = strptokspn_r(buf, matchstr, &next_len, &store);

        if (!next)
        {
            x += w;
            if (x > maxw)
                maxw = x;
            if (y > maxh)
                maxh = y;
            break;
        }
        buf = NULL; /* uses store for continuation */

        lines[line].str = next;
        lines[line].len = next_len;

        w = font_getstringnsize(next, next_len, NULL, NULL, fontnum);
        if (w > width)
        {
            const char *nxp, *nx = next;
            int nw, newlen;
            int oldlen = next_len;

            while (nx - next < oldlen) /* try to split at a space char */
            {
                nxp = nx;
                nx++;
                if (*nxp != ' ' && *nx != '\0') /* split on space or EOL */
                    continue;
                newlen = nxp - next;
                nw = font_getstringnsize(next, newlen, NULL, NULL, fontnum);

                if (nw > width)
                {
                    /* is next word larger than max width & room left on this line? */
                    if (nw - w > width && w + space_w * 8 < width)
                        w = nw;
                    break;
                }
                w = nw;
                next_len = newlen;
                store = nx; /* we want to skip the space char */
            }
            if (w > width) /* split when it fits */
            {
                w = width;
                next_len = font_measurestring(next, oldlen, w, &w, NULL, fontnum);
                store = next + next_len;
            }

            lines[line].len = next_len;
        }
        lastbreak = next + next_len;
    }

    /* prepare viewport
     * First boundaries, then the background filling, then the border and finally
     * the text*/

    screen->scroll_stop();

    width = maxw + 2*RECT_SPACING;
    height = maxh + 2*RECT_SPACING;

    if (width > vp->width)
        width = vp->width;
    if (height > vp->height)
        height = vp->height;

    /* center the vp in the screen area */
    vp->x += (vp->width - width) / 2;
    vp->y += (vp->height - height) / 2;
    vp->width = width;
    vp->height = height;
    if (!has_tabs)
        vp->flags |=  VP_FLAG_ALIGN_CENTER;

    /* prevent artifacts by locking to max width & height observed on repeated calls */
    max_width[screen->screen_type] = width - 2*RECT_SPACING;
    max_height[screen->screen_type] = height - 2*RECT_SPACING;

#if LCD_DEPTH > 1
    unsigned fg = 0, bg = 0;
    bool broken = false;

    if (screen->depth > 1)
    {
        fg = screen->get_foreground();
        bg = screen->get_background();

        broken = (fg == bg) ||
                 (bg == 63422 && fg == 65535); /* -> iPod reFresh themes from '22 */

        vp->drawmode = DRMODE_FG;
        /* can't do vp->fg_pattern here, since set_foreground does a bit more on
         * greyscale */
        screen->set_foreground(broken ? SCREEN_COLOR_TO_NATIVE(screen, LCD_LIGHTGRAY) :
                               bg);     /* gray as fallback for broken themes */
    }
    else
#endif
        vp->drawmode = (DRMODE_SOLID|DRMODE_INVERSEVID);

    screen->fill_viewport();

#if LCD_DEPTH > 1
    if (screen->depth > 1)
        /* can't do vp->fg_pattern here, since set_foreground does a bit more on
         * greyscale */
        screen->set_foreground(broken ? SCREEN_COLOR_TO_NATIVE(screen, LCD_BLACK) :
                               fg);     /* black as fallback for broken themes */
    else
#endif
        vp->drawmode = DRMODE_SOLID;

    screen->draw_border_viewport();

    /* center y within the available splash box */
    if (y < maxh)
        y = (maxh - y) / 2;
    else
        y = RECT_SPACING;

    /* print the message to screen */
    for(int i = 0; i < line && y < height; i++, y+= chr_h)
    {
        if (lines[i].len > 0)
        {
            x = lines[i].x;
            screen->putsxyf(x, y, "%.*s", lines[i].len, lines[i].str);
        }
    }

    return true; /* needs update */
}

void splashf(int ticks, const char *fmt, ...)
{
    va_list ap;

    /* fmt may be a so called virtual pointer. See settings.h. */
    long id;
    if((id = P2ID((const unsigned char*)fmt)) >= 0)
        /* If fmt specifies a voicefont ID, and voice menus are
           enabled, then speak it. */
        cond_talk_ids_fq(id);

    /* If fmt is a lang ID then get the corresponding string (which
       still might contain % place holders). */
    fmt = P2STR((unsigned char *)fmt);
    FOR_NB_SCREENS(i)
    {
#ifdef HAVE_REMOTE_LCD
        /* Don't splash on the remote (top) screen — it shows the WPS */
        if (i == SCREEN_REMOTE && get_current_activity() != ACTIVITY_WPS)
            continue;
#endif
        struct screen * screen = &(screens[i]);
        struct viewport vp;
        viewport_set_defaults(&vp, screen->screen_type);
        struct viewport *last_vp = screen->set_viewport(&vp);

        va_start(ap, fmt);
        if (splash_internal(screen, fmt, ap, &vp, 0))
            screen->update_viewport();
        va_end(ap);

        screen->set_viewport(last_vp);
    }
    if (ticks)
        sleep(ticks);
}

/* set delay before progress meter is shown */
void splash_progress_set_delay(long delay_ticks)
{
    progress_next_tick = current_tick + delay_ticks;
    talked_tick = 0;
}

/* splash a progress meter */
void splash_progress(int current, int total, const char *fmt, ...)
{
    va_list ap;
    int vp_flag = VP_FLAG_VP_DIRTY;
    /* progress update tick */
    long now = current_tick;

    if (current < total)
    {
        if(TIME_BEFORE(now, progress_next_tick))
            return;
        /* limit to 20fps */
        progress_next_tick = now + HZ/20;
        vp_flag = 0; /* don't mark vp dirty to prevent flashing */
    }

    if (global_settings.talk_menu &&
        total > 0 &&
        TIME_AFTER(current_tick, talked_tick + HZ*5))
    {
        talked_tick = current_tick;
        talk_ids(false, LANG_LOADING_PERCENT,
                 TALK_ID(current * 100 / total, UNIT_PERCENT));
    }

    /* If fmt is a lang ID then get the corresponding string (which
       still might contain % place holders). */
    fmt = P2STR((unsigned char *)fmt);
    FOR_NB_SCREENS(i)
    {
        struct screen * screen = &(screens[i]);
        struct viewport vp;
        viewport_set_defaults(&vp, screen->screen_type);
        struct viewport *last_vp = screen->set_viewport_ex(&vp, vp_flag);

        va_start(ap, fmt);
        if (splash_internal(screen, fmt, ap, &vp, 1))
        {
            int size = screen->getcharheight();
            int x = RECT_SPACING;
            int y = vp.height - size - RECT_SPACING;
            int w = vp.width - RECT_SPACING * 2;
            int h = size;
#ifdef HAVE_LCD_COLOR
            const int sb_flags = HORIZONTAL | FOREGROUND;
#else
            const int sb_flags = HORIZONTAL;
#endif
            gui_scrollbar_draw(screen, x, y, w, h, total, 0, current, sb_flags);

            screen->update_viewport();
        }
        va_end(ap);

        screen->set_viewport(last_vp);
    }
}
