/*
 * Metadata hub and access methods.
 * Basically, if you need to store metadata
 * (e.g. for use with the dbus interfaces),
 * then you need a metadata hub,
 * where everything is stored
 * This file is part of Shairport Sync.
 * Copyright (c) Mike Brady 2017--2020
 * All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

#include <stdlib.h>
#include <string.h>

#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "config.h"

#include "common.h"
#include "dacp.h"
#include "metadata_hub.h"

#ifdef CONFIG_MBEDTLS
#include <mbedtls/md5.h>
#include <mbedtls/version.h>
#endif

#ifdef CONFIG_POLARSSL
#include <polarssl/md5.h>
#endif

#ifdef CONFIG_OPENSSL
#include <openssl/md5.h>
#endif

struct metadata_bundle metadata_store;

int metadata_hub_initialised = 0;

pthread_rwlock_t metadata_hub_re_lock = PTHREAD_RWLOCK_INITIALIZER;

int string_update(char **str, int *flag, char *s) {
  if (s)
    return string_update_with_size(str, flag, s, strlen(s));
  else
    return string_update_with_size(str, flag, NULL, 0);
}

void metadata_hub_init(void) {
  // debug(1, "Metadata bundle initialisation.");
  memset(&metadata_store, 0, sizeof(metadata_store));
  metadata_hub_initialised = 1;
}

void metadata_hub_stop(void) {}

void add_metadata_watcher(metadata_watcher fn, void *userdata) {
  int i;
  for (i = 0; i < number_of_watchers; i++) {
    if (metadata_store.watchers[i] == NULL) {
      metadata_store.watchers[i] = fn;
      metadata_store.watchers_data[i] = userdata;
      // debug(1, "Added a metadata watcher into slot %d", i);
      break;
    }
  }
}

void run_metadata_watchers(void) {
  int i;
  for (i = 0; i < number_of_watchers; i++) {
    if (metadata_store.watchers[i]) {
      metadata_store.watchers[i](&metadata_store, metadata_store.watchers_data[i]);
    }
  }
  // turn off changed flags
  metadata_store.cover_art_pathname_changed = 0;
  metadata_store.client_ip_changed = 0;
  metadata_store.server_ip_changed = 0;
  metadata_store.progress_string_changed = 0;
  metadata_store.item_id_changed = 0;
  metadata_store.item_composite_id_changed = 0;
  metadata_store.artist_name_changed = 0;
  metadata_store.album_artist_name_changed = 0;
  metadata_store.album_name_changed = 0;
  metadata_store.track_name_changed = 0;
  metadata_store.genre_changed = 0;
  metadata_store.comment_changed = 0;
  metadata_store.composer_changed = 0;
  metadata_store.file_kind_changed = 0;
  metadata_store.song_description_changed = 0;
  metadata_store.song_album_artist_changed = 0;
  metadata_store.sort_artist_changed = 0;
  metadata_store.sort_album_changed = 0;
  metadata_store.sort_composer_changed = 0;
  metadata_store.songtime_in_milliseconds_changed = 0;
}

void metadata_hub_unlock_hub_mutex_cleanup(__attribute__((unused)) void *arg) {
  debug(1, "metadata_hub_unlock_hub_mutex_cleanup called.");
  metadata_hub_modify_epilog(0);
}

char *last_metadata_hub_modify_prolog_file = NULL;
int last_metadata_hub_modify_prolog_line;
int metadata_hub_re_lock_access_is_delayed;

void _metadata_hub_modify_prolog(const char *filename, const int linenumber) {
  // always run this before changing an entry or a sequence of entries in the metadata_hub
  // debug(1, "locking metadata hub for writing");
  if (pthread_rwlock_trywrlock(&metadata_hub_re_lock) != 0) {
    if (last_metadata_hub_modify_prolog_file)
      debug(2, "Metadata_hub write lock at \"%s:%d\" is already taken at \"%s:%d\" -- must wait.",
            filename, linenumber, last_metadata_hub_modify_prolog_file,
            last_metadata_hub_modify_prolog_line);
    else
      debug(2, "Metadata_hub write lock is already taken by unknown -- must wait.");
    metadata_hub_re_lock_access_is_delayed = 0;
    pthread_rwlock_wrlock(&metadata_hub_re_lock);
    debug(2, "Okay -- acquired the metadata_hub write lock at \"%s:%d\".", filename, linenumber);
  } else {
    if (last_metadata_hub_modify_prolog_file) {
      free(last_metadata_hub_modify_prolog_file);
    }
    last_metadata_hub_modify_prolog_file = strdup(filename);
    last_metadata_hub_modify_prolog_line = linenumber;
    // debug(3, "Metadata_hub write lock acquired.");
  }
  metadata_hub_re_lock_access_is_delayed = 0;
}

void _metadata_hub_modify_epilog(int modified, const char *filename, const int linenumber) {
  metadata_store.dacp_server_has_been_active =
      metadata_store.dacp_server_active; // set the scanner_has_been_active now.
  if (modified) {
    run_metadata_watchers();
  }
  if (metadata_hub_re_lock_access_is_delayed) {
    if (last_metadata_hub_modify_prolog_file) {
      debug(1, "Metadata_hub write lock taken at \"%s:%d\" is freed at \"%s:%d\".",
            last_metadata_hub_modify_prolog_file, last_metadata_hub_modify_prolog_line, filename,
            linenumber);
      free(last_metadata_hub_modify_prolog_file);
      last_metadata_hub_modify_prolog_file = NULL;
    } else {
      debug(1, "Metadata_hub write lock taken at an unknown place is freed at \"%s:%d\".", filename,
            linenumber);
    }
  }
  pthread_rwlock_unlock(&metadata_hub_re_lock);
  // debug(3, "Metadata_hub write lock unlocked.");
}

/*
void _metadata_hub_read_prolog(const char *filename, const int linenumber) {
  // always run this before reading an entry or a sequence of entries in the metadata_hub
  // debug(1, "locking metadata hub for reading");
  if (pthread_rwlock_tryrdlock(&metadata_hub_re_lock) != 0) {
    debug(1, "Metadata_hub read lock is already taken -- must wait.");
    pthread_rwlock_rdlock(&metadata_hub_re_lock);
    debug(1, "Okay -- acquired the metadata_hub read lock.");
  }
}

void _metadata_hub_read_epilog(const char *filename, const int linenumber) {
  // always run this after reading an entry or a sequence of entries in the metadata_hub
  // debug(1, "unlocking metadata hub for reading");
  pthread_rwlock_unlock(&metadata_hub_re_lock);
}
*/
char *metadata_write_image_file(const char *buf, int len) {

  // warning -- this removes all files from the directory apart from this one, if it exists
  // it will return a path to the image file allocated with malloc.
  // free it if you don't need it.

  char *path = NULL;                                 // this will be what is returned
  if (strcmp(config.cover_art_cache_dir, "") != 0) { // an empty string means do not write the file

    uint8_t img_md5[16];
    // uint8_t ap_md5[16];

#ifdef CONFIG_OPENSSL
    MD5_CTX ctx;
    MD5_Init(&ctx);
    MD5_Update(&ctx, buf, len);
    MD5_Final(img_md5, &ctx);
#endif

#ifdef CONFIG_MBEDTLS
#if MBEDTLS_VERSION_MINOR >= 7
    mbedtls_md5_context tctx;
    mbedtls_md5_starts_ret(&tctx);
    mbedtls_md5_update_ret(&tctx, (const unsigned char *)buf, len);
    mbedtls_md5_finish_ret(&tctx, img_md5);
#else
    mbedtls_md5_context tctx;
    mbedtls_md5_starts(&tctx);
    mbedtls_md5_update(&tctx, (const unsigned char *)buf, len);
    mbedtls_md5_finish(&tctx, img_md5);
#endif
#endif

#ifdef CONFIG_POLARSSL
    md5_context tctx;
    md5_starts(&tctx);
    md5_update(&tctx, (const unsigned char *)buf, len);
    md5_finish(&tctx, img_md5);
#endif

    char img_md5_str[33];
    memset(img_md5_str, 0, sizeof(img_md5_str));
    char *ext;
    char png[] = "png";
    char jpg[] = "jpg";
    int i;
    for (i = 0; i < 16; i++)
      snprintf(&img_md5_str[i * 2], 3, "%02x", (uint8_t)img_md5[i]);
    // see if the file is a jpeg or a png
    if (strncmp(buf, "\xFF\xD8\xFF", 3) == 0)
      ext = jpg;
    else if (strncmp(buf, "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A", 8) == 0)
      ext = png;
    else {
      debug(1, "Unidentified image type of cover art -- jpg extension used.");
      ext = jpg;
    }
    mode_t oldumask = umask(000);
    int result = mkpath(config.cover_art_cache_dir, 0777);
    umask(oldumask);
    if ((result == 0) || (result == -EEXIST)) {
      // see if the file exists by opening it.
      // if it exists, we're done
      char *prefix = "cover-";

      size_t pl = strlen(config.cover_art_cache_dir) + 1 + strlen(prefix) + strlen(img_md5_str) +
                  1 + strlen(ext);

      path = malloc(pl + 1);
      snprintf(path, pl + 1, "%s/%s%s.%s", config.cover_art_cache_dir, prefix, img_md5_str, ext);
      int cover_fd = open(path, O_WRONLY | O_CREAT | O_EXCL, S_IRWXU | S_IRGRP | S_IROTH);
      if (cover_fd > 0) {
        // write the contents
        if (write(cover_fd, buf, len) < len) {
          warn("Writing cover art file \"%s\" failed!", path);
          free(path);
          path = NULL;
        }
        close(cover_fd);

        // now delete all other files, if requested
        if (config.retain_coverart == 0) {
          DIR *d;
          struct dirent *dir;
          d = opendir(config.cover_art_cache_dir);
          if (d) {
            int fnl = strlen(prefix) + strlen(img_md5_str) + 1 + strlen(ext) + 1;

            char *full_filename = malloc(fnl);
            if (full_filename == NULL)
              die("Can't allocate memory at metadata_write_image_file.");
            memset(full_filename, 0, fnl);
            snprintf(full_filename, fnl, "%s%s.%s", prefix, img_md5_str, ext);
            int dir_fd = open(config.cover_art_cache_dir, O_DIRECTORY);
            if (dir_fd > 0) {
              while ((dir = readdir(d)) != NULL) {
                if (dir->d_type == DT_REG) {
                  if (strcmp(full_filename, dir->d_name) != 0) {
                    if (unlinkat(dir_fd, dir->d_name, 0) != 0) {
                      debug(1, "Error %d deleting cover art file \"%s\".", errno, dir->d_name);
                    }
                  }
                }
              }
            } else {
              debug(1, "Can't open the directory for deletion.");
            }
            free(full_filename);
            closedir(d);
          }
        }
      } else {
        //      if (errno == EEXIST)
        //        debug(1, "Cover art file \"%s\" already exists!", path);
        //      else {
        if (errno != EEXIST) {
          warn("Could not open file \"%s\" for writing cover art", path);
          free(path);
          path = NULL;
        }
      }
    } else {
      debug(1, "Couldn't access or create the cover art cache directory \"%s\".",
            config.cover_art_cache_dir);
    }
  }
  return path;
}

void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t length) {
  // metadata coming in from the audio source or from Shairport Sync itself passes through here
  // this has more information about tags, which might be relevant:
  // https://code.google.com/p/ytrack/wiki/DMAP

  // all the following items of metadata are contained in one metadata packet
  // they are preceded by an 'ssnc' 'mdst' item and followed by an 'ssnc 'mden' item.
  // we don't set "changed" for them individually; instead we set it when the  'mden' token
  // comes in if the metadata_packet_item_changed is set.

  int changed = 0;
  int metadata_packet_item_changed = 0;
  metadata_hub_modify_prolog();
  pthread_cleanup_push(metadata_hub_unlock_hub_mutex_cleanup, NULL);

  char *cs;
  if (type == 'core') {
    switch (code) {
    case 'mper': {
      // get the 64-bit number as a uint64_t by reading two uint32_t s and combining them
      uint64_t vl = ntohl(*(uint32_t *)data); // get the high order 32 bits
      vl = vl << 32;                          // shift them into the correct location
      uint64_t ul = ntohl(*(uint32_t *)(data + sizeof(uint32_t))); // and the low order 32 bits
      vl = vl + ul;
      debug(2, "MH Item ID seen: \"%" PRIx64 "\" of length %u.", vl, length);
      if (vl != metadata_store.item_id) {
        metadata_store.item_id = vl;
        metadata_store.item_id_changed = 1;
        metadata_store.item_id_received = 1;
        debug(2, "MH Item ID set to: \"%" PRIx64 "\"", metadata_store.item_id);
        metadata_packet_item_changed = 1;
      }
    } break;
    case 'astm': {
      uint32_t ui = ntohl(*(uint32_t *)data);
      debug(2, "MH Song Time seen: \"%u\" of length %u.", ui, length);
      if (ui != metadata_store.songtime_in_milliseconds) {
        metadata_store.songtime_in_milliseconds = ui;
        metadata_store.songtime_in_milliseconds_changed = 1;
        debug(2, "MH Song Time set to: \"%u\"", metadata_store.songtime_in_milliseconds);
        metadata_packet_item_changed = 1;
      }
    } break;
    case 'asal':
      cs = strndup(data, length);
      if (string_update(&metadata_store.album_name, &metadata_store.album_name_changed, cs)) {
        debug(2, "MH Album name set to: \"%s\"", metadata_store.album_name);
        metadata_packet_item_changed = 1;
      }
      free(cs);
      break;
    case 'asar':
      cs = strndup(data, length);
      if (string_update(&metadata_store.artist_name, &metadata_store.artist_name_changed, cs)) {
        debug(2, "MH Artist name set to: \"%s\"", metadata_store.artist_name);
        metadata_packet_item_changed = 1;
      }
      free(cs);
      break;
    case 'assl':
      cs = strndup(data, length);
      if (string_update(&metadata_store.album_artist_name,
                        &metadata_store.album_artist_name_changed, cs)) {
        debug(2, "MH Album Artist name set to: \"%s\"", metadata_store.album_artist_name);
        metadata_packet_item_changed = 1;
      }
      free(cs);
      break;
    case 'ascm':
      cs = strndup(data, length);
      if (string_update(&metadata_store.comment, &metadata_store.comment_changed, cs)) {
        debug(2, "MH Comment set to: \"%s\"", metadata_store.comment);
        metadata_packet_item_changed = 1;
      }
      free(cs);
      break;
    case 'asgn':
      cs = strndup(data, length);
      if (string_update(&metadata_store.genre, &metadata_store.genre_changed, cs)) {
        debug(2, "MH Genre set to: \"%s\"", metadata_store.genre);
        metadata_packet_item_changed = 1;
      }
      free(cs);
      break;
    case 'minm':
      cs = strndup(data, length);
      if (string_update(&metadata_store.track_name, &metadata_store.track_name_changed, cs)) {
        debug(2, "MH Track Name set to: \"%s\"", metadata_store.track_name);
        metadata_packet_item_changed = 1;
      }
      free(cs);
      break;
    case 'ascp':
      cs = strndup(data, length);
      if (string_update(&metadata_store.composer, &metadata_store.composer_changed, cs)) {
        debug(2, "MH Composer set to: \"%s\"", metadata_store.composer);
        metadata_packet_item_changed = 1;
      }
      free(cs);
      break;
    case 'asdt':
      cs = strndup(data, length);
      if (string_update(&metadata_store.song_description, &metadata_store.song_description_changed,
                        cs)) {
        debug(2, "MH Song Description set to: \"%s\"", metadata_store.song_description);
      }
      free(cs);
      break;
    case 'asaa':
      cs = strndup(data, length);
      if (string_update(&metadata_store.song_album_artist,
                        &metadata_store.song_album_artist_changed, cs)) {
        debug(2, "MH Song Album Artist set to: \"%s\"", metadata_store.song_album_artist);
        metadata_packet_item_changed = 1;
      }
      free(cs);
      break;
    case 'assn':
      cs = strndup(data, length);
      if (string_update(&metadata_store.sort_name, &metadata_store.sort_name_changed, cs)) {
        debug(2, "MH Sort Name set to: \"%s\"", metadata_store.sort_name);
        metadata_packet_item_changed = 1;
      }
      free(cs);
      break;
    case 'assa':
      cs = strndup(data, length);
      if (string_update(&metadata_store.sort_artist, &metadata_store.sort_artist_changed, cs)) {
        debug(2, "MH Sort Artist set to: \"%s\"", metadata_store.sort_artist);
        metadata_packet_item_changed = 1;
      }
      free(cs);
      break;
    case 'assu':
      cs = strndup(data, length);
      if (string_update(&metadata_store.sort_album, &metadata_store.sort_album_changed, cs)) {
        debug(2, "MH Sort Album set to: \"%s\"", metadata_store.sort_album);
        metadata_packet_item_changed = 1;
      }
      free(cs);
      break;
    case 'assc':
      cs = strndup(data, length);
      if (string_update(&metadata_store.sort_composer, &metadata_store.sort_composer_changed, cs)) {
        debug(2, "MH Sort Composer set to: \"%s\"", metadata_store.sort_composer);
        metadata_packet_item_changed = 1;
      }
      free(cs);
    default:
      /*
          {
            char typestring[5];
            *(uint32_t *)typestring = htonl(type);
            typestring[4] = 0;
            char codestring[5];
            *(uint32_t *)codestring = htonl(code);
            codestring[4] = 0;
            char *payload;
            if (length < 2048)
              payload = strndup(data, length);
            else
              payload = NULL;
            debug(1, "MH \"%s\" \"%s\" (%d bytes): \"%s\".", typestring, codestring, length,
         payload);
            if (payload)
              free(payload);
          }
      */
      break;
    }
  } else if (type == 'ssnc') {
    switch (code) {
    // ignore the following
    case 'pcst':
    case 'pcen':
      break;
    case 'mdst':
      debug(2, "MH Metadata stream processing start.");
      metadata_packet_item_changed = 0;
      break;
    case 'mden':
      debug(2, "MH Metadata stream processing end.");
      changed = metadata_packet_item_changed;
      break;
    case 'PICT':
      debug(2, "MH Picture received, length %u bytes.", length);

      char uri[2048];
      if ((length > 16) &&
          (strcmp(config.cover_art_cache_dir, "") != 0)) { // if it's okay to write the file
                                                           // make this uncancellable
        int oldState;
        pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable
        char *pathname = metadata_write_image_file(data, length);
        snprintf(uri, sizeof(uri), "file://%s", pathname);
        free(pathname);
        pthread_setcancelstate(oldState, NULL);

      } else {
        uri[0] = '\0';
      }
      if (string_update(&metadata_store.cover_art_pathname,
                        &metadata_store.cover_art_pathname_changed,
                        uri)) // if the picture's file path is different from the stored one...
        changed = 1;
      else
        changed = 0;
      //      pthread_cleanup_pop(0); // don't remove the lock -- it'll have been done
      break;
    case 'clip':
      cs = strndup(data, length);
      if (string_update(&metadata_store.client_ip, &metadata_store.client_ip_changed, cs)) {
        changed = 1;
        debug(2, "MH Client IP set to: \"%s\"", metadata_store.client_ip);
      }
      free(cs);
      break;
    case 'prgr':
      cs = strndup(data, length);
      if (string_update(&metadata_store.progress_string, &metadata_store.progress_string_changed,
                        cs)) {
        changed = 1;
        debug(2, "MH Progress String set to: \"%s\"", metadata_store.progress_string);
      }
      free(cs);
      break;
    case 'svip':
      cs = strndup(data, length);
      if (string_update(&metadata_store.server_ip, &metadata_store.server_ip_changed, cs)) {
        changed = 1;
        debug(2, "MH Server IP set to: \"%s\"", metadata_store.server_ip);
      }
      free(cs);
      break;
    case 'abeg':
      changed = (metadata_store.active_state != AM_ACTIVE);
      metadata_store.active_state = AM_ACTIVE;
      break;
    case 'aend':
      changed = (metadata_store.active_state != AM_INACTIVE);
      metadata_store.active_state = AM_INACTIVE;
      break;
    case 'pbeg':
      changed = ((metadata_store.player_state != PS_PLAYING) ||
                 (metadata_store.player_thread_active == 0));
      metadata_store.player_state = PS_PLAYING;
      metadata_store.player_thread_active = 1;
      break;
    case 'pend':
      changed = ((metadata_store.player_state != PS_STOPPED) ||
                 (metadata_store.player_thread_active == 1));
      metadata_store.player_state = PS_STOPPED;
      metadata_store.player_thread_active = 0;
      break;
    case 'pfls':
      changed = (metadata_store.player_state != PS_PAUSED);
      metadata_store.player_state = PS_PAUSED;
      break;
    case 'pffr': // this is sent when the first frame has been received
    case 'prsm':
      changed = (metadata_store.player_state != PS_PLAYING);
      metadata_store.player_state = PS_PLAYING;
      break;
    case 'pvol': {
      // Note: it's assumed that the config.airplay volume has already been correctly set.
      // int32_t actual_volume;
      // int gv = dacp_get_volume(&actual_volume);
      // metadata_hub_modify_prolog();
      // if ((gv == 200) && (metadata_store.speaker_volume != actual_volume)) {
      //  metadata_store.speaker_volume = actual_volume;
      //  changed = 1;
      //}
      if (metadata_store.airplay_volume != config.airplay_volume) {
        metadata_store.airplay_volume = config.airplay_volume;
        changed = 1;
      }
    } break;

    default: {
      char typestring[5];
      uint32_t tm = htonl(type);
      memcpy(typestring, &tm, sizeof(uint32_t));
      typestring[4] = 0;
      char codestring[5];
      uint32_t cm = htonl(code);
      memcpy(codestring, &cm, sizeof(uint32_t));
      codestring[4] = 0;
      char *payload;
      if (length < 2048)
        payload = strndup(data, length);
      else
        payload = NULL;
      // debug(1, "MH \"%s\" \"%s\" (%d bytes): \"%s\".", typestring, codestring, length, payload);
      if (payload)
        free(payload);
    }
    }
  }
  pthread_cleanup_pop(0); // don't remove the lock
  metadata_hub_modify_epilog(changed);
}
