/*
 * libkarma/properties.c
 * 
 * Copyright (c) Frank Zschockelt <libkarma@freakysoft.de> 2004-2006
 * Copyright (c) Keith Bennett <keith@mcs.st-and.ac.uk> 2006
 * 
 * You may distribute and modify this program under the terms of 
 * the GNU GPL, version 2 or later.
 * 
 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <utime.h>
#include <time.h>

#include "lkarma.h"
#include "properties.h"
#include "playlist.h"
#include "util.h"
#include "fdb.h"

#define HASHSIZE 16
#define TABLECHUNK 4096
#define FIRSTFID 17 /* 272/16 */

struct klist{
    HASH ** fidTable;
    uint32_t ftsize;
    uint32_t count;
    uint32_t next_fid;
};

struct klist props = {NULL, 0, 0, 17};

int properties_updated = 0;
int fdb_updated = 0;
uint32_t device_generation;
uint32_t serial;

void lk_properties_init(void)
{
    uint32_t i;
    props.count=0;
    props.next_fid=17;
    props.ftsize=TABLECHUNK;
    props.fidTable=malloc(props.ftsize*sizeof(HASH *));
    for(i=0;i<props.ftsize;i++) 
        props.fidTable[i]=NULL;
    properties_updated = 0;
    fdb_updated = 0;
}

void lk_properties_destroy(void)
{
    uint32_t i=0;
    
    while(props.count > 0) {
        if(props.fidTable[i]){
            hash_destroy(&props.fidTable[i]);
            props.count--;
        }
        i++;
    }
    free(props.fidTable);
    props.fidTable=NULL;
    props.count=0;
    props.next_fid=17;
    properties_updated = 0;
    fdb_updated = 0;
}

uint32_t lk_properties_import(char * properties)
{
    uint32_t count=0;
    char *key, *value;
    char *ikey, *ivalue;
    HASH * item;
    uint32_t fid,i;
    char *n_tok, *last;
    
    last=&(properties[strlen(properties)])+1; 
    /* otherwise we can't see whether n_tok is outside of properties */

    item=hash_create(HASHSIZE);
    hash_insert(item, strdup("fid"), strdup("0"));
    key=strtok_r(properties, "=", &n_tok);
    if(key==NULL){
        lk_errors_set(E_BADPROP);
        return -1;
    }                
/*  fprintf(stderr, "n_tok = --%c--\n", *n_tok); */
    if(*n_tok == '\n') {value=""; n_tok++;}
    else value=strtok_r(NULL, "\n", &n_tok);

    while(value != NULL){
        ikey=strdup(key);
        ivalue=strdup(value);
/*      hash_insert(item, ikey, ivalue); /////////////// */
        if(strlen(ivalue))
           hash_insert(item, ikey, ivalue);
        else {
           if (ikey) free(ikey);
           if (ivalue) free(ivalue);
        }
/*      fprintf(stderr, "hash_insert -%s--%s-\n", ikey, ivalue); */

        if((n_tok[0] == '\n')||(n_tok[0]=='\0')){
            ++n_tok;
            
            fid=(uint32_t)(atol(lk_properties_get_property_hash(item,
                                                                "fid"))/16);
            while(fid >= props.ftsize){
                i=props.ftsize+TABLECHUNK;
                props.fidTable=realloc(props.fidTable, i*sizeof(HASH *));
                for( ;props.ftsize<i;props.ftsize++)
                    props.fidTable[props.ftsize]=NULL;
            }
            if(props.fidTable[fid])
                hash_destroy(&props.fidTable[fid]);
            else
                ++props.count;
            
            props.fidTable[fid]=item;
            
            item=hash_create(HASHSIZE);
            ++count;
        }
        if(n_tok==last)
            key=value=NULL;
        else{
            key=strtok_r(NULL, "=", &n_tok);
/*          fprintf(stderr, "n_tok = --%c--\n", *n_tok); */
            if(*n_tok == '\n') {value=""; n_tok++;}
            else value=strtok_r(NULL, "\n", &n_tok);
        }
    }
    hash_destroy(&item);
    properties_updated = 1;
    return count;
}

char * lk_properties_export(uint32_t id)
{
    HASH *table; struct hash_elem *tmp; 
    char *p;
    int len, i;

#define \
export_hash(key, data) { \
    len+=strlen(key)+strlen((char *)data)+6; \
    p=realloc(p, len); \
    strcat(p, key); \
    strcat(p, "="); \
    strcat(p, (char*)data); \
    strcat(p, "\n"); \
}

    len=0;
    p=malloc(1);
    *p=0;
    table=lk_properties_idsearch(id);
    if (table==NULL) {
        lk_errors_set(E_BADID);
        return NULL;
    }
    else {
        for (i=0; i < table->nelem; i++) {
            tmp=table->table[i];
            while(tmp){
                export_hash(tmp->key, tmp->data);
                tmp=tmp->next;
            }
        }
    }
    return p;
}

/* The karma only plays files when the ID is > 256 and ID%16==0 */
uint32_t lk_properties_getnextfid(void)
{
    while(props.fidTable[props.next_fid])
        props.next_fid++;
    return props.next_fid*16;
}

uint32_t lk_properties_new_property(void)
{
    HASH *item;
    static uint32_t new_fid;
    uint32_t i;
    
    item=hash_create(HASHSIZE);
    new_fid=lk_properties_getnextfid();
    lk_properties_set_property_hash(item, "fid", simple_itoa(new_fid));
    
    while(new_fid/16 >= props.ftsize){
        i=props.ftsize+TABLECHUNK;
        props.fidTable=realloc(props.fidTable, i*sizeof(HASH *));
        for( ;props.ftsize<i;props.ftsize++)
            props.fidTable[props.ftsize]=NULL;
    }
    props.fidTable[new_fid/16]=item;
    
    props.next_fid++;
    props.count++;
    return new_fid;
}


char * lk_properties_get_property(uint32_t fid, char * key)
{
    HASH *tmp;

    tmp=lk_properties_idsearch(fid);
    if(tmp==NULL){
        lk_errors_set(E_BADID);
        return NULL;
    }
    else return (char *)hash_find(tmp, key);
}

char * lk_properties_get_property_hash(HASH * p, char * key)
{
    return (char *)hash_find(p, key);
}

int lk_properties_del_property(uint32_t id)
{
    id/=16;
    if(props.fidTable[id] == NULL){
        lk_errors_set(E_BADID);
        return -1;
    }

    hash_destroy(&props.fidTable[id]);
    props.fidTable[id]=NULL;

    props.count--;
    if(id < props.next_fid)
        props.next_fid=id;
    properties_updated = 1;
    fdb_updated = 1;
    return 0;
}

int lk_properties_set_property(uint32_t id, char * property, char * data)
{
    id/=16;
    if(props.fidTable[id] == NULL){
        lk_errors_set(E_BADID);
        return -1;
    }
    lk_properties_set_property_hash(props.fidTable[id], property, data);
    if (memcmp(property, "path", 5) == 0)
        fdb_updated = 1;
    
    return 0;
}

void lk_properties_set_property_hash(HASH * p, char * property, char * data)
{
    if (data == NULL) return;

    hash_insert(p, strdup(property), strdup(data));
    properties_updated = 1;
}

HASH * lk_properties_idsearch(uint32_t fid)
{
    return props.fidTable[fid/16];
}

int lk_properties_load(void)
{
    int fd;
    char * buf;
    struct stat info;
    char *fname;

    fname=lk_path_string("fileinfo");

    fd=open(fname,O_RDONLY);
    if(fd==-1) {
        free(fname);
        lk_errors_set(E_OPEN);
        return -1;
    }
    fstat(fd, &info);
    buf=(char *)malloc((int)info.st_size+1);
    read(fd, buf, info.st_size);
    close(fd);
    buf[info.st_size]='\0';
    lk_properties_import(buf);
    free(buf);
    free(fname);
    properties_updated = 0;
    return 0;
}
/*
   caches are stored in $HOME/.openrio/pearl-$SERIAL.

   The $SERIAL value is the Karma's unique serial number.
   This allows multiple Karmas to sync against the same machine without 
   collisions.

   File Format
   At this time, there is only one file specified within the cache 
   directory, named fileinfo. 
   This is a standard text file containing a dump of the File Info properties, 
   as returned by the Get All File Info message to the device.

   The file's last-modified date is set to the device_generation value provided 
   by the Karma  essentially, the last-modified date of the Karma's internal 
   database. If the cache file's last-modified date and the Karma's 
   device_generation do not match, the cache is considered invalid and is 
   rebuilt using information from the device. 
   */
int lk_properties_save(void)
{
    int fd;
    uint32_t i,j;
    char * fname;
    char * buf;
    struct utimbuf time;

    if (properties_updated == 0)
        return 0;

    fname=lk_path_string("fileinfo");
    if(mk_path(fname) == -1){
        free(fname);
        return -1;
    }
    fd=open(fname,O_CREAT|O_WRONLY|O_TRUNC, S_IRUSR|S_IWUSR);
    if(fd==-1) {
        free(fname);
        lk_errors_set(E_OPEN);
        return -1;
    }

    for(i=0,j=16; i<props.count; j++) {
        if(props.fidTable[j]){
            buf=lk_properties_export(j*16);
            write(fd, buf, strlen(buf));
            free(buf);
            write(fd, "\n", 1);
            ++i;
        }
    }
    close(fd);

    time.actime=device_generation;
    time.modtime=time.actime;
    if(utime(fname, &time)==-1){
        free(fname);
        lk_errors_set(E_UTIME);
        return -1;
    }

    free(fname);

    if (fdb_updated != 0)
        return lk_fdb_save();

    return 0;
}

void lk_properties_inc_devgeneration(void)
{
    device_generation++;
}


int lk_properties_cache_obsolete(void)
{
    char *fname;
    struct stat modified;

    fname=lk_path_string("fileinfo");
    if(stat(fname, &modified) == -1){
        free(fname);
        return 1;
    }
    free(fname);
    if(device_generation == (uint32_t)modified.st_mtime){
        return 0;
    }else return 1;
}


uint32_t *lk_properties_andOrSearch(int mode, uint32_t *search_in, char *key,
                                    char *data)
{
/*  Exact/approximate And/Or search support:
    mode == 0 == 0000: Or  approx;          mode == 2 == 0010: Or  exact
         == 1 == 0001: And approx;               == 3 == 0011: And exact  */

    uint32_t count=1, fid=16;
    char *val;
    int hash, exact=0, and=0;
    int i=0,j=0;
    uint32_t * result=NULL;

    if(mode>1) {exact = 1; and = mode-2;} else {exact = 0; and = mode;}

#define STRMATCH(a,b) exact \
      ? (((a==NULL)&&(b==NULL))||(b&&(b[0]==0))||(a&&b&&(strcmp(a,b)==0))) \
      : (((a==NULL)&&(b==NULL))||           (b && a && (strstr(a,b)!=NULL)))

/*  exact ? ((b==NULL)||((a==NULL)&&(b==NULL))||(a && (strcmp(a,b)==0))):\*/
/*          (((a==NULL)&&(b==NULL)) || (b && a && (strstr(a,b)!=NULL)))*/

/*  exact?((data[0]==0)||(val[0]==0)||(strcmp(val, data)==0)):\ */
/*                                    (strstr(val, data)!=NULL) */

/*  fprintf(stderr, "+++> %d ---%s+++%s---\n", mode, key, data); */
    if(search_in==NULL){
        if(and)                           /* AND merge with zero elements */
            return NULL;
        hash=hash_string((const unsigned char *)key, HASHSIZE);
        while(count <= props.count){
            if(props.fidTable[fid]){
                ++count;
                val = (char *)hash_find_hash(props.fidTable[fid], hash, key);
                if(STRMATCH(val, data)) {
                    ++i;
                    result=realloc(result, i*sizeof(uint32_t));
                    result[i-1]=fid*16;
                }
            }
            ++fid;
        }

    }
    else if (and) {                                          /* AND merge */
        while(search_in[j] != 0){
            val=lk_properties_get_property(search_in[j], key);
            if(STRMATCH(val, data)){
                ++i;
                result=realloc(result, i*sizeof(uint32_t));
                result[i-1]=search_in[j];
            }
            ++j;
        }
        free(search_in);
    }else{                                                    /* OR merge */
        while(search_in[i] != 0) ++i;
        result=malloc((i) * sizeof(uint32_t));
        memcpy(result, search_in, (i) * sizeof(uint32_t));
        
        hash=hash_string((const unsigned char *)key, HASHSIZE);
        while(count <= props.count){
            if(props.fidTable[fid]){
                ++count;
                val=(char *)hash_find_hash(props.fidTable[fid], hash, key);
                if(STRMATCH(val, data)){
                    fid*=16;
                    j=0;
                    while((j<i) && (result[j] != fid))
                        j++;
                    if(j==i) {
                        result=realloc(result, (++i)*sizeof(uint32_t));
                        result[i-1]=fid;
                    }
                    fid/=16;
                }
            }
            ++fid;
        }
        free(search_in);
    }
    if(i){
        ++i;
        result=realloc(result, i*sizeof(uint32_t));
        result[i-1]=0;
    }
    return result;
}

static int artistcmp(const void *p1, const void *p2)
{
    int s1 = 0, s2 = 0;
    table_t *t1 = (table_t *)p1;
    table_t *t2 = (table_t *)p2;
    if (memcmp(t1->str, "The ", 4) == 0 ||
        memcmp(t1->str, "the ", 4) == 0 ) s1 = 4;
    if (memcmp(t2->str, "The ", 4) == 0 ||
        memcmp(t2->str, "the ", 4) == 0 ) s2 = 4;
    return strcmp(t1->str+s1, t2->str+s2);
}

static int cmp(const void *p1, const void *p2)
{
    table_t *t1 = (table_t *)p1;
    table_t *t2 = (table_t *)p2;
    return strcmp(t1->str, t2->str);
}

int lk_properties_write_property(FILE *fp, char *attr, smalldb_type_t type,
                                 table_t *table, uint32_t *offset,
                                 uint32_t *arrsz, char **arr)
{
    char *str, *ptr, *offset_c;
    uint32_t i, j, n, osz, sz, len, off, idx, profile = 0;
    uint16_t *offset_16;

    sz = len = 0;
    ptr = *arr;

    offset_c = (char *) offset;
    offset_16 = (uint16_t *) offset;

    if (type == SDB_BLOB || type == SDB_STRING) {
        for(i=0,j=16,off=0; i<props.count; j++) {
            if(!props.fidTable[j]) continue;
            str = lk_properties_get_property(j*16,attr);
            if (str) {
                sz = strlen(str)+1;
                len += sz;
                if (len > *arrsz) {
                    while (len > *arrsz)
                        *arrsz *= 2;
                    *arr = realloc(*arr, *arrsz);
                }
                memcpy(*arr+off, str, sz);
            } else if (type == SDB_STRING) {
                sz = 1;
                len += sz;
                *(*arr+off) = '\0';
            } else
                sz = 0;
            table[i].len = sz;
            table[i].idx = i;
            off = len;
            i++;
        }
        n = i;
        ptr = *arr;
        for (i=0; i<n; i++) {
            table[i].str = ptr;
            ptr += table[i].len;
        }
        if (type == SDB_STRING) {
            if (memcmp(attr, "artist", 6) == 0)
                qsort(table,n,sizeof(table_t),artistcmp);
            else
                qsort(table,n,sizeof(table_t),cmp);
            idx = off = 0;
            ptr = table[0].str;
            sz = table[0].len;
            offset[table[0].idx] = off;
            for (j=1; j<n; j++) {
                if ( (table[j].len && sz != table[j].len) ||
                    memcmp(ptr, table[j].str, sz) != 0) {
                    off += sz;
                    ptr = table[j].str;
                    sz = table[j].len;
                    idx = j;
                }
                offset[table[j].idx] = off;
            }
            len = off + sz;
        } else {
            osz = off = 0;
            if (memcmp(attr, "profile", 7) == 0) profile = 1;
            for (j=0; j<n; j++) {
                offset[j] = off;
                ptr = table[j].str;
                if (!table[j].len)
                    continue;
                sz = lk_playlist_unescape_inplace(ptr);
                if (profile && sz != 40)
                    sz = 0;
                table[j].len = sz;
                if (osz == sz && memcmp(ptr, str, sz) == 0)
                    continue;
                str = ptr;
                off += sz;
                osz = sz;
            }
            len = off;
        }

        if (type == SDB_STRING)
            fwrite(&len, sizeof(uint32_t), 1, fp);
        fwrite(offset, n*sizeof(uint32_t), 1, fp);
        if (type == SDB_BLOB)
            fwrite(&len, sizeof(uint32_t), 1, fp);
        off = -1;
        if (type == SDB_BLOB) {
            off = offset[0];
            for (j=0; j<n-1; j++) {
                if (offset[j+1] != off && table[j].len != 0) {
                    fwrite(table[j].str,table[j].len,1,fp);
                    off = offset[j+1];
                }
            }
            if (offset[n-1] != len)
                fwrite(table[n-1].str,table[n-1].len,1,fp);
        } else {
            for (j=0; j<n; j++) {
                if (offset[table[j].idx] != off && table[j].len != 0) {
                    fwrite(table[j].str,table[j].len,1,fp);
                    off = offset[table[j].idx];
                }
            }
        }
    } else if (type == SDB_LE32) {
        for(i=0,j=16; i<props.count; j++) {
            if(!props.fidTable[j]) continue;
            str = lk_properties_get_property(j*16,attr);
            if(!str)
                offset[i] = 0;
            else
                offset[i] = atoi(str);
            i++;
        }
        fwrite(offset, i*sizeof(uint32_t), 1, fp);
    } else if (type == SDB_LE16) {
        for(i=0,j=16; i<props.count; j++) {
            if(!props.fidTable[j]) continue;
            str = lk_properties_get_property(j*16,attr);
            if(!str)
                offset_16[i] = 0;
            else
                offset_16[i] = atoi(str);
            i++;
        }
        fwrite(offset_16, i*sizeof(uint16_t), 1, fp);
    } else if (type == SDB_CHAR) {
        for(i=0,j=16; i<props.count; j++) {
            if(!props.fidTable[j]) continue;
            str = lk_properties_get_property(j*16,attr);
            if(!str)
                offset_c[i] = 0;
            else
                offset_c[i] = atoi(str);
            i++;
        }
        fwrite(offset_c, i*sizeof(char), 1, fp);
    } else
        return 1;

    return 0;
}


static int smalldb_init(karma_db_t *db)
{
    uint32_t i, len;
    char *ptr, *optr;
    char *propnames = "fid offset trailer duration length samplerate "
        "ctime options bpm rms stddev normalisation play_last "
        "play_count_limit play_count tracknr file_id marked playlist "
        "profile type codec artist source title bitrate genre year "
        "rid fid_generation profiler_version replay_gain_peak "
        "replay_gain_radio replay_gain_audiophile "
        "bytes_silence_sof bytes_silence_eof";
    uint32_t types[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,4,4,3,3,3,2,2,1,1,1,
        1,1,1,1,1,1,0,0,0,0,0,0,0};
    uint32_t dunno[] = {1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,
        0,0,0,0,0,0,0,0,0,0,0,0,0};

    db->version = 2;
    db->eight = 8;
    db->three = 3;
    db->num_playlists = 0;
    db->dmap_size = 0;

    db->num_attribs = 36;
    db->attribs = malloc(db->num_attribs * sizeof(smalldb_attrib_t));
    if (!db->attribs)
        return 1;

    optr = ptr = propnames;
    for (i = 0; i < db->num_attribs; i++) {
        for (; *ptr && *ptr != ' '; ptr++);
        len = ptr - optr;
        db->attribs[i].name = malloc(len+1);
        memcpy(db->attribs[i].name, optr, len);
        db->attribs[i].name[len] = '\0';
        optr = ++ptr;
        db->attribs[i].type = (smalldb_type_t) types[i];
        db->attribs[i].dunno = dunno[i];
    }

    return 0;
}

int lk_properties_write_smalldb(char *usbMountPoint, karma_db_t *db)
{
    uint32_t i, len, arrsz, nfiles = props.count;
    uint32_t ret = 0;
    uint32_t *offset = NULL;
    table_t *table = NULL;
    char *tmp = NULL;
    char *arr = NULL;
    FILE *fp;

    if (properties_updated == 0)
        return 0;

    arrsz = 40*nfiles;
    arr = malloc(arrsz);
    if (!arr)
        goto err;

    offset = malloc(nfiles*sizeof(uint32_t));
    if (!offset)
        goto err;

    table = malloc(nfiles*sizeof(table_t));
    if (!table)
        goto err;

    tmp = calloc(strlen(usbMountPoint) + strlen(RK_SMALLDB) + 1, 1);
    if (!tmp)
        goto err;

    sprintf(tmp, "%s%s", usbMountPoint, RK_SMALLDB);
    fp = fopen(tmp, "w+");
    if (!fp) {
        mk_path(tmp);
        fp = fopen(tmp, "w+");
    }
    if (!fp) {
        perror(tmp);
        goto err;
    }
    free(tmp);
    tmp = NULL;

    if (!db)
        db = calloc(sizeof(karma_db_t), 1);
    if (!db)
        goto err;

    if (db->num_attribs == 0)
    {
        ret = smalldb_init(db);
        if (ret)
            goto err;
    }

    db->ctime = (int) time(NULL);
    fwrite(&db->version, sizeof(uint32_t), 1, fp);
    fwrite(&db->ctime, sizeof(uint32_t), 1, fp);
    fwrite(&db->eight, sizeof(uint32_t), 1, fp);
    fwrite(&db->three, sizeof(uint32_t), 1, fp);
    fwrite(&db->num_attribs, sizeof(uint32_t), 1, fp);
    for (i=0, len=0; i<db->num_attribs; i++) {
        smalldb_attrib_t *atb = &db->attribs[i];
        fwrite(&atb->type, sizeof(uint32_t), 1, fp);
        fwrite(&atb->dunno, sizeof(uint32_t), 1, fp);
        len += strlen(atb->name) + 1;
    }
    fwrite(&len, sizeof(uint32_t), 1, fp);
    for (i=0; i<db->num_attribs; i++) {
        char *name = db->attribs[i].name;
        fwrite(name, strlen(name), 1, fp);
        fputc(' ', fp);
    }

    fwrite(&db->num_playlists, sizeof(uint32_t), 1, fp);

    for (i=0; i<db->num_playlists; i++) {
        /* playlist section, copy as opaque data for now */
        fwrite(&db->playlists[i].index, sizeof(uint32_t), 1, fp);
        fwrite(&db->playlists[i].len, sizeof(uint32_t), 1, fp);
        fwrite(db->playlists[i].playlist, db->playlists[i].len, 1, fp);
    }

    fwrite(&nfiles, sizeof(uint32_t), 1, fp);
    fwrite(&nfiles, sizeof(uint32_t), 1, fp);

    if (nfiles > 0) {
        for (i=0; i<db->num_attribs; i++) {
            ret = lk_properties_write_property(fp, db->attribs[i].name,
                                               db->attribs[i].type, table,
                                               offset, &arrsz, &arr);
            if (ret) {
                fprintf(stderr, "error writing property: %s\n",
                        db->attribs[i].name);
                goto err;
            }
        }
    }

    fwrite(&db->dmap_size, sizeof(uint32_t), 1, fp);

    if (db->dmap_size)
        fwrite(db->dmap, db->dmap_size, 1, fp);

    fwrite(&db->ctime, sizeof(uint32_t), 1, fp);
    fclose(fp);
    goto out;

err:
    ret = 1;
out:
    if (arr) free(arr);
    if (offset) free(offset);
    if (table) free(table);
    if (tmp) free(tmp);

    return ret;
}
