/*
Copyright (C) 2002, 2003 Brian Victor Fernandes

This file is part of XMMS mp3cue.

XMMS mp3cue 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.

XMMS mp3cue is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with XMMS mp3cue; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

XMMS mp3cue: http://brianvictor.tripod.com/mp3cue.htm
Email: brianv@phreaker.net

Brian Fernandes
503, Miaami Apts. II,
St. Vincents Street,
Pune 411001
INDIA
*/

#include <string>
#include <iostream>
using namespace std;

#include <stdio.h>
#include <gtk/gtk.h>
#include <plugin.h>
#include "xmmsctrl.h"
#include "defs.h"

typedef unsigned char BYTE;
typedef unsigned char BOOL;


extern GeneralPlugin mp3cue;
extern gchar *album_name_s;
extern gchar *album_artist_s;

int Read7Int(const BYTE *pBuff);
void Write7Int(BYTE *pBuff, int nVal);
int ReadInt(const BYTE *pBuff);
void WriteInt(BYTE *pBuff, int nVal);

BOOL LoadID3(const char *fname);
BYTE *findGEOB(BYTE *tag, int size);
char *parse_cuesheet_frame(char *framestart, int fsize);
string Make_Cue_Sheet_Frame(short int version);
BYTE * CreateHeader(FILE *fp, int &tSize);
BYTE *CreateTextFrame(const char *ident, const char *text, int &fSize);


void ReadID3v1();

//we parse and save the main parts of the cuesheet using the same routines ...
extern void parse_cue_sheet(FILE *cue_sheet,char *cue_mem);
extern string Make_Cue_String();
extern void quick_message(gchar *message);

struct ID3v1 {
  char tag[3];
  char title[30];
  char artist[30];
  char album[30];
  char year[4];
  char comment[30];
  char genre;
};

struct ID3v2_Header {
  BYTE identifier[3];
  BYTE mjVersion, mnVersion;
  BYTE flags;
  BYTE size[4];
};


struct ID3v2_EHeader {
  BYTE size[4];
  BYTE nflags;
  BYTE flags;
};

ID3v2_Header Hdr;
ID3v2_EHeader EHdr;


BOOL LoadID3(const char *fname) 
{
  int tag_size, e_hdr_size=0;
  BYTE *tag; //this will hold the entire ID3tag

  FILE *fp = fopen(fname,"r");
  if(!fp) return FALSE;

  //read the first 14 bytes for the header and extended header info
  fread(&Hdr, 1, sizeof(Hdr), fp);

  if(strncmp((const char *)Hdr.identifier,"ID3",3)) {
#ifdef DEBUG
    printf("No ID3v2\n");
#endif
    fclose(fp);
    return FALSE; //no ID3v2 tag found, nothing to be gained from here
  }

  //here starts the first of v2.4 changes ... bastards, talk about downward compatibility ..  
  //v2.4 has a footer, not included in the size of the tag
  //v2.3 has no footer.. in any case; we don't give a damn, we don't need the frickin footer
  tag_size = Read7Int(Hdr.size);

  if((Hdr.flags >> 6) % 2)  {
    //read the extended header
    fread(&EHdr, 1, sizeof(EHdr), fp);
    //in v2.4, the extended header size is the size of the whole header
    //in v2.3, the extended header size does not include the 4 bytes for the size
    if(Hdr.mjVersion <= 3)  e_hdr_size = ReadInt(EHdr.size) + 4;
    else e_hdr_size = Read7Int(EHdr.size);
  }

#ifdef DEBUG
  printf("ID3v2.%c.%c Size is  %d\nExtended Header %d\nFlag %d",Hdr.mjVersion+'0',Hdr.mnVersion+'0',tag_size, e_hdr_size, Hdr.flags);
#endif
 
  int offset = 10 + e_hdr_size; //skip both the ID3 and extended header
  int frameslength = tag_size - e_hdr_size; //read size of the remaining frames

  //allocate frameslength amount of memory
  tag = (BYTE *)malloc(frameslength);
  if(!tag) {
#ifdef DEBUG
    printf("Error: ID3v2 Tag too large\n");
#endif
    fclose(fp);
    return FALSE;
  }

  //now read all the frames into tag
  fseek(fp, offset, SEEK_SET);
  fread(tag, 1, frameslength, fp);
  
  BYTE *GEOB_loc;
  char *cue_start;
  GEOB_loc = findGEOB(tag, frameslength);
  if(GEOB_loc) {
    cue_start = parse_cuesheet_frame((char *)GEOB_loc, ((Hdr.mjVersion <= 3) ? ReadInt(GEOB_loc+4) : Read7Int(GEOB_loc+4)) );
    if(cue_start) { 
      parse_cue_sheet(NULL, cue_start);
    }
  }

  free(tag);
  fclose(fp);

  return TRUE;
}




BYTE *findGEOB(BYTE *tag, int size)
{
  BYTE *t_tag = tag;
  int read = 0, fsize;
  while(read < size) {
    //read the four bytes
    if(!strncmp((const char *)t_tag,"GEOB",4) || !strncmp((const char *)t_tag,"XXQ1",4)) {
#ifdef DEBUG
      printf("mp3cue tag located ... parsing.\n");
#endif
      return t_tag;
    }

    else if(!(*t_tag)) break; //we have hit padding

    else {
      //so now the actual nonsense data is fsize + 10 (frame header size)
      //just skip the shit
      if(Hdr.mjVersion <= 3) fsize = ReadInt(t_tag+4); //stored as 32 bit integer
      else fsize = Read7Int(t_tag+4); //stored as synch safe integer
      t_tag += 10 + fsize;    
      read += 10 + fsize;
    }
  }

  return NULL;
}

BYTE *findPadding(BYTE *tag, int size)
{
  int read = 0, fsize;
  while(read < size) {
    //read the four bytes
    if(!(*tag)) return tag; //we have hit padding

    else {
      //just skip the tag
      if(Hdr.mjVersion <= 3) fsize = ReadInt(tag+4); //stored as 32 bit integer
      else fsize = Read7Int(tag+4); //stored as synch safe integer
      tag += 10 + fsize;    
      read += 10 + fsize;
    }
  }
  return NULL;
}


char *parse_cuesheet_frame(char *framestart, int fsize)
{
  char *t_ptr = framestart + 10; //skip the ID, size and flags
  char *cue_start;
  char mime[10], filename[25];

  //the following was almost copied from the original, we handle the 3rd condition properly..
  if(!*t_ptr && ! *(t_ptr + 1)) {
#ifdef DEBUG
    printf("option 1\n");
#endif
    cue_start = t_ptr+4;
  }
  else if(*t_ptr==0x01) {
#ifdef DEBUG
    printf("Unicode mp3cue tag not supported\n");
#endif
    return NULL;
  }
  else if(*t_ptr==0x00) {
#ifdef DEBUG
    printf("option 3\n");
#endif
    //scan the mime type and the "filename"
    t_ptr++;
    sscanf(t_ptr,"%s",mime);
    t_ptr+=strlen(mime)+1;
    sscanf(t_ptr,"%[^0]s",filename);
    t_ptr+=strlen(filename)+2;
#ifdef DEBUG
    printf("%s\n%s\n",mime,filename);
#endif
    cue_start = t_ptr;    
  }
  else {
    cue_start = t_ptr;
#ifdef DEBUG
    printf("option 4\n");
#endif
  }
  return cue_start;
}


string Make_Cue_Sheet_Frame(short int version)
{
  string frame_string;
   
  frame_string = "GEOB";
  frame_string += "0000"; //fill this in with the size later
  frame_string += '\0';
  frame_string += '\0'; //flags

  //now start with the actual text
  frame_string +='\0';  //encoding as ascii
  frame_string +="text"; //mime
  frame_string += '\0';
  frame_string +="mp3cue CueSheet";//filename
  frame_string +='\0';
  frame_string +='Q';	//descriptor

  frame_string += "CUESHEET\r\n"	+ Make_Cue_String() + "ENDCUESHEET";
  frame_string +="CUEVERSION\r\n"	+ string("XMMS mp3cue " VERSION)	+ "ENDCUEVERSION";
  frame_string += "CUEINFO\r\nENDCUEINFO"; //currently no info

  
  //set the size 
  int fsize = frame_string.length() - 10; //remove 10 bytes .. exclude the header size
#ifdef DEBUG
  printf("len is %d\n ",fsize);
#endif

  if(version <= 3) WriteInt((BYTE *)&frame_string[4], fsize);
  else Write7Int((BYTE *)&frame_string[4], fsize);
   
  return frame_string;
}


BYTE * CreateHeader(FILE *fp, int &tSize)
{
  BYTE *ID3_Hdr;

  tSize = sizeof(Hdr); 

  int e_hdr_size;
  //read the header
  fread(&Hdr, sizeof(Hdr), 1, fp);
  if(!strncmp((const char *)Hdr.identifier, "ID3", 3)) {
    //if there is a v2 header already
    ID3_Hdr = (BYTE *)malloc(tSize);
    memcpy(ID3_Hdr, &Hdr, sizeof(Hdr));

    if( (Hdr.flags >> 6) % 2) {
      //if there is an extended header
      fread(&EHdr, 1, sizeof(EHdr), fp);

      //size of the extended header
      if(Hdr.mjVersion <= 3)  e_hdr_size =  ReadInt(EHdr.size) + 4;
      else e_hdr_size = Read7Int(EHdr.size);
      
      tSize += e_hdr_size;

      ID3_Hdr = (BYTE *)realloc(ID3_Hdr, tSize);
      memcpy(ID3_Hdr+sizeof(Hdr), &EHdr, sizeof(EHdr)); 
      //we still have to read e_hdr_size - sizeof(ehdr) bytes ... the actual extended header
      fread(ID3_Hdr+sizeof(Hdr)+sizeof(EHdr), e_hdr_size - sizeof(EHdr), 1, fp); 
    }
  }
  
  else {
    //there was no ID3v2 tag.. create a dummy tag
    strncpy((char *)Hdr.identifier, "ID3", 3);
    Hdr.mjVersion = 3;
    Hdr.mnVersion = 0;
    Hdr.flags = 0x00; 
    //the size will be updated later
    memset(&Hdr.size, 0 , 4);
    ID3_Hdr = (BYTE *)malloc(tSize);
    memcpy(ID3_Hdr, &Hdr, sizeof(Hdr));

    int temp;
    //performer frame 
    BYTE *tFrame = CreateTextFrame("TPE1",album_artist_s,temp);
    ID3_Hdr = (BYTE *)realloc(ID3_Hdr, tSize+temp);
    memcpy(ID3_Hdr+tSize, tFrame, temp);
    tSize += temp;
    free(tFrame);    

    //the title frame
    tFrame = CreateTextFrame("TIT2",album_name_s,temp);
    ID3_Hdr = (BYTE *)realloc(ID3_Hdr, tSize+temp);
    memcpy(ID3_Hdr+tSize, tFrame, temp);
    tSize += temp;
    free(tFrame);

  }
  return ID3_Hdr;
}

BYTE *CreateTextFrame(const char *ident, const char *text, int &fSize) 
{
  int len = 1;
  if(text) len += strlen(text);
  fSize = 10 + len;
  BYTE *frame = (BYTE *)malloc(fSize);
  memset(frame, 0, fSize);
  memcpy(frame, ident, 4);
  if(Hdr.mjVersion <= 3) WriteInt(frame+4, fSize-10);
  else Write7Int(frame+4, fSize-10);
  if(text) memcpy(frame+11, text, len);
  return frame;
}



void SaveID3(GtkWidget *dummy, GdkEvent *dummy_event, gpointer remove)
{
  //start collecting info about the file
  FILE *fp;
  gint pos = xmms_remote_get_playlist_pos(mp3cue.xmms_session);
  gchar *o_fname = xmms_remote_get_playlist_file(mp3cue.xmms_session,pos);

  if(!(fp=fopen(o_fname,"r+"))) {
    quick_message("Error opening file for modification\nCue Data cannot be updated\nPlease check file permissions.");
    return;
  }

  //tag will contain the entire tag, header + frames + footer
  BYTE *header,  *other, *frames = NULL, *nLoc;
  int hSize, ehSize=0,  tagSize, oSize, fSize=0, footSize = 0, nSize;
  header =  CreateHeader(fp, hSize);

  //read all frames and padding into memory for easy manipulation
  tagSize = Read7Int(Hdr.size);
  if( (Hdr.flags >> 6) % 2) {
    if(Hdr.mjVersion <= 3)  ehSize = ReadInt(EHdr.size) + 4;
    else ehSize = Read7Int(EHdr.size);
  }

  oSize = tagSize - ehSize;
  other = (BYTE *)malloc(oSize);
  fread(other, 1, oSize, fp);

  //seek past the footer, if present
  if(Hdr.mjVersion > 3 && (Hdr.flags & 0x10)) {
    footSize = 10;
    fseek(fp, 10, SEEK_CUR);
  }


  //check if we already have a tag in the system
  BYTE *GEOB_loc = findGEOB(other, oSize);
  int preSize = 0;


  if(remove && !GEOB_loc) {
    quick_message("Error: Tag does not already exist in the mp3 file.\nRemoval Failed.");
    free(header);
    if(other) free(other);
    fclose(fp);
    return;   
  }


  if(GEOB_loc) {
    //if we do, copy all frames before the tag
    preSize = GEOB_loc-other;
    frames = (BYTE *)malloc(preSize);
    memcpy(frames, other, preSize);
    fSize = preSize;
    
    //skip this tag
    nLoc = GEOB_loc+10;
    if(Hdr.mjVersion <= 3) nLoc += ReadInt(GEOB_loc+4);
    else nLoc += Read7Int(GEOB_loc+4);
    nSize = oSize-(nLoc-other);
  }
  
  else {
    nLoc = other;
    nSize = oSize;
  }

  if(!remove) {
    string cueFrame = Make_Cue_Sheet_Frame(Hdr.mjVersion);    
#ifdef DEBUG
    cout << endl << cueFrame;
    printf("mp3cue Frame\n%s\n", cueFrame.data());
#endif

    //now copy our cue sheet tags.. woo hoo
    frames = (BYTE *)realloc(frames, fSize+cueFrame.length());
    memcpy(frames+fSize, cueFrame.data(), cueFrame.length());
    fSize += cueFrame.length();
  }

  //now copy the stuff after the GEOB tag (or if the tag wasn't found), upto the padding
  //nLoc and nSize will contain the pointer and size of the remaining data to be copied
  
  //first locate the padding
  BYTE *padding;
  padding = findPadding(nLoc, nSize);  

  //this is actually wrong, we should be going through each of the frames, 
  //checking the preserve and discard flags, beacause their position in the file
  //is likely to have changed if we are updating the GEOB frame

  if(padding) nSize = padding-nLoc;
  frames = (BYTE *)realloc(frames, fSize+nSize);
  memcpy(frames+fSize, nLoc, nSize);
  fSize += nSize;

  //correct the sizes and perform other calculations
  
  //Is our framesize within the original size (osize ?)
  //If so, then we need not rewrite the file, there was sufficient padding
  //If not, we will add some padding and rewrite the entire file
  BOOL rewrite;
  int pSize = 0;
  if(fSize > oSize) {
    //if we exceeded the original tag size, then we create new padding
    pSize = fSize/2;
    rewrite = true;
#ifdef DEBUG
    printf("New padding added: %d bytes\n",pSize);
#endif
  }

  else {
    //we didn't exceed, there was padding left over (or GEOB is smaller than before)
    pSize = oSize - fSize;
    rewrite = false;
#ifdef DEBUG
    printf("No padding added: %d bytes leftover\n",pSize);
#endif
  }
  
  if(pSize) {
    padding = (BYTE *)malloc(pSize);
    memset(padding, 0, pSize);
  }
  
  //In v2.3, correct the size of the padding in the extended header, if present (what a royal pain...)
  if( (Hdr.mjVersion <= 3) && ((Hdr.flags >> 6) % 2) ) 
    WriteInt(header + sizeof(Hdr) + 6, pSize);
 
  
  //now write to the mp3 file

  if(rewrite) {
#ifdef DEBUG
    printf("Rewriting mp3 file\n");
#endif
    //we have to rewrite the entire file.. what fun..
    //this will just shift the mp3 data down.
    int shift = fSize + pSize - oSize;
    //if we created a new header then add 10 shift
    if(!Read7Int(Hdr.size)) {
      shift += hSize;
    }

#ifdef DEBUG
    printf("File shifted down by %d bytes\n",shift);
#endif

    //the mp3 data must be shifted down by shift bytes
    int BSIZE = 1000000;//(1 MB)
    BYTE *buffer;
    buffer = (BYTE *)malloc(BSIZE);
    if(!buffer) {
      BSIZE = 10000; //(10KB)
      buffer = (BYTE *)malloc(BSIZE);
    }


    fseek(fp, 0, SEEK_END);
    int fileSize = ftell(fp);
    int rPos;
    for(rPos = fileSize - BSIZE; rPos >= 0; rPos-=BSIZE) {
      fseek(fp, rPos, SEEK_SET);
      fread(buffer, BSIZE, 1, fp);
      fseek(fp, rPos+shift, SEEK_SET);
      fwrite(buffer, BSIZE, 1, fp);
    }
    //one more copy, for the first BSIZE bytes, also accomodate files smaller than the buffer size
    rewind(fp);
    int read = fread(buffer, 1, rPos+BSIZE, fp);
    fseek(fp, shift, SEEK_SET);
    fwrite(buffer, read, 1, fp);

    free(buffer);
  }



  //correct the size of the tag in the the id3 header
  //  tagSize = (hSize-10) + ehSize + fSize + pSize; //extended header + frames + padding sizes (we include the title in the header..)
  tagSize = (hSize-10) + fSize + pSize; //header (extended and TPE1 and TIT2.. if new) + frame size + padding
  Write7Int(&header[6], tagSize);


  //write the ID3v2 tag at the start of the file
  rewind(fp);
  fwrite(header, hSize, 1, fp);
  fwrite(frames, fSize, 1, fp);
  if(pSize) fwrite(padding, pSize, 1, fp);
  if(footSize) {
    //footer is the same as the header, just change the identifier
    memcpy(header, "3DI", 3);
    fwrite(header, footSize, 1, fp);
  }
  

  free(header);
  free(frames);
  if(other)  free(other);
  if(padding) free(padding);

  fclose(fp);
}

//parse ID3v1
void ReadID3v1()
{
  gint pos = xmms_remote_get_playlist_pos(mp3cue.xmms_session);
  gchar *o_fname = xmms_remote_get_playlist_file(mp3cue.xmms_session,pos);

  FILE *mp3 = fopen(o_fname,"rb");
  
  if(!mp3) {
#ifdef DEBUG
    printf("Error opening file %s\n",o_fname);
#endif
    return;
  }

  fseek(mp3, 0, SEEK_END);
  int tloc = ftell(mp3) - 128;
  fseek(mp3, tloc, SEEK_SET);

  ID3v1 id3v1;
  fread(&id3v1, sizeof(id3v1), 1, mp3);
  
  if(strncmp(id3v1.tag,"TAG",3)) {
#ifdef DEBUG
    printf("%s has no ID3v1 tag\n",o_fname);
#endif
    fclose(mp3);
    return;

  }

  char artist[30];
  strcpy(artist, id3v1.artist);

#ifdef DEBUG
  printf("Title %s\nArtist %s\nAlbum %s\nYear %s\nComment %s\nGenre %d", id3v1.title, id3v1.artist, id3v1.album, id3v1.year, id3v1.comment, id3v1.genre);
#endif  

  fclose(mp3);
}

//Supplementary routines

int Read7Int(const BYTE *pBuff)
{
  //read the 7 bit / byte 4 byte integer 
  BYTE size_a= *(pBuff + 0);
  BYTE size_b= *(pBuff + 1);
  BYTE size_c= *(pBuff + 2);
  BYTE size_d= *(pBuff + 3);
  int ret = size_d + (size_c << 7) + (size_b << 14) + (size_a << 21);
  return ret;
}


void Write7Int(BYTE *pBuff, int nVal)
{
  *(pBuff + 3) = nVal % 128;
  *(pBuff + 2) = (nVal >> 7)  % 128;
  *(pBuff + 1) = (nVal >> 14) % 128;
  *(pBuff + 0) = (nVal >> 21) % 128;
}



int ReadInt(const BYTE *pBuff)
{
  BYTE size_a= *(pBuff + 0);
  BYTE size_b= *(pBuff + 1);
  BYTE size_c= *(pBuff + 2);
  BYTE size_d= *(pBuff + 3);
  int ret = size_d + (size_c << 8) + (size_b << 16) + (size_a << 24);
  return ret;
}

void WriteInt(BYTE *pBuff, int nVal)
{
  //Intel is Little Endian.. the LSB is first MSB is last (higher order bits)
  //We need MSB first, so we shift.. woo hoo
  *(pBuff) = nVal >> 8*3;
  *(pBuff+1) = (nVal >> 8*2) & 0xFF;
  *(pBuff+2) = (nVal >> 8*1) & 0xFF;
  *(pBuff+3) = nVal & 0xFF;
}
