/*
 * SPDX-FileCopyrightText: 2016 Harald Sitter <sitter@kde.org>
 * SPDX-FileCopyrightText: 2010 Canonical Ltd.
 * SPDX-FileCopyrightText: 2008-2012 Red Hat Inc.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "ply-text-progress-bar.h"
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <math.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>
#include <values.h>
#include <wchar.h>

#include <ply-boot-splash-plugin.h>
#include <ply-buffer.h>
#include <ply-event-loop.h>
#include <ply-key-file.h>
#include <ply-list.h>
#include <ply-logger.h>
#include <ply-text-display.h>
#include <ply-trigger.h>
#include <ply-utils.h>

#include <linux/kd.h>

#define CLEAR_LINE_SEQUENCE "\033[2K\r\n"
#define BACKSPACE "\b\033[0K"
#define PLUGIN_NAME "breeze-text"

typedef enum { PLY_BOOT_SPLASH_DISPLAY_NORMAL, PLY_BOOT_SPLASH_DISPLAY_QUESTION_ENTRY, PLY_BOOT_SPLASH_DISPLAY_PASSWORD_ENTRY } ply_boot_splash_display_type_t;

struct _ply_boot_splash_plugin {
    ply_event_loop_t *loop;
    ply_boot_splash_mode_t mode;

    ply_list_t *views;

    ply_boot_splash_display_type_t state;

    char *message;

    uint32_t is_animating : 1;
    uint32_t black;
    uint32_t white;
    uint32_t brown;
    uint32_t blue;
    char *title;
};

typedef struct {
    ply_boot_splash_plugin_t *plugin;
    ply_text_display_t *display;
    breeze_text_progress_bar_t *progress_bar;
} view_t;

static void hide_splash_screen(ply_boot_splash_plugin_t *plugin, ply_event_loop_t *loop);

static view_t *view_new(ply_boot_splash_plugin_t *plugin, ply_text_display_t *display)
{
    view_t *view;

    view = calloc(1, sizeof(view_t));
    view->plugin = plugin;
    view->display = display;

    view->progress_bar = breeze_text_progress_bar_new();

    return view;
}

static void view_free(view_t *view)
{
    breeze_text_progress_bar_free(view->progress_bar);

    free(view);
}

static void view_show_message(view_t *view)
{
    ply_boot_splash_plugin_t *plugin;
    int display_width, display_height, y;
    ply_terminal_color_t color;
    char *message;

    plugin = view->plugin;

    display_width = ply_text_display_get_number_of_columns(view->display);
    display_height = ply_text_display_get_number_of_rows(view->display);

    if (!strncmp(plugin->message, "keys:", 5)) {
        message = plugin->message + 5;
        color = PLY_TERMINAL_COLOR_WHITE;
        y = display_height - 4;
    } else {
        message = plugin->message;
        color = PLY_TERMINAL_COLOR_BLUE;
        y = display_height / 2 + 7;
    }

    ply_text_display_set_cursor_position(view->display, 0, y);
    ply_text_display_clear_line(view->display);
    ply_text_display_set_cursor_position(view->display, (display_width - strlen(message)) / 2, y);

    ply_text_display_set_foreground_color(view->display, color);
    ply_text_display_write(view->display, "%s", message);
}

static void view_show_prompt(view_t *view, const char *prompt, const char *entered_text)
{
    int display_width, display_height;

    display_width = ply_text_display_get_number_of_columns(view->display);
    display_height = ply_text_display_get_number_of_rows(view->display);

    ply_text_display_set_cursor_position(view->display, 0, display_height / 2 + 8);
    ply_text_display_clear_line(view->display);
    ply_text_display_set_cursor_position(view->display, display_width / 2 - (strlen(prompt)), display_height / 2 + 8);

    ply_text_display_write(view->display, "%s:%s", prompt, entered_text);

    ply_text_display_show_cursor(view->display);
}

static void view_start_animation(view_t *view)
{
    ply_boot_splash_plugin_t *plugin;
    ply_terminal_t *terminal;

    assert(view != NULL);

    plugin = view->plugin;

    terminal = ply_text_display_get_terminal(view->display);

    ply_terminal_set_color_hex_value(terminal, PLY_TERMINAL_COLOR_BLACK, plugin->black);
    ply_terminal_set_color_hex_value(terminal, PLY_TERMINAL_COLOR_WHITE, plugin->white);
    ply_terminal_set_color_hex_value(terminal, PLY_TERMINAL_COLOR_BLUE, plugin->blue);
    ply_terminal_set_color_hex_value(terminal, PLY_TERMINAL_COLOR_BROWN, plugin->brown);

    ply_text_display_set_background_color(view->display, PLY_TERMINAL_COLOR_BLACK);
    ply_text_display_clear_screen(view->display);
    ply_text_display_hide_cursor(view->display);

    if (plugin->mode == PLY_BOOT_SPLASH_MODE_SHUTDOWN) {
        breeze_text_progress_bar_hide(view->progress_bar);
        return;
    }

    breeze_text_progress_bar_show(view->progress_bar, view->display);
}

static void view_redraw(view_t *view)
{
    unsigned long screen_width, screen_height;

    screen_width = ply_text_display_get_number_of_columns(view->display);
    screen_height = ply_text_display_get_number_of_rows(view->display);

    ply_text_display_draw_area(view->display, 0, 0, screen_width, screen_height);
}

static void redraw_views(ply_boot_splash_plugin_t *plugin)
{
    ply_list_node_t *node;

    node = ply_list_get_first_node(plugin->views);
    while (node != NULL) {
        ply_list_node_t *next_node;
        view_t *view;

        view = ply_list_node_get_data(node);
        next_node = ply_list_get_next_node(plugin->views, node);

        view_redraw(view);

        node = next_node;
    }
}

static void view_hide(view_t *view)
{
    if (view->display != NULL) {
        ply_terminal_t *terminal;

        terminal = ply_text_display_get_terminal(view->display);

        ply_text_display_set_background_color(view->display, PLY_TERMINAL_COLOR_DEFAULT);
        ply_text_display_clear_screen(view->display);
        ply_text_display_show_cursor(view->display);

        ply_terminal_reset_colors(terminal);
    }
}

static void hide_views(ply_boot_splash_plugin_t *plugin)
{
    ply_list_node_t *node;

    node = ply_list_get_first_node(plugin->views);
    while (node != NULL) {
        ply_list_node_t *next_node;
        view_t *view;

        view = ply_list_node_get_data(node);
        next_node = ply_list_get_next_node(plugin->views, node);

        view_hide(view);

        node = next_node;
    }
}

static void pause_views(ply_boot_splash_plugin_t *plugin)
{
    ply_list_node_t *node;

    node = ply_list_get_first_node(plugin->views);
    while (node != NULL) {
        ply_list_node_t *next_node;
        view_t *view;

        view = ply_list_node_get_data(node);
        next_node = ply_list_get_next_node(plugin->views, node);

        ply_text_display_pause_updates(view->display);

        node = next_node;
    }
}

static void unpause_views(ply_boot_splash_plugin_t *plugin)
{
    ply_list_node_t *node;

    node = ply_list_get_first_node(plugin->views);
    while (node != NULL) {
        ply_list_node_t *next_node;
        view_t *view;

        view = ply_list_node_get_data(node);
        next_node = ply_list_get_next_node(plugin->views, node);

        ply_text_display_unpause_updates(view->display);

        node = next_node;
    }
}

static ply_boot_splash_plugin_t *create_plugin(ply_key_file_t *key_file)
{
    char *option;

    ply_boot_splash_plugin_t *plugin;

    ply_trace("creating plugin");

    plugin = calloc(1, sizeof(ply_boot_splash_plugin_t));
    plugin->message = NULL;

    plugin->views = ply_list_new();

    /* Not a pretty API for setting defaults for your config file... */
    plugin->black = 0x000000;
    plugin->white = 0xeff0f1; // progress bar block 1
    plugin->blue = 0xeff0f1; // progress bar block 2
    plugin->brown = 0xeff0f1; // progress bar block 3

    option = ply_key_file_get_value(key_file, PLUGIN_NAME, "black");
    if (option)
        sscanf(option, "0x%x", &plugin->black);
    option = ply_key_file_get_value(key_file, PLUGIN_NAME, "white");
    if (option)
        sscanf(option, "0x%x", &plugin->white);
    option = ply_key_file_get_value(key_file, PLUGIN_NAME, "brown");
    if (option)
        sscanf(option, "0x%x", &plugin->brown);
    option = ply_key_file_get_value(key_file, PLUGIN_NAME, "blue");
    if (option)
        sscanf(option, "0x%x", &plugin->blue);

    plugin->title = ply_key_file_get_value(key_file, PLUGIN_NAME, "title");

    return plugin;
}

static void detach_from_event_loop(ply_boot_splash_plugin_t *plugin)
{
    plugin->loop = NULL;

    ply_trace("detaching from event loop");
}

static void free_views(ply_boot_splash_plugin_t *plugin)
{
    ply_list_node_t *node;

    node = ply_list_get_first_node(plugin->views);

    while (node != NULL) {
        ply_list_node_t *next_node;
        view_t *view;

        view = ply_list_node_get_data(node);
        next_node = ply_list_get_next_node(plugin->views, node);

        view_free(view);
        ply_list_remove_node(plugin->views, node);

        node = next_node;
    }

    ply_list_free(plugin->views);
    plugin->views = NULL;
}

static void destroy_plugin(ply_boot_splash_plugin_t *plugin)
{
    ply_trace("destroying plugin");

    if (plugin == NULL)
        return;

    /* It doesn't ever make sense to keep this plugin on screen
     * after exit
     */
    hide_splash_screen(plugin, plugin->loop);

    free_views(plugin);
    if (plugin->message != NULL)
        free(plugin->message);

    free(plugin);
}

static void show_message(ply_boot_splash_plugin_t *plugin)
{
    ply_list_node_t *node;

    node = ply_list_get_first_node(plugin->views);
    while (node != NULL) {
        ply_list_node_t *next_node;
        view_t *view;

        view = ply_list_node_get_data(node);
        next_node = ply_list_get_next_node(plugin->views, node);

        view_show_message(view);

        node = next_node;
    }
}

static void start_animation(ply_boot_splash_plugin_t *plugin)
{
    ply_list_node_t *node;

    assert(plugin != NULL);
    assert(plugin->loop != NULL);

    redraw_views(plugin);

    if (plugin->message != NULL)
        show_message(plugin);

    if (plugin->is_animating)
        return;

    node = ply_list_get_first_node(plugin->views);
    while (node != NULL) {
        ply_list_node_t *next_node;
        view_t *view;

        view = ply_list_node_get_data(node);
        next_node = ply_list_get_next_node(plugin->views, node);

        view_start_animation(view);

        node = next_node;
    }

    plugin->is_animating = true;
}

static void stop_animation(ply_boot_splash_plugin_t *plugin)
{
    ply_list_node_t *node;

    assert(plugin != NULL);
    assert(plugin->loop != NULL);

    if (!plugin->is_animating)
        return;

    plugin->is_animating = false;

    node = ply_list_get_first_node(plugin->views);
    while (node != NULL) {
        ply_list_node_t *next_node;
        view_t *view;

        view = ply_list_node_get_data(node);
        next_node = ply_list_get_next_node(plugin->views, node);

        breeze_text_progress_bar_hide(view->progress_bar);

        node = next_node;
    }
    redraw_views(plugin);
}

static void on_draw(view_t *view, ply_terminal_t *terminal, int x, int y, int width, int height)
{
    ply_text_display_clear_screen(view->display);
}

static void add_text_display(ply_boot_splash_plugin_t *plugin, ply_text_display_t *display)
{
    view_t *view;
    ply_terminal_t *terminal;

    view = view_new(plugin, display);

    terminal = ply_text_display_get_terminal(view->display);
    if (ply_terminal_open(terminal)) {
        ply_terminal_set_mode(terminal, PLY_TERMINAL_MODE_TEXT);
        ply_terminal_activate_vt(terminal);
    }

    ply_text_display_set_draw_handler(view->display, (ply_text_display_draw_handler_t)on_draw, view);

    ply_list_append_data(plugin->views, view);
}

static void remove_text_display(ply_boot_splash_plugin_t *plugin, ply_text_display_t *display)
{
    ply_list_node_t *node;

    node = ply_list_get_first_node(plugin->views);
    while (node != NULL) {
        view_t *view;
        ply_list_node_t *next_node;

        view = ply_list_node_get_data(node);
        next_node = ply_list_get_next_node(plugin->views, node);

        if (view->display == display) {
            ply_text_display_set_draw_handler(view->display, NULL, NULL);
            view_free(view);
            ply_list_remove_node(plugin->views, node);
            return;
        }

        node = next_node;
    }
}

static bool show_splash_screen(ply_boot_splash_plugin_t *plugin, ply_event_loop_t *loop, ply_buffer_t *boot_buffer, ply_boot_splash_mode_t mode)
{
    assert(plugin != NULL);

    plugin->loop = loop;
    plugin->mode = mode;
    ply_event_loop_watch_for_exit(loop, (ply_event_loop_exit_handler_t)detach_from_event_loop, plugin);

    ply_show_new_kernel_messages(false);
    start_animation(plugin);

    return true;
}

static void update_status(ply_boot_splash_plugin_t *plugin, const char *status)
{
    assert(plugin != NULL);

    ply_trace("status update");
}

static void on_boot_progress(ply_boot_splash_plugin_t *plugin, double duration, double percent_done)
{
    ply_list_node_t *node;
    double total_duration;

    total_duration = duration / percent_done;

    /* Fun made-up smoothing function to make the growth asymptotic:
     * fraction(time,estimate)=1-2^(-(time^1.45)/estimate) */
    percent_done = 1.0 - pow(2.0, -pow(duration, 1.45) / total_duration) * (1.0 - percent_done);

    node = ply_list_get_first_node(plugin->views);

    while (node != NULL) {
        ply_list_node_t *next_node;
        view_t *view;

        view = ply_list_node_get_data(node);
        next_node = ply_list_get_next_node(plugin->views, node);

        {
            int display_width = ply_text_display_get_number_of_columns(view->display);
            int display_height = ply_text_display_get_number_of_rows(view->display);

            ply_text_display_set_cursor_position(view->display, (display_width - 12) / 2, display_height / 2);

            ply_text_display_set_background_color(view->display, PLY_TERMINAL_COLOR_BLACK);
            ply_text_display_set_foreground_color(view->display, PLY_TERMINAL_COLOR_WHITE);
            ply_text_display_write(view->display, "%s", plugin->title);
        }

        breeze_text_progress_bar_set_percent_done(view->progress_bar, percent_done);
        breeze_text_progress_bar_draw(view->progress_bar);

        node = next_node;
    }
}

static void hide_splash_screen(ply_boot_splash_plugin_t *plugin, ply_event_loop_t *loop)
{
    assert(plugin != NULL);

    ply_trace("hiding splash screen");

    if (plugin->loop != NULL) {
        stop_animation(plugin);

        ply_event_loop_stop_watching_for_exit(plugin->loop, (ply_event_loop_exit_handler_t)detach_from_event_loop, plugin);
        detach_from_event_loop(plugin);
    }

    hide_views(plugin);
    ply_show_new_kernel_messages(true);
}

static void display_normal(ply_boot_splash_plugin_t *plugin)
{
    pause_views(plugin);
    if (plugin->state != PLY_BOOT_SPLASH_DISPLAY_NORMAL) {
        plugin->state = PLY_BOOT_SPLASH_DISPLAY_NORMAL;
        start_animation(plugin);
        redraw_views(plugin);
    }
    unpause_views(plugin);
}

static void display_message(ply_boot_splash_plugin_t *plugin, const char *message)
{
    if (plugin->message != NULL)
        free(plugin->message);

    plugin->message = strdup(message);
    start_animation(plugin);
}

static void show_password_prompt(ply_boot_splash_plugin_t *plugin, const char *prompt, int bullets)
{
    ply_list_node_t *node;
    int i;
    char *entered_text;

    entered_text = calloc(bullets + 1, sizeof(char));

    for (i = 0; i < bullets; i++)
        entered_text[i] = '*';

    node = ply_list_get_first_node(plugin->views);
    while (node != NULL) {
        ply_list_node_t *next_node;
        view_t *view;

        view = ply_list_node_get_data(node);
        next_node = ply_list_get_next_node(plugin->views, node);

        view_show_prompt(view, prompt, entered_text);

        node = next_node;
    }
    free(entered_text);
}

static void show_prompt(ply_boot_splash_plugin_t *plugin, const char *prompt, const char *text)
{
    ply_list_node_t *node;

    node = ply_list_get_first_node(plugin->views);
    while (node != NULL) {
        ply_list_node_t *next_node;
        view_t *view;

        view = ply_list_node_get_data(node);
        next_node = ply_list_get_next_node(plugin->views, node);

        view_show_prompt(view, prompt, text);

        node = next_node;
    }
}

static void display_password(ply_boot_splash_plugin_t *plugin, const char *prompt, int bullets)
{
    pause_views(plugin);
    if (plugin->state == PLY_BOOT_SPLASH_DISPLAY_NORMAL)
        stop_animation(plugin);

    plugin->state = PLY_BOOT_SPLASH_DISPLAY_PASSWORD_ENTRY;

    if (!prompt)
        prompt = "Password";

    show_password_prompt(plugin, prompt, bullets);

    unpause_views(plugin);
}

static void display_question(ply_boot_splash_plugin_t *plugin, const char *prompt, const char *entry_text)
{
    pause_views(plugin);
    if (plugin->state == PLY_BOOT_SPLASH_DISPLAY_NORMAL)
        stop_animation(plugin);

    plugin->state = PLY_BOOT_SPLASH_DISPLAY_PASSWORD_ENTRY;

    if (!prompt)
        prompt = "Password";

    show_prompt(plugin, prompt, entry_text);

    unpause_views(plugin);
}

ply_boot_splash_plugin_interface_t *ply_boot_splash_plugin_get_interface(void)
{
    static ply_boot_splash_plugin_interface_t plugin_interface = {
        .create_plugin = create_plugin,
        .destroy_plugin = destroy_plugin,
        .add_text_display = add_text_display,
        .remove_text_display = remove_text_display,
        .show_splash_screen = show_splash_screen,
        .update_status = update_status,
        .on_boot_progress = on_boot_progress,
        .hide_splash_screen = hide_splash_screen,
        .display_normal = display_normal,
        .display_message = display_message,
        .display_password = display_password,
        .display_question = display_question,
    };

    return &plugin_interface;
}

/* vim: set ts=4 sw=4 expandtab autoindent cindent cino={.5s,(0: */
