/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _// __ \_/ ___\|  |/ /| __ \ / __ \  \/  /
 *   Jukebox    |    |   ( (__) )  \___|    ( | \_\ ( (__) )    (
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2023 William Wilgus
 *
 * 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 "plugin.h"
#include "lang_enum.h"

#include "lib/printcell_helper.h"

#include "lib/configfile.h"
#define CFG_FILE "/lastfm_scrobbler_viewer.cfg"
#define TMP_FILE ""PLUGIN_DATA_DIR "/lscrobbler_viewer_%d.tmp"
#define CFG_VER  1

#ifdef ROCKBOX_HAS_LOGF
#define logf rb->logf
#else
#define logf(...) do { } while(0)
#endif

/*#ARTIST #ALBUM #TITLE #TRACKNUM #LENGTH #RATING #TIMESTAMP #MUSICBRAINZ_TRACKID*/

#define SCROBBLE_HDR_FMT "$*%d$%s$*%d$%s$*%d$%s$Track#$Length$Rating$TimeStamp$TrackId"
//#define SCROBBLE_HDR "$*128$Artist$*128$Album$*128$Title$Track#$Length$Rating$TimeStamp$TrackId"

#define SCROBBLER_MIN_COLUMNS (6) /* a valid scrobbler file should have at least this many columns */

#define GOTO_ACTION_DEFAULT_HANDLER (PLUGIN_OK + 1)
#define CANCEL_CONTEXT_MENU (PLUGIN_OK + 2)
#define QUIT_CONTEXT_MENU (PLUGIN_OK + 3)
/* global lists, for everything */
static struct gui_synclist lists;

/* printcell data for the current file */
struct printcell_data_t {
    int view_columns;
    int view_lastcol;

    int items_buffered;
    int items_total;
    int fd_cur;
    char *filename;

    char *buf;
    size_t buf_size;
    off_t buf_used;
    char header[PRINTCELL_MAXLINELEN];
    bool is_scrobbler;
};

enum e_find_type {
    FIND_ALL = 0,
    FIND_EXCLUDE,
    FIND_EXCLUDE_CASE,
    FIND_EXCLUDE_ANY,
    FIND_INCLUDE,
    FIND_INCLUDE_CASE,
    FIND_INCLUDE_ANY,
    FIND_CUSTOM,
};

static void synclist_set(int selected_item, int items, int sel_size, struct printcell_data_t *pc_data);
static int scrobbler_read_line(int fd, char* buf, size_t buf_size);
static void pc_data_set_header(struct printcell_data_t *pc_data);

static void browse_file(char *buf, size_t bufsz)
{
    struct browse_context browse = {
        .dirfilter = SHOW_ALL,
        .flags = BROWSE_SELECTONLY,
        .title = "Select a scrobbler log file",
        .icon = Icon_Playlist,
        .buf = buf,
        .bufsize = bufsz,
        .root = "/",
    };

    if (rb->rockbox_browse(&browse) != GO_TO_PREVIOUS)
    {
        buf[0] = '\0';
    }
}

static struct plugin_config
{
    bool separator;
    bool talk;
    int  col_width;
    int hidecol_flags;
} gConfig;

static struct configdata config[] =
{
   {TYPE_BOOL, 0, 1,  { .bool_p = &gConfig.separator }, "Cell Separator", NULL},
   {TYPE_BOOL, 0, 1,  { .bool_p = &gConfig.talk }, "Voice", NULL},
   {TYPE_INT, 32, LCD_WIDTH, { .int_p = &gConfig.col_width }, "Cell Width",  NULL},
   {TYPE_INT, INT_MIN, INT_MAX, { .int_p = &gConfig.hidecol_flags }, "Hidden Columns",  NULL},
};
const int gCfg_sz = sizeof(config)/sizeof(*config);
/****************** config functions *****************/
static void config_set_defaults(void)
{
    gConfig.col_width = MIN(LCD_WIDTH, 128);
    gConfig.hidecol_flags = 0;
    gConfig.separator = true;
    gConfig.talk = rb->global_settings->talk_menu;
}

static void config_save(void)
{
    configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER);
}

static int config_settings_menu(struct printcell_data_t *pc_data)
{
    int selection = 0;

    struct viewport parentvp[NB_SCREENS];
    FOR_NB_SCREENS(l)
    {
        rb->viewport_set_defaults(&parentvp[l], l);
        rb->viewport_set_fullscreen(&parentvp[l], l);
    }

    MENUITEM_STRINGLIST(settings_menu, ID2P(LANG_SETTINGS), NULL,
                        ID2P(LANG_LIST_SEPARATOR),
                        "Cell Width",
                        ID2P(LANG_VOICE),
                        ID2P(VOICE_BLANK),
                        ID2P(VOICE_BLANK),
                        ID2P(LANG_CANCEL_0),
                        ID2P(LANG_SAVE_EXIT));

    do {
        selection=rb->do_menu(&settings_menu,&selection, parentvp, true);
        switch(selection) {

            case 0:
                rb->set_bool(rb->str(LANG_LIST_SEPARATOR), &gConfig.separator);
                break;
            case 1:
                rb->set_int("Cell Width", "", UNIT_INT,
                            &gConfig.col_width, NULL, 1, 32, LCD_WIDTH, NULL );
                break;
            case 2:
                rb->set_bool(rb->str(LANG_VOICE), &gConfig.talk);
                break;
            case 3:
                continue;
            case 4: /*sep*/
                continue;
            case 5:
                return -1;
                break;
            case 6:
            {
                pc_data_set_header(pc_data);
                synclist_set(0, pc_data->items_total, 1, pc_data);
                int res = configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER);
                if (res >= 0)
                {
                    logf("Cfg saved %s %d bytes", CFG_FILE, gCfg_sz);
                    return PLUGIN_OK;
                }
                logf("Cfg FAILED (%d) %s", res, CFG_FILE);
                return PLUGIN_ERROR;
            }
            case MENU_ATTACHED_USB:
                return PLUGIN_USB_CONNECTED;
            default:
                return PLUGIN_OK;
        }
    } while ( selection >= 0 );
    return 0;
}

/* open file pass back file descriptor and return file size */
static size_t file_open(const char *filename, int *fd)
{
    size_t fsize = 0;

    if (filename && fd)
    {
        *fd = rb->open(filename, O_RDONLY);
        if (*fd >= 0)
        {
            fsize = rb->filesize(*fd);
        }
    }
    return fsize;
}

static const char* list_get_name_cb(int selected_item, void* data,
                                    char* buf, size_t buf_len)
{
    const int slush_pos = 32; /* entries around the current entry to keep buffered */
    /* keep track of the last position in the buffer */
    static int buf_line_num = 0;
    static off_t buf_last_pos = 0;
    /* keep track of the last position in the file */
    static int file_line_num = 0;
    static off_t file_last_seek = 0;

    if (selected_item < 0)
    {
        logf("[%s] Reset positions", __func__);
        buf_line_num = 0;
        buf_last_pos = 0;
        file_line_num = 0;
        file_last_seek = 0;
        return "";
    }

    int line_num;
    struct printcell_data_t *pc_data = (struct printcell_data_t*) data;
    bool found = false;

    if (pc_data->buf && selected_item < pc_data->items_buffered)
    {
        int buf_pos;

        if (buf_line_num > selected_item || buf_last_pos >= pc_data->buf_used
         || buf_line_num < 0)
        {
            logf("[%s] Set pos buffer 0", __func__);
            buf_line_num = 0;
            buf_last_pos = 0;
        }
        buf_pos = buf_last_pos;
        line_num = buf_line_num;

        while (buf_pos < pc_data->buf_used
            && line_num < pc_data->items_buffered)
        {
            size_t len = rb->strlen(&pc_data->buf[buf_pos]);
            if(line_num == selected_item)
            {
                rb->strlcpy(buf, &pc_data->buf[buf_pos], MIN(buf_len, len));
                logf("(%d) in buffer: %s", line_num, buf);
                found = true;
                break;
            }
            else
            {
                buf_pos += len + 1; /* need to go past the NULL terminator */
                line_num++;

                if (buf_line_num + slush_pos < selected_item)
                {
                    logf("[%s] Set pos buffer %d", __func__, line_num);
                    buf_line_num = line_num;
                    buf_last_pos = buf_pos;
                }
            }
        }
    }

    /* didn't find the item try the file */
    if(!found && pc_data->fd_cur >= 0)
    {
        int fd = pc_data->fd_cur;

        if (file_line_num < 0 || file_line_num > selected_item)
        {
            logf("[%s] Set seek file 0", __func__);
            file_line_num = 0;
            file_last_seek = 0;
        }

        rb->lseek(fd, file_last_seek, SEEK_SET);
        line_num = file_line_num;

        while ((scrobbler_read_line(fd, buf, buf_len)) > 0)
        {
            if(buf[0] == '#')
                continue;
            if(line_num == selected_item)
            {
                logf("(%d) in file: %s", line_num, buf);
                found = true;
                break;
            }
            else
            {
                line_num++;

                if (file_line_num + slush_pos < selected_item)
                {
                    logf("[%s] Set seek file %d", __func__, line_num);
                    file_line_num = line_num;
                    file_last_seek = rb->lseek(fd, 0, SEEK_CUR);
                }
            }
        }
    }

    if(!found)
    {
        logf("(%d) Not Found!", selected_item);
        buf_line_num = -1;
        file_line_num = -1;
        buf[0] = '\0';
    }
    return buf;
}

static int list_voice_cb(int list_index, void* data)
{
    (void) list_index;
    struct printcell_data_t *pc_data = (struct printcell_data_t*) data;
    if (!gConfig.talk)
        return -1;

    int selcol = printcell_get_column_selected();
    char buf[MAX_PATH];
    char* name;
    long id;

    if (pc_data->view_lastcol != selcol)
    {
        name = printcell_get_title_text(selcol, buf, sizeof(buf));

        id = P2ID((const unsigned char *)name);

        if(id>=0)
            rb->talk_id(id, true);
        else if (selcol >= 0)
        {
            switch (selcol)
            {
                case 0:
                    rb->talk_id(LANG_ID3_ARTIST, true);
                    break;
                case 1:
                    rb->talk_id(LANG_ID3_ALBUM, true);
                    break;
                case 2:
                    rb->talk_id(LANG_ID3_TITLE, true);
                    break;
                case 3:
                    rb->talk_id(LANG_ID3_TRACKNUM, true);
                    break;
                case 4:
                    rb->talk_id(LANG_ID3_LENGTH, true);
                    break;

                default:
                    rb->talk_spell(name, true);
                    break;
            }
        }
        else
            rb->talk_id(LANG_ALL, true);

        rb->talk_id(VOICE_PAUSE, true);
    }

    name = printcell_get_column_text(selcol, buf, sizeof(buf));

    id = P2ID((const unsigned char *)name);

    if(id>=0)
        rb->talk_id(id, true);
    else if (selcol >= 0)
        rb->talk_spell(name, true);

    return 0;
}

static enum themable_icons list_icon_cb(int selected_item, void *data)
{
    struct printcell_data_t *pc_data = (struct printcell_data_t*) data;
    if (lists.selected_item == selected_item)
    {
        if (pc_data->view_lastcol < 0)
            return Icon_Config;
        return Icon_Audio;
    }
    return Icon_NOICON;
}

static int scrobbler_read_line(int fd, char* buf, size_t buf_size)
{
    int count = 0;
    unsigned int pos = 0;
    char sep = '\t';
    char ch, last_ch = sep;
    bool comment = false;

    while (rb->read(fd, &ch, 1) > 0)
    {
        if (ch == sep && last_ch == sep && buf_size > pos)
            buf[pos++] = ' ';

        if (count++ == 0 && ch == '#') /* skip comments */
            comment = true;
        else if (!comment && ch != '\r' && buf_size > pos)
            buf[pos++] = ch;

        last_ch = ch;

        if (pos > PRINTCELL_MAXLINELEN * 2)
            break;

        if (ch == '\n')
        {
            if (!comment)
            {
                buf[pos] = '\0';
                if (buf_size > pos)
                return count;
            }
            last_ch = sep;
            comment = false;
            count = 0;
        }
    }
    return count;
}

/* load file entries into pc_data buffer, file should already be opened
 * and will be closed if the whole file was buffered */
static int file_load_entries(struct printcell_data_t *pc_data)
{
    int items = 0;
    int count = 0;
    int buffered = 0;
    unsigned int pos = 0;
    bool comment = false;
    char sep = '\t';
    char ch, last_ch = sep;
    int fd = pc_data->fd_cur;
    if (fd < 0)
        return 0;
    size_t buf_size = pc_data->buf_size - 1;

    rb->lseek(fd, 0, SEEK_SET);

    while (rb->read(fd, &ch, 1) > 0)
    {
        if (ch == sep && last_ch == sep && buf_size > pos)
            pc_data->buf[pos++] = ' ';

        if (count++ == 0 && ch == '#') /* skip comments */
            comment = true;
        else if (!comment && ch != '\r' && buf_size > pos)
            pc_data->buf[pos++] = ch;

        if (items == 0 && pos > PRINTCELL_MAXLINELEN * 2)
            break;

        if (ch == '\n')
        {
            if (!comment)
            {
                pc_data->buf[pos] = '\0';
                if (buf_size > pos)
                {
                    pos++;
                    buffered++;
                }
                items++;
            }
            last_ch = sep;
            comment = false;
            count = 0;
            rb->yield();
        }
    }

    logf("[%s] items: %d buffered: %d", __func__, items, buffered);

    pc_data->items_total = items;
    pc_data->items_buffered = buffered;
    pc_data->buf[pos] = '\0';
    pc_data->buf_used = pos;

    if (items == buffered) /* whole file fit into buffer; close file */
    {
        rb->close(pc_data->fd_cur);
        pc_data->fd_cur = -1;
    }

    list_get_name_cb(-1, NULL, NULL, 0); /* prime name cb */
    return items;
}

static int filter_items(struct printcell_data_t *pc_data,
                        enum e_find_type find_type, int col)
{
    /* saves filtered items to a temp file and loads it */
    int fd;
    bool reload = false;
    char buf[PRINTCELL_MAXLINELEN];
    char find_exclude_buf[PRINTCELL_MAXLINELEN];
    int selcol = printcell_get_column_selected();
    char *find_exclude = printcell_get_column_text(selcol, find_exclude_buf,
                                                      sizeof(find_exclude_buf));
    const char colsep = '\t';
    int find_len = rb->strlen(find_exclude);

    if (find_type == FIND_CUSTOM || find_len == 0)
    {
        int option = 0;
        struct opt_items find_types[] = {
            {"Exclude", -1}, {"Exclude Case Sensitive", -1},
            {"Exclude Any", -1}, {"Include", -1},
            {"Include Case Sensitive", -1}, {"Include Any", -1}
        };
        if (rb->set_option("Find Type", &option, RB_INT,
                           find_types, 6, NULL))
        {
            return 0;
        }
        switch (option)
        {
            case 0:
                find_type = FIND_EXCLUDE;
                break;
            case 1:
                find_type = FIND_EXCLUDE_CASE;
                break;
            case 2:
                find_type = FIND_EXCLUDE_ANY;
                break;
            case 3:
                find_type = FIND_INCLUDE;
                break;
            case 4:
                find_type = FIND_INCLUDE_CASE;
                break;
            case 5:
                find_type = FIND_INCLUDE_ANY;
                break;
            default:
                find_type = FIND_ALL;
                break;
        }

        /* copy data to beginning of buf */
        rb->memmove(find_exclude_buf, find_exclude, find_len);
        find_exclude_buf[find_len] = '\0';

        if (rb->kbd_input(find_exclude_buf, sizeof(find_exclude_buf), NULL) < 0)
            return -1;
        find_exclude = find_exclude_buf;
        find_len = rb->strlen(find_exclude);
    }

    char tmp_filename[MAX_PATH];
    static int tmp_num = 0;
    rb->snprintf(tmp_filename, sizeof(tmp_filename), TMP_FILE, tmp_num);
    tmp_num++;

    fd = rb->open(tmp_filename, O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(fd >= 0)
    {
        rb->splash_progress_set_delay(HZ * 5);
        for (int i = 0; i < pc_data->items_total; i++)
        {
            rb->splash_progress(i, pc_data->items_total, "Filtering...");
            const char *  data = list_get_name_cb(i, pc_data, buf, sizeof(buf));

            if (find_type != FIND_ALL)
            {
                int index = col;

                if (index < 0)
                    index = 0;
                if (find_len > 0)
                {
                    char *bcol = buf;
                    while (*bcol != '\0' && index > 0)
                    {
                        if (*bcol == colsep)
                            index--;
                        bcol++;
                    }
                    if (index > 0)
                        continue;

                    if (find_type == FIND_EXCLUDE)
                    {
                        if (rb->strncasecmp(bcol, find_exclude, find_len) == 0)
                        {
                            logf("[%s] exclude [%s]", find_exclude, bcol);
                            continue;
                        }
                    }
                    else if (find_type == FIND_INCLUDE)
                    {
                        if (rb->strncasecmp(bcol, find_exclude, find_len) != 0)
                        {
                            logf("%s include %s", find_exclude, bcol);
                            continue;
                        }
                    }
                    else if (find_type == FIND_EXCLUDE_CASE)
                    {
                        if (rb->strncmp(bcol, find_exclude, find_len) == 0)
                        {
                            logf("[%s] exclude case [%s]", find_exclude, bcol);
                            continue;
                        }
                    }
                    else if (find_type == FIND_INCLUDE_CASE)
                    {
                        if (rb->strncmp(bcol, find_exclude, find_len) != 0)
                        {
                            logf("%s include case %s", find_exclude, bcol);
                            continue;
                        }
                    }
                    else if (find_type == FIND_EXCLUDE_ANY)
                    {
                        bool found = false;
                        while (*bcol != '\0' && *bcol != colsep)
                        {
                            if (rb->strncasecmp(bcol, find_exclude, find_len) == 0)
                            {
                                logf("[%s] exclude any [%s]", find_exclude, bcol);
                                found = true;
                                break;
                            }
                            bcol++;
                        }
                        if (found)
                            continue;
                    }
                    else if (find_type == FIND_INCLUDE_ANY)
                    {
                        bool found = false;
                        while (*bcol != '\0' && *bcol != colsep)
                        {
                            if (rb->strncasecmp(bcol, find_exclude, find_len) == 0)
                            {
                                found = true;
                                logf("[%s] include any [%s]", find_exclude, bcol);
                                break;
                            }
                            bcol++;
                        }
                        if (!found)
                            continue;
                    }
                }
            }
            int len = rb->strlen(data);
            if (len > 0)
            {
                logf("writing [%d bytes][%s]", len + 1, data);
                rb->write(fd, data, len);
                rb->write(fd, "\n", 1);
            }
        }
        reload = true;
    }

    if (reload)
    {
        if (pc_data->fd_cur >= 0)
            rb->close(pc_data->fd_cur);

        pc_data->fd_cur = fd;
        int items = file_load_entries(pc_data);
        if (items >= 0)
        {
            lists.selected_item = 0;
            rb->gui_synclist_set_nb_items(&lists, items);
        }
    }
    logf("Col text '%s'", find_exclude);
    logf("text '%s'", find_exclude_buf);

   return pc_data->items_total;
}

static int scrobbler_context_menu(struct printcell_data_t *pc_data)
{
    struct viewport parentvp[NB_SCREENS];
    FOR_NB_SCREENS(l)
    {
        rb->viewport_set_defaults(&parentvp[l], l);
        rb->viewport_set_fullscreen(&parentvp[l], l);
    }

    int col = printcell_get_column_selected();
    int selection = 0;
    int items = 0;
    uint32_t visible = printcell_get_column_visibility(-1);
    bool hide_col = PRINTCELL_COLUMN_IS_VISIBLE(visible, col);

    char namebuf[PRINTCELL_MAXLINELEN];
    enum e_find_type find_type = FIND_ALL;

    char *colname = pc_data->filename;
    if (col >= 0)
        colname = printcell_get_title_text(col, namebuf, sizeof(namebuf));

#define MENUITEM_STRINGLIST_CUSTOM(name, str, callback, ... )           \
    const char *name##_[] = {__VA_ARGS__};                                    \
    const struct menu_callback_with_desc name##__ =              \
            {callback,str, Icon_NOICON};                                \
    const struct menu_item_ex name =                                    \
        {MT_RETURN_ID|MENU_HAS_DESC|                                    \
         MENU_ITEM_COUNT(sizeof( name##_)/sizeof(*name##_)),            \
            { .strings = name##_},{.callback_and_desc = & name##__}};

    const char *menu_item[4];

    menu_item[0]= hide_col ? "Hide":"Show";
    menu_item[1]= "Exclude";
    menu_item[2]= "Include";
    menu_item[3]= "Custom Filter";

    if (!pc_data->is_scrobbler)
        col = -1;

    if (col == -1)
    {
        menu_item[0]= hide_col ? "Hide All":"Show All";
        menu_item[1]= "Open";
        menu_item[2]= "Reload";
        menu_item[3]= ID2P(LANG_SETTINGS);
    }

    MENUITEM_STRINGLIST_CUSTOM(context_menu, colname, NULL,
                               menu_item[0],
                               menu_item[1],
                               menu_item[2],
                               menu_item[3],
                               ID2P(VOICE_BLANK),
                               ID2P(LANG_CANCEL_0),
                               ID2P(LANG_MENU_QUIT));

#undef MENUITEM_STRINGLIST_CUSTOM
    do {
        selection=rb->do_menu(&context_menu,&selection, parentvp, true);
        switch(selection) {

            case 0:
            {
                printcell_set_column_visible(col, !hide_col);
                if (hide_col)
                {
                    do
                    {
                        col = printcell_increment_column(1, true);
                    } while (col >= 0 && printcell_get_column_visibility(col) == 1);
                    pc_data->view_lastcol = col;
                }
                gConfig.hidecol_flags = printcell_get_column_visibility(-1);
                break;
            }
            case 1: /* Exclude / Open */
            {
                if (col == -1)/*Open*/
                {
                    char buf[MAX_PATH];
                    browse_file(buf, sizeof(buf));
                    if (rb->file_exists(buf))
                    {
                        rb->strlcpy(pc_data->filename, buf, MAX_PATH);
                        if (pc_data->fd_cur >= 0)
                            rb->close(pc_data->fd_cur);
                        if (file_open(pc_data->filename, &pc_data->fd_cur) > 0)
                            items = file_load_entries(pc_data);
                        if (items >= 0)
                            synclist_set(0, items, 1, pc_data);
                    }
                    else
                        rb->splash(HZ *2, "Error Opening");

                    return CANCEL_CONTEXT_MENU;
                }

                find_type = FIND_EXCLUDE;
            }
            /* fall-through */
            case 2: /* Include / Reload */
            {
                if (col == -1) /*Reload*/
                {
                    if (pc_data->fd_cur >= 0)
                        rb->close(pc_data->fd_cur);
                    if (file_open(pc_data->filename, &pc_data->fd_cur) > 0)
                        items = file_load_entries(pc_data);
                    if (items >= 0)
                        rb->gui_synclist_set_nb_items(&lists, items);
                    return CANCEL_CONTEXT_MENU;
                }

                if (find_type == FIND_ALL)
                    find_type = FIND_INCLUDE;
            } /* fall-through */
            case 3: /*Custom Filter / Settings */
            {
                if (col == -1)/*Settings*/
                    return config_settings_menu(pc_data);

                if (find_type == FIND_ALL)
                    find_type = FIND_CUSTOM;
                items = filter_items(pc_data, find_type, col);

                if (items >= 0)
                    rb->gui_synclist_set_nb_items(&lists, items);
                break;
            }
            case 4: /*sep*/
                continue;
            case 5:
                return CANCEL_CONTEXT_MENU;
                break;
            case 6: /* Quit */
                return QUIT_CONTEXT_MENU;
                break;
            case MENU_ATTACHED_USB:
                return PLUGIN_USB_CONNECTED;
            default:
                return PLUGIN_OK;
        }
    } while ( selection < 0 );
    return 0;
}

static void cleanup(void *parameter)
{
    struct printcell_data_t *pc_data = (struct printcell_data_t*) parameter;
    if (pc_data->fd_cur >= 0)
        rb->close(pc_data->fd_cur);
    pc_data->fd_cur = -1;
}

static void menu_action_printcell(int *action, int selected_item, bool* exit, struct gui_synclist *lists)
{
    (void) exit;
    struct printcell_data_t *pc_data = (struct printcell_data_t*) lists->data;
    if (*action == ACTION_STD_OK)
    {
        if (selected_item < lists->nb_items)
        {
            pc_data->view_lastcol = printcell_increment_column(1, true);
            *action = ACTION_NONE;
        }
    }
    else if (*action == ACTION_STD_CANCEL)
    {
        pc_data->view_lastcol = printcell_increment_column(-1, true);
        if (pc_data->view_lastcol != pc_data->view_columns - 1)
        {
            *action = ACTION_NONE;
        }
    }
    else if (*action == ACTION_STD_CONTEXT)
    {
        int ctxret = scrobbler_context_menu(pc_data);
        if (ctxret == QUIT_CONTEXT_MENU)
            *exit = true;
    }
}

int menu_action_cb(int *action, int selected_item, bool* exit, struct gui_synclist *lists)
{

    menu_action_printcell(action, selected_item, exit, lists);

    if (rb->default_event_handler_ex(*action, cleanup, lists->data) == SYS_USB_CONNECTED)
    {
        *exit = true;
        return PLUGIN_USB_CONNECTED;
    }
    return PLUGIN_OK;
}

static int count_max_columns(int items, char delimeter,
                             int expected_cols, struct printcell_data_t *pc_data)
{
    int max_cols = 0;
    int cols = 0;
    char buf[PRINTCELL_MAXLINELEN];
    for (int i = 0; i < items; i++)
    {
        const char *txt = list_get_name_cb(i, pc_data, buf, sizeof(buf));
        while (*txt != '\0')
        {
            if (*txt == delimeter)
            {
                cols++;
                if (cols == expected_cols)
                {
                    max_cols = cols;
                    break;
                }
            }
            txt++;
        }

        if(max_cols < expected_cols && i > 32)
            break;

        if (cols > max_cols)
            max_cols = cols;
        cols = 0;
    }
    return max_cols;
}

static void synclist_set(int selected_item, int items, int sel_size, struct printcell_data_t *pc_data)
{
    if (items <= 0)
        return;
    if (selected_item < 0)
        selected_item = 0;

    rb->gui_synclist_init(&lists,list_get_name_cb,
                          pc_data, false, sel_size, NULL);

    rb->gui_synclist_set_icon_callback(&lists, list_icon_cb);
    rb->gui_synclist_set_voice_callback(&lists, list_voice_cb);
    rb->gui_synclist_set_nb_items(&lists,items);
    rb->gui_synclist_select_item(&lists, selected_item);

    struct printcell_settings pcs = {.cell_separator = gConfig.separator,
                                     .title_delimeter = '$',
                                     .text_delimeter = '\t',
                                     .hidecol_flags = gConfig.hidecol_flags};

    pc_data->view_columns = printcell_set_columns(&lists, &pcs,
                                                  pc_data->header, Icon_Rockbox);
    printcell_enable(true);

    int max_cols = count_max_columns(items, pcs.text_delimeter,
                                     SCROBBLER_MIN_COLUMNS, pc_data);
    if (max_cols < SCROBBLER_MIN_COLUMNS) /* not a scrobbler file? */
    {
        pc_data->is_scrobbler = false;
        /*check for a playlist_control file or a playback log*/

        max_cols = count_max_columns(items, ':', 3, pc_data);

        if (max_cols >= 3)
        {
            char headerbuf[32];
            int w = gConfig.col_width;
            rb->snprintf(headerbuf, sizeof(headerbuf),
                     "$*%d$$*%d$$*%d$$*%d$", w, w, w, w);


            struct printcell_settings pcs = {.cell_separator = gConfig.separator,
                                            .title_delimeter = '$',
                                            .text_delimeter = ':',
                                            .hidecol_flags = gConfig.hidecol_flags};
            rb->gui_synclist_set_voice_callback(&lists, NULL);
            pc_data->view_columns = printcell_set_columns(&lists, &pcs,
                                                  headerbuf, Icon_Rockbox);
        }
        else
        {
            rb->gui_synclist_set_voice_callback(&lists, NULL);
            pc_data->view_columns = printcell_set_columns(&lists, NULL,
                                                        "$*512$", Icon_Questionmark);
        }
    }

    int curcol = printcell_get_column_selected();
    while (curcol >= 0)
        curcol = printcell_increment_column(-1, false);

    if (pc_data->view_lastcol >= pc_data->view_columns)
        pc_data->view_lastcol = -1;

    /* restore column position */
    while (pc_data->view_lastcol > -1 && curcol != pc_data->view_lastcol)
    {
        curcol = printcell_increment_column(1, true);
    }
    pc_data->view_lastcol = curcol;
    list_voice_cb(0, pc_data);
}

static void pc_data_set_header(struct printcell_data_t *pc_data)
{
    int col_w = gConfig.col_width;
    rb->snprintf(pc_data->header, PRINTCELL_MAXLINELEN,
                 SCROBBLE_HDR_FMT, col_w, rb->str(LANG_ID3_ARTIST),
                                   col_w, rb->str(LANG_ID3_ALBUM),
                                   col_w, rb->str(LANG_ID3_TITLE));
}

enum plugin_status plugin_start(const void* parameter)
{
    int ret = PLUGIN_OK;
    int selected_item = -1;
    int action;
    int items = 0;
#if CONFIG_RTC
    static char filename[MAX_PATH] = HOME_DIR "/.scrobbler.log";
#else /* !CONFIG_RTC */
    static char filename[MAX_PATH] = HOME_DIR "/.scrobbler-timeless.log";
#endif /* CONFIG_RTC */
    bool redraw = true;
    bool exit = false;

    static struct printcell_data_t printcell_data;

    if (parameter)
    {
        rb->strlcpy(filename, (const char*)parameter, sizeof(filename));
        filename[MAX_PATH - 1] = '\0';
        parameter = NULL;
    }

    if (!rb->file_exists(filename))
    {
        if (rb->strcmp(filename, "-scrobbler_view_pbl") == 0)
        {
            parameter = PLUGIN_APPS_DIR "/lastfm_scrobbler.rock";
            rb->strlcpy(filename, ROCKBOX_DIR "/playback.log", MAX_PATH);
            if (!rb->file_exists(filename))
            {
                rb->splashf(HZ * 2, "Viewer: NO log! %s", filename);
                return rb->plugin_open(parameter, "-resume");
            }
        }
        else
        {
            browse_file(filename, sizeof(filename));
            if (!rb->file_exists(filename))
            {
                rb->splash(HZ, "No file Goodbye.");
                return PLUGIN_ERROR;
            }
        }
    }

    config_set_defaults();
    if (configfile_load(CFG_FILE, config, gCfg_sz, CFG_VER) < 0)
    {
        /* If the loading failed, save a new config file */
        config_set_defaults();
        configfile_save(CFG_FILE, config, gCfg_sz, CFG_VER);

        rb->splash(HZ, ID2P(LANG_REVERT_TO_DEFAULT_SETTINGS));
    }

    rb->memset(&printcell_data, 0, sizeof (struct printcell_data_t));
    printcell_data.fd_cur = -1;
    printcell_data.view_lastcol = -1;
    printcell_data.is_scrobbler = true;

    if (rb->file_exists(filename))
    {
        if (file_open(filename, &printcell_data.fd_cur) == 0)
            printcell_data.fd_cur = -1;
        else
        {
            size_t buf_size;
            printcell_data.buf = rb->plugin_get_buffer(&buf_size);
            printcell_data.buf_size = buf_size;
            printcell_data.buf_used = 0;
            printcell_data.filename = filename;
            items = file_load_entries(&printcell_data);
        }
    }
    int col_w = gConfig.col_width;
    rb->snprintf(printcell_data.header, PRINTCELL_MAXLINELEN,
                 SCROBBLE_HDR_FMT, col_w, rb->str(LANG_ID3_ARTIST),
                                   col_w, rb->str(LANG_ID3_ALBUM),
                                   col_w, rb->str(LANG_ID3_TITLE));

    if (!exit && items > 0)
    {
        synclist_set(0, items, 1, &printcell_data);
        rb->gui_synclist_draw(&lists);

        while (!exit)
        {
            action = rb->get_action(CONTEXT_LIST, HZ / 10);

            if (action == ACTION_NONE)
            {
                if (redraw)
                {
                    action = ACTION_REDRAW;
                    redraw = false;
                }
            }
            else
                redraw = true;

            ret = menu_action_cb(&action, selected_item, &exit, &lists);
            if (rb->gui_synclist_do_button(&lists, &action))
                continue;
            selected_item = rb->gui_synclist_get_sel_pos(&lists);

        }
    }
    config_save();
    cleanup(&printcell_data);
    if (parameter)
    {
        return rb->plugin_open((const char*)parameter, "-resume");
    }
    return ret;
}
