#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include "ppport.h"

// If we are on MSVC, disable some stupid MSVC warnings
#ifdef _MSC_VER
# pragma warning( disable: 4996 )
# pragma warning( disable: 4127 )
# pragma warning( disable: 4711 )
#endif

// Headers for stat support
#ifdef _MSC_VER
# include <windows.h>
#else
# include <sys/stat.h>
#endif

#include "common.c"
#include "ape.c"
#include "id3.c"

#include "aac.c"
#include "asf.c"
#include "mac.c"
#include "mp3.c"
#include "mp4.c"
#include "mpc.c"
#include "ogg.c"
#include "opus.c"
#include "wav.c"
#include "flac.c"
#include "wavpack.c"
#include "dsf.c"
#include "dsdiff.c"

#include "md5.c"
#include "jenkins_hash.c"

#define FILTER_TYPE_INFO 0x01
#define FILTER_TYPE_TAGS 0x02

#define MD5_BUFFER_SIZE 4096

#define MAX_PATH_STR_LEN 1024

struct _types {
  char *type;
  char *suffix[15];
};

typedef struct {
  char*	type;
  int (*get_tags)(PerlIO *infile, char *file, HV *info, HV *tags);
  int (*get_fileinfo)(PerlIO *infile, char *file, HV *tags);
  int (*find_frame)(PerlIO *infile, char *file, int offset);
  int (*find_frame_return_info)(PerlIO *infile, char *file, int offset, HV *info);
} taghandler;

struct _types audio_types[] = {
  {"mp4", {"mp4", "m4a", "m4b", "m4p", "m4v", "m4r", "k3g", "skm", "3gp", "3g2", "mov", 0}},
  {"aac", {"aac", "adts", 0}},
  {"mp3", {"mp3", "mp2", 0}},
  {"ogg", {"ogg", "oga", 0}},
  {"opus", {"opus", 0}},
  {"mpc", {"mpc", "mp+", "mpp", 0}},
  {"ape", {"ape", "apl", 0}},
  {"flc", {"flc", "flac", "fla", 0}},
  {"asf", {"wma", "asf", "wmv", 0}},
  {"wav", {"wav", "aif", "aiff", 0}},
  {"wvp", {"wv", 0}},
  {"dsf", {"dsf", 0}},
  {"dff", {"dff", 0}},
  {0, {0, 0}}
};

static taghandler taghandlers[] = {
  { "mp4", get_mp4tags, 0, mp4_find_frame, mp4_find_frame_return_info },
  { "aac", get_aacinfo, 0, 0, 0 },
  { "mp3", get_mp3tags, get_mp3fileinfo, mp3_find_frame, 0 },
  { "ogg", get_ogg_metadata, 0, ogg_find_frame, 0 },
  { "opus", get_opus_metadata, 0, opus_find_frame, 0 },
  { "mpc", get_ape_metadata, get_mpcfileinfo, 0, 0 },
  { "ape", get_ape_metadata, get_macfileinfo, 0, 0 },
  { "flc", get_flac_metadata, 0, flac_find_frame, 0 },
  { "asf", get_asf_metadata, 0, asf_find_frame, 0 },
  { "wav", get_wav_metadata, 0, 0, 0 },
  { "wvp", get_ape_metadata, get_wavpack_info, 0 },
  { "dsf", get_dsf_metadata, 0, 0, 0 },
  { "dff", get_dsdiff_metadata, 0, 0, 0 },
  { NULL, 0, 0, 0 }
};

static taghandler *
_get_taghandler(char *suffix)
{
  int typeindex = -1;
  int i, j;
  taghandler *hdl = NULL;
  
  for (i=0; typeindex==-1 && audio_types[i].type; i++) {
    for (j=0; typeindex==-1 && audio_types[i].suffix[j]; j++) {
#ifdef _MSC_VER
      if (!stricmp(audio_types[i].suffix[j], suffix)) {
#else
      if (!strcasecmp(audio_types[i].suffix[j], suffix)) {
#endif
        typeindex = i;
        break;
      }
    }
  }
    
  if (typeindex > -1) {
    for (hdl = taghandlers; hdl->type; ++hdl)
      if (!strcmp(hdl->type, audio_types[typeindex].type))
        break;
  }
  
  return hdl;
}

static void
_generate_md5(PerlIO *infile, const char *file, int size, int start_offset, HV *info)
{
  md5_state_t md5;
  md5_byte_t digest[16];
  char hexdigest[33];
  Buffer buf;
  int audio_offset, audio_size, di;
  
  buffer_init(&buf, MD5_BUFFER_SIZE);
  md5_init(&md5);
  
  audio_offset = SvIV(*(my_hv_fetch(info, "audio_offset")));
  audio_size = SvIV(*(my_hv_fetch(info, "audio_size")));
  
  if (!start_offset) {
    // Read bytes from middle of file to reduce chance of silence generating false matches
    start_offset = audio_offset;
    start_offset += (audio_size / 2) - (size / 2);
    if (start_offset < audio_offset)
      start_offset = audio_offset;
  }
  
  if (size >= audio_size) {
    size = audio_size;
  }
  
  DEBUG_TRACE("Using %d bytes for audio MD5, starting at %d\n", size, start_offset);
  
  if (PerlIO_seek(infile, start_offset, SEEK_SET) < 0) {
    warn("Audio::Scan unable to determine MD5 for %s\n", file);
    goto out;
  }
  
  while (size > 0) {
    if ( !_check_buf(infile, &buf, 1, MIN(size, MD5_BUFFER_SIZE)) ) {
      warn("Audio::Scan unable to determine MD5 for %s\n", file);
      goto out;
    }
    
    md5_append(&md5, buffer_ptr(&buf), buffer_len(&buf));
    
    size -= buffer_len(&buf);
    buffer_consume(&buf, buffer_len(&buf));
    DEBUG_TRACE("%d bytes left\n", size);
  }
  
  md5_finish(&md5, digest);
  
  for (di = 0; di < 16; ++di)
    sprintf(hexdigest + di * 2, "%02x", digest[di]);
  
  my_hv_store(info, "audio_md5", newSVpvn(hexdigest, 32));
  
out:
  buffer_free(&buf);
}

static uint32_t
_generate_hash(const char *file)
{
  char hashstr[MAX_PATH_STR_LEN];
  int mtime = 0;
  uint64_t size = 0;
  uint32_t hash;

#ifdef _MSC_VER
  BOOL fOk;
  WIN32_FILE_ATTRIBUTE_DATA fileInfo;

  fOk = GetFileAttributesEx(file, GetFileExInfoStandard, (void *)&fileInfo);
  mtime = fileInfo.ftLastWriteTime.dwLowDateTime;
  size = (uint64_t)fileInfo.nFileSizeLow;
#else
  struct stat buf;

  if (stat(file, &buf) != -1) {
    mtime = (int)buf.st_mtime;
    size = (uint64_t)buf.st_size;
  }
#endif

  memset(hashstr, 0, sizeof(hashstr));
  snprintf(hashstr, sizeof(hashstr) - 1, "%s%d%llu", file, mtime, size);
  hash = hashlittle(hashstr, strlen(hashstr), 0);
  
  return hash;
}

MODULE = Audio::Scan		PACKAGE = Audio::Scan

HV *
_scan( char *, char *suffix, PerlIO *infile, SV *path, int filter, int md5_size, int md5_offset )
CODE:
{
  taghandler *hdl;
  RETVAL = newHV();
  
  // don't leak
  sv_2mortal( (SV*)RETVAL );
  
  hdl = _get_taghandler(suffix);
  
  if (hdl) {
    HV *info = newHV();

    // Ignore filter if a file type has only one function (FLAC/Ogg)
    if ( !hdl->get_fileinfo ) {
      filter = FILTER_TYPE_INFO | FILTER_TYPE_TAGS;
    }

    if ( hdl->get_fileinfo && (filter & FILTER_TYPE_INFO) ) {
      hdl->get_fileinfo(infile, SvPVX(path), info);
    }

    if ( hdl->get_tags && (filter & FILTER_TYPE_TAGS) ) {
      HV *tags = newHV();
      hdl->get_tags(infile, SvPVX(path), info, tags);
      hv_store( RETVAL, "tags", 4, newRV_noinc( (SV *)tags ), 0 );
    }
    
    // Generate audio MD5 value
    if ( md5_size > 0
      && my_hv_exists(info, "audio_offset")
      && my_hv_exists(info, "audio_size")
      && !my_hv_exists(info, "audio_md5")
    ) {
      _generate_md5(infile, SvPVX(path), md5_size, md5_offset, info);
    }
    
    // Generate hash value
    my_hv_store(info, "jenkins_hash", newSVuv( _generate_hash(SvPVX(path)) ));

    // Info may be used in tag function, i.e. to find tag version
    hv_store( RETVAL, "info", 4, newRV_noinc( (SV *)info ), 0 );
  }
  else {
    croak("Audio::Scan unsupported file type: %s (%s)", suffix, SvPVX(path));
  }
}
OUTPUT:
  RETVAL
  
int
_find_frame( char *, char *suffix, PerlIO *infile, SV *path, int offset )
CODE:
{
  taghandler *hdl;
  
  RETVAL = -1;
  hdl = _get_taghandler(suffix);
  
  if (hdl && hdl->find_frame) {
    RETVAL = hdl->find_frame(infile, SvPVX(path), offset);
  }
}
OUTPUT:
  RETVAL

HV *
_find_frame_return_info( char *, char *suffix, PerlIO *infile, SV *path, int offset )
CODE:
{
  taghandler *hdl = _get_taghandler(suffix);
  RETVAL = newHV();
  sv_2mortal((SV*)RETVAL);
  
  if (hdl && hdl->find_frame_return_info) {
    hdl->find_frame_return_info(infile, SvPVX(path), offset, RETVAL);
  }
}
OUTPUT:
  RETVAL

int
has_flac(void)
CODE:
{
  RETVAL = 1;
}
OUTPUT:
  RETVAL

int
is_supported(char *, SV *path)
CODE:
{
  char *suffix = strrchr( SvPVX(path), '.' );

  if (suffix != NULL && *suffix == '.' && _get_taghandler(suffix + 1)) {
    RETVAL = 1;
  }
  else {
    RETVAL = 0;
  }
}
OUTPUT:
  RETVAL

SV *
type_for(char *, SV *suffix)
CODE:
{
  taghandler *hdl = NULL;
  char *suff = SvPVX(suffix);

  if (suff == NULL || *suff == '\0') {
    RETVAL = newSV(0);
  }
  else {
    hdl = _get_taghandler(suff);
    if (hdl == NULL) {
      RETVAL = newSV(0);
    }
    else {
      RETVAL = newSVpv(hdl->type, 0);
    }
  }
}
OUTPUT:
  RETVAL

AV *
get_types(void)
CODE:
{
  int i;

  RETVAL = newAV();
  sv_2mortal((SV*)RETVAL);
  for (i = 0; audio_types[i].type; i++) {
    av_push(RETVAL, newSVpv(audio_types[i].type, 0));
  }
}
OUTPUT:
  RETVAL

AV *
extensions_for(char *, SV *type)
CODE:
{
  int i, j;
  char *t = SvPVX(type);

  RETVAL = newAV();
  sv_2mortal((SV*)RETVAL);
  for (i = 0; audio_types[i].type; i++) {
#ifdef _MSC_VER
    if (!stricmp(audio_types[i].type, t)) {
#else
    if (!strcasecmp(audio_types[i].type, t)) {
#endif

      for (j = 0; audio_types[i].suffix[j]; j++) {
        av_push(RETVAL, newSVpv(audio_types[i].suffix[j], 0));
      }
      break;

    }
  }
}
OUTPUT:
  RETVAL