/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2008-2009 Teruaki Kawashima
 *
 * 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 "lib/playback_control.h"
#include "lib/configfile.h"
#include "lib/helper.h"
#include <ctype.h>



#define MAX_LINE_LEN    256
#define LRC_BUFFER_SIZE 0x3000 /* 12 kiB */
#if PLUGIN_BUFFER_SIZE >= 0x10000 /* no id3 support for low mem targets */
/* define this to read lyrics in id3 tag */
#define LRC_SUPPORT_ID3
#endif
/* define this to show debug info in menu */
/* #define LRC_DEBUG */

enum lrc_screen {
    PLUGIN_OTHER = 0x200,
    LRC_GOTO_MAIN,
    LRC_GOTO_MENU,
    LRC_GOTO_EDITOR,
};

struct lrc_word {
    long time_start;
    short count;
    short width;
    unsigned char *word;
};

struct lrc_brpos {
    short count;
    short width;
};

struct lrc_line {
    long time_start;
    long old_time_start;
    off_t file_offset; /* offset of time tag in file */
    short nword;
    short width;
    short nline[NB_SCREENS];
    struct lrc_line *next;
    struct lrc_word *words;
};

struct preferences {
    /* display settings */
#if LCD_DEPTH > 1
    unsigned active_color;
    unsigned inactive_color;
#endif
    bool wrap;
    bool wipe;
    bool active_one_line;
    bool autoscroll;
    int  align; /* 0: left, 1: center, 2: right */
    bool statusbar_on;
    bool display_title;
    bool display_time;
    bool backlight_on;

    /* file settings */
    char lrc_directory[64];
    int encoding;
#ifdef LRC_SUPPORT_ID3
    bool read_id3;
#endif
};

static struct preferences prefs, old_prefs;
static unsigned char *lrc_buffer;
static size_t lrc_buffer_size;
static size_t lrc_buffer_used, lrc_buffer_end;
enum extention_types {LRC, LRC8, SNC, TXT, NUM_TYPES, ID3_SYLT, ID3_USLT};
static const char *extentions[NUM_TYPES] = {
    ".lrc", ".lrc8", ".snc", ".txt",
};
static struct lrc_info {
    struct mp3entry *id3;
    long elapsed;
    long scroll;
    long length;
    long ff_rewind;
    int  audio_status;
    char mp3_file[MAX_PATH];
    char lrc_file[MAX_PATH];
    char *title;  /* use lrc_buffer */
    char *artist; /* use lrc_buffer */
    enum extention_types type;
    long offset; /* msec */
    off_t offset_file_offset; /* offset of offset tag in file */
    int  nlrcbrpos;
    int  nlrcline;
    struct lrc_line *ll_head, **ll_tail;
    bool found_lrc;
    bool loaded_lrc;
    bool changed_lrc;
    bool too_many_lines; /* true if nlrcline >= max_lrclines after calc pos */
    bool wipe; /* false if lyrics is unsynched */
} current;
static char temp_buf[MAX(MAX_LINE_LEN,MAX_PATH)];
static int uifont = -1;
static int font_ui_height = 1;
static struct viewport vp_info[NB_SCREENS];
static struct viewport vp_lyrics[NB_SCREENS];

#define AUDIO_PAUSE (current.audio_status & AUDIO_STATUS_PAUSE)
#define AUDIO_PLAY  (current.audio_status & AUDIO_STATUS_PLAY)
#define AUDIO_STOP  (!(current.audio_status & AUDIO_STATUS_PLAY))

/*******************************
 * lrc_set_time
 *******************************/
#define LST_SET_MSEC    0x00010000
#define LST_SET_SEC     0x00020000
#define LST_SET_MIN     0x00040000
#define LST_SET_HOUR    0x00080000

#include "lib/pluginlib_actions.h"
#define LST_SET_TIME    (LST_SET_MSEC|LST_SET_SEC|LST_SET_MIN|LST_SET_HOUR)
#define LST_OFF_Y 1
static int lrc_set_time(const char *title, const char *unit, long *pval,
                 int step, int min, int max, int flags)
{
    const struct button_mapping *lst_contexts[] = {
        pla_main_ctx,
#ifdef HAVE_REMOTE_LCD
        pla_remote_ctx,
#endif
    };
    /* how many */
    const unsigned char formats[4][8] = {"%03ld.", "%02ld.", "%02ld:", "%02ld:"};
    const unsigned int maxs[4] = {1000, 60, 60, 24};
    const unsigned int scls[4] = {1, 1000, 60*1000, 60*60*1000};
    char buffer[32];
    long value = *pval, scl_step = step, i = 0;
    int pos = 0, last_pos = 0, pos_min = 3, pos_max = 0;
    int x = 0, y = 0, p_start = 0, p_end = 0;
    int ret = 10;

    if (!(flags&LST_SET_TIME))
        return -1;

    for (i = 0; i < 4; i++)
    {
        if (flags&(LST_SET_MSEC<<i))
        {
            if (pos_min > i) pos_min = i;
            if (pos_max < i) pos_max = i;
        }
    }
    pos = pos_min;

    rb->button_clear_queue();
    rb->lcd_clear_display();
    rb->lcd_puts_scroll(0, LST_OFF_Y, title);
    while (ret == 10)
    {
        int len = 0;
        long abs_val = value;
        long segvals[4] = {-1, -1, -1, -1};
        /* show negative value like -00:01 but 00:-1 */
        if (value < 0)
        {
            buffer[len++] = '-';
            abs_val = -value;
        }
        buffer[len] = 0;
        /* calc value of each segments */
        for (i = pos_min; i <= pos_max; i++)
        {
            segvals[i] = abs_val % maxs[i];
            abs_val /= maxs[i];
        }
        segvals[i-1] += abs_val * maxs[i-1];
        for (i = pos_max; i >= pos_min; i--)
        {
            if (pos == i)
            {
                rb->lcd_getstringsize(buffer, &x, &y);
                p_start = len;
            }
            rb->snprintf(&buffer[len], 32-len, formats[i], segvals[i]);
            len += rb->strlen(&buffer[len]);
            if (pos == i)
                p_end = len;
        }
        buffer[len-1] = 0; /* remove last separater */
        if (unit != NULL)
        {
            rb->snprintf(&buffer[len], 32-len, " (%s)", unit);
        }
        rb->lcd_puts(0, LST_OFF_Y+1, buffer);
        if (pos_min != pos_max)
        {
            /* draw cursor */
            buffer[p_end-1] = 0;
            rb->lcd_set_drawmode(DRMODE_SOLID|DRMODE_INVERSEVID);
            rb->lcd_putsxy(x, y*(1+LST_OFF_Y), &buffer[p_start]);
            rb->lcd_set_drawmode(DRMODE_SOLID);
        }
        rb->lcd_update();
        int button = pluginlib_getaction(TIMEOUT_BLOCK, lst_contexts, ARRAYLEN(lst_contexts));
        int mult = 1;
        switch (button)
        {
            case PLA_UP_REPEAT:
            case PLA_DOWN_REPEAT:
                mult *= 10;
                /* fallthrough */
            case PLA_DOWN:
            case PLA_UP:
                if (button == PLA_DOWN_REPEAT || button == PLA_DOWN)
                    mult *= -1;
                if (pos != last_pos)
                {
                    scl_step = ((scls[pos]/scls[pos_min]+step-1)/step) * step;
                    last_pos = pos;
                }
                value += scl_step * mult;
                if (value > max)
                    value = max;
                if (value < min)
                    value = min;
                break;
            case PLA_LEFT:
            case PLA_LEFT_REPEAT:
                if (++pos > pos_max)
                    pos = pos_min;
                break;
            case PLA_RIGHT:
            case PLA_RIGHT_REPEAT:
                if (--pos < pos_min)
                    pos = pos_max;
                break;
            case PLA_SELECT:
                *pval = value;
                ret = 0;
                break;
            case PLA_CANCEL:
            case PLA_EXIT:
                rb->splash(HZ, "Cancelled");
                ret = -1;
                break;
            default:
                if (rb->default_event_handler(button) == SYS_USB_CONNECTED)
                    ret = 1;
                break;
        }
    }
    rb->lcd_clear_display();
    rb->lcd_update();
    return ret;
}

/*******************************
 * misc stuff
 *******************************/
static void reset_current_data(void)
{
    current.title = NULL;
    current.artist = NULL;
    current.offset = 0;
    current.offset_file_offset = -1;
    current.nlrcbrpos = 0;
    current.nlrcline = 0;
    current.ll_head = NULL;
    current.ll_tail = &current.ll_head;
    current.loaded_lrc = false;
    current.changed_lrc = false;
    current.too_many_lines = false;
    lrc_buffer_used = 0;
    lrc_buffer_end = lrc_buffer_size;
}

/* check space and add str to lrc_buffer.
 * return NULL if there is not enough buffer. */
static char *lrcbufadd(const char*str, bool join)
{
    if (join) lrc_buffer_used--;
    size_t siz = rb->strlen(str)+1;
    char *pos = &lrc_buffer[lrc_buffer_used];
    if (lrc_buffer_used + siz > lrc_buffer_end)
        return NULL;
    rb->strcpy(pos, str);
    lrc_buffer_used += siz;
    return pos;
}
static void *alloc_buf(size_t siz)
{
    siz = (siz+3) & ~3;
    if (lrc_buffer_used + siz > lrc_buffer_end)
        return NULL;
    lrc_buffer_end -= siz;
    return &lrc_buffer[lrc_buffer_end];
}
static void *new_lrc_word(long time_start, char *word, bool join)
{
    struct lrc_word *lrc_word;
    if ((lrc_word = alloc_buf(sizeof(struct lrc_word))) == NULL)
        return NULL;
    if ((lrc_word->word = lrcbufadd(word, join)) == NULL)
        return NULL;
    lrc_word->time_start = time_start;
    return lrc_word;
}
static bool add_lrc_line(struct lrc_line *lrc_line, char *word)
{
    lrc_line->nword = 0;
    lrc_line->next = NULL;
    lrc_line->words = NULL;
    if (word)
    {
        if ((lrc_line->words = new_lrc_word(-1, word, false)) == NULL)
            return false;
        lrc_line->nword++;
    }
    *current.ll_tail = lrc_line;
    current.ll_tail = &(lrc_line->next);
    current.nlrcline++;
    return true;
}
static struct lrc_line *get_lrc_line(int idx)
{
    static struct lrc_line *lrc_line = NULL;
    static int n = 0;
    if (idx < n)
    {
        lrc_line = current.ll_head;
        n = 0;
    }
    while (n < idx && lrc_line)
    {
        lrc_line = lrc_line->next;
        n++;
    }
    return lrc_line;
}
static char *get_lrc_str(struct lrc_line *lrc_line)
{
    return lrc_line->words[lrc_line->nword-1].word;
}
static long get_time_start(struct lrc_line *lrc_line)
{
    if (!lrc_line) return current.length+20;
    long time = lrc_line->time_start + current.offset;
    return time < 0? 0: time;
}
static void set_time_start(struct lrc_line *lrc_line, long time_start)
{
    time_start -= current.offset;
    time_start -= time_start%10;
    if (lrc_line->time_start != time_start)
    {
        lrc_line->time_start = time_start;
        current.changed_lrc = true;
    }
}
#define get_word_time_start(x)      get_time_start((struct lrc_line *)(x))
#define set_word_time_start(x, t)   set_time_start((struct lrc_line *)(x), (t))

static int format_time_tag(char *buf, long t)
{
    return rb->snprintf(buf, 16, "%02ld:%02ld.%02ld",
                        t/60000, (t/1000)%60, (t/10)%100);
}
/* find start of next line */
static const char *lrc_skip_space(const char *str)
{
    if (prefs.wrap)
    {
        while (*str && *str != '\n' && isspace(*str))
            str++;
    }
    if (*str == '\n')
        str++;
    return str;
}

static bool isbrchr(const unsigned char *str, int len)
{
    const unsigned char *p = "!,-.:;?　、。！，．：；？―";
    if (isspace(*str))
        return true;

    while(*p)
    {
        int n = rb->utf8seek(p, 1);
        if (len == n && !rb->strncmp(p, str, len))
            return true;
        p += n;
    }
    return false;
}

/* calculate how many lines is needed to display and store it.
 * create cache if there is enough space in lrc_buffer. */
static struct lrc_brpos *calc_brpos(struct lrc_line *lrc_line, int i)
{
    struct lrc_brpos *lrc_brpos;
    struct lrc_word *lrc_word;
    int nlrcbrpos = 0, max_lrcbrpos;
    uifont = rb->screens[0]->getuifont();
    struct font* pf = rb->font_get(uifont);
    ucschar_t ch;
    struct snap {
        int count, width;
        int nword;
        int word_count, word_width;
        const unsigned char *str;
    }
        sp,
        cr;

    lrc_buffer_used = (lrc_buffer_used+3)&~3; /* 4 bytes aligned */
    lrc_brpos = (struct lrc_brpos *) &lrc_buffer[lrc_buffer_used];
    max_lrcbrpos = (lrc_buffer_end-lrc_buffer_used) / sizeof(struct lrc_brpos);

    if (!lrc_line)
    {
        /* calc info for all lrcs and store them if possible */
        size_t buffer_used = lrc_buffer_used;
        bool too_many_lines = false;
        current.too_many_lines = true;
        for (lrc_line = current.ll_head; lrc_line; lrc_line = lrc_line->next)
        {
            FOR_NB_SCREENS(i)
            {
                lrc_brpos = calc_brpos(lrc_line, i);
                if (!too_many_lines)
                {
                    lrc_buffer_used += lrc_line->nline[i]*sizeof(struct lrc_brpos);
                    if (nlrcbrpos + lrc_line->nline[i] >= max_lrcbrpos)
                    {
                        too_many_lines = true;
                        lrc_buffer_used = buffer_used;
                        calc_brpos(lrc_line, i);
                    }
                }
                nlrcbrpos += lrc_line->nline[i];
            }
        }
        current.too_many_lines = too_many_lines;
        lrc_buffer_used = buffer_used;
        current.nlrcbrpos = nlrcbrpos;
        return NULL;
    }

    if (!current.too_many_lines)
    {
        /* use stored infos. */
        struct lrc_line *temp_lrc = current.ll_head;
        for (; temp_lrc != lrc_line; temp_lrc = temp_lrc->next)
        {
            lrc_brpos += temp_lrc->nline[SCREEN_MAIN];
#ifdef HAVE_REMOTE_LCD
            lrc_brpos += temp_lrc->nline[SCREEN_REMOTE];
#endif
        }
#if NB_SCREENS >= 2
        while (i)
            lrc_brpos += lrc_line->nline[--i];
#endif
        return lrc_brpos;
    }

    /* calculate number of lines, line width and char count for each line. */
    lrc_line->width = 0;
    cr.nword = lrc_line->nword;
    lrc_word = lrc_line->words+cr.nword;
    cr.str = (lrc_word-1)->word;
    sp.word_count = 0;
    sp.word_width = 0;
    sp.nword = 0;
    sp.count = 0;
    sp.width = 0;
    do {
        cr.count = 0;
        cr.width = 0;
        sp.str = NULL;

        while (1)
        {
            while(cr.nword > 0 && cr.str >= (lrc_word-1)->word)
            {
                cr.nword--;
                lrc_word--;
                lrc_word->count = 0;
                lrc_word->width = 0;
            }
            if (*cr.str == 0 || *cr.str == '\n')
                break;

            int c, w;
            c = ((intptr_t)rb->utf8decode(cr.str, &ch) - (intptr_t)cr.str);
            if (rb->is_diacritic(ch, NULL))
                w = 0;
            else
                w = rb->font_get_width(pf, ch);
            if (cr.count && prefs.wrap && isbrchr(cr.str, c))
            {
                /* remember position of last space */
                rb->memcpy(&sp, &cr, sizeof(struct snap));
                sp.word_count = lrc_word->count;
                sp.word_width = lrc_word->width;
                if (!isspace(*cr.str) && cr.width+w <= vp_lyrics[i].width)
                {
                    sp.count += c;
                    sp.width += w;
                    sp.word_count += c;
                    sp.word_width += w;
                    sp.str += c;
                }
            }
            if (cr.count && cr.width+w > vp_lyrics[i].width)
            {
                if (sp.str != NULL) /* wrap */
                {
                    rb->memcpy(&cr, &sp, sizeof(struct snap));
                    lrc_word = lrc_line->words+cr.nword;
                    lrc_word->count = sp.word_count;
                    lrc_word->width = sp.word_width;
                }
                break;
            }
            cr.count += c;
            cr.width += w;
            lrc_word->count += c;
            lrc_word->width += w;
            cr.str += c;
        }
        lrc_line->width += cr.width;
        lrc_brpos->count = cr.count;
        lrc_brpos->width = cr.width;
        nlrcbrpos++;
        lrc_brpos++;
        cr.str = lrc_skip_space(cr.str);
    } while (*cr.str && nlrcbrpos < max_lrcbrpos);
    lrc_line->nline[i] = nlrcbrpos;

    while (cr.nword > 0)
    {
        cr.nword--;
        lrc_word--;
        lrc_word->count = 0;
        lrc_word->width = 0;
    }
    return lrc_brpos-nlrcbrpos;
}

/* sort lyrics by time using stable sort. */
static void sort_lrcs(void)
{
    struct lrc_line *p = current.ll_head, **q = NULL, *t;
    long time_max = 0;

    current.ll_head = NULL;
    current.ll_tail = &current.ll_head;
    while (p != NULL)
    {
        t = p->next;
        /* remove problematic lrc_lines.
         * it would cause problem in display_lrc_line() if nword is 0. */
        if (p->nword)
        {
            q = p->time_start >= time_max? current.ll_tail: &current.ll_head;
            while ((*q) && (*q)->time_start <= p->time_start)
                q = &((*q)->next);
            p->next = *q;
            *q = p;
            if (!p->next)
            {
                time_max = p->time_start;
                current.ll_tail = &p->next;
            }
        }
        p = t;
    }
    if (!current.too_many_lines)
        calc_brpos(NULL, 0); /* stored data depends on order of lrcs if exist */
}
static void init_time_tag(void)
{
    struct lrc_line *lrc_line = current.ll_head;
    int nline = 0;
    if (current.type == TXT || current.type == ID3_USLT)
    {
        /* set time tag according to length of audio and total line count
         * for not synched lyrics, so that scroll speed is almost constant. */
        for (; lrc_line; lrc_line = lrc_line->next)
        {
            lrc_line->time_start = nline * current.length / current.nlrcbrpos;
            lrc_line->time_start -= lrc_line->time_start%10;
            lrc_line->old_time_start = -1;
            nline += lrc_line->nline[SCREEN_MAIN];
#ifdef HAVE_REMOTE_LCD
            nline += lrc_line->nline[SCREEN_REMOTE];
#endif
        }
    }
    else
    {
        /* reset timetags to the value read from file */
        for (; lrc_line; lrc_line = lrc_line->next)
        {
            lrc_line->time_start = lrc_line->old_time_start;
        }
        sort_lrcs();
    }
    current.changed_lrc = false;
}

/*******************************
 * Serch lrc file.
 *******************************/

/* search in same or parent directries of playing file.
 * assume playing file is /aaa/bbb/ccc/ddd.mp3,
 * this function searchs lrc file following order.
 * /aaa/bbb/ccc/ddd.lrc
 * /aaa/bbb/ddd.lrc
 * /aaa/ddd.lrc
 * /ddd.lrc
 */

static bool find_lrc_file_helper(const char *base_dir)
{
    char fname[MAX_PATH];
    char *names[3] = {NULL, NULL, NULL};
    char *p, *dir;
    int i, len;
    /* /aaa/bbb/ccc/ddd.mp3
     * dir  <--q    names[0]
     */

    /* assuming file name starts with '/' */
    rb->strcpy(temp_buf, current.mp3_file);
    /* get file name and remove extension */
    names[0] = rb->strrchr(temp_buf, '/')+1;
    if ((p = rb->strrchr(names[0], '.')) != NULL)
        *p = 0;
    if (current.id3->title && rb->strcmp(names[0], current.id3->title))
    {
        rb->strlcpy(fname, current.id3->title, sizeof(fname));
        rb->fix_path_part(fname, 0, sizeof(fname) - 1);
        names[1] = fname;
    }

    dir = temp_buf;
    p = names[0]-1;
    do {
        int n;
        *p = 0;
        for (n = 0; ; n++)
        {
            if (n == 0)
            {
                len = rb->snprintf(current.lrc_file, MAX_PATH, "%s%s/",
                                    base_dir, dir);
            }
            else if (n == 1)
            {
                /* check file in subfolder named prefs.lrc_directory
                 * in the directory of mp3 file. */
                if (prefs.lrc_directory[0] == '/')
                {
                    len = rb->snprintf(current.lrc_file, MAX_PATH, "%s%s/",
                                        dir, prefs.lrc_directory);
                }
                else
                    continue;
            }
            else
                break;
            DEBUGF("check file in %s\n", current.lrc_file);
            if (!rb->dir_exists(current.lrc_file))
                continue;
            for (current.type = 0; current.type < NUM_TYPES; current.type++)
            {
                for (i = 0; names[i] != NULL; i++)
                {
                    rb->snprintf(&current.lrc_file[len], MAX_PATH-len,
                            "%s%s", names[i], extentions[current.type]);
                    if (rb->file_exists(current.lrc_file))
                    {
                        DEBUGF("found: `%s'\n", current.lrc_file);
                        return true;
                    }
                }
            }
        }
    } while ((p = rb->strrchr(dir, '/')) != NULL);
    return false;
}

/* return true if a lrc file is found */
static bool find_lrc_file(void)
{
    reset_current_data();

    DEBUGF("find lrc file for `%s'\n", current.mp3_file);
    /* find .lrc file */
    if (find_lrc_file_helper(""))
        return true;
    if (prefs.lrc_directory[0] == '/' && rb->dir_exists(prefs.lrc_directory))
    {
        if (find_lrc_file_helper(prefs.lrc_directory))
            return true;
    }

    current.lrc_file[0] = 0;
    return false;
}

/*******************************
 * Load file.
 *******************************/

/* check tag format and calculate value of the tag.
 * supported tag: ti, ar, offset
 * supported format of time tag: [mm:ss], [mm:ss.xx], [mm:ss.xxx]
 * returns value of timega if tag is time tag, -1 if tag is supported tag,
 * -10 otherwise.
 */
static char *parse_int(char *ptr, int *val)
{
    *val = rb->atoi(ptr);
    while (isdigit(*ptr)) ptr++;
    return ptr;
}
static long get_time_value(char *tag, bool read_id_tags, off_t file_offset)
{
    long time;
    char *ptr;
    int val;

    if (read_id_tags)
    {
        if (!rb->strncmp(tag, "ti:", 3))
        {
            if (!current.id3->title || rb->strcmp(&tag[3], current.id3->title))
                current.title = lrcbufadd(&tag[3], false);
            return -1;
        }
        if (!rb->strncmp(tag, "ar:", 3))
        {
            if (!current.id3->artist || rb->strcmp(&tag[3], current.id3->artist))
                current.artist = lrcbufadd(&tag[3], false);
            return -1;
        }
        if (!rb->strncmp(tag, "offset:", 7))
        {
            current.offset = rb->atoi(&tag[7]);
            current.offset_file_offset = file_offset;
            return -1;
        }
    }

    /* minute */
    ptr = parse_int(tag, &val);
    if (ptr-tag < 1 || ptr-tag > 2 || *ptr != ':')
        return -10;
    time = val * 60000;
    /* second */
    tag = ptr+1;
    ptr = parse_int(tag, &val);
    if (ptr-tag != 2 || (*ptr != '.' && *ptr != ':' && *ptr != '\0'))
        return -10;
    time += val * 1000;

    if (*ptr != '\0')
    {
        /* milliseccond */
        tag = ptr+1;
        ptr = parse_int(tag, &val);
        if (ptr-tag < 2 || ptr-tag > 3 || *ptr != '\0')
            return -10;
        time += ((ptr-tag)==3 ?val: val*10);
    }

    return time;
}

/* format:
 * [time tag]line
 * [time tag]...[time tag]line
 * [time tag]<word time tag>word<word time tag>...<word time tag>
 */
static bool parse_lrc_line(char *line, off_t file_offset)
{
    struct lrc_line *lrc_line = NULL, *first_lrc_line = NULL;
    long time, time_start;
    char *str, *tagstart, *tagend;
    struct lrc_word *lrc_word;
    int nword = 0;

    /* parse [time tag]...[time tag] type tags */
    str = line;
    while (1)
    {
        if (*str != '[') break;
        tagend = rb->strchr(str, ']');
        if (tagend == NULL) break;
        *tagend = 0;
        time = get_time_value(str+1, !lrc_line, file_offset);
        *tagend++ = ']';
        if (time < 0)
            break;
        lrc_line = alloc_buf(sizeof(struct lrc_line));
        if (lrc_line == NULL)
            return false;
        if (!first_lrc_line)
            first_lrc_line = lrc_line;
        lrc_line->file_offset = file_offset;
        lrc_line->time_start = (time/10)*10;
        lrc_line->old_time_start = lrc_line->time_start;
        add_lrc_line(lrc_line, NULL);
        file_offset += (intptr_t)tagend - (intptr_t)str;
        str = tagend;
    }
    if (!first_lrc_line)
        return true; /* no time tag in line */

    lrc_line = first_lrc_line;
    if (lrcbufadd("", false) == NULL)
        return false;

    /* parse <word time tag>...<word time tag> type tags */
    /* [time tag]...[time tag]line type tags share lrc_line->words and can't
     * use lrc_line->words->timestart. use lrc_line->time_start instead. */
    time_start = -1;
    tagstart = str;
    while (*tagstart)
    {
        tagstart = rb->strchr(tagstart, '<');
        if (!tagstart) break;
        tagend = rb->strchr(tagstart, '>');
        if (!tagend) break;
        *tagend = 0;
        time = get_time_value(tagstart+1, false,
                                file_offset + ((intptr_t)tagstart - (intptr_t)str));
        *tagend++ = '>';
        if (time < 0)
        {
            tagstart++;
            continue;
        }
        *tagstart = 0;
        /* found word time tag. */
        if (*str || time_start != -1)
        {
            if ((lrc_word = new_lrc_word(time_start, str, true)) == NULL)
                return false;
            nword++;
        }
        file_offset += (intptr_t)tagend - (intptr_t)str;
        tagstart = str = tagend;
        time_start = time;
    }
    if ((lrc_word = new_lrc_word(time_start, str, true)) == NULL)
        return false;
    nword++;

    /* duplicate lrc_lines */
    while (lrc_line)
    {
        lrc_line->nword = nword;
        lrc_line->words = lrc_word;
        lrc_line = lrc_line->next;
    }

    return true;
}

/* format:
 * \xa2\xe2hhmmssxx\xa2\xd0
 * line 1
 * line 2
 * \xa2\xe2hhmmssxx\xa2\xd0
 * line 3
 * ...
 */
static bool parse_snc_line(char *line, off_t file_offset)
{
#define SNC_TAG_START "\xa2\xe2"
#define SNC_TAG_END   "\xa2\xd0"

    /* SNC_TAG can be dencoded, so use
     * temp_buf which contains native data */
    if (!rb->memcmp(temp_buf, SNC_TAG_START, 2)
     && !rb->memcmp(temp_buf+10, SNC_TAG_END, 2)) /* time tag */
    {
        const char *pos = temp_buf+2; /* skip SNC_TAG_START */
        int hh, mm, ss, xx;

        hh = (pos[0]-'0')*10+(pos[1]-'0'); pos += 2;
        mm = (pos[0]-'0')*10+(pos[1]-'0'); pos += 2;
        ss = (pos[0]-'0')*10+(pos[1]-'0'); pos += 2;
        xx = (pos[0]-'0')*10+(pos[1]-'0'); pos += 2;
        pos += 2; /* skip SNC_TAG_END */

        /* initialize */
        struct lrc_line *lrc_line = alloc_buf(sizeof(struct lrc_line));
        if (lrc_line == NULL)
            return false;
        lrc_line->file_offset = file_offset+2;
        lrc_line->time_start = hh*3600000+mm*60000+ss*1000+xx*10;
        lrc_line->old_time_start = lrc_line->time_start;
        if (!add_lrc_line(lrc_line, ""))
            return false;
        if (pos[0]==0)
            return true;

        /* encode rest of line and add to buffer */
        rb->iso_decode(pos, line, prefs.encoding, rb->strlen(pos)+1);
    }
    if (current.ll_head)
    {
        rb->strcat(line, "\n");
        if (lrcbufadd(line, true) == NULL)
            return false;
    }
    return true;
}

static bool parse_txt_line(char *line, off_t file_offset)
{
    /* initialize */
    struct lrc_line *lrc_line = alloc_buf(sizeof(struct lrc_line));
    if (lrc_line == NULL)
        return false;
    lrc_line->file_offset = file_offset;
    lrc_line->time_start = 0;
    lrc_line->old_time_start = -1;
    if (!add_lrc_line(lrc_line, line))
        return false;
    return true;
}

static void load_lrc_file(void)
{
    char utf8line[MAX_LINE_LEN*3];
    int fd;
    int encoding = prefs.encoding;
    bool (*line_parser)(char *line, off_t) = NULL;
    off_t file_offset, readsize;

    switch(current.type)
    {
        case LRC8:
            encoding = UTF_8; /* .lrc8 is utf8 */
            /* fall through */
        case LRC:
            line_parser = parse_lrc_line;
            break;
        case SNC:
            line_parser = parse_snc_line;
            break;
        case TXT:
            line_parser = parse_txt_line;
            break;
        default:
            return;
    }

    fd = rb->open(current.lrc_file, O_RDONLY);
    if (fd < 0) return;

    {
        /* check encoding */
        #define BOM "\xef\xbb\xbf"
        #define BOM_SIZE 3
        unsigned char header[BOM_SIZE];
        unsigned char* (*utf_decode)(const unsigned char *,
                                     unsigned char *, int) = NULL;
        rb->read(fd, header, BOM_SIZE);
        if (!rb->memcmp(header, BOM, BOM_SIZE))      /* UTF-8 */
        {
            encoding = UTF_8;
        }
        else if (!rb->memcmp(header, "\xff\xfe", 2)) /* UTF-16LE */
        {
            utf_decode = rb->utf16LEdecode;
        }
        else if (!rb->memcmp(header, "\xfe\xff", 2)) /* UTF-16BE */
        {
            utf_decode = rb->utf16BEdecode;
        }
        else
        {
            rb->lseek(fd, 0, SEEK_SET);
        }

        if (utf_decode)
        {
            /* convert encoding of file from UTF-16 to UTF-8 */
            char temp_file[MAX_PATH];
            int fe;
            rb->lseek(fd, 2, SEEK_SET);
            rb->snprintf(temp_file, MAX_PATH, "%s~", current.lrc_file);
            fe = rb->creat(temp_file, 0666);
            if (fe < 0)
            {
                rb->close(fd);
                return;
            }
            rb->write(fe, BOM, BOM_SIZE);
            while ((readsize = rb->read(fd, temp_buf, MAX_LINE_LEN)) > 0)
            {
                char *end = utf_decode(temp_buf, utf8line, readsize/2);
                rb->write(fe, utf8line, end-utf8line);
            }
            rb->close(fe);
            rb->close(fd);
            rb->remove(current.lrc_file);
            rb->rename(temp_file, current.lrc_file);
            fd = rb->open(current.lrc_file, O_RDONLY);
            if (fd < 0) return;
            rb->lseek(fd, BOM_SIZE, SEEK_SET); /* skip bom */
            encoding = UTF_8;
        }
    }

    file_offset = rb->lseek(fd, 0, SEEK_CUR); /* used in line_parser */
    while ((readsize = rb->read_line(fd, temp_buf, MAX_LINE_LEN)) > 0)
    {
        /* note: parse_snc_line() reads temp_buf for native data. */
        rb->iso_decode(temp_buf, utf8line, encoding, readsize+1);
        if (!line_parser(utf8line, file_offset))
            break;
        file_offset += readsize;
    }
    rb->close(fd);

    current.loaded_lrc = true;
    calc_brpos(NULL, 0);
    init_time_tag();

    return;
}

#ifdef LRC_SUPPORT_ID3
/*******************************
 * read lyrics from id3
 *******************************/
static unsigned long unsync(unsigned long b0, unsigned long b1,
                            unsigned long b2, unsigned long b3)
{
    return (((long)(b0 & 0x7F) << (3*7)) |
            ((long)(b1 & 0x7F) << (2*7)) |
            ((long)(b2 & 0x7F) << (1*7)) |
            ((long)(b3 & 0x7F) << (0*7)));
}

static unsigned long bytes2int(unsigned long b0, unsigned long b1,
                               unsigned long b2, unsigned long b3)
{
    return (((long)(b0 & 0xFF) << (3*8)) |
            ((long)(b1 & 0xFF) << (2*8)) |
            ((long)(b2 & 0xFF) << (1*8)) |
            ((long)(b3 & 0xFF) << (0*8)));
}

static int unsynchronize(char* tag, int len, bool *ff_found)
{
    int i;
    unsigned char c;
    unsigned char *rp, *wp;
    bool _ff_found = false;
    if(ff_found) _ff_found = *ff_found;

    wp = rp = (unsigned char *)tag;

    rp = (unsigned char *)tag;
    for(i = 0; i<len; i++) {
        /* Read the next byte and write it back, but don't increment the
           write pointer */
        c = *rp++;
        *wp = c;
        if(_ff_found) {
            /* Increment the write pointer if it isn't an unsynch pattern */
            if(c != 0)
                wp++;
            _ff_found = false;
        } else {
            if(c == 0xff)
                _ff_found = true;
            wp++;
        }
    }
    if(ff_found) *ff_found = _ff_found;
    return (intptr_t)wp - (intptr_t)tag;
}

static int read_unsynched(int fd, void *buf, int len, bool *ff_found)
{
    int i;
    int rc;
    int remaining = len;
    char *wp;

    wp = buf;

    while(remaining) {
        rc = rb->read(fd, wp, remaining);
        if(rc <= 0)
            return rc;

        i = unsynchronize(wp, remaining, ff_found);
        remaining -= i;
        wp += i;
    }

    return len;
}

static unsigned char* utf8cpy(const unsigned char *src,
                              unsigned char *dst, int count)
{
    rb->strlcpy(dst, src, count+1);
    return dst+rb->strlen(dst);
}

static void parse_id3v2(int fd)
{
    int minframesize;
    int size;
    long framelen;
    char header[10];
    char tmp[8];
    unsigned char version;
    int bytesread = 0;
    unsigned char global_flags;
    int flags;
    bool global_unsynch = false;
    bool global_ff_found = false;
    bool unsynch = false;
    int rc;
    enum {NOLT, SYLT, USLT} type = NOLT;

    /* Bail out if the tag is shorter than 10 bytes */
    if(current.id3->id3v2len < 10)
        return;

    /* Read the ID3 tag version from the header */
    if(10 != rb->read(fd, header, 10))
        return;

    /* Get the total ID3 tag size */
    size = current.id3->id3v2len - 10;

    version = current.id3->id3version;
    switch ( version )
    {
        case ID3_VER_2_2:
            minframesize = 8;
            break;

        case ID3_VER_2_3:
            minframesize = 12;
            break;

        case ID3_VER_2_4:
            minframesize = 12;
            break;

        default:
            /* unsupported id3 version */
            return;
    }

    global_flags = header[5];

    /* Skip the extended header if it is present */
    if(global_flags & 0x40) {

        if(version == ID3_VER_2_3) {
            if(10 != rb->read(fd, header, 10))
                return;
            /* The 2.3 extended header size doesn't include the header size
               field itself. Also, it is not unsynched. */
            framelen =
                bytes2int(header[0], header[1], header[2], header[3]) + 4;

            /* Skip the rest of the header */
            rb->lseek(fd, framelen - 10, SEEK_CUR);
        }

        if(version >= ID3_VER_2_4) {
            if(4 != rb->read(fd, header, 4))
                return;

            /* The 2.4 extended header size does include the entire header,
               so here we can just skip it. This header is unsynched. */
            framelen = unsync(header[0], header[1],
                              header[2], header[3]);

            rb->lseek(fd, framelen - 4, SEEK_CUR);
        }
    }

    /* Is unsynchronization applied? */
    if(global_flags & 0x80) {
        global_unsynch = true;
    }

    /* We must have at least minframesize bytes left for the
     * remaining frames to be interesting */
    while (size >= minframesize) {
        flags = 0;

        /* Read frame header and check length */
        if(version >= ID3_VER_2_3) {
            if(global_unsynch && version <= ID3_VER_2_3)
                rc = read_unsynched(fd, header, 10, &global_ff_found);
            else
                rc = rb->read(fd, header, 10);
            if(rc != 10)
                return;
            /* Adjust for the 10 bytes we read */
            size -= 10;

            flags = bytes2int(0, 0, header[8], header[9]);

            if (version >= ID3_VER_2_4) {
                framelen = unsync(header[4], header[5],
                                  header[6], header[7]);
            } else {
                /* version .3 files don't use synchsafe ints for
                 * size */
                framelen = bytes2int(header[4], header[5],
                                     header[6], header[7]);
            }
        } else {
            if(6 != rb->read(fd, header, 6))
                return;
            /* Adjust for the 6 bytes we read */
            size -= 6;

            framelen = bytes2int(0, header[3], header[4], header[5]);
        }

        if(framelen == 0){
            if (header[0] == 0 && header[1] == 0 && header[2] == 0)
                return;
            else
                continue;
        }

        unsynch = false;

        if(flags)
        {
            if (version >= ID3_VER_2_4) {
                if(flags & 0x0040) { /* Grouping identity */
                    rb->lseek(fd, 1, SEEK_CUR); /* Skip 1 byte */
                    framelen--;
                }
            } else {
                if(flags & 0x0020) { /* Grouping identity */
                    rb->lseek(fd, 1, SEEK_CUR); /* Skip 1 byte */
                    framelen--;
                }
            }

            if(flags & 0x000c) /* Compression or encryption */
            {
                /* Skip it */
                size -= framelen;
                rb->lseek(fd, framelen, SEEK_CUR);
                continue;
            }

            if(flags & 0x0002) /* Unsynchronization */
                unsynch = true;

            if (version >= ID3_VER_2_4) {
                if(flags & 0x0001) { /* Data length indicator */
                    if(4 != rb->read(fd, tmp, 4))
                        return;

                    /* We don't need the data length */
                    framelen -= 4;
                }
            }
        }

        if (framelen == 0)
            continue;

        if (framelen < 0)
            return;

        if(!rb->memcmp( header, "SLT", 3 ) ||
           !rb->memcmp( header, "SYLT", 4 ))
        {
            /* found a supported tag */
            type = SYLT;
            break;
        }
        else if(!rb->memcmp( header, "ULT", 3 ) ||
                !rb->memcmp( header, "USLT", 4 ))
        {
            /* found a supported tag */
            type = USLT;
            break;
        }
        else
        {
            /* not a supported tag*/
            if(global_unsynch && version <= ID3_VER_2_3) {
                size -= read_unsynched(fd, lrc_buffer, framelen, &global_ff_found);
            } else {
                size -= framelen;
                if( rb->lseek(fd, framelen, SEEK_CUR) == -1 )
                    return;
            }
        }
    }
    if(type == NOLT)
        return;

    int encoding = 0, chsiz;
    char *tag, *p, utf8line[MAX_LINE_LEN*3];
    unsigned char* (*utf_decode)(const unsigned char *,
                                 unsigned char *, int) = NULL;
    /* use middle of lrc_buffer to store tag data. */
    if(framelen >= LRC_BUFFER_SIZE/3)
        framelen = LRC_BUFFER_SIZE/3-1;
    tag = lrc_buffer+LRC_BUFFER_SIZE*2/3-framelen-1;
    if(global_unsynch && version <= ID3_VER_2_3)
        bytesread = read_unsynched(fd, tag, framelen, &global_ff_found);
    else
        bytesread = rb->read(fd, tag, framelen);

    if( bytesread != framelen )
        return;

    if(unsynch || (global_unsynch && version >= ID3_VER_2_4))
        bytesread = unsynchronize(tag, bytesread, NULL);

    tag[bytesread] = 0;
    encoding = tag[0];
    p = tag;
    /* skip some data */
    if(type == SYLT) {
        p += 6;
    } else {
        p += 4;
    }

    /* check encoding and skip content descriptor */
    switch (encoding) {
        case 0x01: /* Unicode with or without BOM */
        case 0x02:

            /* Now check if there is a BOM
               (zero-width non-breaking space, 0xfeff)
               and if it is in little or big endian format */
            if(!rb->memcmp(p, "\xff\xfe", 2)) { /* Little endian? */
                utf_decode = rb->utf16LEdecode;
            } else if(!rb->memcmp(p, "\xfe\xff", 2)) { /* Big endian? */
                utf_decode = rb->utf16BEdecode;
            } else
                utf_decode = NULL;

            encoding = NUM_CODEPAGES;
            do {
                size = p[0] | p[1];
                p += 2;
            } while(size);
            chsiz = 2;
            break;

        default:
            utf_decode = utf8cpy;
            if(encoding == 0x03) /* UTF-8 encoded string */
                encoding = UTF_8;
            else
                encoding = prefs.encoding;
            p += rb->strlen(p)+1;
            chsiz = 1;
            break;
    }
    if(encoding == NUM_CODEPAGES)
    {
        /* check if there is a BOM */
        if(!rb->memcmp(p, "\xff\xfe", 2)) { /* Little endian? */
            utf_decode = rb->utf16LEdecode;
            p += 2;
        } else if(!rb->memcmp(p, "\xfe\xff", 2)) { /* Big endian? */
            utf_decode = rb->utf16BEdecode;
            p += 2;
        } else if(!utf_decode) {
            /* If there is no BOM (which is a specification violation),
               let's try to guess it. If one of the bytes is 0x00, it is
               probably the most significant one. */
            if(p[1] == 0)
                utf_decode = rb->utf16LEdecode;
            else
                utf_decode = rb->utf16BEdecode;
        }
    }
    bytesread -= (intptr_t)p - (intptr_t)tag;
    tag = p;

    while ( bytesread > 0
        && lrc_buffer_used+bytesread < LRC_BUFFER_SIZE*2/3
        && LRC_BUFFER_SIZE*2/3 < lrc_buffer_end)
    {
        bool is_crlf = false;
        struct lrc_line *lrc_line = alloc_buf(sizeof(struct lrc_line));
        if(!lrc_line)
            break;
        lrc_line->file_offset = -1;
        if(type == USLT)
        {
            /* replace 0x0a and 0x0d with 0x00 */
            p = tag;
            while(1) {
                utf_decode(p, tmp, 2);
                if(!tmp[0]) break;
                if(tmp[0] == 0x0d || tmp[0] == 0x0a)
                {
                    if(tmp[0] == 0x0d && tmp[1] == 0x0a)
                        is_crlf = true;
                    p[0] = 0;
                    p[chsiz-1] = 0;
                    break;
                }
                p += chsiz;
            }
        }
        if(encoding == NUM_CODEPAGES)
        {
            unsigned char* utf8 = utf8line;
            p = tag;
            do {
                utf8 = utf_decode(p, utf8, 1);
                p += 2;
            } while(*(utf8-1));
        }
        else
        {
            size = rb->strlen(tag)+1;
            rb->iso_decode(tag, utf8line, encoding, size);
            p = tag+size;
        }

        if(type == SYLT) { /* timestamp */
            lrc_line->time_start = bytes2int(p[0], p[1], p[2], p[3]);
            lrc_line->old_time_start = lrc_line->time_start;
            p += 4;
            utf_decode(p, tmp, 1);
            if(tmp[0] == 0x0a)
                p += chsiz;
        } else { /* USLT */
            lrc_line->time_start = 0;
            lrc_line->old_time_start = -1;
            if(is_crlf) p += chsiz;
        }
        bytesread -= (intptr_t)p - (intptr_t)tag;
        tag = p;
        if(!add_lrc_line(lrc_line, utf8line))
            break;
    }

    current.type = ID3_SYLT-SYLT+type;
    rb->strcpy(current.lrc_file, current.mp3_file);

    current.loaded_lrc = true;
    calc_brpos(NULL, 0);
    init_time_tag();

    return;
}

static bool read_id3(void)
{
    int fd;

    if(current.id3->codectype != AFMT_MPA_L1
    && current.id3->codectype != AFMT_MPA_L2
    && current.id3->codectype != AFMT_MPA_L3)
        return false;

    fd = rb->open(current.mp3_file, O_RDONLY);
    if(fd < 0) return false;
    current.loaded_lrc = false;
    parse_id3v2(fd);
    rb->close(fd);
    return current.loaded_lrc;
}
#endif /* LRC_SUPPORT_ID3 */

/*******************************
 * Display information
 *******************************/
static void display_state(void)
{
    const char *str = NULL;

    if (AUDIO_STOP)
        str = "Audio Stopped";
    else if (current.found_lrc)
    {
        if (!current.loaded_lrc)
            str = "Loading lrc";
        else if (!current.ll_head)
            str = "No lyrics";
    }

    const char *info = NULL;

    if (AUDIO_PLAY && prefs.display_title)
    {
        char *title = (current.title? current.title: current.id3->title);
        char *artist = (current.artist? current.artist: current.id3->artist);

        if (artist != NULL && title != NULL)
        {
            rb->snprintf(temp_buf, MAX_LINE_LEN, "%s/%s", title, artist);
            info = temp_buf;
        }
        else if (title != NULL)
            info = title;
        else if (current.mp3_file[0] == '/')
            info = rb->strrchr(current.mp3_file, '/')+1;
        else
            info = "(no info)";
    }

    int w, h;
    struct screen* display;
    FOR_NB_SCREENS(i)
    {
        display = rb->screens[i];
        display->set_viewport(&vp_info[i]);
        display->clear_viewport();
        if (info)
            display->puts_scroll(0, 0, info);
        if (str)
        {
            display->set_viewport(&vp_lyrics[i]);
            display->clear_viewport();
            display->getstringsize(str, &w, &h);
            if (vp_lyrics[i].width - w < 0)
                display->puts_scroll(0, vp_lyrics[i].height/font_ui_height/2,
                                     str);
            else
                display->putsxy((vp_lyrics[i].width - w)*prefs.align/2,
                                (vp_lyrics[i].height-font_ui_height)/2, str);
            display->set_viewport(&vp_info[i]);
        }
        display->update_viewport();
        display->set_viewport(NULL);
    }
}

static void display_time(void)
{
    rb->snprintf(temp_buf, MAX_LINE_LEN, "%ld:%02ld/%ld:%02ld",
                            current.elapsed/60000, (current.elapsed/1000)%60,
                            current.length/60000, (current.length)/1000%60);
    int y = (prefs.display_title? font_ui_height:0);
    FOR_NB_SCREENS(i)
    {
        struct screen* display = rb->screens[i];
        display->set_viewport(&vp_info[i]);
        display->setfont(FONT_SYSFIXED);
        display->putsxy(0, y, temp_buf);
        rb->gui_scrollbar_draw(display, 0, y+SYSFONT_HEIGHT+1,
                               vp_info[i].width, SYSFONT_HEIGHT-2,
                               current.length, 0, current.elapsed, HORIZONTAL);
        display->update_viewport_rect(0, y, vp_info[i].width, SYSFONT_HEIGHT*2);
        display->setfont(uifont);
        display->set_viewport(NULL);
    }
}

/*******************************
 * Display lyrics
 *******************************/
static inline void set_to_default(struct screen *display)
{
#if (LCD_DEPTH > 1)
#ifdef HAVE_REMOTE_LCD
    if (display->screen_type != SCREEN_REMOTE)
#endif
        display->set_foreground(prefs.active_color);
#endif
    display->set_drawmode(DRMODE_SOLID);
}
static inline void set_to_active(struct screen *display)
{
#if (LCD_DEPTH > 1)
#ifdef HAVE_REMOTE_LCD
    if (display->screen_type == SCREEN_REMOTE)
        display->set_drawmode(DRMODE_INVERSEVID);
    else
#endif
    {
        display->set_foreground(prefs.active_color);
        display->set_drawmode(DRMODE_SOLID);
    }
#else /* LCD_DEPTH == 1 */
    display->set_drawmode(DRMODE_INVERSEVID);
#endif
}
static inline void set_to_inactive(struct screen *display)
{
#if (LCD_DEPTH > 1)
#ifdef HAVE_REMOTE_LCD
    if (display->screen_type != SCREEN_REMOTE)
#endif
        display->set_foreground(prefs.inactive_color);
#endif
    display->set_drawmode(DRMODE_SOLID);
}

static int display_lrc_line(struct lrc_line *lrc_line, int ypos, int i)
{
    struct screen *display = rb->screens[i];
    struct lrc_word *lrc_word;
    struct lrc_brpos *lrc_brpos;
    long time_start, time_end, time_elapsed, elapsed;
    int count, width, nword;
    int xpos;
    const char *str;
    bool active_line;

    if (!prefs.autoscroll)
        time_elapsed = current.scroll;
    else
        time_elapsed = current.elapsed;
    time_start = get_time_start(lrc_line);
    time_end = get_time_start(lrc_line->next);
    active_line = (time_start <= time_elapsed
                    && time_end > time_elapsed);

    if (!lrc_line->width)
    {
        /* empty line. draw bar during interval. */
        long rin = time_elapsed - time_start;
        long len = time_end - time_start;
        if (current.wipe && active_line && len >= 3500)
        {
            elapsed = rin * vp_lyrics[i].width / len;
            set_to_inactive(display);
            display->fillrect(elapsed, ypos+font_ui_height/4+1,
                 vp_lyrics[i].width-elapsed-1, font_ui_height/2-2);
            set_to_active(display);
            display->drawrect(0, ypos+font_ui_height/4,
                              vp_lyrics[i].width, font_ui_height/2);
            display->fillrect(1, ypos+font_ui_height/4+1,
                              elapsed-1, font_ui_height/2-2);
            set_to_default(display);
        }
        return ypos + font_ui_height;
    }

    lrc_brpos = calc_brpos(lrc_line, i);
    /* initialize line */
    xpos = (vp_lyrics[i].width - lrc_brpos->width)*prefs.align/2;
    count = 0;
    width = 0;

    active_line = active_line || !prefs.active_one_line;
    nword = lrc_line->nword-1;
    lrc_word = lrc_line->words + nword;
    str = lrc_word->word;
    /* only time_start of first word could be -1 */
    if (lrc_word->time_start != -1)
        time_end = get_word_time_start(lrc_word);
    else
        time_end = time_start;
    do {
        time_start = time_end;
        if (nword > 0)
            time_end = get_word_time_start(lrc_word-1);
        else
            time_end = get_time_start(lrc_line->next);

        if (time_start > time_elapsed || !active_line)
        {
            /* inactive */
            elapsed = 0;
        }
        else if (!current.wipe || time_end <= time_elapsed)
        {
            /* active whole word */
            elapsed = lrc_word->width;
        }
        else
        {
            /* wipe word */
            long rin = time_elapsed - time_start;
            long len = time_end - time_start;
            elapsed = rin * lrc_word->width / len;
        }

        int word_count = lrc_word->count;
        int word_width = lrc_word->width;
        set_to_active(display);
        while (word_count > 0 && word_width > 0)
        {
            int c = lrc_brpos->count - count;
            int w = lrc_brpos->width - width;
            if (c > word_count || w > word_width)
            {
                c = word_count;
                w = word_width;
            }
            if (prefs.autoscroll)
            {
                if (elapsed <= 0)
                {
                    set_to_inactive(display);
                }
                else if (elapsed < w)
                {
                    /* wipe text */
                    display->fillrect(xpos, ypos, elapsed, font_ui_height);
                    set_to_inactive(display);
                    display->fillrect(xpos+elapsed, ypos,
                                      w-elapsed, font_ui_height);
#if (LCD_DEPTH > 1)
#ifdef HAVE_REMOTE_LCD
                    if (display->screen_type == SCREEN_REMOTE)
                        display->set_drawmode(DRMODE_INVERSEVID);
                    else
#endif
                        display->set_drawmode(DRMODE_BG);
#else
                    display->set_drawmode(DRMODE_INVERSEVID);
#endif
                }
            }
            rb->strlcpy(temp_buf, str, c+1);
            display->putsxy(xpos, ypos, temp_buf);
            str += c;
            xpos += w;
            count += c;
            width += w;
            word_count -= c;
            word_width -= w;
            elapsed -= w;
            if (count >= lrc_brpos->count || width >= lrc_brpos->width)
            {
                /* prepare for next line */
                lrc_brpos++;
                str = lrc_skip_space(str);
                xpos = (vp_lyrics[i].width - lrc_brpos->width)*prefs.align/2;
                ypos += font_ui_height;
                count = 0;
                width = 0;
            }
        }
        lrc_word--;
    } while (nword--);
    set_to_default(display);
    return ypos;
}

static void display_lrcs(void)
{
    long time_start, time_end, time_elapsed, rin, len;
    int nline[NB_SCREENS] = {0};
    struct lrc_line *lrc_center = current.ll_head;

    if (!lrc_center) return;

    if (!prefs.autoscroll)
        time_elapsed = current.scroll;
    else
        time_elapsed = current.elapsed;

    while (get_time_start(lrc_center->next) <= time_elapsed)
    {
        nline[SCREEN_MAIN] += lrc_center->nline[SCREEN_MAIN];
#ifdef HAVE_REMOTE_LCD
        nline[SCREEN_REMOTE] += lrc_center->nline[SCREEN_REMOTE];
#endif
        lrc_center = lrc_center->next;
    }

    time_start = get_time_start(lrc_center);
    time_end = get_time_start(lrc_center->next);
    rin = time_elapsed - time_start;
    len = time_end - time_start;

    struct screen *display;
    FOR_NB_SCREENS(i)
    {
        display = rb->screens[i];
        /* display current line at the center of the viewport */
        display->set_viewport(&vp_lyrics[i]);
        display->clear_viewport();

        struct lrc_line *lrc_line;
        int y, ypos = 0, nblines = vp_lyrics[i].height/font_ui_height;
        y = (nblines-1)/2;
        if (rin < 0)
        {
            /* time_elapsed < time of first lrc */
            if (!current.wipe)
                ypos = (time_start - time_elapsed)
                            * font_ui_height / time_start;
            else
                y++;
        }
        else if (len > 0)
        {
            if (!current.wipe)
                ypos = - rin * lrc_center->nline[i] * font_ui_height / len;
            else
            {
                long elapsed = rin * lrc_center->width / len;
                struct lrc_brpos *lrc_brpos = calc_brpos(lrc_center, i);
                while (elapsed > lrc_brpos->width)
                {
                    elapsed -= lrc_brpos->width;
                    y--;
                    lrc_brpos++;
                }
            }
        }

        /* find first line to display */
        y -= nline[i];
        lrc_line = current.ll_head;
        while (y < -lrc_line->nline[i])
        {
            y += lrc_line->nline[i];
            lrc_line = lrc_line->next;
        }

        ypos += y*font_ui_height;
        while (lrc_line && ypos < vp_lyrics[i].height)
        {
            ypos = display_lrc_line(lrc_line, ypos, i);
            lrc_line = lrc_line->next;
        }
        if (!lrc_line && ypos < vp_lyrics[i].height && prefs.autoscroll)
            display->putsxy(0, ypos, "[end]");

        display->update_viewport();
        display->set_viewport(NULL);
    }
}

/*******************************
 * Browse lyrics and edit time.
 *******************************/
/* point playing line in lyrics */
static enum themable_icons get_icon(int selected, void * data)
{
    (void) data;
    struct lrc_line *lrc_line = get_lrc_line(selected);
    if (lrc_line)
    {
        long time_start = get_time_start(lrc_line);
        long time_end = get_time_start(lrc_line->next);
        long elapsed = current.id3->elapsed;
        if (time_start <= elapsed && time_end > elapsed)
            return Icon_Moving;
    }
    return Icon_NOICON;
}
static const char *get_lrc_timeline(int selected, void *data,
                                    char *buffer, size_t buffer_len)
{
    (void) data;
    struct lrc_line *lrc_line = get_lrc_line(selected);
    if (lrc_line)
    {
        format_time_tag(temp_buf, get_time_start(lrc_line));
        rb->snprintf(buffer, buffer_len, "[%s]%s",
                     temp_buf, get_lrc_str(lrc_line));
        return buffer;
    }
    return NULL;
}

static void save_changes(void)
{
    char new_file[MAX_PATH], *p;
    bool success = false;
    int fd, fe;
    if (!current.changed_lrc)
        return;
    rb->splash(HZ/2, "Saving changes...");
    if (current.type == TXT || current.type > NUM_TYPES)
    {
        /* save changes to new .lrc file */
        rb->strcpy(new_file, current.lrc_file);
        p = rb->strrchr(new_file, '.');
        rb->strcpy(p, extentions[LRC]);
    }
    else
    {
        /* file already exists. use temp file. */
        rb->snprintf(new_file, MAX_PATH, "%s~", current.lrc_file);
    }
    fd = rb->creat(new_file, 0666);
    fe = rb->open(current.lrc_file, O_RDONLY);
    if (fd >= 0 && fe >= 0)
    {
        struct lrc_line *lrc_line, *temp_lrc;
        off_t curr = 0, next = 0, size = 0, offset = 0;
        for (lrc_line = current.ll_head; lrc_line;
            lrc_line = lrc_line->next)
        {
            /* apply offset and set old_time_start -1 to indicate
               that time tag is not saved yet. */
            lrc_line->time_start = get_time_start(lrc_line);
            lrc_line->old_time_start = -1;
        }
        current.offset = 0;
        if (current.type > NUM_TYPES)
        {
            curr = -1;
            rb->write(fd, BOM, BOM_SIZE);
        }
        else
            size = rb->filesize(fe);
        while (curr < size)
        {
            /* find offset of next tag */
            lrc_line = NULL;
            for (temp_lrc = current.ll_head, next = size;
                temp_lrc; temp_lrc = temp_lrc->next)
            {
                offset = temp_lrc->file_offset;
                if (offset < next && temp_lrc->old_time_start == -1)
                {
                    lrc_line = temp_lrc;
                    next = offset;
                    if (offset <= curr) break;
                }
            }
            offset = current.offset_file_offset;
            if (offset >= 0 && offset < next)
            {
                lrc_line = NULL;
                next = offset;
                current.offset_file_offset = -1;
            }
            if (next > curr)
            {
                if (curr == -1) curr = 0;
                /* copy before the next tag */
                while (curr < next)
                {
                    ssize_t count = next-curr;
                    if (count > MAX_LINE_LEN)
                        count = MAX_LINE_LEN;
                    if (rb->read(fe, temp_buf, count)!=count)
                        break;
                    rb->write(fd, temp_buf, count);
                    curr += count;
                }
                if (curr < next || curr >= size) break;
            }
            /* write tag to new file and skip tag in backup */
            if (lrc_line != NULL)
            {
                lrc_line->file_offset = rb->lseek(fd, 0, SEEK_CUR);
                lrc_line->old_time_start = lrc_line->time_start;
                long t = lrc_line->time_start;
                if (current.type == SNC)
                {
                    rb->fdprintf(fd, "%02ld%02ld%02ld%02ld", (t/3600000)%100,
                                     (t/60000)%60, (t/1000)%60, (t/10)%100);
                    /* skip time tag */
                    curr += rb->read(fe, temp_buf, 8);
                }
                else /* LRC || LRC8 */
                {
                    format_time_tag(temp_buf, t);
                    rb->fdprintf(fd, "[%s]", temp_buf);
                }
                if (next == -1)
                {
                    rb->fdprintf(fd, "%s\n", get_lrc_str(lrc_line));
                }
            }
            if (current.type == LRC || current.type == LRC8)
            {
                /* skip both time tag and offset tag */
                while (curr++<size && rb->read(fe, temp_buf, 1)==1)
                    if (temp_buf[0]==']') break;
            }
        }
        success = (curr>=size);
    }
    if (fe >= 0) rb->close(fe);
    if (fd >= 0) rb->close(fd);

    if (success)
    {
        if (current.type == TXT || current.type > NUM_TYPES)
        {
            current.type = LRC;
            rb->strcpy(current.lrc_file, new_file);
        }
        else
        {
            rb->remove(current.lrc_file);
            rb->rename(new_file, current.lrc_file);
        }
    }
    else
    {
        rb->remove(new_file);
        rb->splash(HZ, "Could not save changes.");
    }
    rb->reload_directory();
    current.changed_lrc = false;
}
static int timetag_editor(void)
{
    struct gui_synclist gui_editor;
    struct lrc_line *lrc_line;
    bool exit = false;
    int button, idx, selected = 0;

    if (current.id3 == NULL || !current.ll_head)
    {
        rb->splash(HZ, "No lyrics");
        return LRC_GOTO_MAIN;
    }

    get_lrc_line(-1); /* initialize static variables */

    for (lrc_line = current.ll_head, idx = 0;
        lrc_line; lrc_line = lrc_line->next, idx++)
    {
        long time_start = get_time_start(lrc_line);
        long time_end = get_time_start(lrc_line->next);
        long elapsed = current.id3->elapsed;
        if (time_start <= elapsed && time_end > elapsed)
            selected = idx;
    }

    rb->gui_synclist_init(&gui_editor, &get_lrc_timeline, NULL, false, 1, NULL);
    rb->gui_synclist_set_nb_items(&gui_editor, current.nlrcline);
    rb->gui_synclist_set_icon_callback(&gui_editor, get_icon);
    rb->gui_synclist_set_title(&gui_editor, "Timetag Editor",
                               Icon_Menu_functioncall);
    rb->gui_synclist_select_item(&gui_editor, selected);
    rb->gui_synclist_draw(&gui_editor);

    while (!exit)
    {
        button = rb->get_action(CONTEXT_TREE, TIMEOUT_BLOCK);
        if (rb->gui_synclist_do_button(&gui_editor, &button))
            continue;

        switch (button)
        {
            case ACTION_STD_OK:
                idx = rb->gui_synclist_get_sel_pos(&gui_editor);
                lrc_line = get_lrc_line(idx);
                if (lrc_line)
                {
                    set_time_start(lrc_line, current.id3->elapsed-500);
                    rb->gui_synclist_draw(&gui_editor);
                }
                break;
            case ACTION_STD_CONTEXT:
                idx = rb->gui_synclist_get_sel_pos(&gui_editor);
                lrc_line = get_lrc_line(idx);
                if (lrc_line)
                {
                    long temp_time = get_time_start(lrc_line);
                    if (lrc_set_time(get_lrc_str(lrc_line), NULL,
                                     &temp_time, 10, 0, current.length,
                                     LST_SET_MSEC|LST_SET_SEC|LST_SET_MIN) == 1)
                        return PLUGIN_USB_CONNECTED;
                    set_time_start(lrc_line, temp_time);
                    rb->gui_synclist_draw(&gui_editor);
                }
                break;
            case ACTION_TREE_STOP:
            case ACTION_STD_CANCEL:
                exit = true;
                break;
            default:
                if (rb->default_event_handler(button) == SYS_USB_CONNECTED)
                    return PLUGIN_USB_CONNECTED;
                break;
        }
    }

    FOR_NB_SCREENS(idx)
        rb->screens[idx]->scroll_stop();

    if (current.changed_lrc)
    {
        MENUITEM_STRINGLIST(save_menu, "Save Changes?", NULL,
                            "Yes", "No (save later)", "Discard All Changes")
        button = 0;
        exit = false;
        while (!exit)
        {
            switch (rb->do_menu(&save_menu, &button, NULL, false))
            {
                case 0:
                    sort_lrcs();
                    save_changes();
                    exit = true;
                    break;
                case 1:
                    sort_lrcs();
                    exit = true;
                    break;
                case 2:
                    init_time_tag();
                    exit = true;
                    break;
                case MENU_ATTACHED_USB:
                    return PLUGIN_USB_CONNECTED;
            }
        }
    }
    return LRC_GOTO_MAIN;
}

/*******************************
 * Settings.
 *******************************/
static void load_or_save_settings(bool save)
{
    static const char config_file[] = "lrcplayer.cfg";
    static struct configdata config[] = {
#ifdef HAVE_LCD_COLOR
        { TYPE_INT, 0, 0xffffff, { .int_p = &prefs.inactive_color },
            "inactive color", NULL },
#endif
        { TYPE_BOOL, 0, 1, { .bool_p = &prefs.wrap }, "wrap", NULL },
        { TYPE_BOOL, 0, 1, { .bool_p = &prefs.wipe }, "wipe", NULL },
        { TYPE_BOOL, 0, 1, { .bool_p = &prefs.active_one_line },
            "active one line", NULL },
        { TYPE_BOOL, 0, 1, { .bool_p = &prefs.autoscroll }, "autoscroll", NULL },
        { TYPE_INT,  0, 2, { .int_p =  &prefs.align }, "align", NULL },
        { TYPE_BOOL, 0, 1, { .bool_p = &prefs.statusbar_on },
            "statusbar on", NULL },
        { TYPE_BOOL, 0, 1, { .bool_p = &prefs.display_title },
            "display title", NULL },
        { TYPE_BOOL, 0, 1, { .bool_p = &prefs.display_time },
            "display time", NULL },
        { TYPE_BOOL, 0, 1, { .bool_p = &prefs.backlight_on },
            "backlight on", NULL },

        { TYPE_STRING, 0, sizeof(prefs.lrc_directory),
            { .string = prefs.lrc_directory }, "lrc directory", NULL },
        { TYPE_INT,  -1, NUM_CODEPAGES-1, { .int_p = &prefs.encoding },
            "encoding", NULL },
#ifdef LRC_SUPPORT_ID3
        { TYPE_BOOL, 0, 1, { .bool_p = &prefs.read_id3 }, "read id3", NULL },
#endif
    };

    if (!save)
    {
        /* initialize setting */
#if LCD_DEPTH > 1
        prefs.active_color = rb->lcd_get_foreground();
        prefs.inactive_color = LCD_LIGHTGRAY;
#endif
        prefs.wrap = true;
        prefs.wipe = true;
        prefs.active_one_line = false;
        prefs.autoscroll = true;
        prefs.align = 1; /* center */
        prefs.statusbar_on = false;
        prefs.display_title = true;
        prefs.display_time = true;
        prefs.backlight_on = false;
#ifdef LRC_SUPPORT_ID3
        prefs.read_id3 = true;
#endif
        rb->strcpy(prefs.lrc_directory, "/Lyrics");
        prefs.encoding = -1; /* default codepage */

        configfile_load(config_file, config, ARRAYLEN(config), 0);
    }
    else if (rb->memcmp(&old_prefs, &prefs, sizeof(prefs)))
    {
        rb->splash(0, "Saving Settings");
        configfile_save(config_file, config, ARRAYLEN(config), 0);
    }
    rb->memcpy(&old_prefs, &prefs, sizeof(prefs));
}

static bool lrc_theme_menu(void)
{
    enum {
        LRC_MENU_STATUSBAR,
        LRC_MENU_DISP_TITLE,
        LRC_MENU_DISP_TIME,
#ifdef HAVE_LCD_COLOR
        LRC_MENU_INACTIVE_COLOR,
#endif
        LRC_MENU_BACKLIGHT,
    };

    int selected = 0;
    bool exit = false, usb = false;

    MENUITEM_STRINGLIST(menu, "Theme Settings", NULL,
                        "Show Statusbar", "Display Title",
                        "Display Time",
#ifdef HAVE_LCD_COLOR
                        "Inactive Colour",
#endif
                        "Backlight Always On");

    while (!exit && !usb)
    {
        switch (rb->do_menu(&menu, &selected, NULL, false))
        {
            case LRC_MENU_STATUSBAR:
                usb = rb->set_bool("Show Statusbar", &prefs.statusbar_on);
                break;
            case LRC_MENU_DISP_TITLE:
                usb = rb->set_bool("Display Title", &prefs.display_title);
                break;
            case LRC_MENU_DISP_TIME:
                usb = rb->set_bool("Display Time", &prefs.display_time);
                break;
#ifdef HAVE_LCD_COLOR
            case LRC_MENU_INACTIVE_COLOR:
                usb = rb->set_color(NULL, "Inactive Colour",
                                    &prefs.inactive_color, -1);
                break;
#endif
            case LRC_MENU_BACKLIGHT:
                usb = rb->set_bool("Backlight Always On", &prefs.backlight_on);
                break;
            case MENU_ATTACHED_USB:
                usb = true;
                break;
            default:
                exit = true;
                break;
        }
    }

    return usb;
}

static bool lrc_display_menu(void)
{
    enum {
        LRC_MENU_WRAP,
        LRC_MENU_WIPE,
        LRC_MENU_ALIGN,
        LRC_MENU_LINE_MODE,
        LRC_MENU_SCROLL_MODE,
    };

    int selected = 0;
    bool exit = false, usb = false;

    MENUITEM_STRINGLIST(menu, "Display Settings", NULL,
                        "Wrap", "Wipe", "Alignment",
                        "Activate Only Current Line",
                        "Automatic Scrolling");

    struct opt_items align_names[] = {
        {"Left", -1}, {"Centre", -1}, {"Right", -1},
    };

    while (!exit && !usb)
    {
        switch (rb->do_menu(&menu, &selected, NULL, false))
        {
            case LRC_MENU_WRAP:
                usb = rb->set_bool("Wrap", &prefs.wrap);
                break;
            case LRC_MENU_WIPE:
                usb = rb->set_bool("Wipe", &prefs.wipe);
                break;
            case LRC_MENU_ALIGN:
                usb = rb->set_option("Alignment", &prefs.align, RB_INT,
                                     align_names, 3, NULL);
                break;
            case LRC_MENU_LINE_MODE:
                usb = rb->set_bool("Activate Only Current Line",
                                        &prefs.active_one_line);
                break;
            case LRC_MENU_SCROLL_MODE:
                usb = rb->set_bool("Automatic Scrolling", &prefs.autoscroll);
                break;
            case MENU_ATTACHED_USB:
                usb = true;
                break;
            default:
                exit = true;
                break;
        }
    }

    return usb;
}

static bool lrc_lyrics_menu(void)
{
    enum {
        LRC_MENU_ENCODING,
#ifdef LRC_SUPPORT_ID3
        LRC_MENU_READ_ID3,
#endif
        LRC_MENU_LRC_DIR,
    };

    int selected = 0;
    bool exit = false, usb = false;

    struct opt_items cp_names[NUM_CODEPAGES+1];
    int old_val;

    MENUITEM_STRINGLIST(menu, "Lyrics Settings", NULL,
                        "Encoding",
#ifdef LRC_SUPPORT_ID3
                        "Read ID3 tag",
#endif
                        "Lrc Directry");

    cp_names[0].string = "Use default codepage";
    cp_names[0].voice_id = -1;
    for (old_val = 1; old_val < NUM_CODEPAGES+1; old_val++)
    {
        cp_names[old_val].string = rb->get_codepage_name(old_val-1);
        cp_names[old_val].voice_id = -1;
    }

    while (!exit && !usb)
    {
        switch (rb->do_menu(&menu, &selected, NULL, false))
        {
            case LRC_MENU_ENCODING:
                prefs.encoding++;
                old_val = prefs.encoding;
                usb = rb->set_option("Encoding", &prefs.encoding, RB_INT,
                                     cp_names, NUM_CODEPAGES+1, NULL);
                if (prefs.encoding != old_val)
                {
                    save_changes();
                    if (current.type < NUM_TYPES)
                    {
                        /* let reload lrc file to apply encoding setting */
                        reset_current_data();
                    }
                }
                prefs.encoding--;
                break;
#ifdef LRC_SUPPORT_ID3
            case LRC_MENU_READ_ID3:
                usb = rb->set_bool("Read ID3 tag", &prefs.read_id3);
                break;
#endif
            case LRC_MENU_LRC_DIR:
                rb->strcpy(temp_buf, prefs.lrc_directory);
                if (!rb->kbd_input(temp_buf, sizeof(prefs.lrc_directory), NULL))
                    rb->strcpy(prefs.lrc_directory, temp_buf);
                break;
            case MENU_ATTACHED_USB:
                usb = true;
                break;
            default:
                exit = true;
                break;
        }
    }

    return usb;
}

#ifdef LRC_DEBUG
static const char* lrc_debug_data(int selected, void * data,
                                  char * buffer, size_t buffer_len)
{
    (void)data;
    switch (selected)
    {
        case 0:
            rb->strlcpy(buffer, current.mp3_file, buffer_len);
            break;
        case 1:
            rb->strlcpy(buffer, current.lrc_file, buffer_len);
            break;
        case 2:
            rb->snprintf(buffer, buffer_len, "buf usage: %d,%d/%d",
                            (int)lrc_buffer_used, (int)lrc_buffer_end,
                            (int)lrc_buffer_size);
            break;
        case 3:
            rb->snprintf(buffer, buffer_len, "line count: %d,%d",
                            current.nlrcline, current.nlrcbrpos);
            break;
        case 4:
            rb->snprintf(buffer, buffer_len, "loaded lrc? %s",
                            current.loaded_lrc?"yes":"no");
            break;
        case 5:
            rb->snprintf(buffer, buffer_len, "too many lines? %s",
                            current.too_many_lines?"yes":"no");
            break;
        default:
            return NULL;
    }
    return buffer;
}

static bool lrc_debug_menu(void)
{
    struct simplelist_info info;
    rb->simplelist_info_init(&info, "Debug", 6, NULL);
    info.scroll_all = true;
    info.get_name = lrc_debug_data;
    return rb->simplelist_show_list(&info);
}
#endif

/* returns one of enum lrc_screen or enum plugin_status */
static int lrc_menu(void)
{
    enum {
        LRC_MENU_THEME,
        LRC_MENU_DISPLAY,
        LRC_MENU_LYRICS,
        LRC_MENU_PLAYBACK,
#ifdef LRC_DEBUG
        LRC_MENU_DEBUG,
#endif
        LRC_MENU_OFFSET,
        LRC_MENU_TIMETAG_EDITOR,
        LRC_MENU_QUIT,
    };

    MENUITEM_STRINGLIST(menu, "Lrcplayer", NULL,
                        "Theme Settings",
                        "Display Settings",
                        "Lyrics Settings",
                        "Playback Control",
#ifdef LRC_DEBUG
                        "Debug",
#endif
                        "Time Offset", "Timetag Editor",
                        "Quit");
    int selected = 0, ret = LRC_GOTO_MENU;
    bool usb = false;

    while (ret == LRC_GOTO_MENU)
    {
        switch (rb->do_menu(&menu, &selected, NULL, false))
        {
            case LRC_MENU_THEME:
                usb = lrc_theme_menu();
                break;
            case LRC_MENU_DISPLAY:
                usb = lrc_display_menu();
                break;
            case LRC_MENU_LYRICS:
                usb = lrc_lyrics_menu();
                break;
            case LRC_MENU_PLAYBACK:
                usb = playback_control(NULL);
                ret = LRC_GOTO_MAIN;
                break;
#ifdef LRC_DEBUG
            case LRC_MENU_DEBUG:
                usb = lrc_debug_menu();
                ret = LRC_GOTO_MAIN;
                break;
#endif
            case LRC_MENU_OFFSET:
                usb = (lrc_set_time("Time Offset", "sec", &current.offset,
                                    10, -60*1000, 60*1000,
                                    LST_SET_MSEC|LST_SET_SEC) == 1);
                ret = LRC_GOTO_MAIN;
                break;
            case LRC_MENU_TIMETAG_EDITOR:
                ret = LRC_GOTO_EDITOR;
                break;
            case LRC_MENU_QUIT:
                ret = PLUGIN_OK;
                break;
            case MENU_ATTACHED_USB:
                usb = true;
                break;
            default:
                ret = LRC_GOTO_MAIN;
                break;
        }
        if (usb)
            ret = PLUGIN_USB_CONNECTED;
    }
    return ret;
}

/*******************************
 * Main.
 *******************************/
/* returns true if song has changed to know when to load new lyrics. */
static bool check_audio_status(void)
{
    static int last_audio_status = 0;
    if (current.ff_rewind == -1)
        current.audio_status = rb->audio_status();
    current.id3 = rb->audio_current_track();
    if ((last_audio_status^current.audio_status)&AUDIO_STATUS_PLAY)
    {
        last_audio_status = current.audio_status;
        return true;
    }
    if (AUDIO_STOP || current.id3 == NULL)
        return false;
    if (rb->strcmp(current.mp3_file, current.id3->path))
    {
        return true;
    }
    return false;
}
static void ff_rewind(long time)
{
    rb->audio_ff_rewind(time);
    rb->sleep(HZ/10); /* take effect seeking */
}

static int handle_button(void)
{
    int ret = LRC_GOTO_MAIN;
    static int step = 0;
    int limit, button = rb->get_action(CONTEXT_WPS, HZ/10);
    switch (button)
    {
        case ACTION_WPS_BROWSE:
        case ACTION_WPS_STOP:
            save_changes();
            ret = PLUGIN_OK;
            break;
        case ACTION_WPS_PLAY:
            if (AUDIO_STOP && rb->global_status->resume_index != -1)
            {
                if (rb->playlist_resume() != -1)
                {
                    rb->playlist_resume_track(rb->global_status->resume_index,
                        rb->global_status->resume_crc32,
                        rb->global_status->resume_elapsed,
                        rb->global_status->resume_offset);
                }
            }
            else if (AUDIO_PAUSE)
                rb->audio_resume();
            else
                rb->audio_pause();
            break;
        case ACTION_WPS_SEEKFWD:
        case ACTION_WPS_SEEKBACK:
            if (AUDIO_STOP)
                break;
            if (current.ff_rewind > -1)
            {
                if (button == ACTION_WPS_SEEKFWD)
                    /* fast forwarding, calc max step relative to end */
                    limit = (current.length - current.ff_rewind) * 3 / 100;
                else
                    /* rewinding, calc max step relative to start */
                    limit = (current.ff_rewind) * 3 / 100;
                limit = MAX(limit, 500);

                if (step > limit)
                    step = limit;

                if (button == ACTION_WPS_SEEKFWD)
                    current.ff_rewind += step;
                else
                    current.ff_rewind -= step;

                if (current.ff_rewind > current.length-100)
                    current.ff_rewind = current.length-100;
                if (current.ff_rewind < 0)
                    current.ff_rewind = 0;

                /* smooth seeking by multiplying step by: 1 + (2 ^ -accel) */
                step += step >> (rb->global_settings->ff_rewind_accel + 3);
            }
            else
            {
                current.ff_rewind = current.elapsed;
                rb->audio_pre_ff_rewind();
                step = 1000 * rb->global_settings->ff_rewind_min_step;
            }
            break;
        case ACTION_WPS_STOPSEEK:
            if (current.ff_rewind == -1)
                break;
            ff_rewind(current.ff_rewind);
            current.elapsed = current.ff_rewind;
            current.ff_rewind = -1;
            break;
        case ACTION_WPS_SKIPNEXT:
            rb->audio_next();
            break;
        case ACTION_WPS_SKIPPREV:
            if (current.elapsed < 3000)
            {
                rb->audio_prev();
            }
            else
            {
                rb->audio_pre_ff_rewind();
                ff_rewind(0);
            }
            break;
        case ACTION_WPS_VOLDOWN:
            if (!prefs.autoscroll)
            {
                current.scroll -= 3000;
                if (current.scroll < 0)
                    current.scroll = 0;
            }
            else
            {
                rb->adjust_volume(-1);
            }
            break;
        case ACTION_WPS_VOLUP:
            if (!prefs.autoscroll)
            {
                current.scroll += 3000;
                if (current.scroll > current.length)
                    current.scroll = current.length;
            }
            else
            {
                rb->adjust_volume(1);
            }
            break;
        case ACTION_WPS_CONTEXT:
            ret = LRC_GOTO_EDITOR;
            break;
        case ACTION_WPS_MENU:
            ret = LRC_GOTO_MENU;
            break;
        default:
            if(rb->default_event_handler(button) == SYS_USB_CONNECTED)
                ret = PLUGIN_USB_CONNECTED;
            break;
    }
    return ret;
}

static int lrc_main(void)
{
    int ret = LRC_GOTO_MAIN;
    long id3_timeout = 0;
    bool update_display_state = true;

    /* y offset of vp_lyrics */
    int h = (prefs.display_title?font_ui_height:0)+
            (prefs.display_time?SYSFONT_HEIGHT*2:0);


    FOR_NB_SCREENS(i)
    {
        rb->viewportmanager_theme_enable(i, prefs.statusbar_on, &vp_info[i]);
        vp_info[i].font = uifont;
        vp_lyrics[i] = vp_info[i];
        vp_lyrics[i].flags &= ~VP_FLAG_ALIGNMENT_MASK;
        vp_lyrics[i].y += h;
        vp_lyrics[i].height -= h;
    }

#ifdef HAVE_BACKLIGHT
    if (prefs.backlight_on)
        backlight_ignore_timeout();
#endif

    /* in case settings that may affect break position
     * are changed (statusbar_on and wrap). */
    if (!current.too_many_lines)
        calc_brpos(NULL, 0);

    while (ret == LRC_GOTO_MAIN)
    {
        if (check_audio_status())
        {
            update_display_state = true;
            if (AUDIO_STOP)
            {
                current.id3 = NULL;
                id3_timeout = 0;
            }
            else if (rb->strcmp(current.mp3_file, current.id3->path))
            {
                save_changes();
                reset_current_data();
                rb->strcpy(current.mp3_file, current.id3->path);
                id3_timeout = *rb->current_tick+HZ*3;
                current.found_lrc = false;
            }
        }
        if (current.id3 && current.id3->length)
        {
            if (current.ff_rewind == -1)
            {
                long di = current.id3->elapsed - current.elapsed;
                if (di < -250 || di > 0)
                    current.elapsed = current.id3->elapsed;
            }
            else
                current.elapsed = current.ff_rewind;
            current.length = current.id3->length;
            if (current.elapsed > current.length)
                current.elapsed = current.length;
        }
        else
        {
            current.elapsed = 0;
            current.length =  1;
            current.scroll =  0;
        }

        if (current.id3 && id3_timeout &&
            (TIME_AFTER(*rb->current_tick, id3_timeout) ||
                current.id3->artist))
        {
            update_display_state = true;
            id3_timeout = 0;

            current.found_lrc = find_lrc_file();
#ifdef LRC_SUPPORT_ID3
            if (!current.found_lrc && prefs.read_id3)
            {
                /* no lyrics file found. try to read from id3 tag. */
                current.found_lrc = read_id3();
            }
#endif
        }
        else if (current.found_lrc && !current.loaded_lrc)
        {
            /* current.loaded_lrc is false after changing encode setting */
            update_display_state = true;
            display_state();
            load_lrc_file();
        }
        if (update_display_state)
        {
            if (current.type == TXT || current.type == ID3_USLT)
                current.wipe = false;
            else
                current.wipe = prefs.wipe;
            display_state();
            update_display_state = false;
        }
        if (AUDIO_PLAY)
        {
            if (prefs.display_time)
                display_time();
            if (!id3_timeout)
                display_lrcs();
        }

        ret = handle_button();
    }

    FOR_NB_SCREENS(i)
        rb->viewportmanager_theme_undo(i, false);

#ifdef HAVE_BACKLIGHT
    if (prefs.backlight_on)
        backlight_use_settings();
#endif

    return ret;
}

/* this is the plugin entry point */
enum plugin_status plugin_start(const void* parameter)
{
    int ret = LRC_GOTO_MAIN;

    /* initialize settings. */
    load_or_save_settings(false);

    uifont = rb->screens[0]->getuifont();
    font_ui_height = rb->font_get(uifont)->height;

    lrc_buffer = rb->plugin_get_buffer(&lrc_buffer_size);
    lrc_buffer = ALIGN_UP(lrc_buffer, 4); /* 4 bytes aligned */
    lrc_buffer_size = (lrc_buffer_size - 4)&~3;

    reset_current_data();
    current.id3 = NULL;
    current.mp3_file[0] = 0;
    current.lrc_file[0] = 0;
    current.ff_rewind = -1;
    current.found_lrc = false;
    current.scroll = 0;
    if (parameter && check_audio_status())
    {
        const char *ext;
        rb->strcpy(current.mp3_file, current.id3->path);
        /* use passed parameter as lrc file. */
        rb->strcpy(current.lrc_file, parameter);
        if (!rb->file_exists(current.lrc_file))
        {
            rb->splash(HZ, "Specified file does not exist.");
            return PLUGIN_ERROR;
        }
        ext = rb->strrchr(current.lrc_file, '.');
        if (!ext) ext = current.lrc_file;
        for (current.type = 0; current.type < NUM_TYPES; current.type++)
        {
            if (!rb->strcasecmp(ext, extentions[current.type]))
                break;
        }
        if (current.type == NUM_TYPES)
        {
            rb->splashf(HZ, "%s is not supported", ext);
            return PLUGIN_ERROR;
        }
        current.found_lrc = true;
    }

    while (ret >= PLUGIN_OTHER)
    {
        switch (ret)
        {
            case LRC_GOTO_MAIN:
                ret = lrc_main();
                break;
            case LRC_GOTO_MENU:
                ret = lrc_menu();
                break;
            case LRC_GOTO_EDITOR:
                ret = timetag_editor();
                break;
            default:
                ret = PLUGIN_ERROR;
                break;
        }
    }

    load_or_save_settings(true);
    return ret;
}
