#define SHR3D_SHRED
#define SHR3D_PSARC
#define SHR3D_MINIMAL_WIN32
#define MINIZ_STDIO

#include "../../src/configuration.h"
#undef SHR3D_OPENXR
#undef SHR3D_OPENXR_PCVR

#include "../../src/base64.cpp"
#include "../../src/collection.cpp"
#include "../../src/file.cpp"
#include "../../src/getopt.cpp"
#include "../../src/global.cpp"
#include "../../src/helper.cpp"
#include "../../src/inflate.cpp"
#include "../../src/ini.cpp"
#include "../../src/json.cpp"
#include "../../src/miniz.cpp"
#include "../../src/ogg.cpp"
#include "../../src/png.cpp"
#include "../../src/psarc.cpp"
#include "../../src/recorder.cpp"
#include "../../src/revorb.cpp"
#include "../../src/rijndael.cpp"
#include "../../src/settings.cpp"
#include "../../src/shred.cpp"
#include "../../src/sng.cpp"
#include "../../src/song.cpp"
#include "../../src/string_.cpp"
#include "../../src/version.h"
#include "../../src/wem.cpp"

#include <set>
#include <regex>
#include <random>

// stubs
namespace Player
{
  void playSong(SongIndex songIndex, ArrangementIndex arrangementIndex) {}
}
namespace Sfx
{
  std::map<SfxSystem, std::vector<std::string>> names;
  std::string saveParameters(SfxId sfxId, i32 instance) { return {}; }
  void loadParameters(SfxId sfxId, i32 instance, const std::string& parameters) { }
}
namespace Tones
{
  void loadSongToneFile(const std::vector<Arrangement::Info>& arrangementInfos, const std::map<std::string, std::vector<Ini::KeyValue>>& tonesIni) {}
}

enum {
  DDSD_CAPS = 0x1,
  DDSD_HEIGHT = 0x2,
  DDSD_WIDTH = 0x4,
  DDSD_PITCH = 0x8,
  DDSD_PIXELFORMAT = 0x1000,
  DDSD_MIPMAPCOUNT = 0x20000,
  DDSD_LINEARSIZE = 0x80000,
  DDSD_DEPTH = 0x800000
};

enum {
  DDSCAPS_COMPLEX = 0x8,
  DDSCAPS_MIPMAP = 0x400000,
  DDSCAPS_TEXTURE = 0x1000
};

enum {
  DDPF_ALPHAPIXELS = 0x1,
  DDPF_ALPHA = 0x2,
  DDPF_FOURCC = 0x4,
  DDPF_RGB = 0x40,
  DDPF_YUV = 0x200,
  DDPF_LUMINANCE = 0x20000
};

struct dds_pixelformat {
  u32 size;
  u32 flags;
  u32 four_cc;
  u32 rgb_bit_count;
  u32 r_bit_mask;
  u32 g_bit_mask;
  u32 b_bit_mask;
  u32 a_bit_mask;
};

struct dds_header {
  u32 size;
  u32 flags;
  u32 height;
  u32 width;
  u32 pitch_linear_size;
  u32 depth;
  u32 mipmap_count;
  u32 reserved1[11];
  dds_pixelformat pixel_format;
  u32 caps;
  u32 caps2;
  u32 caps3;
  u32 caps4;
  u32 reserved2;
};

struct dds_header_dxt10
{
  u32 dxgi_format;
  u32 resource_dimension;
  u32 misc_flag;
  u32 array_size;
  u32 misc_flags2;
};

struct dds_image {
  dds_header header;
  dds_header_dxt10 header10;

  u8* pixels; // always RGBA; currently no support for mipmaps
};

#define FOURCC(str) (u8)(((str)[3] << 24U) | ((str)[2] << 16U) | ((str)[1] << 8U) | (str)[0])
#define MAX(X, Y) (((X) > (Y)) ? (X) : (Y))
#define IMAGE_PITCH(width, block_size) MAX(1, ((width + 3) / 4)) * block_size

static u32 dds_calculate_left_shift(u32* right_shift, u32 bit_count)
{
  if (bit_count >= 8) {
    right_shift = right_shift + (bit_count - 8);
    return 0;
  }
  return 8 - bit_count;
}

static void dds_parse_uncompressed(dds_image* image, const u8* data, i32 data_length)
{
  u32 img_width = MAX(1, image->header.width);
  u32 img_height = MAX(1, image->header.height);
  u32 img_depth = MAX(1, image->header.depth);

  // will this even work? have I envisioned this solution correctly?
  u32 r_right_shift = 0xFFFFFFFF, g_right_shift = 0xFFFFFFFF, b_right_shift = 0xFFFFFFFF, a_right_shift = 0xFFFFFFFF;
  u32 r_left_shift = 0, g_left_shift = 0, b_left_shift = 0, a_left_shift = 0;
  for (u8 i = 0; i < 32; i++) {
    if ((image->header.pixel_format.r_bit_mask) >> i & 1) {
      if (r_right_shift == 0xFFFFFFFF)
        r_right_shift = i;
      r_left_shift++;
    }
    if ((image->header.pixel_format.g_bit_mask) >> i & 1) {
      if (g_right_shift == 0xFFFFFFFF)
        g_right_shift = i;
      g_left_shift++;
    }
    if ((image->header.pixel_format.b_bit_mask) >> i & 1) {
      if (b_right_shift == 0xFFFFFFFF)
        b_right_shift = i;
      b_left_shift++;
    }
    if ((image->header.pixel_format.a_bit_mask) >> i & 1) {
      if (a_right_shift == 0xFFFFFFFF)
        a_right_shift = i;
      a_left_shift++;
    }
  }

  // to avoid undefined behavior:
  if (r_right_shift == 0xFFFFFFFF) r_right_shift = 0;
  if (g_right_shift == 0xFFFFFFFF) g_right_shift = 0;
  if (b_right_shift == 0xFFFFFFFF) b_right_shift = 0;
  if (a_right_shift == 0xFFFFFFFF) a_right_shift = 0;

  // fix left/right shift based on the bit count (currently stored in X_left_shift)
  r_left_shift = dds_calculate_left_shift(&r_right_shift, r_left_shift);
  g_left_shift = dds_calculate_left_shift(&g_right_shift, g_left_shift);
  b_left_shift = dds_calculate_left_shift(&b_right_shift, b_left_shift);
  a_left_shift = dds_calculate_left_shift(&a_right_shift, a_left_shift);

  u8 bytes_per_pixel = image->header.pixel_format.rgb_bit_count / 8;

  data += 128; // skip the header

  // read the actual data
  for (u32 z = 0; z < img_depth; z++)
    for (u32 x = 0; x < img_width; x++)
      for (u32 y = 0; y < img_height; y++) {
        u32 px_index = (z * img_width * img_height + y * img_width + x) * bytes_per_pixel;
        u32 data_index = (z * img_width * img_height + (img_height - y - 1) * img_width + x) * 4;

        // get the data into uint
        u32 px = 0;
        memcpy(&px, data + px_index, bytes_per_pixel);

        // decode
        image->pixels[data_index + 0] = ((px & image->header.pixel_format.r_bit_mask) >> r_right_shift) << r_left_shift;
        image->pixels[data_index + 1] = ((px & image->header.pixel_format.g_bit_mask) >> g_right_shift) << g_left_shift;
        image->pixels[data_index + 2] = ((px & image->header.pixel_format.b_bit_mask) >> b_right_shift) << b_left_shift;
        if (image->header.pixel_format.a_bit_mask == 0)
          image->pixels[data_index + 3] = 0xFF;
        else
          image->pixels[data_index + 3] = ((px & image->header.pixel_format.a_bit_mask) >> a_right_shift) << a_left_shift;
      }
}

static void dds_parse_dxt1(dds_image* image, const u8* data, i32 data_length)
{
  u32 img_width = MAX(1, image->header.width);
  u32 img_height = MAX(1, image->header.height);
  u32 img_depth = MAX(1, image->header.depth);

  u32 blocks_x = MAX(1, img_width / 4);
  u32 blocks_y = MAX(1, img_height / 4);

  for (u32 z = 0; z < img_depth; z++) {
    for (u32 x = 0; x < blocks_x; x++)
      for (u32 y = 0; y < blocks_y; y++) {
        unsigned short color0 = 0, color1 = 0;
        u32 codes = 0;

        // read the block data
        u32 block_offset = (y * blocks_x + x) * 8; // * 8 == * 64bits cuz blocks are 64 bits (2x shorts, 1x uint)
        memcpy(&color0, data + block_offset + 0, 2);
        memcpy(&color1, data + block_offset + 2, 2);
        memcpy(&codes, data + block_offset + 4, 4);

        // unpack the color data
        u8 r0 = (color0 & 0b1111100000000000) >> 8;
        u8 g0 = (color0 & 0b0000011111100000) >> 3;
        u8 b0 = (color0 & 0b0000000000011111) << 3;
        u8 r1 = (color1 & 0b1111100000000000) >> 8;
        u8 g1 = (color1 & 0b0000011111100000) >> 3;
        u8 b1 = (color1 & 0b0000000000011111) << 3;

        // process the data
        for (u32 b = 0; b < 16; b++) {
          u32 px_index = ((z * 4) * img_height * img_width + (img_height - ((y * 4) + b / 4) - 1) * img_width + x * 4 + b % 4) * 4;

          u8 code = (codes >> (2 * b)) & 3;
          image->pixels[px_index + 3] = 0xFF;
          if (code == 0) {
            // color0
            image->pixels[px_index + 0] = r0;
            image->pixels[px_index + 1] = g0;
            image->pixels[px_index + 2] = b0;
          }
          else if (code == 1) {
            // color1
            image->pixels[px_index + 0] = r1;
            image->pixels[px_index + 1] = g1;
            image->pixels[px_index + 2] = b1;
          }
          else if (code == 2) {
            // (2*color0 + color1) / 3
            if (color0 > color1) {
              image->pixels[px_index + 0] = (2 * r0 + r1) / 3;
              image->pixels[px_index + 1] = (2 * g0 + g1) / 3;
              image->pixels[px_index + 2] = (2 * b0 + b1) / 3;
            }
            // (color0 + color1) / 2
            else {
              image->pixels[px_index + 0] = (r0 + r1) / 2;
              image->pixels[px_index + 1] = (g0 + g1) / 2;
              image->pixels[px_index + 2] = (b0 + b1) / 2;
            }
          }
          else if (code == 3) {
            // (color0 + 2*color1) / 3
            if (color0 > color1) {
              image->pixels[px_index + 0] = (r0 + 2 * r1) / 3;
              image->pixels[px_index + 1] = (g0 + 2 * g1) / 3;
              image->pixels[px_index + 2] = (b0 + 2 * b1) / 3;
            }
            // black
            else {
              image->pixels[px_index + 0] = 0x00;
              image->pixels[px_index + 1] = 0x00;
              image->pixels[px_index + 2] = 0x00;
            }
          }
        }
      }

    // skip this slice
    data += blocks_x * blocks_y * 16;
  }
}

static void dds_parse_dxt3(dds_image* image, const u8* data, i32 data_length)
{
  u32 img_width = MAX(1, image->header.width);
  u32 img_height = MAX(1, image->header.height);
  u32 img_depth = MAX(1, image->header.depth);

  u32 blocks_x = MAX(1, img_width / 4);
  u32 blocks_y = MAX(1, img_height / 4);

  for (u32 z = 0; z < img_depth; z++) {
    for (u32 x = 0; x < blocks_x; x++)
      for (u32 y = 0; y < blocks_y; y++) {
        unsigned short color0 = 0, color1 = 0;
        u32 codes = 0;
        unsigned long long alpha_data = 0;

        // read the block data
        u32 block_offset = (y * blocks_x + x) * 16; // 16 == 128bits
        memcpy(&alpha_data, data + block_offset, 8);
        memcpy(&color0, data + block_offset + 8, 2);
        memcpy(&color1, data + block_offset + 10, 2);
        memcpy(&codes, data + block_offset + 12, 4);

        // unpack the color data
        u8 r0 = (color0 & 0b1111100000000000) >> 8;
        u8 g0 = (color0 & 0b0000011111100000) >> 3;
        u8 b0 = (color0 & 0b0000000000011111) << 3;
        u8 r1 = (color1 & 0b1111100000000000) >> 8;
        u8 g1 = (color1 & 0b0000011111100000) >> 3;
        u8 b1 = (color1 & 0b0000000000011111) << 3;

        // process the data
        for (u32 b = 0; b < 16; b++) {
          u32 px_index = ((z * 4) * img_height * img_width + (img_height - ((y * 4) + b / 4) - 1) * img_width + x * 4 + b % 4) * 4;
          u8 code = (codes >> (2 * b)) & 0b0011;

          u8 alpha = (alpha_data >> (4 * b)) & 0b1111;
          image->pixels[px_index + 3] = alpha;

          // color0
          if (code == 0) {
            image->pixels[px_index + 0] = r0;
            image->pixels[px_index + 1] = g0;
            image->pixels[px_index + 2] = b0;
          }
          // color1
          else if (code == 1) {
            image->pixels[px_index + 0] = r1;
            image->pixels[px_index + 1] = g1;
            image->pixels[px_index + 2] = b1;
          }
          // (2*color0 + color1) / 3
          else if (code == 2) {
            image->pixels[px_index + 0] = (2 * r0 + r1) / 3;
            image->pixels[px_index + 1] = (2 * g0 + g1) / 3;
            image->pixels[px_index + 2] = (2 * b0 + b1) / 3;
          }
          // (color0 + 2*color1) / 3
          else if (code == 3) {
            image->pixels[px_index + 0] = (r0 + 2 * r1) / 3;
            image->pixels[px_index + 1] = (g0 + 2 * g1) / 3;
            image->pixels[px_index + 2] = (b0 + 2 * b1) / 3;
          }
        }
      }

    // skip this slice
    data += blocks_x * blocks_y * 16;
  }
}
void dds_parse_dxt5(dds_image* image, const u8* data, i32 data_length)
{

  u32 img_width = MAX(1, image->header.width);
  u32 img_height = MAX(1, image->header.height);
  u32 img_depth = MAX(1, image->header.depth);

  u32 blocks_x = MAX(1, img_width / 4);
  u32 blocks_y = MAX(1, img_height / 4);

  for (u32 z = 0; z < img_depth; z++) {
    for (u32 x = 0; x < blocks_x; x++)
      for (u32 y = 0; y < blocks_y; y++) {
        unsigned short color0 = 0, color1 = 0;
        u32 codes = 0;
        unsigned long long alpha_codes = 0;
        u8 alpha0 = 0, alpha1 = 0;

        // read the block data
        u32 block_offset = (y * blocks_x + x) * 16; // 16 == 128bits
        alpha0 = data[block_offset + 0];
        alpha1 = data[block_offset + 1];
        memcpy(&alpha_codes, data + block_offset + 2, 6);
        memcpy(&color0, data + block_offset + 8, 2);
        memcpy(&color1, data + block_offset + 10, 2);
        memcpy(&codes, data + block_offset + 12, 4);

        // unpack the color data
        u8 r0 = (color0 & 0b1111100000000000) >> 8;
        u8 g0 = (color0 & 0b0000011111100000) >> 3;
        u8 b0 = (color0 & 0b0000000000011111) << 3;
        u8 r1 = (color1 & 0b1111100000000000) >> 8;
        u8 g1 = (color1 & 0b0000011111100000) >> 3;
        u8 b1 = (color1 & 0b0000000000011111) << 3;

        // process the data
        for (u32 b = 0; b < 16; b++) {
          u32 px_index = (z * img_height * img_width + (img_height - ((y * 4) + b / 4) - 1) * img_width + x * 4 + b % 4) * 4;
          u8 code = (codes >> (2 * b)) & 0b0011;
          u8 alpha_code = (alpha_codes >> (3 * b)) & 0b0111;

          /* COLOR */
          // color0
          if (code == 0) {
            image->pixels[px_index + 0] = r0;
            image->pixels[px_index + 1] = g0;
            image->pixels[px_index + 2] = b0;
          }
          // color1
          else if (code == 1) {
            image->pixels[px_index + 0] = r1;
            image->pixels[px_index + 1] = g1;
            image->pixels[px_index + 2] = b1;
          }
          // (2*color0 + color1) / 3
          else if (code == 2) {
            image->pixels[px_index + 0] = (2 * r0 + r1) / 3;
            image->pixels[px_index + 1] = (2 * g0 + g1) / 3;
            image->pixels[px_index + 2] = (2 * b0 + b1) / 3;
          }
          // (color0 + 2*color1) / 3
          else if (code == 3) {
            image->pixels[px_index + 0] = (r0 + 2 * r1) / 3;
            image->pixels[px_index + 1] = (g0 + 2 * g1) / 3;
            image->pixels[px_index + 2] = (b0 + 2 * b1) / 3;
          }

          /* ALPHA */
          u8 alpha = 0xFF;
          if (code == 0)
            alpha = alpha0;
          else if (code == 1)
            alpha = alpha1;
          else {
            if (alpha0 > alpha1)
              alpha = ((8 - alpha_code) * alpha0 + (alpha_code - 1) * alpha1) / 7;
            else {
              if (alpha_code == 6)
                alpha = 0;
              else if (alpha_code == 7)
                alpha = 255;
              else
                alpha = ((6 - alpha_code) * alpha0 + (alpha_code - 1) * alpha1) / 5;
            }
          }
          image->pixels[px_index + 3] = alpha;
        }
      }

    // skip this slice
    data += blocks_x * blocks_y * 16;
  }
}

static void dds_image_free(dds_image* image)
{
  free(image->pixels);
  free(image);
}

static dds_image* dds_load_from_memory(const u8* data, i32 data_length)
{
  const u8* data_loc = data;

  u32 magic = 0x00;
  memcpy(&magic, data_loc, sizeof(magic));
  data_loc += 4;

  if (magic != 0x20534444) // 'DDS '
    return NULL;

  dds_image* ret = (dds_image*)malloc(sizeof(struct dds_image));


  // read the header
  memcpy(&ret->header, data_loc, sizeof(struct dds_header));
  data_loc += sizeof(struct dds_header);

  // check if the dds_header::dwSize (must be equal to 124)
  if (ret->header.size != 124) {
    dds_image_free(ret);
    return NULL;
  }

  // check the dds_header::flags (DDSD_CAPS, DDSD_HEIGHT, DDSD_WIDTH, DDSD_PIXELFORMAT must be set)
  if (!((ret->header.flags & DDSD_CAPS) && (ret->header.flags & DDSD_HEIGHT) && (ret->header.flags & DDSD_WIDTH) && (ret->header.flags & DDSD_PIXELFORMAT))) {
    dds_image_free(ret);
    return NULL;
  }

  // check the dds_header::caps
  if ((ret->header.caps & DDSCAPS_TEXTURE) == 0) {
    dds_image_free(ret);
    return NULL;
  }

  // check if we need to load dds_header_dxt10
  if ((ret->header.pixel_format.flags & DDPF_FOURCC) && ret->header.pixel_format.four_cc == FOURCC("DX10")) {
    // read the header10
    memcpy(&ret->header10, data_loc, sizeof(struct dds_header_dxt10));
    data_loc += sizeof(struct dds_header_dxt10);
  }

  // allocate pixel data
  u32 img_width = MAX(1, ret->header.width);
  u32 img_height = MAX(1, ret->header.height);
  u32 img_depth = MAX(1, ret->header.depth);
  ret->pixels = (u8*)malloc(img_width * img_height * img_depth * 4);


  //dds_parse_dxt5(ret, data_loc, data_length - (data_loc - data));

  switch (data[87]) {
  case '1': // DXT1
    dds_parse_dxt1(ret, data_loc, data_length - (data_loc - data));
    break;
  case '3': // DXT3
    dds_parse_dxt3(ret, data_loc, data_length - (data_loc - data));
    break;
  case '5': // DXT5
    dds_parse_dxt5(ret, data_loc, data_length - (data_loc - data));
    break;
  case '0': // DX10
  default:
    //ASSERT(false);
    return nullptr; // invalid texture
  }

  return ret;
}

static i32 bestArrangementInfoForMetadata(const std::vector<Arrangement::Info>& arrangementInfos)
{
  for (i32 i = 0; i < arrangementInfos.size(); ++i)
    if (arrangementInfos[i].arrangementName == "Lead")
      return i;

  for (i32 i = 0; i < arrangementInfos.size(); ++i)
    if (arrangementInfos[i].arrangementName == "Rhythm")
      return i;

  for (i32 i = 0; i < arrangementInfos.size(); ++i)
    if (arrangementInfos[i].arrangementName == "Bass")
      return i;

  for (i32 i = 0; i < arrangementInfos.size(); ++i)
    if (arrangementInfos[i].arrangementName == "Combo")
      return i;

  ASSERT(false);
  return 0;
}

enum struct Instrument2 : u8
{
  invalid,
  LeadGuitar,
  RhythmGuitar,
  BassGuitar,
};

static Instrument2 instrumentFromSngFileName(const std::string& filename)
{
  ASSERT(filename.ends_with(".sng"));

  const std::string filename2(reinterpret_cast<const char*>(filename.c_str()));
  {
    static const std::regex patternLead(R"(_lead[2-9]?\.sng)");
    if (std::regex_search(filename2, patternLead))
      return Instrument2::LeadGuitar;
  }
  {
    static const std::regex patternRhythm(R"(_rhythm[2-9]?\.sng)");
    if (std::regex_search(filename2, patternRhythm))
      return Instrument2::RhythmGuitar;
  }
  {
    static const std::regex patternBass(R"(_bass[2-9]?\.sng)");
    if (std::regex_search(filename2, patternBass))
      return Instrument2::BassGuitar;
  }
  {
    static const std::regex patternLead(R"(_combo[2-9]?\.sng)");
    if (std::regex_search(filename2, patternLead))
      return Instrument2::LeadGuitar;
  }
  {
    static const std::regex patternVocals(R"(_j?vocals[2-9]?\.sng)");
    if (std::regex_search(filename2, patternVocals))
      return Instrument2::invalid;
  }

  ASSERT(false);
  return Instrument2::invalid;
}

static std::string arranagementNameFromSngFileName(const std::string& filename)
{
  std::string arrangementName = filename.substr(0, filename.size() - 4);
  const u64 pos = arrangementName.find_last_of('_');
  ASSERT(pos != std::string::npos);
  arrangementName = arrangementName.substr(pos + 1);

  return arrangementName;
}

static std::string arranagementName(const std::string& tocEntryName)
{
  std::string arrangementName = arranagementNameFromSngFileName(tocEntryName);
  arrangementName[0] -= 32;
  ASSERT(arrangementName[0] >= 'A');
  ASSERT(arrangementName[0] <= 'Z');
  return arrangementName;
}

static const char* instrumentName(Instrument2 instrument)
{
  switch (instrument)
  {
  case Instrument2::LeadGuitar:
    return "lead";
  case Instrument2::RhythmGuitar:
    return "rhythm";
  case Instrument2::BassGuitar:
    return "bass";
  }

  ASSERT(false);
  return {};
}

static std::string parseNote(const Sng::Info::Arrangement::Note& note, const bool isBass, i8& shredString)
{
  std::string str;

  str += TimeNS_To_String(timeNS_From_Seconds(note.time)) + "=";
  size_t equalsPos = str.length();

  if (note.stringIndex >= 0)
  {
    shredString = (isBass ? 3 : 5) - note.stringIndex;
    ASSERT(shredString >= 0);
  }

  if (note.chordId >= 0)
    str += ",chordId:" + to_string(note.chordId);
  if (note.chordNotesId >= 0)
    str += ",chordNotesId:" + to_string(note.chordNotesId);
  if (note.fretId >= 0)
    str += ",fret:" + to_string(note.fretId);
  if (note.stringIndex >= 0)
    str += ",string:" + to_string(shredString);
  if (note.sustain > 0.0f)
    str += ",sustain:" + TimeNS_To_String(timeNS_From_Seconds(note.sustain));
  if (note.slap >= 0)
    str += ",slap:" + to_string(note.slap);
  if (note.pluck >= 0)
    str += ",pluck:" + to_string(note.pluck);
  if (note.slideTo >= 0)
    str += ",slideTo:" + to_string(note.slideTo);
  if (note.tap >= 0)
    //str += ",tap:" + to_string(note.tap);
    str += ",tap";
  if (note.leftHand >= 0)
    str += ",finger:" + to_string(note.leftHand);
  if (note.slideUnpitchTo >= 0)
    str += ",slideUnpitchTo:" + to_string(note.slideUnpitchTo);
  if (note.maxBend > 0.0f)
    str += ",bend:" + to_string(note.maxBend);
  if (note.vibrato != 0)
    str += ",vibrato:" + to_string(note.vibrato);
  if (bool(note.noteMask & Sng::NoteMask::palmMute))
    str += ",palmMute";
  if (bool(note.noteMask & Sng::NoteMask::frethandMute))
    str += ",frethandMute";
  if (bool(note.noteMask & Sng::NoteMask::hammerOn))
    str += ",hammerOn";
  if (bool(note.noteMask & Sng::NoteMask::pullOff))
    str += ",pullOff";
  if (bool(note.noteMask & Sng::NoteMask::harmonic))
    str += ",harmonic";
  if (bool(note.noteMask & Sng::NoteMask::pinchHarmonic))
    str += ",pinchHarmonic";
  if (bool(note.noteMask & Sng::NoteMask::tremolo))
    str += ",tremolo";
  if (bool(note.noteMask & Sng::NoteMask::accent))
    str += ",accent";
  if (bool(note.noteMask & Sng::NoteMask::highDensity))
    str += ",highDensity";
  if (bool(note.noteMask & Sng::NoteMask::strum))
    str += ",strumUp";
  if (bool(note.noteMask & Sng::NoteMask::ignore))
    str += ",ignore";
  if (bool(note.noteMask & Sng::NoteMask::parent))
    str += ",linkNext";

  for (i32 i = 0; i < note.bendData.size(); ++i)
  {
    str += ",bend" + to_string(i) + "time:" + TimeNS_To_String(timeNS_From_Seconds(note.bendData[i].time));
    str += ",bend" + to_string(i) + "step:" + TimeNS_To_String(timeNS_From_Seconds(note.bendData[i].step));
  }

  if (equalsPos != str.length())
    str.erase(equalsPos, 1);

  return str;
}

static void printUsage_()
{
  puts("Useage: shredRipper [options] artist_songname_v1_p.psarc");
  puts("Useage: shredRipper [options] input-psarc-directory output-shred-directory");
  puts(" options:");
  puts("  --noAudio          exclude ogg audio file");
  puts("  --noAudioPreview   exclude preview ogg audio file");
  puts("  --noAlbum          exclude album cover");
  puts("  --noLyrics         exclude lyrics from .psarc");
  puts("  --no1337           disable colorful console output");
  puts("  --noDirTree        output .shred files in a flat structure");
  puts("  --cidBasename      IPFSCID.shred output file names");
  puts("  --noPsarcFilename  output Artist-Name_Song-Name.shred filenames.");
  puts("  --writeSql         enable sql file output");
  puts("  --replaceShred     overwrite existing shred file");
  puts("  --begin            index of the first file to process");
  puts("  --end              index of the last file to process");
  puts("  --user             username");
  puts("  --permission       permission for sql");
  puts("  --blacklist        path to .txt with .psarc filenames that should be skipped");
  puts("  --version          print the shredRipper version.");
}

#define RED "\x1b[31m"
#define GREEN "\x1b[32m"
#define YELLOW "\x1b[33m"
#define BLUE "\x1b[34m"
#define MAGENTA "\x1b[35m"
#define CYAN "\x1b[36m"
#define BOLD "\x1b[1m"
#define RESET "\x1b[0m"

static bool no1337 = false;
static const char* color(const char* color)
{
  if (no1337)
    return "";
  return color;
}

static std::string fixSqlQuote(const std::string& s)
{
  struct detail
  {
    static std::string rtrim(const std::string& s)
    {
      std::string ss = s;
      ss.erase(s.find_last_not_of(' ') + 1);
      return ss;
    }
    static std::string ltrim(const std::string& s)
    {
      std::string ss = s;
      ss.erase(0, s.find_first_not_of(' '));
      return ss;
    }
  };

  std::string ss = detail::ltrim(detail::rtrim(s));

  while (ss.find("'") != std::string::npos)
    ss.replace(ss.find("'"), 1, "{SQLQUOTE}");

  while (ss.find("{SQLQUOTE}") != std::string::npos)
    ss.replace(ss.find("{SQLQUOTE}"), sizeof("{SQLQUOTE}") - 1, "''");

  return ss;
}

static std::string getDisplayChordName(const char* name)
{
  std::string tName = name;

  tName.erase(tName.find_last_not_of(' ') + 1);
  tName.erase(0, tName.find_first_not_of(' '));

  if (const size_t pos = tName.find("min"); pos != std::string::npos)
    tName.replace(tName.find("min"), sizeof("min") - 1, "m");

  if (const size_t pos = tName.find("Maj"); pos != std::string::npos)
    tName.replace(tName.find("Maj"), sizeof("Maj") - 1, "");

  return tName;
}

static void writePngToDirTree(const std::string& outputAlbumDirPath, const u8* pngData, const u64 pngSize)
{
  if (!File::exists((outputAlbumDirPath + "album.png").c_str()))
  {
    File::write((outputAlbumDirPath + "album.png").c_str(), pngData, pngSize);
  }
  else
  {
    // bad (, but working) code ahead. It checks if the same album.png already exists.
    // should be rewritten.
    std::vector<u8> existingPng = File::read((outputAlbumDirPath + "album.png").c_str());
    bool isDifferentPng = pngSize != existingPng.size();
    if (!isDifferentPng)
    {
      for (i32 i = 0; i < pngSize; ++i)
      {
        if (pngData[i] != existingPng[i])
        {
          isDifferentPng = true;
          break;
        }
      }
      if (isDifferentPng)
      {
        i32 i = 0;
        while (File::exists((outputAlbumDirPath + to_string(i) + ".png").c_str()))
        {
          std::vector<u8> existingPng = File::read((outputAlbumDirPath + to_string(i) + ".png").c_str());
          isDifferentPng = pngSize != existingPng.size();
          if (!isDifferentPng)
          {
            for (i32 i = 0; i < pngSize; ++i)
            {
              if (pngData[i] != existingPng[i])
              {
                isDifferentPng = true;
                break;
              }
            }
          }
          if (isDifferentPng)
          {
            break;
          }
          ++i;
        }
      }
    }
    if (isDifferentPng)
    {
      i32 i = 0;
      while (File::exists((outputAlbumDirPath + to_string(i) + ".png").c_str()))
        ++i;
      File::write((outputAlbumDirPath + to_string(i) + ".png").c_str(), pngData, pngSize);
    }
  }
}

static void addAlbumCoverFromPNG(const char* albumPath, mz_zip_archive& zip_archive, const std::string& outputAlbumDirPath, bool dirTree)
{
  // re-encode the png into a fpng.
  const std::vector<u8> origPngData = File::read(albumPath);

  std::vector<u8> rgbaPixels;
  unsigned long width;
  unsigned long height;
  Png::decode(rgbaPixels, width, height, origPngData.data(), origPngData.size(), true);

  std::vector<u8> rgbPixels(width * height * 3); // rgb
  for (i32 i = 0, j = 0; i < rgbaPixels.size(); ++i)
    if (i % 4 != 3)
      rgbPixels[j++] = rgbaPixels[i];

  std::vector<u8> fpngData;
  Png::fpng_encode_image_to_memory(rgbPixels.data(), width, height, 3, fpngData);
  mz_zip_writer_add_mem(&zip_archive, "album.png", fpngData.data(), fpngData.size(), 0);

  if (dirTree)
    writePngToDirTree(outputAlbumDirPath, fpngData.data(), fpngData.size());
}


static void addAlbumCoverFromDDS(const Psarc::Info::TOCEntry* dds, mz_zip_archive& zip_archive, const std::string& outputAlbumDirPath, bool dirTree)
{
  if (dds != nullptr)
  { // handle album cover
    dds_image* image = dds_load_from_memory(dds->content.data(), i32(dds->content.size()));
    if (image == nullptr)
    {
      fprintf(stderr, "%s%sWarning:%s DDS image could not be loaded. Ignoring it.\n", color(BOLD), color(YELLOW), color(RESET));
      return;
    }

    Color* flippedYPixels = reinterpret_cast<Color*>(image->pixels);
    for (u32 y = 0; y < image->header.height / 2; ++y) {
      for (u32 x = 0; x < image->header.width; ++x) {
        const Color tempColor = flippedYPixels[y * image->header.width + x];
        flippedYPixels[y * image->header.width + x] = flippedYPixels[(image->header.height - y - 1) * image->header.width + x];
        flippedYPixels[(image->header.height - y - 1) * image->header.width + x] = tempColor;
      }
    }

    // rgba -> rgb
    const i32 len = image->header.width * image->header.height * 4; // rgba
    std::vector<u8> rgbPixels(image->header.width * image->header.height * 3); // rgb
    for (i32 i = 0, j = 0; i < len; ++i)
      if (i % 4 != 3)
        rgbPixels[j++] = image->pixels[i];

    std::vector<u8> pngData;
    Png::fpng_encode_image_to_memory(rgbPixels.data(), image->header.width, image->header.height, 3, pngData);
    mz_zip_writer_add_mem(&zip_archive, "album.png", pngData.data(), pngData.size(), 0);

    if (dirTree)
      writePngToDirTree(outputAlbumDirPath, pngData.data(), pngData.size());

    free(image->pixels);
    free(image);
  }
}

struct Arrangement_
{
  Tuning tuning;
  i8 stringCount;
  i8 capoFret{};
  f32 centOffset{};
  bool bassPick{};
};

static std::string serializeSongIni(const std::string& artistName, const std::string& songName, const std::string& albumName, const i32 songYear, const TimeNS songLength, const std::string& spotifyTrackId, const u32 track, const TimeNS delayBegin, const TimeNS delayEnd, const std::map<std::string, Arrangement_>& arrangementMap)
{
  std::string str = "[Song]\n";

  {
    str += "Song=" + songName + '\n';
    str += "Artist=" + artistName + '\n';
    str += "Album=" + albumName + '\n';
    //str += "Genere=none\n";
    str += "Length=" + TimeNS_To_String(songLength) + '\n';
    str += "Year=" + to_string(songYear) + '\n';
    if (!spotifyTrackId.empty())
      str += "SpotifyTrackId=" + spotifyTrackId + '\n';
    if (track != 0)
      str += "Track=" + to_string(track) + '\n';
    if (delayBegin != 0)
      str += "DelayBegin=" + TimeNS_To_String(delayBegin) + '\n';
    if (delayEnd != 0)
      str += "DelayEnd=" + TimeNS_To_String(delayEnd) + '\n';
    str += "Version=" + std::string("" VERSION_STR) + '\n';
  }

  for (const auto& [arrangementName, arrangement] : arrangementMap)
  {
    str += "\n[" + arrangementName + "]\n";
    if (arrangement.bassPick)
      str += "BassPick=1\n";
    if (arrangement.capoFret > 0)
      str += "CapoFret=" + to_string(arrangement.capoFret) + "\n";
    if (arrangement.centOffset != 0.0f)
      str += "CentOffset=" + to_string(arrangement.centOffset) + "\n";

    std::string tuningStr;
    for (i8 i = 0; i < arrangement.stringCount; ++i)
    {
      ASSERT(arrangement.tuning.string[i] != -128);
      tuningStr += to_string(arrangement.tuning.string[i]) + ',';
    }
    ASSERT(tuningStr[tuningStr.size() - 1] == ',');
    tuningStr.resize(tuningStr.size() - 1);

    str += "Tuning=" + tuningStr + "\n";
  }

  return str;
}

static std::string exec_ipfsGetCID(const char* cmd)
{
  std::string cid;
  cid.resize(47);
#ifdef PLATFORM_WINDOWS
  FILE* pipe = _wpopen(String::s2ws(cmd, strlen(reinterpret_cast<const char*>(cmd))).c_str(), L"r");
#else // PLATFORM_WINDOWS
  FILE* pipe = popen(reinterpret_cast<const char*>(cmd), "r");
#endif // PLATFORM_WINDOWS
  fgets(reinterpret_cast<char*>(cid.data()), 47, pipe);
#ifdef PLATFORM_WINDOWS
  _pclose(pipe);
#else // PLATFORM_WINDOWS
  pclose(pipe);
#endif // PLATFORM_WINDOWS
  ASSERT(cid[46] == '\0');
  cid.pop_back();

  return cid;
}

static std::string absolutePath(const char* filePath)
{
#ifdef PLATFORM_WINDOWS
  const std::wstring wFilepath = String::s2ws(filePath, i32(strlen(reinterpret_cast<const char*>(filePath))));
  const std::wstring wFilepathExpanded = String::ws_ExpandEnvironmentStrings(wFilepath);
  const std::wstring extendedPath = windowsExtendedPath(wFilepathExpanded);
  const std::filesystem::path path(extendedPath);
#else // PLATFORM_WINDOWS
  const std::filesystem::path path(filePath);
#endif // PLATFORM_WINDOWS

  return std::filesystem::absolute(path).string();
}

static std::string parentPath(const char* filePath)
{
#ifdef PLATFORM_WINDOWS
  const std::wstring wFilepath = String::s2ws(filePath, i32(strlen(reinterpret_cast<const char*>(filePath))));
  const std::wstring wFilepathExpanded = String::ws_ExpandEnvironmentStrings(wFilepath);
  const std::wstring extendedPath = windowsExtendedPath(wFilepathExpanded);
  const std::filesystem::path path(extendedPath);
#else // PLATFORM_WINDOWS
  const std::filesystem::path path(filePath);
#endif // PLATFORM_WINDOWS

  return path.parent_path().string();
}

int main(int argc, char* argv[])
{
#ifdef PLATFORM_WINDOWS
  SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
#endif // PLATFORM_WINDOWS

  if (argc <= 1)
  {
    printUsage_();
    return -1;
  }

  if (argc == 2 && strcmp("--version", argv[1]) == 0)
  {
    puts("shreddRipper " VERSION_STR);
    return 0;
  }

  Global::installMode = InstallMode::installed;

  Png::fpng_init();

  std::string outputDir = "shred/";

  i32 lastInputParamIdx = argc - 1;
  { // is last parameter output directory?
    const std::string lastParam(reinterpret_cast<const char*>(argv[lastInputParamIdx]));
    if (!lastParam.ends_with(".psarc"))
    {
      --lastInputParamIdx;
      outputDir = lastParam;
      if (outputDir[outputDir.size() - 1] != '/')
        outputDir += '/';
    }
  }

  bool audio = true;
  bool audioPreview = true;
  bool album = true;
  bool lyrics = true;
  bool dirTree = true;
  bool cidBasename = false;
  bool psarcFilename = true;
  bool noSql = false;
  bool replaceShred = false;
  i32 begin = 0;
  i32 end = I32::max;
  std::string user = "anon";
  std::string permission = "8";
  std::string blacklist;

  i32 i = 0;
  for (; i < lastInputParamIdx; ++i)
  {
    if (argv[i + 1][0] != '-')
      break;

    if (strcmp("--noAudio", argv[i + 1]) == 0)
      audio = false;
    else if (strcmp("--noAudioPreview", argv[i + 1]) == 0)
      audioPreview = false;
    else if (strcmp("--noAlbum", argv[i + 1]) == 0)
      album = false;
    else if (strcmp("--noLyrics", argv[i + 1]) == 0)
      lyrics = false;
    else if (strcmp("--no1337", argv[i + 1]) == 0)
      no1337 = true;
    else if (strcmp("--noDirTree", argv[i + 1]) == 0)
      dirTree = false;
    else if (strcmp("--cidBasename", argv[i + 1]) == 0)
      cidBasename = true;
    else if (strcmp("--noPsarcFilename", argv[i + 1]) == 0)
      psarcFilename = false;
    else if (strcmp("--noSql", argv[i + 1]) == 0)
      noSql = true;
    else if (strcmp("--replaceShred", argv[i + 1]) == 0)
      replaceShred = true;
    else if (strcmp("--begin", argv[i + 1]) == 0)
      begin = atoi(argv[++i + 1]);
    else if (strcmp("--end", argv[i + 1]) == 0)
      end = atoi(argv[++i + 1]);
    else if (strcmp("--user", argv[i + 1]) == 0)
      user = reinterpret_cast<const char*>(argv[++i + 1]);
    else if (strcmp("--permission", argv[i + 1]) == 0)
      permission = reinterpret_cast<const char*>(argv[++i + 1]);
    else if (strcmp("--blacklist", argv[i + 1]) == 0)
      blacklist = reinterpret_cast<const char*>(argv[++i + 1]);
    else
    {
      printUsage_();
      return -1;
    }
  }

  if (!no1337)
  {
#ifdef PLATFORM_WINDOWS
    SetConsoleOutputCP(CP_UTF8);
#endif // PLATFORM_WINDOWS
    printf("\n\n" RED);
    printf(R"(                ██████  ██░ ██  ██▀███  ▓█████ ▓█████▄      ██▀███   ██▓ ██▓███   ██▓███  ▓█████  ██▀███)""\n");
    printf(R"(              ▒██    ▒ ▓██░ ██▒▓██ ▒ ██▒▓█   ▀ ▒██▀ ██▌    ▓██ ▒ ██▒▓██▒▓██░  ██▒▓██░  ██▒▓█   ▀ ▓██ ▒ ██▒)""\n");
    printf(R"(              ░ ▓██▄   ▒██▀▀██░▓██ ░▄█ ▒▒███   ░██   █▌    ▓██ ░▄█ ▒▒██▒▓██░ ██▓▒▓██░ ██▓▒▒███   ▓██ ░▄█ ▒)""\n");
    printf(R"(                ▒   ██▒░▓█ ░██ ▒██▀▀█▄  ▒▓█  ▄ ░▓█▄   ▌    ▒██▀▀█▄  ░██░▒██▄█▓▒ ▒▒██▄█▓▒ ▒▒▓█  ▄ ▒██▀▀█▄)""\n");
    printf(R"(              ▒██████▒▒░▓█▒░██▓░██▓ ▒██▒░▒████▒░▒████▓     ░██▓ ▒██▒░██░▒██▒ ░  ░▒██▒ ░  ░░▒████▒░██▓ ▒██▒)""\n");
    printf(R"(              ▒ ▒▓▒ ▒ ░ ▒ ░░▒░▒░ ▒▓ ░▒▓░░░ ▒░ ░ ▒▒▓  ▒     ░ ▒▓ ░▒▓░░▓  ▒▓▒░ ░  ░▒▓▒░ ░  ░░░ ▒░ ░░ ▒▓ ░▒▓░)""\n");
    printf(R"(              ░ ░▒  ░ ░ ▒ ░▒░ ░  ░▒ ░ ▒░ ░ ░  ░ ░ ▒  ▒       ░▒ ░ ▒░ ▒ ░░▒ ░     ░▒ ░      ░ ░  ░  ░▒ ░ ▒░)""\n");
    printf(R"(              ░  ░  ░   ░  ░░ ░  ░░   ░    ░    ░ ░  ░       ░░   ░  ▒ ░░░       ░░          ░     ░░   ░)""\n");
    printf(R"(                    ░   ░  ░  ░   ░        ░  ░   ░           ░      ░                       ░  ░   ░)""\n");
    printf(R"(                                                ░)""%s\n", color(RESET));
  }

  const std::string inputFile = absolutePath(reinterpret_cast<const char*>(argv[i + 1]));
  std::vector<std::string> filesToProcess;
  for (; i < lastInputParamIdx; ++i)
  {
    if (inputFile.ends_with(".psarc"))
    {
      std::string inputFileForwardSlash = inputFile;
      std::replace(inputFileForwardSlash.begin(), inputFileForwardSlash.end(), '\\', '/');
      filesToProcess.push_back(inputFileForwardSlash);
    }
    else
    {
      std::vector<std::string> filesInDir = File::filesInDirectory(inputFile.c_str());
      std::sort(filesInDir.begin(), filesInDir.end()); // make sure files are alphabetically sorted. On directory_iterator the order is unspecified.
      for (const std::string& file : filesInDir)
      {
        if (file.ends_with(".psarc"))
        {
          filesToProcess.push_back(file);
        }
      }
    }
  }

  std::set<std::string> blacklistPsarcs;
  if (File::exists(blacklist.c_str()))
  {
    const std::vector<u8> blackListContent = File::read(blacklist.c_str());
    std::string psarcFilename;
    for (u64 i = 0; i < blackListContent.size(); ++i)
    {
      if (blackListContent[i] != '\n')
      {
        psarcFilename += blackListContent[i];
      }
      else
      {
        blacklistPsarcs.insert(psarcFilename);
        psarcFilename.clear();
      }
    }
  }

  std::string sql;

  // 25051 path to long for writing zip file
  if (end >= filesToProcess.size())
    end = filesToProcess.size() - 1;
  for (i32 i = begin; i <= end; ++i)
  {
    const std::string& fileToProcess = filesToProcess[i];

    if (!fileToProcess.ends_with(".psarc"))
      continue;

    if (blacklistPsarcs.contains(File::filename(fileToProcess.c_str())))
      continue;

    const u64 slashPos = fileToProcess.find_last_of('/');
    std::string basename = slashPos == std::string::npos
      ? fileToProcess.substr(0, fileToProcess.size() - 6)
      : fileToProcess.substr(slashPos + 1, fileToProcess.size() - slashPos - 7);
    if (basename.ends_with("_p"))
      basename = basename.substr(0, basename.size() - 2);

    mz_zip_archive zip_archive{};

    // print progress
    printf("[%s%d%s/%s%d%s] %s%s%s", color(GREEN), i, color(CYAN), color(MAGENTA), end, color(RESET), color(CYAN), fileToProcess.c_str(), color(RESET));

    const std::vector<u8> psarcData = File::read(fileToProcess.c_str());

    const Psarc::Info::TOCEntry* psarcWem0 = nullptr;
    const Psarc::Info::TOCEntry* psarcWem1 = nullptr;
    const Psarc::Info::TOCEntry* psarcDds = nullptr;
    const Psarc::Info::TOCEntry* psarcHsan = nullptr;

    Psarc::Info psarcInfo;
    if (psarcData.size() > 0)
    {
      psarcInfo = Psarc::parse(psarcData.data(), psarcData.size());

      for (const Psarc::Info::TOCEntry& tocEntry : psarcInfo.tocEntries)
      {
        if (tocEntry.name.ends_with("_256.dds"))
        {
          ASSERT(psarcDds == nullptr);
          psarcDds = &tocEntry;
        }

        if (tocEntry.name.ends_with(".wem"))
        {
          if (psarcWem0 == nullptr)
            psarcWem0 = &tocEntry;
          else
          {
            ASSERT(psarcWem1 == nullptr);
            psarcWem1 = &tocEntry;
          }
        }

        if (tocEntry.name.ends_with(".hsan"))
        {
          ASSERT(psarcHsan == nullptr);
          psarcHsan = &tocEntry;
        }
      }
    }

    std::string artistName;
    std::string songName;
    std::string albumName;
    i32 songYear{};
    TimeNS songLength{};
    TimeNS delayBegin{};
    TimeNS delayEnd{};
    std::string spotifyTrackId;
    i32 track{};
    std::map<std::string, Arrangement_> arrangementMap;
    std::vector<std::string> backlistedArrangements;

    std::vector<Arrangement::Info> psarcArrangementInfos;
    if (psarcHsan != nullptr)
    {
      psarcArrangementInfos = Song::PsarcParser::readPsarcHsan(psarcHsan->content, artistName, songName, albumName, songYear, songLength);
      Song::PsarcParser::patchPsarcArrangementInfo(psarcArrangementInfos, "");
    }

    { // patch arrangement though optional song.ini file
      const std::string songIniFilepath = File::replaceExtension(fileToProcess.c_str(), ".song.ini");
      if (File::exists(songIniFilepath.c_str()))
      {
        const std::vector<u8> iniFileContent = File::read(songIniFilepath.c_str());
        const std::map<std::string, std::map<std::string, std::string>> songIniContent = Ini::loadIniContent(reinterpret_cast<const char*>(iniFileContent.data()), iniFileContent.size());

        ASSERT(songIniContent.size() != 0);

        for (auto& [section, map] : songIniContent)
        {
          if (section == "Song")
          {
            if (map.contains("Album"))
              albumName = map.at("Album");
            if (map.contains("Artist"))
              artistName = map.at("Artist");
            if (map.contains("Length"))
              songLength = timeNS_From_String(map.at("Length"));
            if (map.contains("DelayBegin"))
              delayBegin = timeNS_From_String(map.at("DelayBegin"));
            if (map.contains("DelayEnd"))
              delayEnd = timeNS_From_String(map.at("DelayEnd"));
            if (map.contains("Song"))
              songName = map.at("Song");
            if (map.contains("Year"))
              songYear = atoi2(map.at("Year").c_str());
            if (map.contains("SpotifyTrackId"))
              spotifyTrackId = map.at("SpotifyTrackId");
            if (map.contains("Track"))
              track = atoi2(map.at("Track").c_str());
          }
          else if (section[0] == '-')
          {
            backlistedArrangements.push_back(section.substr(1));
          }
          else
          {
            Arrangement_& arrangement = arrangementMap[section];
            if (map.contains("Tuning"))
            {
              const std::string& tuningStr = map.at("Tuning");
              const std::vector<std::string> tuningValues = String::split(tuningStr, ',');
              arrangement.stringCount = tuningValues.size();
              for (i32 i = 0; i < arrangement.stringCount; ++i)
                arrangement.tuning.string[i] = atoi2(tuningValues[i].c_str());
            }
          }
        }
      }
    }

    std::string relOutputAlbumDirPath;
    std::string relOutputSongDirPath;
    if (dirTree)
    {
      relOutputAlbumDirPath = File::fixInvalidCharsInFilepath(artistName) + '/' + to_string(songYear) + "-" + File::fixInvalidCharsInFilepath(albumName) + '/';
      relOutputSongDirPath = relOutputAlbumDirPath + File::fixInvalidCharsInFilepath(songName) + '/' + user + '/';
    }

    std::string filename;
    if (psarcFilename)
    {
      filename = basename + ".shred";
    }
    else
    {
      basename = File::fixInvalidCharsInFilepath(artistName) + "_" + File::fixInvalidCharsInFilepath(songName);
      while (basename.find(" ") != std::string::npos)
        basename.replace(basename.find(" "), 1, "-");
      filename = basename + ".shred";
    }
    const std::string outputAlbumDirPath = outputDir + relOutputAlbumDirPath;
    const std::string outputDirPath = outputDir + relOutputSongDirPath;
    const std::string outputFilePath = outputDirPath + filename;

    bool createOutputFile = true;

    if (!replaceShred && File::exists(outputFilePath.c_str()))
    {
      printf("%s->%sShred file exists, use --replaceShred to overwrite the output files!%s\n", color(RESET), color(GREEN), color(RESET));
      createOutputFile = false;
    }

    if (createOutputFile)
    {
      if (!File::directoryExists(outputDirPath.c_str()))
        File::createDirectories(outputDirPath.c_str());

      // print progress
      printf("%s->%s%s%s\n", color(RESET), color(YELLOW), outputFilePath.c_str(), color(RESET));

      mz_bool status = mz_zip_writer_init_file(&zip_archive, reinterpret_cast<const char*>(outputFilePath.c_str()), 0);

      // Here we are super lazy.
      // .wem files have filenames like 123456789.wem.
      // We can't know if a .wem file is the song or just the preview.
      // If the .psarc contains only one .wem file it must be the song.
      // If there are two .wem files we check the file size and assume the larger one is the song while the smaller one is the preview.
      // This works well enough.
      // The correct way would be to parse the song_somename.bnk file.
      // It would contain the .wem filename as a id field.
      std::vector<u8> psarcOgg0;
      std::vector<u8> psarcOgg1;
      const std::vector<u8>* psarcOgg{};
      const std::vector<u8>* psarcOggPreview{};
      if (audio || audioPreview)
      {
        if (psarcWem0 != nullptr)
        {
          psarcOgg0 = Wem::to_ogg(psarcWem0->content.data(), psarcWem0->length);
          psarcOgg0 = Revorb::revorbOgg(psarcOgg0.data(), psarcOgg0.size());
        }
        if (psarcWem1 != nullptr)
        {
          psarcOgg1 = Wem::to_ogg(psarcWem1->content.data(), psarcWem1->length);
          psarcOgg1 = Revorb::revorbOgg(psarcOgg1.data(), psarcOgg1.size());
        }
      }
      if (psarcOgg0.size() > psarcOgg1.size())
      {
        psarcOgg = &psarcOgg0;
        psarcOggPreview = &psarcOgg1;
      }
      else
      {
        psarcOgg = &psarcOgg1;
        psarcOggPreview = &psarcOgg0;
      }
      ASSERT(psarcOgg != nullptr);
      ASSERT(psarcOggPreview != nullptr);

      if (audio)
      {
        const std::string& songOggFromFileName = fileToProcess.substr(0, fileToProcess.size() - 5) + "song.ogg";
        if (File::exists(songOggFromFileName.c_str()))
        {
          const std::vector<u8> fileData = File::read(songOggFromFileName.c_str());
          mz_zip_writer_add_mem(&zip_archive, "song.ogg", fileData.data(), fileData.size(), 0);
        }
        else
        {
          if (psarcOgg->size() == 0)
            fprintf(stderr, "%s%sWarning:%s No song audio found.\n", color(BOLD), color(YELLOW), color(RESET));
          else
            mz_zip_writer_add_mem(&zip_archive, "song.ogg", psarcOgg->data(), psarcOgg->size(), 0);
        }
      }
      if (audioPreview)
      {
        const std::string& previewOggFromFileName = fileToProcess.substr(0, fileToProcess.size() - 5) + "preview.ogg";
        if (File::exists(previewOggFromFileName.c_str()))
        {
          const std::vector<u8> fileData = File::read(previewOggFromFileName.c_str());
          mz_zip_writer_add_mem(&zip_archive, "preview.ogg", fileData.data(), fileData.size(), 0);
        }
        else if (psarcOggPreview != nullptr && psarcOggPreview->size() > 0)
        {
          mz_zip_writer_add_mem(&zip_archive, "preview.ogg", psarcOggPreview->data(), psarcOggPreview->size(), 0);
        }
      }

      if (album)
      {
        const std::string& pngFromFileName = fileToProcess.substr(0, fileToProcess.size() - 5) + "album.png";
        const std::string pngFromAlbumDir = inputFile + "/album/" + File::fixInvalidCharsInFilepath(artistName) + '/' + to_string(songYear) + '-' + File::fixInvalidCharsInFilepath(albumName) + "/album.png";
        if (File::exists(pngFromFileName.c_str()))
          addAlbumCoverFromPNG(pngFromFileName.c_str(), zip_archive, outputAlbumDirPath, dirTree);
        else if (File::exists(pngFromAlbumDir.c_str()))
          addAlbumCoverFromPNG(pngFromAlbumDir.c_str(), zip_archive, outputAlbumDirPath, dirTree);
        else
          addAlbumCoverFromDDS(psarcDds, zip_archive, outputAlbumDirPath, dirTree);
      }

      if (lyrics)
      { // handle lyrics
        const std::string& lyricsIniFromFileName = fileToProcess.substr(0, fileToProcess.size() - 5) + "lyrics.ini";
        if (File::exists(lyricsIniFromFileName.c_str()))
        {
          const std::vector<u8> fileData = File::read(lyricsIniFromFileName.c_str());
          mz_zip_writer_add_mem(&zip_archive, "lyrics.ini", fileData.data(), fileData.size(), 0);
        }
        else
        {
          const std::vector<Song::Vocal> vocals = Song::PsarcParser::loadVocals(psarcInfo);
          if (vocals.size() > 0)
          {
            std::string str;
            str += "[Lyrics]\n";
            for (const Song::Vocal& vocal : vocals)
              str += TimeNS_To_String(vocal.timeNS) + ',' + TimeNS_To_String(vocal.lengthNS) + '=' + vocal.lyric + '\n';

            mz_zip_writer_add_mem(&zip_archive, "lyrics.ini", str.data(), str.size(), 0);
          }
        }
      }

      if (psarcData.size() > 0)
      {
        Song::Info songInfo = Song::PsarcParser::loadSongInfo(psarcInfo, File::filename(fileToProcess.c_str()));
      }

      std::unordered_map<std::string, i8> lowestPlayedStringIndecies; // some psarc bass arrangements contain notes on 5th and 6th bass strings. If so, we should overwrite the string count.

      {
        for (const Psarc::Info::TOCEntry& tocEntry : psarcInfo.tocEntries)
        {
          if (!tocEntry.name.ends_with(".sng"))
            continue;

          const std::string arrangementName = arranagementName(tocEntry.name);

          // skip if arrangement is in blacklist
          if (std::find(backlistedArrangements.cbegin(), backlistedArrangements.cend(), arrangementName) != backlistedArrangements.cend())
            continue;

          const std::string& arrangementFromFileName = fileToProcess.substr(0, fileToProcess.size() - 5) + arrangementName + ".ini";
          std::vector<Ini::KeyValue> extraToneSwitches;
          if (File::exists(arrangementFromFileName.c_str()))
          {
            const std::vector<u8> fileData = File::read(arrangementFromFileName.c_str());
            const std::string filename = arrangementName + ".ini";

            const std::map<std::string, std::vector<Ini::KeyValue>> arrangementIni = Ini::loadIniContent_keepKeyOrder(reinterpret_cast<const char*>(fileData.data()), fileData.size());
            if (arrangementIni.size() == 1 && arrangementIni.contains("Tones"))
            {
              extraToneSwitches = arrangementIni.at("Tones");
            }
            else
            {
              mz_zip_writer_add_mem(&zip_archive, reinterpret_cast<const char*>(filename.c_str()), fileData.data(), fileData.size(), 0);
              continue;
            }
          }

          const Instrument2 instrument = instrumentFromSngFileName(tocEntry.name);
          if (instrument == Instrument2::invalid)
            continue;

          i8& lowestPlayedStringIndex = lowestPlayedStringIndecies[arrangementName];

          const Sng::Info sngInfo = Sng::parse(tocEntry.content);

          std::string str;

          if (sngInfo.bpm.size() > 0)
          {
            str += "[Beats]\n";
            for (i32 i = 0; i < sngInfo.bpm.size(); ++i)
            {
              const Sng::Info::Bpm& bpm = sngInfo.bpm[i];

              //ASSERT(bpm.mask == 0 || bpm.mask == 1);

              str += TimeNS_To_String(timeNS_From_Seconds(bpm.time)) + "=" + (bpm.mask != 0 ? "1" : "0") + '\n';
            }
          }

          { // Phrases -> Level
            if (sngInfo.phrase.size() <= 1)
              fprintf(stderr, "%s%sWarning:%s Expected phrase count to be at least 2.\n", color(BOLD), color(YELLOW), color(RESET));
            ASSERT(sngInfo.phraseIteration.size() >= 2);
            bool writeSections = false;
            for (i32 i = 0; i < sngInfo.phraseIteration.size(); ++i)
            {
              const i32 phraseId = sngInfo.phraseIteration[i].phraseId;
              if (sngInfo.phrase[phraseId].maxDifficulty != 0)
              {
                writeSections = true;
                break;
              }
            }
            if (writeSections)
            {
              str += "\n[LeveledSections]\n";
              for (i32 i = 0; i < sngInfo.phraseIteration.size(); ++i)
              {
                const TimeNS startTime = timeNS_From_Seconds(sngInfo.phraseIteration[i].startTime);
                const TimeNS endTime = timeNS_From_Seconds(sngInfo.phraseIteration[i].nextPhraseTime);
                if (endTime > 0)
                {
                  const i32 phraseId = sngInfo.phraseIteration[i].phraseId;
                  ASSERT(phraseId >= 0);
                  ASSERT(phraseId < sngInfo.phrase.size());
                  const i32 maxDifficulty = sngInfo.phrase[phraseId].maxDifficulty;
                  if (maxDifficulty >= 1)
                  {
                    str += TimeNS_To_String(startTime) + '-' + TimeNS_To_String(endTime) + '=' + to_string(maxDifficulty) + '\n';
                  }
                }
              }
            }
          }

          for (i32 j = 0; j < sngInfo.arrangements.size(); ++j)
          {
            const bool isBass = (instrument == Instrument2::BassGuitar);
            ASSERT(j == sngInfo.arrangements[j].difficulty);

            if (sngInfo.arrangements[j].anchors.size() > 0)
            {
              str += "\n[Level" + to_string(j) + "Anchors]\n";
              for (i32 i = 0; i < sngInfo.arrangements[j].anchors.size(); ++i)
              {
                const Sng::Info::Arrangement::Anchor& anchor = sngInfo.arrangements[j].anchors[i];

                str += TimeNS_To_String(timeNS_From_Seconds(anchor.startBeatTime)) /*+ "-" + TimeNS_To_String(timeNS_From_Seconds(anchor.endBeatTime))*/ + "=fret:" + to_string(anchor.fretId) + ",width:" + to_string(anchor.width) + '\n';
              }
            }

            if (sngInfo.arrangements[j].notes.size() > 0)
            {
              str += "\n[Level" + to_string(j) + "Notes]\n";
              for (i32 i = 0; i < sngInfo.arrangements[j].notes.size(); ++i)
              {
                const Sng::Info::Arrangement::Note& note = sngInfo.arrangements[j].notes[i];

                if (note.chordId == -1)
                {
                  ASSERT(note.stringIndex != -1);
                  ASSERT(note.fretId != -1);

                  i8 shredString = 0;
                  str += parseNote(note, isBass, shredString);
                  lowestPlayedStringIndex = max_(lowestPlayedStringIndex, shredString);

                  str += '\n';
                }
              }
            }

            if (sngInfo.arrangements[j].notes.size() > 0)
            {
              bool hasChords = false;
              for (i32 i = 0; i < sngInfo.arrangements[j].notes.size(); ++i)
              {
                const Sng::Info::Arrangement::Note& note = sngInfo.arrangements[j].notes[i];

                if (note.chordId != -1)
                {
                  hasChords = true;
                  break;
                }
              }
              if (hasChords)
              {
                str += "\n[Level" + to_string(j) + "Chords]\n";
                for (i32 i = 0; i < sngInfo.arrangements[j].notes.size(); ++i)
                {
                  const Sng::Info::Arrangement::Note& note = sngInfo.arrangements[j].notes[i];

                  if (note.chordId != -1)
                  {
                    ASSERT(note.stringIndex == -1);
                    ASSERT(note.fretId == -1);

                    str += TimeNS_To_String(timeNS_From_Seconds(note.time)) + "=";

                    Sng::NoteMask noteMask = note.noteMask;

                    // Remove flags from know techniques
                    if (bool(noteMask & Sng::NoteMask::chord))
                      noteMask &= ~Sng::NoteMask::chord;
                    if (bool(noteMask & Sng::NoteMask::chordNotes))
                      noteMask &= ~Sng::NoteMask::chordNotes;
                    if (bool(noteMask & Sng::NoteMask::sustain))
                      noteMask &= ~Sng::NoteMask::sustain;
                    if (bool(noteMask & Sng::NoteMask::doubleStop))
                      noteMask &= ~Sng::NoteMask::doubleStop;
                    if (bool(noteMask & Sng::NoteMask::arpeggio))
                      noteMask &= ~Sng::NoteMask::arpeggio;

                    // TODO: make changes to fix inacurrate 'strum ='
                    //str += ",strumUp";
                    //if (bool(noteMask & Sng::NoteMask::strum))
                    //{
                    //  noteMask &= ~Sng::NoteMask::strum;
                    //  chord.strum = 1; // up //TODO: Wrong, need research about it later
                    //}

                    if (noteMask != Sng::NoteMask::undefined)
                    {
                      // Setup techniques and remove flags
                      if (bool(noteMask & Sng::NoteMask::parent))
                      {
                        //noteMask &= ~Sng::NoteMask::parent;
                        str += "linkNext,";
                      }
                      if (bool(noteMask & Sng::NoteMask::accent))
                      {
                        //noteMask &= ~Sng::NoteMask::accent;
                        str += "accent,";
                      }
                      if (bool(noteMask & Sng::NoteMask::frethandMute))
                      {
                        //noteMask &= ~Sng::NoteMask::frethandMute;
                        str += "fretHandMute,";
                      }
                      if (bool(noteMask & Sng::NoteMask::highDensity))
                      {
                        //noteMask &= ~Sng::NoteMask::highDensity;
                        str += "highDensity,";
                      }
                      if (bool(noteMask & Sng::NoteMask::ignore))
                      {
                        //noteMask &= ~Sng::NoteMask::ignore;
                        str += "ignore,";
                      }
                      if (bool(noteMask & Sng::NoteMask::palmMute))
                      {
                        //noteMask &= ~Sng::NoteMask::palmMute;
                        str += "palmMute,";
                      }
                    }

                    //if (note.chordId >= 0)
                    //  str += ",chordId:" + to_string(note.chordId);
                    //if (note.chordNotesId >= 0)
                    //  str += ",chordNoteId:" + to_string(note.chordNotesId);
                    if (note.sustain != 0.0f)
                    {
                      str += "sustain:" + TimeNS_To_String(timeNS_From_Seconds(note.sustain)) + ',';
                    }

                    ASSERT(sngInfo.chord.size() > note.chordId);
                    const Sng::Info::Chord& chordTemplate = sngInfo.chord[note.chordId];

                    const Sng::Info::ChordNotes* chordNotes = nullptr;
                    if (note.chordNotesId >= 0)
                    {
                      ASSERT(sngInfo.chordNotes.size() > note.chordNotesId);
                      chordNotes = &sngInfo.chordNotes[note.chordNotesId];
                    }

                    std::vector<Song::Note> songChordNotes;
                    for (i8 i = 5; i >= 0; --i)
                    {
                      if (chordTemplate.frets[i] >= 0)
                      {
                        const i8 shredString = (isBass ? 3 : 5) - i;
                        ASSERT(shredString >= 0);

                        lowestPlayedStringIndex = max_(lowestPlayedStringIndex, i8(shredString));

                        str += to_string(shredString) + "fret:" + to_string(chordTemplate.frets[i]) + ",";
                        if (chordTemplate.fingers[i] >= 0)
                          str += to_string(shredString) + "finger:" + to_string(chordTemplate.fingers[i]) + ",";
                        //str += "," + std::to_string(i) + "time:" + stringFromSeconds(note.time);
                        //str += "," + std::to_string(i) + "sustain:" + stringFromSeconds(note.sustain);

                        if (chordNotes != nullptr)
                        {
                          if (bool(chordNotes->noteMask[i] & Sng::NoteMask::palmMute))
                            str += to_string(shredString) + "palmMute" + ",";
                          if (bool(chordNotes->noteMask[i] & Sng::NoteMask::frethandMute))
                            str += to_string(shredString) + "frethandMute" + ",";
                          if (bool(chordNotes->noteMask[i] & Sng::NoteMask::hammerOn))
                            str += to_string(shredString) + "hammerOn" + ",";
                          if (bool(chordNotes->noteMask[i] & Sng::NoteMask::pullOff))
                            str += to_string(shredString) + "pullOff" + ",";
                          if (bool(chordNotes->noteMask[i] & Sng::NoteMask::harmonic))
                            str += to_string(shredString) + "harmonic" + ",";
                          if (bool(chordNotes->noteMask[i] & Sng::NoteMask::pinchHarmonic))
                            str += to_string(shredString) + "pinchHarmonic" + ",";
                          if (bool(chordNotes->noteMask[i] & Sng::NoteMask::tremolo))
                            str += to_string(shredString) + "tremolo" + ",";
                          if (bool(chordNotes->noteMask[i] & Sng::NoteMask::accent))
                            str += to_string(shredString) + "accent" + ",";
                          if (bool(chordNotes->noteMask[i] & Sng::NoteMask::highDensity))
                            str += to_string(shredString) + "highDensity" + ",";
                          if (bool(chordNotes->noteMask[i] & Sng::NoteMask::strum))
                            str += to_string(shredString) + "strumUp" + ",";
                          if (bool(chordNotes->noteMask[i] & Sng::NoteMask::ignore))
                            str += to_string(shredString) + "ignore" + ",";
                          if (bool(chordNotes->noteMask[i] & Sng::NoteMask::parent))
                            str += to_string(shredString) + "linkNext" + ",";
                          if (chordNotes->slideTo[i] != 255)
                            str += to_string(shredString) + "slideTo:" + to_string(chordNotes->slideTo[i]) + ",";
                          if (chordNotes->slideUnpitchTo[i] != 255)
                            str += to_string(shredString) + "slideUnpitchTo:" + to_string(chordNotes->slideUnpitchTo[i]) + ",";
                          if (chordNotes->vibrato[i] > 0)
                            str += to_string(shredString) + "vibrato:" + to_string(chordNotes->vibrato[i]) + ",";
                        }
                      }
                    }

                    if (chordTemplate.name[0] != '\0')
                    {
                      const std::string name = getDisplayChordName(chordTemplate.name);
                      if (name.size() >= 1)
                        str += "name:" + name + ",";
                    }

                    str[str.size() - 1] = '\n';
                  }
                }
              }
            }

            if (sngInfo.arrangements[j].fingerprints1.size() > 0)
            {
              str += "\n[Level" + to_string(j) + "Sustains]\n";
              for (const Sng::Info::Arrangement::Fingerprint& fingerprint : sngInfo.arrangements[j].fingerprints1)
              {
                str += TimeNS_To_String(timeNS_From_Seconds(fingerprint.startTime)) + "-" + TimeNS_To_String(timeNS_From_Seconds(fingerprint.endTime)) + "=";

                const Sng::Info::Chord& chordTemplate = sngInfo.chord[fingerprint.chordId];

                for (i32 i = 5; i >= 0; --i)
                {
                  if (chordTemplate.fingers[i] >= 0)
                  {
                    const i8 shredString = (isBass ? 3 : 5) - i;
                    ASSERT(shredString >= 0);
                    str += to_string(shredString) + "finger:" + to_string(chordTemplate.fingers[i]) + ",";
                  }
                }

                if (chordTemplate.name[0] != '\0')
                {
                  const std::string name = getDisplayChordName(chordTemplate.name);
                  if (name.size() >= 1)
                    str += "name:" + name;
                }

                if (str[str.size() - 1] == ',')
                  str[str.size() - 1] = '\n';
                else
                  str += '\n';
              }
            }

            if (sngInfo.arrangements[j].fingerprints2.size() > 0)
            {
              str += "\n[Level" + to_string(j) + "Arpeggios]\n";
              for (const Sng::Info::Arrangement::Fingerprint& fingerprint : sngInfo.arrangements[j].fingerprints2)
              {
                str += TimeNS_To_String(timeNS_From_Seconds(fingerprint.startTime)) + "-" + TimeNS_To_String(timeNS_From_Seconds(fingerprint.endTime)) + "=";

                const Sng::Info::Chord& chordTemplate = sngInfo.chord[fingerprint.chordId];

                for (i32 i = 5; i >= 0; --i)
                {
                  if (chordTemplate.fingers[i] >= 0)
                  {
                    const i8 shredString = (isBass ? 3 : 5) - i;
                    ASSERT(shredString >= 0);
                    str += to_string(shredString) + "finger:" + to_string(chordTemplate.fingers[i]) + ",";
                  }
                }

                if (chordTemplate.name[0] != '\0')
                {
                  const std::string name = getDisplayChordName(chordTemplate.name);
                  if (name.size() >= 1)
                    str += "name:" + name;
                }

                if (str[str.size() - 1] == ',')
                  str[str.size() - 1] = '\n';
                else
                  str += '\n';
              }
            }
          }

          if (sngInfo.tone.size() > 0 || extraToneSwitches.size() > 0)
          { // handle Tones
            str += "\n[Tones]\n";
            for (i32 i = 0; i < sngInfo.tone.size(); ++i)
            {
              const Sng::Info::Tone& tone = sngInfo.tone[i];
              str += TimeNS_To_String(timeNS_From_Seconds(tone.time)) + "=" + to_string(tone.toneId) + '\n';
            }
            for (const Ini::KeyValue& keyValue : extraToneSwitches)
            {
              str += keyValue.key + "=" + keyValue.value + '\n';
            }
          }


          const std::string arrangementNameIni = arrangementName + ".ini";
          mz_zip_writer_add_mem(&zip_archive, reinterpret_cast<const char*>(arrangementNameIni.c_str()), str.data(), str.size(), 0);
        }
      }

      { // add additional files to .shred. This is for the Neural Amp Modeller 
        const std::string psarcFilename = File::filename(fileToProcess.c_str());
        const std::string parentPath_ = parentPath(fileToProcess.c_str());
        const std::vector<std::string> filesInDir = File::filesInDirectory(parentPath_.c_str());
        const std::string& startPath = fileToProcess.substr(0, fileToProcess.size() - 5);
        for (const std::string& file : filesInDir)
        {
          if (!file.starts_with(startPath))
            continue;
          if (file.ends_with(".psarc"))
            continue;
          if (file.ends_with(".shred"))
            continue;
          if (file.ends_with("song.ini"))
            continue;
          if (file.ends_with("album.png"))
            continue;
          if (file.ends_with("lyrics.ini"))
            continue;
          if (file.ends_with("song.ogg"))
            continue;
          if (file.ends_with("preview.ogg"))
            continue;

          bool isArranement = false;
          for (const Psarc::Info::TOCEntry& tocEntry : psarcInfo.tocEntries)
          {
            if (!tocEntry.name.ends_with(".sng"))
              continue;

            const std::string arrangementIni = arranagementName(tocEntry.name) + ".ini";
            if (file.ends_with(arrangementIni))
            {
              isArranement = true;
              break;
            }
          }
          if (isArranement)
            continue;

          const std::string filename = file.substr(fileToProcess.size() - 5);
          const std::vector<u8> fileData = File::read(file.c_str());
          mz_zip_writer_add_mem(&zip_archive, reinterpret_cast<const char*>(filename.c_str()), fileData.data(), fileData.size(), 0);
        }
      }

      {
        for (const Arrangement::Info& arrangementInfo : psarcArrangementInfos)
        {
          if (arrangementInfo.arrangementName == "Vocals")
            continue;

          // skip if arrangement is in blacklist
          if (std::find(backlistedArrangements.cbegin(), backlistedArrangements.cend(), arrangementInfo.arrangementName) != backlistedArrangements.cend())
            continue;

          if (!arrangementMap.contains(arrangementInfo.arrangementName))
          {
            Arrangement_& arrangement = arrangementMap[arrangementInfo.arrangementName];
            arrangement.bassPick = arrangementInfo.bassPick;
            arrangement.capoFret = arrangementInfo.capoFret;
            arrangement.centOffset = arrangementInfo.centOffset;

            for (i8 i = 0; i < 8; ++i)
              arrangementMap[arrangementInfo.arrangementName].tuning.string[i] = arrangementInfo.tuning.string[i];

            {

              for (; arrangement.stringCount < 8; ++arrangement.stringCount)
              {
                if (arrangement.tuning.string[arrangement.stringCount] == -128)
                  break;
              }

              { // some psarc bass arrangements contain notes on 5th and 6th bass strings. If so, we should overwrite the string count.
                const i8 playedMaxStringCount = lowestPlayedStringIndecies.contains(arrangementInfo.arrangementName) ? lowestPlayedStringIndecies[arrangementInfo.arrangementName] + 1 : arrangement.stringCount;
                ASSERT(playedMaxStringCount <= 8);

                if (playedMaxStringCount > arrangement.stringCount)
                {
                  bool warnMissingTuningOffset = false;
                  for (i8 i = arrangement.stringCount; i < playedMaxStringCount; ++i)
                  {
                    // The psarc might not contain the actual tuning offset of the note. In this case try the same tuning offset as the lowest known string.
                    if (arrangement.tuning.string[i] == -128)
                    {
                      arrangement.tuning.string[i] = arrangementInfo.tuning.string[arrangement.stringCount - 1];
                      warnMissingTuningOffset = true;
                    }
                    else
                    {
                      arrangement.tuning.string[i] = arrangementInfo.tuning.string[i];
                    }
                  }

                  if (warnMissingTuningOffset)
                    fprintf(stderr, "%s%sWarning:%s %s%s:%s Notes outside of expected string count. Increased string count. Tuning offset might be wrong!\n", color(BOLD), color(YELLOW), color(RESET), color(GREEN), arrangementInfo.arrangementName.c_str(), color(RESET));
                  else
                    fprintf(stderr, "%s%sWarning:%s %s%s:%s Notes outside of expected string count. Increased string count.\n", color(BOLD), color(YELLOW), color(RESET), color(GREEN), arrangementInfo.arrangementName.c_str(), color(RESET));

                  arrangement.stringCount = playedMaxStringCount;
                }
              }

              std::string tuningStr;
              for (i8 i = 0; i < arrangement.stringCount; ++i)
              {
                ASSERT(arrangement.tuning.string[i] != -128);
                tuningStr += to_string(arrangement.tuning.string[i]) + ',';
              }
              ASSERT(tuningStr[tuningStr.size() - 1] == ',');
              tuningStr.resize(tuningStr.size() - 1);
            }
          }
        }

        const std::string serializedSongIni = serializeSongIni(artistName, songName, albumName, songYear, songLength, spotifyTrackId, track, delayBegin, delayEnd, arrangementMap);

        mz_zip_writer_add_mem(&zip_archive, "song.ini", serializedSongIni.data(), serializedSongIni.size(), 0);
      }

      mz_zip_writer_finalize_archive(&zip_archive);
      mz_zip_writer_end(&zip_archive);
    }

    std::string songImage;
    if (File::exists((outputAlbumDirPath + "album.png").c_str()))
      songImage = relOutputAlbumDirPath + "album.png";

    std::string arrangementString;
    std::string maxTuningName;
    {
      Tuning sqlMaxTuning{ 0, 0, 0, 0, 0, 0, 0 };
      i32 absMaxTuning = -9999;
      for (const auto& [arrangementName, arrangement] : arrangementMap)
      {
        arrangementString += fixSqlQuote(arrangementName) + '|';

        i32 absMaxTuning_ = 0;
        for (i8 i = 0; i < arrangement.stringCount; ++i)
          absMaxTuning_ += abs(arrangement.tuning.string[i]);

        if (absMaxTuning_ > absMaxTuning)
        {
          absMaxTuning = absMaxTuning_;
          sqlMaxTuning = arrangement.tuning;
        }
      }
      if (arrangementString.size() > 0)
      {
        ASSERT(arrangementString[arrangementString.size() - 1] == '|');
        arrangementString.resize(arrangementString.size() - 1);
      }
      maxTuningName = fixSqlQuote(Song::tuningName(sqlMaxTuning));
    }

    if (!noSql)
    {
      std::string psarcCID;
      std::string shredCID;
      std::string unpackCID;

      if (File::exists("cidv0.exe"))
      {
        psarcCID = exec_ipfsGetCID(("cidv0.exe \"" + fileToProcess + '\"').c_str()); // ipfs.exe add -q --only-hash
        shredCID = exec_ipfsGetCID(("cidv0.exe \"" + outputFilePath + '\"').c_str());
        //unpackCID = exec_ipfsGetCID("cid ");

        if (cidBasename)
        {
          basename = shredCID;
          const std::filesystem::path oldPath(outputFilePath);
          const std::filesystem::path newPath = oldPath.parent_path() / (basename + ".shred");
          std::filesystem::rename(oldPath, newPath);
        }
      }

      sql += "/*" + to_string(i) + "*/" + "('" + fixSqlQuote(songName) + "','" + fixSqlQuote(artistName) + "','" + fixSqlQuote(albumName) + "','" + arrangementString + "','" + maxTuningName + "'," + to_string(songYear) + "," + to_string(songLength) + "," + to_string(track) + ",'','" + user + "',0,'" + fixSqlQuote(relOutputSongDirPath) + "','" + fixSqlQuote(basename) + "','" + fixSqlQuote(songImage) + "','" + shredCID + "','" + unpackCID + "','" + psarcCID + "','" + spotifyTrackId + "'," + permission + "),\n";
    }
  }

  if (!noSql)
  {
    std::string sqlOutputFile = outputDir + "../sql/";
    if (!File::directoryExists(sqlOutputFile.c_str()))
      File::createDirectories(sqlOutputFile.c_str());

    const std::string sqlBeginFile = sqlOutputFile + "_begin.sql";
    if (!File::exists(sqlBeginFile.c_str()))
    {
      const std::string sqlBegin = "DELETE FROM shreddb;\nINSERT INTO shreddb (song,artist,album,arrangements,tuning,year,length,track,genre,user,date,path,basename,image,shredCID,unpackCID,psarcCID,spotifyTrackId,permission) VALUES\n";
      File::write(sqlBeginFile.c_str(), sqlBegin.data(), sqlBegin.size());
    }

    sqlOutputFile += "part_" + user + '_' + to_string(begin) + ".sql";

    File::write(sqlOutputFile.c_str(), sql.data(), sql.size());
  }

  return 0;
}
