/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2007 Nicolas Pennequin, Jonathan Gordon
 *
 * 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 <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <ctype.h>
#include <string.h>
#include "system.h"
#include "audio.h"
#include "kernel.h"
#include "logf.h"
#include "misc.h"
#include "screens.h"
#include "list.h"
#include "action.h"
#include "lang.h"
#include "debug.h"
#include "settings.h"
#include "plugin.h"
#include "playback.h"
#include "cuesheet.h"
#include "gui/wps.h"

#define CUE_DIR ROCKBOX_DIR "/cue"

static bool search_for_cuesheet(const char *path, struct cuesheet_file *cue_file)
{
    size_t len;
    char cuepath[MAX_PATH];
    char *dot, *slash, *slash_cuepath;

    cue_file->pos = 0;
    cue_file->size = 0;
    cue_file->path[0] = '\0';
    slash = strrchr(path, '/');
    if (!slash)
        return false;
    len = strlcpy(cuepath, path, MAX_PATH);
    slash_cuepath = &cuepath[slash - path];
    dot = strrchr(slash_cuepath, '.');
    if (dot)
        strmemccpy(dot, ".cue", MAX_PATH - (dot-cuepath));

    if (!dot || !file_exists(cuepath))
    {
        strcpy(cuepath, CUE_DIR);
        if (strlcat(cuepath, slash, MAX_PATH) >= MAX_PATH)
            goto skip; /* overflow */
        dot = strrchr(cuepath, '.');
        if (dot)
            strcpy(dot, ".cue");
        if (!file_exists(cuepath))
        {
skip:
            if ((len+4) >= MAX_PATH)
                return false;
            strmemccpy(cuepath, path, MAX_PATH);
            strlcat(cuepath, ".cue", MAX_PATH);
            if (!file_exists(cuepath))
                return false;
        }
    }

    strmemccpy(cue_file->path, cuepath, MAX_PATH);
    return true;
}

bool look_for_cuesheet_file(struct mp3entry *track_id3, struct cuesheet_file *cue_file)
{
    /* DEBUGF("look for cue file\n"); */
    if (track_id3->has_embedded_cuesheet)
    {
        cue_file->pos = track_id3->embedded_cuesheet.pos;
        cue_file->size = track_id3->embedded_cuesheet.size;
        cue_file->encoding = track_id3->embedded_cuesheet.encoding;
        strmemccpy(cue_file->path, track_id3->path, MAX_PATH);
        return true;
    }

    return search_for_cuesheet(track_id3->path, cue_file);
}

static char *get_string(const char *line)
{
    char *start, *end;

    start = strchr(line, '"');
    if (!start)
    {
        start = strchr(line, ' ');

        if (!start)
            return NULL;
    }

    end = strchr(++start, '"');
    if (end)
        *end = '\0';

    return start;
}

static unsigned long parse_cue_index(const char *line)
{
    /* assumes strncmp(line, "INDEX 01", 8) & NULL terminated string */
    /* INDEX 01 MM:SS:FF\0 (00:00:00\0 - 99:99:99\0)*/
    const unsigned field_m[3] = {60 * 1000, 1000, 13}; /* MM:SS:~FF*/
    const unsigned field_max[3] = {30000, 59, 74}; /* MM, SS, FF */
    const char f_sep = ':';
    int field = -1;
    unsigned long offset = 0; /* ms from start of track */
    unsigned long value = 0;
    while (*line)
    {
        if (!isdigit(*line))  /* search for numbers */
        {
            line++;
            continue;
        }

        while (isdigit(*line))
        {
            value = 10 * value + (*line - '0');
            if (field >= 0 && value > field_max[field]) /* Sanity check bail early */
                return 0;
            line++;
        }

        if (field < 0) /*Filter INDEX 01*/
        {
            /* safe to assume value == 1 */
        }
        else if (field <= 2)
        {
            while(*line && *line != f_sep)
                    line++;

            if (*line || field == 2) /* if *line valid we found f_sep */
                offset += (unsigned long) field_m[field] * value;
        }
        else
            break;

        value = 0;
        field++;
    }

    return offset;
}

enum eCS_SUPPORTED_TAGS {
    eCS_TRACK = 0, eCS_INDEX_01, eCS_TITLE,
    eCS_PERFORMER, eCS_SONGWRITER, eCS_FILE,
    eCS_COUNT_TAGS_COUNT, eCS_NOTFOUND = -1
};

static enum eCS_SUPPORTED_TAGS cuesheet_tag_get_option(const char *option)
{
    #define CS_OPTN(str) {str, sizeof(str)-1}
    static const struct cs_option_t {
        const char *str;
        const int len;
    } cs_options[eCS_COUNT_TAGS_COUNT + 1] = {
        [eCS_TRACK] = CS_OPTN("TRACK"),
        [eCS_INDEX_01] = CS_OPTN("INDEX 01"),
        [eCS_TITLE] =CS_OPTN("TITLE"),
        [eCS_PERFORMER] =CS_OPTN("PERFORMER"),
        [eCS_SONGWRITER] =CS_OPTN("SONGWRITER"),
        [eCS_FILE] =CS_OPTN("FILE"),
        [eCS_COUNT_TAGS_COUNT] = {NULL, 0} /*SENTINEL*/
    };

    const struct cs_option_t *op;
    for (int i=0; ((op=&cs_options[i]))->str != NULL; i++)
    {
        if (strncmp(option, op->str, op->len) == 0)
            return i;
    }
    return eCS_NOTFOUND;
#undef CS_OPTN
}

/* parse cuesheet "cue_file" and store the information in "cue" */
bool parse_cuesheet(struct cuesheet_file *cue_file, struct cuesheet *cue)
{
    char line[MAX_PATH];
    char *s;
    unsigned char char_enc = CHAR_ENC_ISO_8859_1;
    bool is_embedded = false;
    int line_len;
    int bytes_left = 0;
    int read_bytes = MAX_PATH;
    unsigned char utf16_buf[MAX_PATH];

    int fd = open(cue_file->path, O_RDONLY, 0644);
    if(fd < 0)
        return false;
    if (cue_file->pos > 0)
    {
        is_embedded = true;
        lseek(fd, cue_file->pos, SEEK_SET);
        bytes_left = cue_file->size;
        char_enc = cue_file->encoding;
    }

    /* Look for a Unicode BOM */
    unsigned char bom_read = 0;
    if (read(fd, line, BOM_UTF_8_SIZE) > 0)
    {
        if(!memcmp(line, BOM_UTF_8, BOM_UTF_8_SIZE))
        {
            char_enc = CHAR_ENC_UTF_8;
            bom_read = BOM_UTF_8_SIZE;
        }
        else
        {
            bool le;
            if (utf16_has_bom(line, &le))
            {
                char_enc = le ? CHAR_ENC_UTF_16_LE : CHAR_ENC_UTF_16_BE;
                bom_read = BOM_UTF_16_SIZE;
            }
        }
    }

    if (bom_read < BOM_UTF_8_SIZE)
        lseek(fd, cue_file->pos + bom_read, SEEK_SET);
    if (is_embedded)
    {
        if (bom_read  > 0)
            bytes_left -= bom_read;
        if (read_bytes > bytes_left)
            read_bytes = bytes_left;
    }

    /* Initialization */
    memset(cue, 0, sizeof(struct cuesheet));
    strcpy(cue->path, cue_file->path);
    cue->curr_track = cue->tracks;

    if (is_embedded)
        strcpy(cue->file, cue->path);

    while ((line_len = read_line(fd, line, read_bytes)) > 0
        && cue->track_count < MAX_TRACKS )
    {
        if (char_enc == CHAR_ENC_UTF_16_LE)
        {
            s = utf16decode(line, utf16_buf, line_len>>1, sizeof(utf16_buf) - 1, true);
            /* terminate the string at the newline */
            *s = '\0';
            strcpy(line, utf16_buf);
            /* chomp the trailing 0 after the newline */
            lseek(fd, 1, SEEK_CUR);
            line_len++;
        }
        else if (char_enc == CHAR_ENC_UTF_16_BE)
        {
            s = utf16decode(line, utf16_buf, line_len>>1, sizeof(utf16_buf) - 1, false);
            *s = '\0';
            strcpy(line, utf16_buf);
        }
        s = skip_whitespace(line);

/*   RECOGNIZED  TAGS *********************** 
*    eCS_TRACK = 0, eCS_INDEX_01, eCS_TITLE,
*    eCS_PERFORMER, eCS_SONGWRITER, eCS_FILE,
*/
        enum eCS_SUPPORTED_TAGS option = cuesheet_tag_get_option(s);
        if (option == eCS_TRACK)
        {
            cue->track_count++;
        }
        else if (option == eCS_INDEX_01)
        {
#if 0
            s = strchr(s,' ');
            s = skip_whitespace(s);
            s = strchr(s,' ');
            s = skip_whitespace(s);
            cue->tracks[cue->track_count-1].offset = 60*1000 * atoi(s);
            s = strchr(s,':') + 1;
            cue->tracks[cue->track_count-1].offset += 1000 * atoi(s);
            s = strchr(s,':') + 1;
            cue->tracks[cue->track_count-1].offset += 13 * atoi(s);
#else
            cue->tracks[cue->track_count-1].offset = parse_cue_index(s);
#endif
        }
        else if (option != eCS_NOTFOUND) 
        {
            char *dest = NULL;
            char *string = get_string(s);
            if (!string)
                break;

            size_t count = MAX_NAME*3 + 1;

            switch (option)
            {
                case eCS_TITLE: /* TITLE */
                    dest = (cue->track_count <= 0) ? cue->title :
                            cue->tracks[cue->track_count-1].title;
                    break;

                case eCS_PERFORMER: /* PERFORMER */
                    dest = (cue->track_count <= 0) ? cue->performer :
                        cue->tracks[cue->track_count-1].performer;
                    break;

                case eCS_SONGWRITER: /* SONGWRITER */
                    dest = (cue->track_count <= 0) ? cue->songwriter :
                            cue->tracks[cue->track_count-1].songwriter;
                    break;

                case eCS_FILE: /* FILE */
                    if (is_embedded || cue->track_count > 0)
                        break;

                    dest = cue->file;
                    count = MAX_PATH;
                    break;
                case eCS_TRACK:
                    /*Fall-Through*/
                case eCS_INDEX_01:
                    /*Fall-Through*/
                case eCS_COUNT_TAGS_COUNT:
                    /*Fall-Through*/
                case eCS_NOTFOUND: /*Shouldn't happen*/
                    logf(HZ * 2, "Bad Tag %d @ %s", (int) option, __func__);
                    dest = NULL;
                    break;
            }

            if (dest)
            {
                if (char_enc == CHAR_ENC_ISO_8859_1)
                {
                    dest = iso_decode_ex(string, dest, -1,
                        strlen(string), count - 1);
                    *dest = '\0';
                }
                else
                {
                    strmemccpy(dest, string, count);
                }
            }
        }

        if (is_embedded)
        {
            bytes_left -= line_len;
            if (bytes_left <= 0)
                break;
            if (bytes_left < read_bytes)
                read_bytes = bytes_left;
        }
    }
    close(fd);

    /* If just a filename, add path information from cuesheet path */
    if (*cue->file && !strrchr(cue->file, '/'))
    {
        strcpy(line, cue->file);
        strcpy(cue->file, cue->path);
        char *slash = strrchr(cue->file, '/');
        if (!slash++) slash = cue->file;
        strmemccpy(slash, line, MAX_PATH - (slash - cue->file));
    }

    /* If some songs don't have performer info, we copy the cuesheet performer */
    int i;
    for (i = 0; i < cue->track_count; i++)
    {
        if (*(cue->tracks[i].performer) == '\0')
            strmemccpy(cue->tracks[i].performer, cue->performer, MAX_NAME*3);

        if (*(cue->tracks[i].songwriter) == '\0')
            strmemccpy(cue->tracks[i].songwriter, cue->songwriter, MAX_NAME*3);
    }

    return true;
}

/* takes care of seeking to a track in a playlist
 * returns false if audio  isn't playing */
static bool seek(unsigned long pos)
{
    if (!(audio_status() & AUDIO_STATUS_PLAY))
    {
        return false;
    }
    else
    {
        audio_pre_ff_rewind();
        audio_ff_rewind(pos);
        return true;
    }
}

/* returns the index of the track currently being played
   and updates the information about the current track. */
int cue_find_current_track(struct cuesheet *cue, unsigned long curpos)
{
    int i=0;
    while (i < cue->track_count-1 && cue->tracks[i+1].offset < curpos)
        i++;

    cue->curr_track_idx = i;
    cue->curr_track = cue->tracks + i;
    return i;
}

/* callback that gives list item titles for the cuesheet browser */
static const char* list_get_name_cb(int selected_item,
                                    void *data,
                                    char *buffer,
                                    size_t buffer_len)
{
    struct cuesheet *cue = (struct cuesheet *)data;

    if (selected_item & 1)
        strmemccpy(buffer, cue->tracks[selected_item/2].title, buffer_len);
    else
        snprintf(buffer, buffer_len, "%02d. %s", selected_item/2+1,
                 cue->tracks[selected_item/2].performer);

    return buffer;
}

void browse_cuesheet(struct cuesheet *cue)
{
    struct gui_synclist lists;
    int action;
    bool done = false;
    char title[MAX_PATH];
    int len;

    struct cuesheet_file cue_file;
    struct mp3entry *id3 = audio_current_track();

    len = snprintf(title, sizeof(title), "%s: %s", cue->performer, cue->title);

    if ((unsigned) len > sizeof(title))
        title[sizeof(title) - 2] = '~'; /* give indication of truncation */


    gui_synclist_init(&lists, list_get_name_cb, cue, false, 2, NULL);
    gui_synclist_set_nb_items(&lists, 2*cue->track_count);
    gui_synclist_set_title(&lists, title, 0);


    if (id3)
    {
        gui_synclist_select_item(&lists,
                                 2*cue_find_current_track(cue, id3->elapsed));
    }

    while (!done)
    {
        gui_synclist_draw(&lists);
        action = get_action(CONTEXT_LIST,TIMEOUT_BLOCK);
        if (gui_synclist_do_button(&lists, &action))
            continue;
        switch (action)
        {
            case ACTION_STD_OK:
            {
                bool startit = true;
                unsigned long elapsed =
                    cue->tracks[gui_synclist_get_sel_pos(&lists)/2].offset;

                id3 = audio_current_track();
                if (id3 && *id3->path)
                {
                    look_for_cuesheet_file(id3, &cue_file);
                    if (!strcmp(cue->path, cue_file.path))
                        startit = false;
                }

                if (!startit)
                    startit = !seek(elapsed);

                if (!startit || !*cue->file)
                    break;

                /* check that this cue is the same one that would be found by
                   a search from playback */
                char file[MAX_PATH];
                strmemccpy(file, cue->file, MAX_PATH);

                if (!strcmp(cue->path, file) || /* if embedded */
                    (search_for_cuesheet(file, &cue_file) &&
                     !strcmp(cue->path, cue_file.path)))
                {
                    char *fname = strrsplt(file, '/');
                    char *dirname = fname <= file + 1 ? "/" : file;
                    bookmark_play(dirname, 0, elapsed, 0, current_tick, fname);
                }
                break;
                } /* ACTION_STD_OK */

            case ACTION_STD_CANCEL:
                done = true;
            default:
                break;
        }
    }
}

bool display_cuesheet_content(char* filename)
{
    size_t bufsize = 0;
    struct cuesheet_file cue_file;
    struct cuesheet *cue = (struct cuesheet *)plugin_get_buffer(&bufsize);
    if (!cue || bufsize < sizeof(struct cuesheet))
        return false;

    strmemccpy(cue_file.path, filename, MAX_PATH);
    cue_file.pos = 0;
    cue_file.size = 0;

    if (!parse_cuesheet(&cue_file, cue))
        return false;

    browse_cuesheet(cue);
    return true;
}

/* skips backwards or forward in the current cuesheet
 * the return value indicates whether we're still in a cusheet after skipping
 * it also returns false if we weren't in a cuesheet.
 * direction should be 1 or -1.
 */
bool curr_cuesheet_skip(struct cuesheet *cue, int direction, unsigned long curr_pos)
{
    int track = cue_find_current_track(cue, curr_pos);

    if (direction >= 0 && track == cue->track_count - 1)
    {
        /* we want to get out of the cuesheet */
        return false;
    }
    else
    {
        if (!(direction <= 0 && track == 0))
        {
            /* If skipping forward, skip to next cuesheet segment. If skipping
            backward before DEFAULT_SKIP_THRESH milliseconds have elapsed, skip
            to previous cuesheet segment. If skipping backward after
            DEFAULT_SKIP_THRESH seconds have elapsed, skip to the start of the
            current cuesheet segment */
            if (direction == 1 ||
                  ((curr_pos - cue->tracks[track].offset) < DEFAULT_SKIP_THRESH))
            {
                track += direction;
            }
        }

        seek(cue->tracks[track].offset);
        return true;
    }

}

static inline void draw_veritcal_line_mark(struct screen * screen,
                                           int x, int y, int h)
{
    screen->set_drawmode(DRMODE_COMPLEMENT);
    screen->vline(x, y, y+h-1);
}

/* draw the cuesheet markers for a track of length "tracklen",
   between (x,y) and (x+w,y) */
void cue_draw_markers(struct screen *screen, struct cuesheet *cue,
                      unsigned long tracklen,
                      int x, int y, int w, int h)
{
    int i,xi;
    unsigned long tracklen_seconds = tracklen/1000; /* duration in seconds */

    for (i=1; i < cue->track_count; i++)
    {
        /* Convert seconds prior to multiplication to avoid overflow. */
        xi = x + (w * (cue->tracks[i].offset/1000)) / tracklen_seconds;
        draw_veritcal_line_mark(screen, xi, y, h);
    }
}

bool cuesheet_subtrack_changed(struct mp3entry *id3)
{
    struct cuesheet *cue = id3->cuesheet;
    if (cue && (id3->elapsed < cue->curr_track->offset
            || (cue->curr_track_idx < cue->track_count - 1
                && id3->elapsed >= (cue->curr_track+1)->offset)))
    {
        cue_find_current_track(cue, id3->elapsed);
        return true;
    }
    return false;
}
