/* $Id: Header.xs 360 2005-11-26 08:02:13Z dsully $ */

/* 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 program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * Chunks of this code have been borrowed and influenced from the FLAC source.
 *
 */

#ifdef __cplusplus
"C" {
#endif
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#ifdef __cplusplus
}
#endif

#include <FLAC/all.h>

/* for PRIu64 */
#include <inttypes.h>

#define FLACHEADERFLAG "fLaC"
#define ID3HEADERFLAG  "ID3"

#ifdef _MSC_VER
# define stat _stat
#endif

/* strlen the length automatically */
#define my_hv_store(a,b,c)   (void)hv_store(a,b,strlen(b),c,0)
#define my_hv_store_ent(a,b,c) (void)hv_store_ent(a,b,c,0)
#define my_hv_fetch(a,b)     hv_fetch(a,b,strlen(b),0)

void _cuesheet_frame_to_msf(unsigned frame, unsigned *minutes, unsigned *seconds, unsigned *frames) {

  *frames = frame % 75;
  frame /= 75;
  *seconds = frame % 60;
  frame /= 60;
  *minutes = frame;
}

void _read_metadata(HV *self, char *path, FLAC__StreamMetadata *block, unsigned block_number) {

  unsigned i;
  int storePicture = 0;

  HV *pictureContainer = newHV();
  AV *allpicturesContainer = NULL;

  switch (block->type) {

    case FLAC__METADATA_TYPE_STREAMINFO:
    {
      HV *info = newHV();
      float totalSeconds;

      my_hv_store(info, "MINIMUMBLOCKSIZE", newSVuv(block->data.stream_info.min_blocksize));
      my_hv_store(info, "MAXIMUMBLOCKSIZE", newSVuv(block->data.stream_info.max_blocksize));

      my_hv_store(info, "MINIMUMFRAMESIZE", newSVuv(block->data.stream_info.min_framesize));
      my_hv_store(info, "MAXIMUMFRAMESIZE", newSVuv(block->data.stream_info.max_framesize));

      my_hv_store(info, "SAMPLERATE", newSVuv(block->data.stream_info.sample_rate));
      my_hv_store(info, "NUMCHANNELS", newSVuv(block->data.stream_info.channels));
      my_hv_store(info, "BITSPERSAMPLE", newSVuv(block->data.stream_info.bits_per_sample));
      my_hv_store(info, "TOTALSAMPLES", newSVnv(block->data.stream_info.total_samples));

      if (block->data.stream_info.md5sum != NULL) {

        /* Initialize an SV with the first element,
           and then append to it. If we don't do it this way, we get a "use of
           uninitialized element" in subroutine warning. */
        SV *md5 = newSVpvf("%02x", (unsigned)block->data.stream_info.md5sum[0]);

        for (i = 1; i < 16; i++) {
          sv_catpvf(md5, "%02x", (unsigned)block->data.stream_info.md5sum[i]);
        }

        my_hv_store(info, "MD5CHECKSUM", md5);
      }

      my_hv_store(self, "info", newRV_noinc((SV*) info));

      /* Store some other metadata for backwards compatability with the original Audio::FLAC */
      /* needs to be higher resolution */
      totalSeconds = block->data.stream_info.total_samples / (float)block->data.stream_info.sample_rate;

      if (totalSeconds <= 0) {
        warn("File: %s - %s\n%s\n",
          path,
          "totalSeconds is 0 - we couldn't find either TOTALSAMPLES or SAMPLERATE!",
          "setting totalSeconds to 1 to avoid divide by zero error!"
        );

        totalSeconds = 1;
      }

      my_hv_store(self, "trackTotalLengthSeconds", newSVnv(totalSeconds));

      my_hv_store(self, "trackLengthMinutes", newSVnv((int)totalSeconds / 60));
      my_hv_store(self, "trackLengthSeconds", newSVnv((int)totalSeconds % 60));
      my_hv_store(self, "trackLengthFrames", newSVnv((totalSeconds - (int)totalSeconds) * 75));

      break;
    }

    case FLAC__METADATA_TYPE_PADDING:
    case FLAC__METADATA_TYPE_SEEKTABLE:
      /* Don't handle these yet. */
      break;

    case FLAC__METADATA_TYPE_APPLICATION:
    {
      if (block->data.application.id[0]) {

        HV *app   = newHV();
        SV *tmpId = newSVpvf("%02x", (unsigned)block->data.application.id[0]);
        SV *appId;

        for (i = 1; i < 4; i++) {
          sv_catpvf(tmpId, "%02x", (unsigned)block->data.application.id[i]);
        }

        /* Be compatible with the pure perl version */
        appId = newSVpvf("%ld", strtol(SvPV_nolen(tmpId), NULL, 16));

        if (block->data.application.data != 0) {
          my_hv_store_ent(app, appId, newSVpvn((char*)block->data.application.data, block->length));
        }

        my_hv_store(self, "application",  newRV_noinc((SV*) app));

        SvREFCNT_dec(tmpId);
        SvREFCNT_dec(appId);
      }

      break;
    }

    case FLAC__METADATA_TYPE_VORBIS_COMMENT:
    {
      AV *rawTagArray = newAV();
      HV *tags = newHV();
      SV **tag = NULL;
      SV **separator = NULL;

      if (block->data.vorbis_comment.vendor_string.entry) {
        my_hv_store(tags, "VENDOR", newSVpv((char*)block->data.vorbis_comment.vendor_string.entry, 0));
      }

      for (i = 0; i < block->data.vorbis_comment.num_comments; i++) {

        if (!block->data.vorbis_comment.comments[i].entry || !block->data.vorbis_comment.comments[i].length) {
          warn("Empty comment, skipping...\n");
          continue;
        }

        /* store the pointer location of the '=', poor man's split() */
        char *entry = (char*)block->data.vorbis_comment.comments[i].entry;
        char *half  = strchr(entry, '=');

        /* store the raw tags */
        av_push(rawTagArray, newSVpv(entry, 0));

        if (half == NULL) {
          warn("Comment \"%s\" missing \'=\', skipping...\n", entry);
          continue;
        }

        if (hv_exists(tags, entry, half - entry)) {
          /* fetch the existing entry */
          tag = hv_fetch(tags, entry, half - entry, 0);

          /* fetch the multi-value separator or default and append to the entry */
          if (hv_exists(self, "separator", 9)) {
            separator = hv_fetch(self, "separator", 9, 0);
            sv_catsv(*tag, *separator);
          } else {
            sv_catpv(*tag, "/");
          }

          /* concatenate with the new entry */
          sv_catpv(*tag, half + 1);
        } else {
          (void)hv_store(tags, entry, half - entry, newSVpv(half + 1, 0), 0);
        }
      }

      my_hv_store(self, "tags", newRV_noinc((SV*) tags));
      my_hv_store(self, "rawTags", newRV_noinc((SV*) rawTagArray));

      break;
    }

    case FLAC__METADATA_TYPE_CUESHEET:
    {
      AV *cueArray = newAV();

      /*
       * buffer for decimal representations of uint64_t values
       *
       * newSVpvf() and sv_catpvf() can't handle 64-bit values
       * in some cases, so we need to do the conversion "manually"
       * with sprintf() and the PRIu64 format macro for portability
       *
       * see http://bugs.debian.org/462249
       *
       * maximum string length: ceil(log10(2**64)) == 20 (+trailing \0)
       */
      char decimal[21];

      /* A lot of this comes from flac/src/share/grabbag/cuesheet.c */
      const FLAC__StreamMetadata_CueSheet *cs;
      unsigned track_num, index_num;

      cs = &block->data.cue_sheet;

      if (*(cs->media_catalog_number)) {
        av_push(cueArray, newSVpvf("CATALOG %s\n", cs->media_catalog_number));
      }

      av_push(cueArray, newSVpvf("FILE \"%s\" FLAC\n", path));

      for (track_num = 0; track_num < cs->num_tracks-1; track_num++) {

        const FLAC__StreamMetadata_CueSheet_Track *track = cs->tracks + track_num;

        av_push(cueArray, newSVpvf("  TRACK %02u %s\n",
          (unsigned)track->number, track->type == 0? "AUDIO" : "DATA"
        ));

        if (track->pre_emphasis) {
          av_push(cueArray, newSVpv("    FLAGS PRE\n", 0));
        }

        if (*(track->isrc)) {
          av_push(cueArray, newSVpvf("    ISRC %s\n", track->isrc));
        }

        for (index_num = 0; index_num < track->num_indices; index_num++) {

          const FLAC__StreamMetadata_CueSheet_Index *index = track->indices + index_num;

          SV *indexSV = newSVpvf("    INDEX %02u ", (unsigned)index->number);

          if (cs->is_cd) {

            unsigned logical_frame = (unsigned)((track->offset + index->offset) / (44100 / 75));
            unsigned m, s, f;

            _cuesheet_frame_to_msf(logical_frame, &m, &s, &f);

            sv_catpvf(indexSV, "%02u:%02u:%02u\n", m, s, f);

          } else {
            sprintf(decimal, "%"PRIu64, track->offset + index->offset);
            sv_catpvf(indexSV, "%s\n", decimal);
          }


          av_push(cueArray, indexSV);
        }
      }

      sprintf(decimal, "%"PRIu64, cs->lead_in);
      av_push(cueArray, newSVpvf("REM FLAC__lead-in %s\n", decimal));
      sprintf(decimal, "%"PRIu64, cs->tracks[track_num].offset);
      av_push(cueArray, newSVpvf("REM FLAC__lead-out %u %s\n",
        (unsigned)cs->tracks[track_num].number, decimal)
      );

      my_hv_store(self, "cuesheet",  newRV_noinc((SV*) cueArray));

      break;
    }

/* The PICTURE metadata block came about in FLAC 1.1.3 */
#ifdef FLAC_API_VERSION_CURRENT
    case FLAC__METADATA_TYPE_PICTURE:
    {
      HV *picture = newHV();
      SV *type;

      my_hv_store(picture, "mimeType", newSVpv(block->data.picture.mime_type, 0));
      my_hv_store(picture, "description", newSVpv((const char*)block->data.picture.description, 0));
      my_hv_store(picture, "width", newSViv(block->data.picture.width));
      my_hv_store(picture, "height", newSViv(block->data.picture.height));
      my_hv_store(picture, "depth", newSViv(block->data.picture.depth));
      my_hv_store(picture, "colorIndex", newSViv(block->data.picture.colors));
      my_hv_store(picture, "imageData", newSVpv((const char*)block->data.picture.data, block->data.picture.data_length));
      my_hv_store(picture, "pictureType", newSViv(block->data.picture.type));

      type = newSViv(block->data.picture.type);

      my_hv_store_ent(pictureContainer, type, newRV_noinc((SV*) picture));

      SvREFCNT_dec(type);

      storePicture = 1;

      /* update allpictures */
      if (hv_exists(self, "allpictures", 11)) {
        allpicturesContainer = (AV *) SvRV(*my_hv_fetch(self, "allpictures"));
      } else {
        allpicturesContainer = newAV();

        /* store the 'allpictures' array */
        my_hv_store(self, "allpictures", newRV_noinc((SV*) allpicturesContainer));
      }

      av_push(allpicturesContainer, (SV*) newRV((SV*) picture));

      break;
    }
#endif

    /* XXX- Just ignore for now */
    default:
      break;
  }

  /* store the 'picture' hash */
  if (storePicture && hv_scalar(pictureContainer)) {
    my_hv_store(self, "picture", newRV_noinc((SV*) pictureContainer));
  } else {
    SvREFCNT_dec((SV*) pictureContainer);
  }
}

/* From src/metaflac/operations.c */
void print_error_with_chain_status(FLAC__Metadata_Chain *chain, const char *format, ...) {

  const FLAC__Metadata_ChainStatus status = FLAC__metadata_chain_status(chain);
  va_list args;

  FLAC__ASSERT(0 != format);

  va_start(args, format);
  (void) vfprintf(stderr, format, args);
  va_end(args);

  warn("status = \"%s\"\n", FLAC__Metadata_ChainStatusString[status]);

  if (status == FLAC__METADATA_CHAIN_STATUS_ERROR_OPENING_FILE) {

    warn("The FLAC file could not be opened. Most likely the file does not exist or is not readable.");

  } else if (status == FLAC__METADATA_CHAIN_STATUS_NOT_A_FLAC_FILE) {

    warn("The file does not appear to be a FLAC file.");

  } else if (status == FLAC__METADATA_CHAIN_STATUS_NOT_WRITABLE) {

    warn("The FLAC file does not have write permissions.");

  } else if (status == FLAC__METADATA_CHAIN_STATUS_BAD_METADATA) {

    warn("The metadata to be writted does not conform to the FLAC metadata specifications.");

  } else if (status == FLAC__METADATA_CHAIN_STATUS_READ_ERROR) {

    warn("There was an error while reading the FLAC file.");

  } else if (status == FLAC__METADATA_CHAIN_STATUS_WRITE_ERROR) {

    warn("There was an error while writing FLAC file; most probably the disk is full.");

  } else if (status == FLAC__METADATA_CHAIN_STATUS_UNLINK_ERROR) {

    warn("There was an error removing the temporary FLAC file.");
  }
}

MODULE = Audio::FLAC::Header PACKAGE = Audio::FLAC::Header

PROTOTYPES: DISABLE

SV*
_new_XS(class, path)
  char *class;
  char *path;

  CODE:

  HV *self = newHV();
  SV *obj_ref = newRV_noinc((SV*) self);

  /* Start to walk the metadata list */
  FLAC__Metadata_Chain *chain = FLAC__metadata_chain_new();

  if (chain == 0) {
    die("Out of memory allocating chain");
    XSRETURN_UNDEF;
  }

  if (!FLAC__metadata_chain_read(chain, path)) {
    print_error_with_chain_status(chain, "%s: ERROR: reading metadata", path);
    XSRETURN_UNDEF;
  }

  {
    FLAC__Metadata_Iterator *iterator = FLAC__metadata_iterator_new();
    FLAC__StreamMetadata *block = 0;
    FLAC__bool ok = true;
    unsigned block_number = 0;

    if (iterator == 0) {
      die("out of memory allocating iterator");
    }

    FLAC__metadata_iterator_init(iterator, chain);

    do {
             block = FLAC__metadata_iterator_get_block(iterator);
      ok &= (0 != block);

      if (!ok) {

        warn("%s: ERROR: couldn't get block from chain", path);

      } else {

        _read_metadata(self, path, block, block_number);
      }

      block_number++;

    } while (ok && FLAC__metadata_iterator_next(iterator));

    FLAC__metadata_iterator_delete(iterator);
  }

  FLAC__metadata_chain_delete(chain);

  /* Make sure tags is an empty HV if there were no VCs in the file */
  if (!hv_exists(self, "tags", 4)) {
    my_hv_store(self, "tags", newRV_noinc((SV*) newHV()));
  }

  /* Find the offset of the start pos for audio blocks (ie: after metadata) */
  {
    unsigned int  is_last = 0;
    unsigned char buf[4];
    long len;
    struct stat st;
    float totalSeconds;
    PerlIO *fh;

    if ((fh = PerlIO_open(path, "r")) == NULL) {
      warn("Couldn't open file [%s] for reading!\n", path);
      XSRETURN_UNDEF;
    }

    if (PerlIO_read(fh, &buf, 4) == -1) {
      warn("Couldn't read magic fLaC header!\n");
      PerlIO_close(fh);
      XSRETURN_UNDEF;
    }

    if (memcmp(buf, ID3HEADERFLAG, 3) == 0) {

      unsigned id3size = 0;
      int c = 0;

      /* How big is the ID3 header? Skip the next two bytes */
      if (PerlIO_read(fh, &buf, 2) == -1) {
        warn("Couldn't read ID3 header length!\n");
        PerlIO_close(fh);
        XSRETURN_UNDEF;
      }

      /* The size of the ID3 tag is a 'synchsafe' 4-byte uint */
      for (c = 0; c < 4; c++) {

        if (PerlIO_read(fh, &buf, 1) == -1 || buf[0] & 0x80) {
          warn("Couldn't read ID3 header length (syncsafe)!\n");
          PerlIO_close(fh);
          XSRETURN_UNDEF;
        }

        id3size <<= 7;
        id3size |= (buf[0] & 0x7f);
      }

      if (PerlIO_seek(fh, id3size, SEEK_CUR) < 0) {
        warn("Couldn't seek past ID3 header!\n");
        PerlIO_close(fh);
        XSRETURN_UNDEF;
      }

      if (PerlIO_read(fh, &buf, 4) == -1) {
        warn("Couldn't read magic fLaC header!\n");
        PerlIO_close(fh);
        XSRETURN_UNDEF;
      }
    }

    if (memcmp(buf, FLACHEADERFLAG, 4)) {
      warn("Couldn't read magic fLaC header - got gibberish instead!\n");
      PerlIO_close(fh);
      XSRETURN_UNDEF;
    }

    while (!is_last) {

      if (PerlIO_read(fh, &buf, 4) != 4) {
        warn("Couldn't read 4 bytes of the metadata block!\n");
        PerlIO_close(fh);
        XSRETURN_UNDEF;
      }

      is_last = (unsigned int)(buf[0] & 0x80);

      len = (long)((buf[1] << 16) | (buf[2] << 8) | (buf[3]));

      PerlIO_seek(fh, len, SEEK_CUR);
    }

    len = PerlIO_tell(fh);
    PerlIO_close(fh);

    my_hv_store(self, "startAudioData", newSVnv(len));

    /* Now calculate the bit rate and file size */
    totalSeconds = (float)SvIV(*(my_hv_fetch(self, "trackTotalLengthSeconds")));

    /* Find the file size */
    if (stat(path, &st) == 0) {
      my_hv_store(self, "fileSize", newSViv(st.st_size));
    } else {
      warn("Couldn't stat file: [%s], might be more problems ahead!", path);
    }

    my_hv_store(self, "bitRate", newSVnv(8.0 * (st.st_size - len) / totalSeconds));
  }

  my_hv_store(self, "filename", newSVpv(path, 0));

  /* Bless the hashref to create a class object */
  sv_bless(obj_ref, gv_stashpv(class, FALSE));

  RETVAL = obj_ref;

  OUTPUT:
  RETVAL

SV*
_write_XS(obj)
  SV* obj

  CODE:

  FLAC__bool ok = true;

  HE *he;
  HV *self = (HV *) SvRV(obj);
  HV *tags = (HV *) SvRV(*(my_hv_fetch(self, "tags")));

  char *path = (char *) SvPV_nolen(*(my_hv_fetch(self, "filename")));

  FLAC__Metadata_Chain *chain = FLAC__metadata_chain_new();

  if (chain == 0) {
    die("Out of memory allocating chain");
    XSRETURN_UNDEF;
  }

  if (!FLAC__metadata_chain_read(chain, path)) {
    print_error_with_chain_status(chain, "%s: ERROR: reading metadata", path);
    XSRETURN_UNDEF;
  }

  FLAC__Metadata_Iterator *iterator = FLAC__metadata_iterator_new();
  FLAC__StreamMetadata *block = 0;
  FLAC__bool found_vc_block = false;

  if (iterator == 0) {
    die("out of memory allocating iterator");
  }

  FLAC__metadata_iterator_init(iterator, chain);

  do {
    block = FLAC__metadata_iterator_get_block(iterator);

    if (block->type == FLAC__METADATA_TYPE_VORBIS_COMMENT) {
      found_vc_block = true;
    }

  } while (!found_vc_block && FLAC__metadata_iterator_next(iterator));

  if (found_vc_block) {

    /* Empty out the existing block */
    if (0 != block->data.vorbis_comment.comments) {

      FLAC__ASSERT(block->data.vorbis_comment.num_comments > 0);

      if (!FLAC__metadata_object_vorbiscomment_resize_comments(block, 0)) {

        die("%s: ERROR: memory allocation failure\n", path);
      }

    } else {

      FLAC__ASSERT(block->data.vorbis_comment.num_comments == 0);
    }

  } else {

    /* create a new block if necessary */
    block = FLAC__metadata_object_new(FLAC__METADATA_TYPE_VORBIS_COMMENT);

    if (0 == block) {
      die("out of memory allocating VORBIS_COMMENT block");
    }

    while (FLAC__metadata_iterator_next(iterator));

    if (!FLAC__metadata_iterator_insert_block_after(iterator, block)) {

      print_error_with_chain_status(chain, "%s: ERROR: adding new VORBIS_COMMENT block to metadata", path);
      XSRETURN_UNDEF;
    }

    /* iterator is left pointing to new block */
    FLAC__ASSERT(FLAC__metadata_iterator_get_block(iterator) == block);
  }

  FLAC__StreamMetadata_VorbisComment_Entry entry = { 0 };
  FLAC__metadata_object_vorbiscomment_append_comment(block, entry, /*copy=*/true);

  if (hv_iterinit(tags)) {

    while ((he = hv_iternext(tags))) {

      FLAC__StreamMetadata_VorbisComment_Entry entry;

      char *key = HePV(he, PL_na);
      char *val = SvPV_nolen(HeVAL(he));
      char *ent = form("%s=%s", key, val);

      if (ent == NULL) {
        warn("Couldn't create key/value pair!\n");
        XSRETURN_UNDEF;
      }

      if (strEQ(key, "VENDOR")) {
        entry.entry = (FLAC__byte *)val;
      } else {
        entry.entry = (FLAC__byte *)ent;
      }

      entry.length = strlen((const char *)entry.entry);

      if (strEQ(key, "VENDOR")) {

        if (!FLAC__metadata_object_vorbiscomment_set_vendor_string(block, entry, /*copy=*/true)) {
          warn("%s: ERROR: memory allocation failure\n", path);
          XSRETURN_UNDEF;
        }

      } else {

        if (!FLAC__format_vorbiscomment_entry_is_legal(entry.entry, entry.length)) {

          warn("%s: ERROR: tag value for '%s' is not valid UTF-8\n", path, ent);
          XSRETURN_UNDEF;
        }

        if (!FLAC__metadata_object_vorbiscomment_append_comment(block, entry, /*copy=*/true)) {

          warn("%s: ERROR: memory allocation failure\n", path);
          XSRETURN_UNDEF;
        }
      }
    }
  }

  FLAC__metadata_iterator_delete(iterator);
  FLAC__metadata_chain_sort_padding(chain);

  ok = FLAC__metadata_chain_write(chain, /* padding */true, /*modtime*/ false);

  if (!ok) {
    print_error_with_chain_status(chain, "%s: ERROR: writing FLAC file", path);
    RETVAL = &PL_sv_no;
  } else {
    RETVAL = &PL_sv_yes;
  }

  FLAC__metadata_chain_delete(chain);

  OUTPUT:
  RETVAL