/*
 * libkarma/rio_rw.c
 *
 * Copyright (c) Frank Zschockelt <libkarma@freakysoft.de> 2004
 * 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 <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <limits.h>
#include <ctype.h>

#include <taglib/tag_c.h>

#include "mp3.h"
#include "wav.h"

#include "lkarma.h"
#include "karma.h"
#include "properties.h"
#include "util.h"
#include "rio_rw.h"
#include "fdb.h"

/* #define BLOCKSIZE 32768 */
#define BLOCKSIZE 131072
/* #define NOT_TAGGED "[karmalib 0.4]" */
#define NOT_TAGGED "unTagged" /* something more explicit and readable ... */

enum { TYPE_DATA, TYPE_MP3, TYPE_OGG, TYPE_FLAC, TYPE_WAV, TYPE_WMA };

static int write_dupes = 0;

static void add_general(HASH * p, const char *filename);
static int get_ogg_props(HASH *p, const char *filename);
static int get_flac_props(HASH *p, const char *filename);
/* *********************** EV *********************** */
/* static int get_mp3_props(HASH *p, mp3info *mp3); */
static int get_mp3_props(HASH *p, mp3info *mp3, uint64_t length);
/* ************************************************** */
static int get_taxi_props(HASH *p);
static int get_wave_props(HASH *p, const char *filename);
static void add_tag(HASH *p, char *key, char *value);
static int set_tag_props(HASH *p, const char *filename, int type);
static int get_file_type(mp3info *mp3);

int lk_synchronize_necessary(int rio)
{
    uint32_t old;

    old = device_generation;
    lk_karma_get_device_settings(rio, NULL);
    return (old != device_generation);
}

int lk_rio_read_playlist(int rio, uint32_t fid, const char * filename)
{
    playlist *pl;
    int dst;

    pl=lk_playlist_fid_read(fid);
    if (pl==NULL)
        return -1;  /* Can't get playlist for this fid */
    
    if (mk_path(filename)) {
        lk_errors_set(E_PATHCREAT);
/*      fprintf(stderr,"** Error creating directory of path: %s\n",filename); */
    }
    dst=open(filename,
             O_CREAT|O_EXCL|O_WRONLY,S_IREAD|S_IWRITE|S_IRGRP|S_IROTH);
    if(dst==-1)
        return -1;  /* Can't open: e.g., file already exist */

    write(dst, pl->playlist, pl->length);
    close(dst); free(pl->playlist); free(pl);

    return 0;
}

int lk_rio_read(int rio, uint32_t fid, const char * filename)
{
    int dst;
    uint64_t blk=BLOCKSIZE, offs=0, filesize=0;
    uint64_t got=0;
    char *tmp, *tmp1;

/*  dst=creat(filename, O_EXCL); */
    if (mk_path(filename)) {
        lk_errors_set(E_PATHCREAT);
/*      fprintf(stderr,"** Error creating directory of path: %s\n",filename); */
    }
    dst=open(filename,
             O_CREAT|O_EXCL|O_WRONLY,S_IREAD|S_IWRITE|S_IRGRP|S_IROTH);
    if(dst==-1) 
        return -1; /* Can not open (e.g., file already exist) */

    tmp1=lk_properties_get_property(fid, "length");
    if(tmp1 != NULL)
        filesize=atoll(tmp1);
    else
        return -1; /* tmp1==NULL => filesize undefined */
    if(filesize<=0)
        return -1; /* Mystery: filesize sometimes can be 0 ???!!! */

    while(offs<filesize){
        if(lk_karma_read_file_chunk(rio,offs,blk,fid,&tmp,&got) != 0){
            close(dst);
            unlink(filename);
            return -1;
        }
        write(dst, tmp, got);
        free(tmp);
        offs+=got;
        if((filesize-offs) < blk)
            blk=filesize-offs;
    }
    
    close(dst);
    return 0;
}

static int lk_rio_do_update_props_from_tags(int rio, uint32_t fid,
                                            const char *filename)
{
    int got=-1;
    struct stat len;
    HASH * props;
    mp3info mp3;
    char *rid, *tmp;
    uint64_t length=0;
    int type, rid_fd;
/** uint32_t *fids; **/

    if(lstat(filename, &len)==-1)
        return -1;

    memset(&mp3, 0, sizeof(mp3info));
    mp3.filename = (char *)filename;
    type = get_file_type(&mp3);

    rid_fd = open(filename, O_RDONLY);
    rid = (char*)lk_generate_rid(rid_fd, mp3.offset, mp3.datasize);
    close(rid_fd);

    /* uses lk_properties_getnextfid()...*/
    props=lk_properties_idsearch(fid);
    if(props==NULL){
        lk_errors_set(E_NOHASH);
/*      printf("huh, no hash found?\n"); */
        return -1;
    }

    /* *********************** EV *********************** */
    /* to fix bad length & duration yield by lk_rio_do_update_props_from_tags */
    tmp = lk_properties_get_property(fid, "length");
    if(tmp != NULL)
      length = strtoull(tmp, NULL, 10);
    /* ************************************************** */

    add_general(props, filename);
    lk_properties_set_property_hash(props, "rid", rid);

    if (type == TYPE_OGG)
        got=get_ogg_props(props, filename);
    else if (type == TYPE_FLAC)
        got=get_flac_props(props, filename);
    else if (type == TYPE_MP3)
        /* *********************** EV *********************** */
        /* got=get_mp3_props(props, &mp3); */
        got=get_mp3_props(props, &mp3, length);
        /* ************************************************** */
    else if (type == TYPE_WAV)
        got=get_wave_props(props, filename);
    /*
    else if (type == TYPE_WMA)
        got=get_wma_props(props, filename);
     */    
    else
        got=get_taxi_props(props);

    return got;
}

/*
 * Create a temporary file containing the head, tail and middle 64k
 * of data needed for finding the tags and rid.
 */
static char *lk_rio_create_temp_file(int rio, uint32_t fid)
{
#define RID_BLOCKS 65536
    int fd, ret;
    int64_t len;
    uint64_t blk, got, off, length;
    char *filename = NULL, *tmp;
    mp3info mp3;

    tmp = lk_properties_get_property(fid, "length");
    if (tmp == NULL)
        goto err;

    length = strtoull(tmp, NULL, 10);

    /* 4k should be enough for the header in most cases */
    blk = RID_BLOCKS + 4096;
    off = 0;
    ret = lk_karma_read_file_chunk(rio, off, blk, fid, &tmp, &got);
    if (ret != 0 || got != blk)
        goto err;

    len = strlen(karma_tmpdir) + strlen("/lkarmaXXXXXX");
    filename = malloc(len+1);
    strcpy(filename, karma_tmpdir);
    strcat(filename, "/lkarmaXXXXXX");

    fd = mkstemp(filename); /* filename X's are replaced with a unique string */
    if (fd == -1)
        goto err;

    ret = write(fd, tmp, got);
    free(tmp);
    close(fd);

    memset(&mp3, 0, sizeof(mp3));
    mp3.filename = filename;
    get_file_type(&mp3);

    fd = open(filename, O_WRONLY);

    /* read the rest of the header if 4k wasnt enough */
    len = mp3.offset + RID_BLOCKS - blk;
    if (len > 0) {
        off = blk;
        blk = len;
        ret = lk_karma_read_file_chunk(rio, off, blk, fid, &tmp, &got);
        if (ret != 0 || got != blk)
            goto err;
        ret = lseek(fd, off, SEEK_SET);
        ret = write(fd, tmp, got);
        free(tmp);
    }

    /* tail */
    blk = RID_BLOCKS + 128;
    off = length - blk;
    ret = lk_karma_read_file_chunk(rio, off, blk, fid, &tmp, &got);
    if (ret != 0 || got != blk)
        goto err;

    ret = lseek(fd, (mp3.offset + 2*RID_BLOCKS), SEEK_SET);
    if (memcmp(tmp+got-128, "TAG", 3) == 0) {
        ret = write(fd, tmp, got);
        length -= 128;
    } else
        ret = write(fd, tmp+128, got-128);
    free(tmp);

    /* middle 64k */
    blk = RID_BLOCKS;
    off = (mp3.offset + length - RID_BLOCKS)/2;
    ret = lk_karma_read_file_chunk(rio, off, blk, fid, &tmp, &got);
    if (ret != 0 || got != blk)
        goto err;
    ret = lseek(fd, (mp3.offset + RID_BLOCKS), SEEK_SET);
    ret = write(fd, tmp, got);
    free(tmp);

    close(fd);

    mp3.datasize = mp3.offset + 3*RID_BLOCKS;

    return filename;

  err:
    if (filename) {
        close(fd);
        unlink(filename);
    }
    if (tmp)
        free(tmp);
    lk_errors_set(E_TMPCREAT);
    return NULL;
}

int lk_rio_update_props_from_tags(int rio, uint32_t fid)
{
    int ret;
    char *filename;

    if (using_usb)
        filename = lk_karma_fidToPath(rio, fid);
    else
        filename = lk_rio_create_temp_file(rio, fid);

    if (!filename)
        return -1;

    ret = lk_rio_do_update_props_from_tags(rio, fid, filename);

    if (using_usb == 0 || ret)
        unlink(filename);

    free(filename);

    return ret;
}

/*
     To use that function properly:
         1. Get an IO Lock for Writing
         2. call synchronize_necessary 
            and synchronize the properties if necessary ;)
         3. use rio_write as often as you need to :)
         4. release the IO Lock.
*/
static uint32_t lk_rio_do_write(int fdb, int rio, const char * filename)
{
    int got, fd, len;
    uint32_t fid = 0, *ids;
    uint64_t offs=0;
    char *p, tmp[BLOCKSIZE];
    const uint64_t siz=BLOCKSIZE;
    char *rid, *path, fullpath[257+PATH_MAX];

    fd=open(filename, O_RDONLY);
    if (fd == -1)
        return -1;

    if (fdb) {
        ids = lk_properties_andOrSearch(EXACT|ORS, NULL, "title", FDB_FILENAME);
        if (ids) {
            ids = lk_properties_andOrSearch(EXACT|ANDS, ids, "type", "taxi");
            if (ids) {
                fid = *ids;
                free(ids);
            }
        }
    }
    if (!fid)
        fid = lk_properties_new_property(); 

    got = lk_rio_do_update_props_from_tags(rio, fid, filename);
    if (got != 0) {
        lk_properties_del_property(fid);
        close(fd);
        return -1;
    }

    /** Brought here from lk_rio_update_props_from_tags() **/
    rid = lk_properties_get_property(fid, "rid");
    if (write_dupes == 0 && fdb == 0) {
        ids = lk_properties_andOrSearch(EXACT|ORS, NULL, "rid", rid);
        if ((ids != NULL) && (ids[1] != 0)) { /* more than 1 fid with this rid*/
            free(ids);
            lk_errors_set(E_DUPE);
            lk_properties_del_property(fid);
            close(fd);
            return -1;
	}
    }

    while((got=read(fd, &tmp, siz)) > 0) {  /* Now writing the data */
        if(lk_karma_write_file_chunk(rio, offs, got, fid, 0, tmp)!=0) {
	    lk_errors_set(E_WRCHUNK);
            lk_properties_del_property(fid);
            close(fd);
            return -1;
        }
        offs+=got;
    }
    close(fd);

    if (!fdb) {
        if (gethostname(fullpath, 255) != 0) fullpath[0] = '\0';
        len = strlen(fullpath);
        fullpath[len] = ':';
        path = realpath(filename, fullpath+len+1);
        if (path) {
            lk_properties_set_property(fid, "path", fullpath);
            fdb_updated = 1;
        }
    }

    p=lk_properties_export(fid);
    got=lk_karma_update_file_details(rio, fid, p);
    free(p);

    return fid;
}

uint32_t lk_rio_write(int rio, const char * filename)
{
    return lk_rio_do_write(0, rio, filename);
}

uint32_t lk_rio_write_fdb(int rio, const char * filename)
{
    return lk_rio_do_write(1, rio, filename);
}

static void add_general(HASH * p, const char *filename)
{
    time_t generation_time;
    char *t;
    struct stat len;
    
    lstat(filename, &len);
    
    generation_time=time(NULL);
    lk_properties_set_property_hash(p, "profile", "");
    lk_properties_set_property_hash(p, "fid_generation", 
                                    simple_itoa(generation_time));
    lk_properties_set_property_hash(p, "ctime", 
                                    simple_itoa(generation_time));
    lk_properties_set_property_hash(p, "length", simple_itoa(len.st_size));
    t=strrchr(filename, '/');
    t=utf8_to_codeset((char *)(t?t+1:filename));
    lk_properties_set_property_hash(p, "title", t);
    free(t);
}

static int get_ogg_props(HASH *p, const char *filename)
{
    lk_properties_set_property_hash(p, "type",  "tune");
    lk_properties_set_property_hash(p, "codec", "vorbis");
    
    /*default values*/
    lk_properties_set_property_hash(p, "genre",  NOT_TAGGED);
    lk_properties_set_property_hash(p, "artist", NOT_TAGGED);
    lk_properties_set_property_hash(p, "source", NOT_TAGGED);
    
    return set_tag_props(p, filename, TagLib_File_OggVorbis);
}

static int get_flac_props(HASH *p, const char *filename)
{
    lk_properties_set_property_hash(p, "type",  "tune");
    lk_properties_set_property_hash(p, "codec", "flac");
    
    /*default values*/
    lk_properties_set_property_hash(p, "genre",  NOT_TAGGED);
    lk_properties_set_property_hash(p, "artist", NOT_TAGGED);
    lk_properties_set_property_hash(p, "source", NOT_TAGGED);

#ifndef TagLib_File_FLAC  /* older tag_c libs don't have TagLib_File_FLAC */
#define TagLib_File_FLAC -1
#endif

    return set_tag_props(p, filename, TagLib_File_FLAC);
}

/* *********************** EV *********************** */
/* static int get_mp3_props(HASH *p, mp3info *mp3) */
static int get_mp3_props(HASH *p, mp3info *mp3, uint64_t length)
/* ************************************************** */
{
    int ret = 0, brate = 0;
    char bitrate[5];
    
    lk_properties_set_property_hash(p, "type", "tune");
    lk_properties_set_property_hash(p, "codec", "mp3");
    lk_properties_set_property_hash(p, "offset", simple_itoa(mp3->offset));

    /*default values*/
    lk_properties_set_property_hash(p, "genre",  NOT_TAGGED);
    lk_properties_set_property_hash(p, "artist", NOT_TAGGED);
    lk_properties_set_property_hash(p, "source", NOT_TAGGED);
    
    ret = set_tag_props(p, mp3->filename, TagLib_File_MPEG);
    
    if(!mp3->vbr){
        /* *********************** EV *********************** */
        brate = header_bitrate(&mp3->header);
        strcpy(bitrate, "fs");
        strcat(bitrate, simple_itoa(brate));
        lk_properties_set_property_hash(p, "bitrate", bitrate);
        /* fixing bad length & duration yield
           by lk_rio_do_update_props_from_tags */
	if(length>0) {
            lk_properties_set_property_hash(p, "length", simple_itoa(length));
            lk_properties_set_property_hash(p, "duration", 
                                               simple_itoa(8*(length/brate)));
	}
        /* ************************************************** */
    }
    
    return ret;
}

static int get_taxi_props(HASH *p)
{
    lk_properties_set_property_hash(p, "duration", "0");
    lk_properties_set_property_hash(p, "offset", "0");
/*  lk_properties_set_property_hash(p, "bitrate", "fs128"); */ /* unneeded */
    lk_properties_set_property_hash(p, "codec", "taxi");
    lk_properties_set_property_hash(p, "type", "taxi");
    /* Set read-only access permissions for lkarmafs */
    lk_properties_set_property_hash(p, MODE_PROPERTY, "33060");
    return 0;
}
/*
int get_wma_props(HASH *p, const char *filename)
{
    return 0;
}
*/
static int get_wave_props(HASH *p, const char *filename)
{
    wave_header *wh;

    if(openwav(&wh, (char*)filename) == -1){
        cleanup(&wh);
        return 1;
    }
    lk_properties_set_property_hash(p, "codec", "wave");
    lk_properties_set_property_hash(p, "type", "tune");
    lk_properties_set_property_hash(p, "bitrate", "fs999");
    lk_properties_set_property_hash(p, "genre",  NOT_TAGGED);
    lk_properties_set_property_hash(p, "artist", "wave_file");
    lk_properties_set_property_hash(p, "source", "wave_file");
    lk_properties_set_property_hash(p, "samplerate", 
                                    simple_itoa(wh->nSamplesPerSec));
    lk_properties_set_property_hash(p, "duration", simple_itoa(1000 * 
                                    wh->RiffSize/wh->nChannels/
                                    (wh->nChannels * wh->nSamplesPerSec)));
    lk_properties_set_property_hash(p, "file_id", "0");
    lk_properties_set_property_hash(p, "tracknr", "0");
    
    cleanup(&wh);    
    return 0;
}

static void add_tag(HASH *p, char *key, char *value)
{
    if (value[0])
        lk_properties_set_property_hash(p, key, value);

    /* printf("Key = %s ; Value = %s\n", key, value); */
}

char *strrtrim (char *s) {
    size_t n;
    if(!s) return NULL;
    n = strlen(s) - 1;
    while (n && (isspace(s[n])))
        n--;
    s[n+1] = '\0';
    return s;
}
/*** You can use this with all tag-formats, which taglib supports.
  */
static int set_tag_props(HASH *p, const char *filename, int type)
{
    TagLib_File *tl_file;
    TagLib_Tag *tl_tag;
    const TagLib_AudioProperties * tl_audio;
    int i;
    int fdTestOpen;
    char bitrate[6];
    uint32_t *ids;
    char *title, *source, *artist;

/** Test needed since taglib_file_new[_type] returns !NULL if its open fails */
    fdTestOpen = open(filename, O_RDONLY);
    if (fdTestOpen != -1) 
        close(fdTestOpen);
    else {
        lk_errors_set(E_NOTAGFILE);
        return 0;
    }
/** end Test */
    if (type != -1) 
        tl_file = taglib_file_new_type(filename, type);
    else
        tl_file = taglib_file_new(filename);
    if (! tl_file) {
        lk_errors_set(E_UNSUPTAG);
        return 0;
    }

    tl_tag = taglib_file_tag(tl_file);
    if (! tl_tag)
        return 0;
    tl_audio = taglib_file_audioproperties(tl_file);

    title  = taglib_tag_title (tl_tag);
    source = taglib_tag_album (tl_tag);
    artist = taglib_tag_artist(tl_tag);
    title  = strrtrim(title);
    source = strrtrim(source);
    artist = strrtrim(artist);
/*  fprintf(stderr, "set_tag_props -%s-%s-%s-\n", artist, source, title); */

    if (write_dupes == 0) {
        ids = lk_properties_andOrSearch(EXACT|ORS, NULL, "title", title);
        if (ids) {
            ids = lk_properties_andOrSearch(EXACT|ANDS, ids, "source", source);
            if (ids) {
                ids = lk_properties_andOrSearch(EXACT|ANDS, ids, "artist",
                                                artist);
                if (ids) {
                    free(ids);
                    lk_errors_set(E_DUPE);
                    return -1;
                }
            }
        }
    }

    add_tag(p, "title", title);
    add_tag(p, "source", source);
    add_tag(p, "artist", artist);
    add_tag(p, "comment", taglib_tag_comment(tl_tag));
    add_tag(p, "genre", taglib_tag_genre(tl_tag));

    i = taglib_tag_year(tl_tag);
    if (i)
        add_tag(p, "year", simple_itoa(i)); 
    i = taglib_tag_track(tl_tag);
    if (i) {
        add_tag(p, "tracknr", simple_itoa(i));
        add_tag(p, "file_id", simple_itoa(i));
    }
    i=taglib_audioproperties_samplerate(tl_audio);
    if(i)
        add_tag(p, "samplerate", simple_itoa(i));
    i=taglib_audioproperties_length(tl_audio)*1000;
    if(i)
        add_tag(p, "duration", simple_itoa(i));
    strcpy(bitrate, "vs");
    i=taglib_audioproperties_bitrate(tl_audio);
    if(i){
        if(i>999) strcat(bitrate, "999");
        else strcat(bitrate, simple_itoa(i));
        add_tag(p, "bitrate", bitrate);
    }

    taglib_file_free(tl_file);
    /*taglib_tag_free_strings();*/
    return 0;
}

static int get_file_type(mp3info *mp3)
{
    int ret, type = TYPE_DATA;
    unsigned int i, off;
    unsigned char buf[48];
    struct stat st;

    ret = stat(mp3->filename, &st);
    if (ret != 0)
        return -1;

    mp3->datasize = st.st_size;

    mp3->file = fopen(mp3->filename, "r");
    if (mp3->file == NULL)
        return -1;

    if (mp3->datasize >= 128) {
        if (fseek(mp3->file, -128, SEEK_END )) {
            lk_errors_set(E_SMALLMP3);
/*          fprintf(stderr,"read err: last 128 bytes of %s.\n",mp3->filename);*/
            return -1;
        } else {
            fread(buf, 1, 3, mp3->file);
            if (memcmp(buf, "TAG", 3) == 0)
                mp3->datasize -= 128;
        }
        fseek(mp3->file, 0, SEEK_SET);
    }

    fread(buf, 1, 48, mp3->file);
    if (memcmp(buf, "ID3", 3) == 0) {
        mp3->offset = (((buf[6]&0x7f)<<21)|((buf[7]&0x7f)<<14)|
                       ((buf[8]&0x7f)<<7) |(buf[9]&0x7f)) + 10;
        fseek(mp3->file, mp3->offset, SEEK_SET);
        fread(buf, 1, 48, mp3->file);
    }

    if (memcmp(buf, "OggS", 4) == 0) {
        mp3->offset += buf[27] + 28;
        if (memcmp(buf+29, "vorbis", 6) == 0) {
            type = TYPE_OGG;
            fseek(mp3->file, mp3->offset, SEEK_SET);
            fread(buf, 1, 48, mp3->file);
            mp3->offset += buf[26] + 27;
            for (i=0; i<buf[26]; i++)
                mp3->offset += buf[27+i];
        }
    } else if (memcmp(buf, "fLaC", 4) == 0) {
        type = TYPE_FLAC;
        mp3->offset += 4;
        memcpy(buf, buf+4, 4);
        while ((buf[0]&0x80) == 0) {
            mp3->offset += (((buf[1]&0xff)<<16)|
                            ((buf[2]&0xff)<<8) |(buf[3]&0xff)) + 4;
            fseek(mp3->file, mp3->offset, SEEK_SET);
            fread(buf, 1, 4, mp3->file);
        }
        mp3->offset += (((buf[1]&0xff)<<16)|
                        ((buf[2]&0xff)<<8) |(buf[3]&0xff)) + 4;
    } else if (memcmp(buf, "RIFF", 4) == 0) {
        wave_header *wh;

        if (openwav(&wh, mp3->filename) == -1){
            cleanup(&wh);
            return -1;
        }
        type = TYPE_WAV;
        mp3->offset = 0;
        cleanup(&wh);
    } else {
        fseek(mp3->file, 0, SEEK_SET);
        get_mp3_info(mp3);
        if (mp3->header_isvalid)
            type = TYPE_MP3;
    }
    fclose(mp3->file);

    return type;
}

void lk_karma_write_dupes(int set)
{
    write_dupes = set;
}
