/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2005 by Björn Stenberg
 *
 * All files in this archive are subject to the GNU General Public License.
 * See the file COPYING in the source tree root for full license agreement.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include "file.h"
#include "screens.h"
#include "kernel.h"
#include "tree.h"
#include "lcd.h"
#include "font.h"
#include "settings.h"
#include "icons.h"
#include "status.h"
#include "debug.h"
#include "button.h"
#include "menu.h"
#include "main_menu.h"
#include "mpeg.h"
#include "misc.h"
#include "ata.h"
#include "playlist.h"
#include "filetypes.h"
#include "applimits.h"
#include "dbtree.h"
#include "icons.h"
#include "lang.h"
#include "keyboard.h"
#include "autoconf.h"
#include "list.h"
#include "splash.h"

static int db_play_folder(struct tree_context* c);
static int db_search(struct tree_context* c, char* string);

static char searchstring[32];

int db_load(struct tree_context* c)
{
    int i, offset, rc;
    int dcachesize = global_settings.max_files_in_dir * sizeof(struct entry);
    int itemcount, stringlen, hits=0;
    unsigned long* nptr = (void*) c->name_buffer;
    unsigned long* dptr = c->dircache;
    unsigned long* safeplace = NULL;
    int safeplacelen = 0;

    int table = c->currtable;
    int extra = c->currextra;

    char* end_of_nbuf = c->name_buffer + c->name_buffer_size;

    if (!tagdb_initialized) {
        DEBUGF("ID3 database is not initialized.\n");
        c->filesindir = 0;
        return 0;
    }

    c->dentry_size = 2;
    c->dirfull = false;

    DEBUGF("db_load() table: %d extra: 0x%x  firstpos: %d\n", table, extra,
                                                              c->firstpos);

    if (!table) {
        table = root;
        c->currtable = table;
    }

    switch (table) {
        case root: {
            static const int tables[] = {allartists, allalbums, allsongs,
                                         search };
            char* nbuf = (char*)nptr;
            unsigned char* labels[] = { str(LANG_ID3DB_ARTISTS),
                                        str(LANG_ID3DB_ALBUMS),
                                        str(LANG_ID3DB_SONGS),
                                        str(LANG_ID3DB_SEARCH)};
            DEBUGF("dbload table root\n");
            for (i=0; i < 4; i++) {
                strcpy(nbuf, (char *)labels[i]);
                dptr[0] = (unsigned long)nbuf;
                dptr[1] = tables[i];
                nbuf += strlen(nbuf) + 1;
                dptr += 2;
            }
            c->dirlength = c->filesindir = i;
            return i;
        }

        case search: {
            static const int tables[] = {searchartists,
                                         searchalbums,
                                         searchsongs};
            char* nbuf = (char*)nptr;
            unsigned char* labels[] = { str(LANG_ID3DB_SEARCH_ARTISTS),
                                        str(LANG_ID3DB_SEARCH_ALBUMS),
                                        str(LANG_ID3DB_SEARCH_SONGS)};
            DEBUGF("dbload table search\n");
            for (i=0; i < 3; i++) {
                strcpy(nbuf, (char *)labels[i]);
                dptr[0] = (unsigned long)nbuf;
                dptr[1] = tables[i];
                nbuf += strlen(nbuf) + 1;
                dptr += 2;
            }
            c->dirlength = c->filesindir = i;
            return i;
        }

        case searchartists:
        case searchalbums:
        case searchsongs:
            DEBUGF("dbload table searchsongs/searchartists/searchalbums\n");
            i = db_search(c, searchstring);
            c->dirlength = c->filesindir = i;
            if (c->dirfull) {
                gui_syncsplash(HZ, true, str(LANG_SHOWDIR_BUFFER_FULL));
                c->dirfull = false;
            }
            else
                gui_syncsplash(HZ, true, str(LANG_ID3DB_MATCHES), i);
            return i;

        case allsongs:
            DEBUGF("dbload table allsongs\n");
            offset = tagdbheader.songstart + c->firstpos * SONGENTRY_SIZE;
            itemcount = tagdbheader.songcount;
            stringlen = tagdbheader.songlen;
            break;

        case allalbums:
            DEBUGF("dbload table allalbums\n");
            offset = tagdbheader.albumstart + c->firstpos * ALBUMENTRY_SIZE;
            itemcount = tagdbheader.albumcount;
            stringlen = tagdbheader.albumlen;
            break;

        case allartists:
            DEBUGF("dbload table allartists\n");
            offset = tagdbheader.artiststart + c->firstpos * ARTISTENTRY_SIZE;
            itemcount = tagdbheader.artistcount;
            stringlen = tagdbheader.artistlen;
            break;

        case albums4artist:
            DEBUGF("dbload table albums4artist\n");
            /* 'extra' is offset to the artist */
            safeplacelen = tagdbheader.albumarraylen * 4;
            safeplace = (void*)(end_of_nbuf - safeplacelen);
            lseek(tagdb_fd, extra + tagdbheader.artistlen, SEEK_SET);
            rc = read(tagdb_fd, safeplace, safeplacelen);
            if (rc < safeplacelen)
                return -1;

#ifdef ROCKBOX_LITTLE_ENDIAN
            for (i=0; i<tagdbheader.albumarraylen; i++)
                safeplace[i] = BE32(safeplace[i]);
#endif
            offset = safeplace[0];
            itemcount = tagdbheader.albumarraylen;
            stringlen = tagdbheader.albumlen;
            break;

        case songs4album:
            DEBUGF("dbload table songs4album\n");
            /* 'extra' is offset to the album */
            safeplacelen = tagdbheader.songarraylen * 4;
            safeplace = (void*)(end_of_nbuf - safeplacelen);
            lseek(tagdb_fd, extra + tagdbheader.albumlen + 4, SEEK_SET);
            rc = read(tagdb_fd, safeplace, safeplacelen);
            if (rc < safeplacelen)
                return -1;

#ifdef ROCKBOX_LITTLE_ENDIAN
            for (i=0; i<tagdbheader.songarraylen; i++) {
                safeplace[i] = BE32(safeplace[i]);
                DEBUGF("db_load songs4album song %d: 0x%x\n",i,safeplace[i]);
            }
#endif
            offset = safeplace[0];
            itemcount = tagdbheader.songarraylen;
            stringlen = tagdbheader.songlen;
            break;

        case songs4artist:
            DEBUGF("dbload table songs4artist\n");
            /* 'extra' is offset to the artist, used as filter */
            offset = tagdbheader.songstart + c->firstpos * SONGENTRY_SIZE;
            itemcount = tagdbheader.songcount;
            stringlen = tagdbheader.songlen;
            break;

        default:
            DEBUGF("Unsupported table %d\n", table);
            return -1;
    }
    end_of_nbuf -= safeplacelen;

    c->dirlength = itemcount;
    itemcount -= c->firstpos;

    if (!safeplace)
        lseek(tagdb_fd, offset, SEEK_SET);

    /* name_buffer (nptr) contains only names, null terminated.
       the first word of dcache (dptr) is a pointer to the name,
       the rest is table specific. see below. */

    for ( i=0; i < itemcount; i++ ) {
        int rc, skip=0;
        int intbuf[4];

        if (safeplace) {
            if (!safeplace[i]) {
                c->dirlength = i;
                break;
            }
            lseek(tagdb_fd, safeplace[i], SEEK_SET);
            offset = safeplace[i];
        }

        /* read name */
        rc = read(tagdb_fd, nptr, stringlen);
        if (rc < stringlen)
        {
            DEBUGF("%d read(%d) returned %d\n", i, stringlen, rc);
            return -1;
        }

        switch (table) {
            case allsongs:
            case songs4album:
            case songs4artist:
                rc = read(tagdb_fd, intbuf, 12);
                 /* skip the rest of the song info */
                skip = SONGENTRY_SIZE-stringlen-12;
                if (rc < 12) {
                    DEBUGF("%d read(%d) returned %d\n", i, 12, rc);
                    return -1;
                }
                /* continue to next song if wrong artist */
                if (table == songs4artist && (int)BE32(intbuf[0]) != extra) {
                     lseek(tagdb_fd, skip, SEEK_CUR);
                    continue;
                }

                /* save offset of filename */
                dptr[1] = BE32(intbuf[2]);
                break;

            case allalbums:
            case albums4artist:
                /* save offset of this album */
                skip = tagdbheader.songarraylen * 4 + 4;
                dptr[1] = offset;
                break;

            case allartists:
                /* save offset of this artist */
                skip = tagdbheader.albumarraylen * 4;
                dptr[1] = offset;
                break;
        }

        /* store name pointer in dir cache */
        dptr[0] = (unsigned long)nptr;

        if (skip)
            lseek(tagdb_fd, skip, SEEK_CUR);

        hits++;

        if(table==songs4artist)
                c->dirlength=hits;

        /* next name is stored immediately after this */
        nptr = (void*)nptr + strlen((char*)nptr) + 1;
        if ((void*)nptr + stringlen > (void*)end_of_nbuf) {
            c->dirfull = true;
            break;
        }

        /* limit dir buffer */
        dptr = (void*)dptr + c->dentry_size * sizeof(int);
        if ((void*)(dptr + c->dentry_size) >
            (void*)(c->dircache + dcachesize))
        {
            c->dirfull = true;
            break;
        }

        if (!safeplace)
            offset += stringlen + skip;
    }

    if (c->currtable == albums4artist && !c->dirfull) {
        strcpy((char*)nptr, (char *)str(LANG_ID3DB_ALL_SONGS));
        dptr[0] = (unsigned long)nptr;
        dptr[1] = extra; /* offset to artist */
        hits++;
    }

    c->filesindir = hits;

    return hits;
}

static int db_search(struct tree_context* c, char* string)
{
    int i, count, size, hits=0;
    long start;

    char* nptr = c->name_buffer;
    const char* end_of_nbuf = nptr + c->name_buffer_size;

    unsigned long* dptr = c->dircache;
    const long dcachesize = global_settings.max_files_in_dir *
        sizeof(struct entry);

    switch (c->currtable) {
        case searchartists:
            start = tagdbheader.artiststart;
            count = tagdbheader.artistcount;
            size = ARTISTENTRY_SIZE;
            break;

        case searchalbums:
            start = tagdbheader.albumstart;
            count = tagdbheader.albumcount;
            size = ALBUMENTRY_SIZE;
            break;

        case searchsongs:
            start = tagdbheader.songstart;
            count = tagdbheader.songcount;
            size = SONGENTRY_SIZE;
            break;

        default:
            DEBUGF("Invalid table %d\n", c->currtable);
            return 0;
    }

    lseek(tagdb_fd, start, SEEK_SET);

    for (i=0; i<count; i++) {
        if (read(tagdb_fd, nptr, size) < size) {
            DEBUGF("Short read(%d) in db_search()\n",size);
            break;
        }
        if (strcasestr(nptr, string)) {
            hits++;

            dptr[0] = (unsigned long)nptr;
            if (c->currtable == searchsongs) {
                /* store offset of filename */
                dptr[1] = BE32(*((long*)(nptr + tagdbheader.songlen + 8)));
            }
            else
                /* store offset of database record */
                dptr[1] = start + i * size;

            dptr += 2;

            /* limit dir buffer */
            if ((void*)(dptr + c->dentry_size) >
                (void*)(c->dircache + dcachesize))
            {
                c->dirfull = true;
                break;
            }

            nptr += strlen(nptr) + 1;
            while ((unsigned long)nptr & 3)
                nptr++;

            /* limit name buffer */
            if ((void*)nptr + size > (void*)end_of_nbuf) {
                c->dirfull = true;
                break;
            }
        }
    }

    return hits;
}

int db_enter(struct tree_context* c)
{
    int rc = 0;
    int offset = (c->selected_item) * c->dentry_size + 1;
    int newextra = ((int*)c->dircache)[offset];

    if (c->dirlevel >= MAX_DIR_LEVELS)
        return 0;

    c->selected_item_history[c->dirlevel]=c->selected_item;
    c->table_history[c->dirlevel] = c->currtable;
    c->extra_history[c->dirlevel] = c->currextra;
    c->pos_history[c->dirlevel] = c->firstpos;
    c->dirlevel++;

    switch (c->currtable) {
        case root:
            c->currtable = newextra;
            c->currextra = newextra;
            break;

        case allartists:
        case searchartists:
            c->currtable = albums4artist;
            c->currextra = newextra;
            break;

        case allalbums:
        case albums4artist:
        case searchalbums:
            /* virtual <all albums> entry points to the artist,
               all normal entries point to the album */
            if (newextra < tagdbheader.albumstart)
                c->currtable = songs4artist;
            else
                c->currtable = songs4album;

            c->currextra = newextra;
            break;

        case allsongs:
        case songs4album:
        case songs4artist:
        case searchsongs:
            c->dirlevel--;
            if (db_play_folder(c) >= 0)
                rc = 2;
            break;

        case search:
            rc = kbd_input(searchstring, sizeof(searchstring));
            if (rc == -1 || !searchstring[0])
                c->dirlevel--;
            else
                c->currtable = newextra;
            break;

        default:
            c->dirlevel--;
            break;
    }
    c->selected_item=0;
    gui_synclist_select_item(&tree_lists, c->selected_item);

    return rc;
}

void db_exit(struct tree_context* c)
{
    c->dirlevel--;
    c->selected_item=c->selected_item_history[c->dirlevel];
    gui_synclist_select_item(&tree_lists, c->selected_item);
    c->currtable = c->table_history[c->dirlevel];
    c->currextra = c->extra_history[c->dirlevel];
    c->firstpos  = c->pos_history[c->dirlevel];
}

int db_get_filename(struct tree_context* c, char *buf, int buflen)
{
    int rc;
    int pathoffset = ((int*)c->dircache)[c->selected_item * c->dentry_size + 1];

    lseek(tagdb_fd, pathoffset, SEEK_SET);
    rc = read(tagdb_fd, buf, buflen);

    if (rc < tagdbheader.songlen) {
        DEBUGF("short path read(%ld) = %d\n", sizeof(buf), rc);
        return -2;
    }
    return 0;
}

static int db_play_folder(struct tree_context* c)
{
    char buf[MAX_PATH];
    int rc, i;

    if (playlist_create(NULL, NULL) < 0) {
        DEBUGF("Failed creating playlist\n");
        return -1;
    }

    /* TODO: add support for very long tables */

    for (i=0; i < c->filesindir; i++) {
        int pathoffset = ((int*)c->dircache)[i * c->dentry_size + 1];
        lseek(tagdb_fd, pathoffset, SEEK_SET);
        rc = read(tagdb_fd, buf, sizeof(buf));
        if (rc < tagdbheader.songlen) {
            DEBUGF("short path read(%ld) = %d\n", sizeof(buf), rc);
            return -2;
        }

        playlist_insert_track(NULL, buf, PLAYLIST_INSERT, false);
    }

    if (global_settings.playlist_shuffle)
        c->selected_item = playlist_shuffle(current_tick, c->selected_item);
    if (!global_settings.play_selected)
        c->selected_item = 0;
    gui_synclist_select_item(&tree_lists, c->selected_item);

    playlist_start(c->selected_item,0);

    return 0;
}

#ifdef HAVE_LCD_BITMAP
const char* db_get_icon(struct tree_context* c)
#else
int   db_get_icon(struct tree_context* c)
#endif
{
    int icon;

    switch (c->currtable)
    {
        case allsongs:
        case songs4album:
        case songs4artist:
        case searchsongs:
            icon = Icon_Audio;
            break;

        default:
            icon = Icon_Folder;
            break;
    }

#ifdef HAVE_LCD_BITMAP
    return (char *)bitmap_icons_6x8[icon];
#else
    return icon;
#endif
}
