#ifdef __linux__
#  include <sys/stat.h>
#  include <sys/types.h>
#else
#  include <windows.h>
#  include <direct.h>
#endif

#include <stdio.h>
#include <stdlib.h>
#include <stdexcept>
#include <sstream>
#include <vector>
#include <fstream>
#include <iostream>

bool has_suffix(const std::string& str, const std::string& suffix)
{
  if (str.length() >= suffix.length())
    return str.compare(str.length() - suffix.length(), suffix.length(), suffix) == 0;
  else
    return false;
}

std::string get_cwd()
{
#ifdef __linux__
  return "./";
#else
  // Get path to executable:
  char szDllName[_MAX_PATH];
  char szDrive[_MAX_DRIVE];
  char szDir[_MAX_DIR];
  char szFilename[_MAX_FNAME];
  char szExt[_MAX_EXT];
  GetModuleFileName(0, szDllName, _MAX_PATH);
  _splitpath(szDllName, szDrive, szDir, szFilename, szExt);

  return std::string(szDrive) + std::string(szDir);
#endif
}

std::string tolowercase(const std::string& str)
{
  std::string out;
  for(std::string::const_iterator i = str.begin();  i != str.end(); ++i)
    out += tolower(*i);
  return out;
}

int create_dir(const std::string& path)
{
#ifdef __linux__
  return mkdir(path.c_str(), 0755);
#else
  return _mkdir(path.c_str());
#endif
}

struct FileEntry
{
  int offset;
  int filesize;

  // Those could be analog to:
  // FileCount:    923
  // NumberCount:  371
  // ByteCount:   4598
  
  // if filesize is != 0 unknown1 is 0
  //                  range  : count
  int unknown1;  // ~ 0-897  : 289 (like number1, reference to other file?)
  int unknown2;  // ~ 0-85   : 78  (always ~100 range)
  int unknown3;  // ~ 0-4585 : 427 (like number3)
  // Maybe one of those marks the directory?
};

struct TLJPak
{
  std::string filename;

  char magic[12];
  int  file_count;   // file entries
  int  number_count; // number of entries in the second and last section
  int  byte_count;   // a count that is similar to the last number in numbers
  
  std::vector<FileEntry> files;   // array of FileEntries with length file_count
  std::vector<char>      bytes;   // array of bytes with length byte_count, values are never larger then 43
  std::vector<int>       numbers; // array of int32 with length number_count

  TLJPak(const std::string& filename_)
    : filename(filename_)
  {
    std::ifstream in(filename.c_str(), std::ios::binary);
    if (!in)
      {
        throw std::runtime_error("Error: Couldn't open " + filename);
      }

    in.read(reinterpret_cast<char*>(&magic), 12);
    in.read(reinterpret_cast<char*>(&file_count), sizeof(int));
    in.read(reinterpret_cast<char*>(&number_count), sizeof(int));
    in.read(reinterpret_cast<char*>(&byte_count), sizeof(int));

    for(int i = 0; i < file_count; ++i)
      {
        FileEntry entry;
        int vals[5];

        in.read(reinterpret_cast<char*>(vals), sizeof(int)*5); 

        entry.offset    = vals[0];
        entry.filesize  = vals[1];
        entry.unknown1  = vals[2];
        entry.unknown2  = vals[3];
        entry.unknown3  = vals[4];

        files.push_back(entry);
      }

    for(int i = 0; i < byte_count; ++i)
      {
        char character;
        in.read(reinterpret_cast<char*>(&character), sizeof(char));      
        bytes.push_back(character);
      }

    for(int i = 0; i < number_count; ++i)
      {
        int number;
        in.read(reinterpret_cast<char*>(&number), sizeof(int));
        numbers.push_back(number);
      }   
  }

  void print_bytes()
  {
    for(int i = 0; i < int(bytes.size()); ++i)
      std::cout << int(bytes[i]) << std::endl;
  }

  void print_numbers()
  {
    for(int i = 0; i < int(numbers.size()); ++i)
      std::cout << numbers[i] << std::endl;
  }

  void print_file_table()
  {
    if (0)
      {
        std::cout << "  Nr.     offset   filesize   unknown1  unknown2 unknown3" << std::endl;
        std::cout << "================================================================" << std::endl;
      }

    for(int i = 0; i < int(files.size()); ++i)
      printf("%4d) %10d %10d %10d %10d %10d\n", 
             i,
             files[i].offset, 
             files[i].filesize,
             files[i].unknown1,
             files[i].unknown2,
             files[i].unknown3);
  }

  void print_info()
  {
    //std::cout << "Filename: " << filename << std::endl;
    //std::cout << "Filesize: " << st.st_size/1024/1024 << "mb (" << st.st_size << " bytes)" << std::endl;
    if (0)
      {
        std::cout << "Magic:         ";
        std::cout.write(magic, 12);
        std::cout << std::endl;

        std::cout << "File_Count:    " << file_count << std::endl;
        std::cout << "Number_Count:  " << number_count << std::endl;
        std::cout << "Bytes Count:   " << byte_count << std::endl;


        int sum = 0;
        for(int i = 0; i < int(files.size()); ++i)
          if (files[i].filesize != 0)
            sum += 1;
        std::cout << "Real Files:    " << sum << " (files with non-null filesize)" << std::endl;

        std::cout << std::endl;

        std::cout << "relative: " << numbers.back() << " < " << byte_count << std::flush;
        if (numbers.back() >= byte_count) std::cout << "!!!WRONG!!!" << std::endl; 
        else std::cout << std::endl;
    
      }
    //print_bytes();
    print_file_table();
  }
};

void extract(TLJPak& pak, std::istream& in, const std::string& outpath)
{
  std::string path = outpath;
  std::string prefix = get_cwd() + "dreamfall-extractor/";

  create_dir(prefix);
  create_dir(prefix + path);
  create_dir(prefix + path + "/fonts");
  create_dir(prefix + path + "/textures");
  create_dir(prefix + path + "/wav");
  create_dir(prefix + path + "/mp3");
  create_dir(prefix + path + "/dat");
  create_dir(prefix + path + "/shader");
  create_dir(prefix + path + "/misc");
  create_dir(prefix + path + "/scripts");

  for(int i = 0; i < int(pak.files.size()); ++i)
    {
      if (pak.files[i].filesize > 0)
        {
          char filename[1024];

          char magic[8];
          in.seekg(pak.files[i].offset, std::ios::beg);
          in.read(magic, 8);

          if (strncmp(magic, "DDS", 3) == 0)
            snprintf(filename, 1024, "textures/%04d.dds", i);
          else if (strncmp(magic, "tljbone", 7) == 0)
            snprintf(filename, 1024, "misc/%04d.tljbone", i);
          else if (strncmp(magic, "ID3", 3) == 0)
            snprintf(filename, 1024, "mp3/%04d.mp3", i);
          else if (strncmp(magic, "vs", 2) == 0 || strncmp(magic, "xvs", 3) == 0)
            snprintf(filename, 1024, "shader/%04d.vs", i);
          else if (strncmp(magic, "ps", 2) == 0 || strncmp(magic, "xps", 3) == 0)
            snprintf(filename, 1024, "shader/%04d.ps", i);
          else if (strncmp(magic, "; ", 2) == 0 || strncmp(magic, "//", 2) == 0)
            snprintf(filename, 1024, "shader/%04d.us", i);
          else if (strncmp(magic, "STFU4", 4) == 0)
            snprintf(filename, 1024, "misc/%04d.stfu4", i);
          else if (strncmp(magic, "RIFF", 4) == 0)
            snprintf(filename, 1024, "wav/%04d.wav", i);
          else if (strncmp(magic, "shark3d", 7) == 0)
            snprintf(filename, 1024, "scripts/%04d.s3dsb", i);
          else if (((unsigned char)magic[0]) == 255 && ((unsigned char)magic[1]) == 0xFB)
            snprintf(filename, 1024, "mp3/%04d.mp3", i);
          //else if (int(magic[0]) == 0x07 && int(magic[1]) == 0x54)
          //  snprintf(filename, 1024, "%04d.directory", i);
          else if (strncmp(magic, "art/gui/", 8) == 0)
            snprintf(filename, 1024, "fonts/%04d.font", i);
          else
            snprintf(filename, 1024, "dat/%04d.dat", i);

          std::cout << prefix + path + "/" + filename << " " << pak.files[i].offset << " " << pak.files[i].filesize << std::endl;
          
          std::ofstream out((prefix + path + "/" + filename).c_str(), std::ios::binary);
          out.write(magic, 8);         
          for(int j = 0; j < pak.files[i].filesize-8; ++j)
            out.put(in.get());
          out.close();
        }
    }
}

std::string directory_name_from_pak(const std::string& name)
{
  std::string::size_type pos1 = name.find_last_of('\\');
  std::string::size_type pos2 = name.find_last_of('/');
  std::string::size_type dotpos = name.find_last_of('.');
  std::string::size_type namestart;

  if (pos1 == std::string::npos && pos2 == std::string::npos)
    namestart = 0;
  else if (pos1 == std::string::npos)
    namestart = pos2 + 1;
  else
    namestart = pos1 + 1;

  if (dotpos == std::string::npos || dotpos <= namestart || namestart > name.size())
    throw std::runtime_error("Error: Not a valid .pak pathname: " + name);

  return name.substr(namestart, dotpos - namestart);
}

void inject(const std::string& pak_filename, int offset, const std::string& file, const std::string& outfile)
{
  TLJPak pak(pak_filename);

  // write out the pak exactly as originally written
  std::ofstream out(outfile.c_str(), std::ios::binary);

  out.write(pak.magic, 12);
  out.write(reinterpret_cast<char*>(&pak.file_count),   sizeof(int));
  out.write(reinterpret_cast<char*>(&pak.number_count), sizeof(int));
  out.write(reinterpret_cast<char*>(&pak.byte_count),   sizeof(int));

  // write file index table
  for(int i = 0; i < pak.file_count; ++i)
    {
      out.write(reinterpret_cast<char*>(&pak.files[i].offset),   sizeof(int)); 
      out.write(reinterpret_cast<char*>(&pak.files[i].filesize), sizeof(int)); 
      out.write(reinterpret_cast<char*>(&pak.files[i].unknown1), sizeof(int)); 
      out.write(reinterpret_cast<char*>(&pak.files[i].unknown2), sizeof(int)); 
      out.write(reinterpret_cast<char*>(&pak.files[i].unknown3), sizeof(int)); 
    }

  // write byte block
  for(int i = 0; i < pak.byte_count; ++i)
    {
      out.write(reinterpret_cast<char*>(&pak.bytes[i]), sizeof(char));
    }

  // write number block
  for(int i = 0; i < pak.number_count; ++i)
    {
      out.write(reinterpret_cast<char*>(&pak.numbers[i]), sizeof(int));
    }   

  // write files
  {
    std::ifstream in(pak_filename.c_str(), std::ios::binary);
    
    // blocksize: 131072
    for(int i = 0; i < pak.file_count; ++i)
      {
        std::vector<char> buffer;

        if (pak.files[i].filesize > 0)
          {
            buffer.resize(pak.files[i].filesize);
            
            in.seekg(pak.files[i].offset, std::ios::beg);
            in.read(reinterpret_cast<char*>(&*buffer.begin()),   buffer.size());

            out.seekp(pak.files[i].offset, std::ios::beg);
            out.write(reinterpret_cast<char*>(&*buffer.begin()), buffer.size());
          }
      }

    { // fill in padding bytes at the end of file to reach a total
      // filesize of a multiple of 131072, not sure if these are
      // needed, but we do so to have exactly the same file structure
      // like in the original paks
      out.seekp(0, std::ios::end);
      int end = out.tellp();

      if ((end % 131072) != 0)
        {
          for(int i = 0; i < 131072 - (end % 131072); ++i)
            out.put(0xae);
        }
    }

    in.close();
  }

  out.close();
}

int main(int argc, char** argv)
{
  try 
    {
      if (argc == 1)
        {
          std::cout << "Dreamfall Extractor V0.1.1\n"
                    << "==========================\n"
                    << "\n"
                    << "This program allows you to extract the .pak files from\n"
                    << "Dreamfall, which are located in the 'Dreamfall/bin/res/'\n"
                    << "directory. The .pak files contain textures, wav and mp3\n"
                    << "files.\n"
                    << "\n"
                    << "To use this program simply drag&drop the .pak files you\n"
                    << "want to extract onto the dreamfall-extractor.exe, the\n"
                    << "{filename}.pak filewill then be extracted to a directory\n"
                    << "named dreamfall-extractor/{filename}/, located in the same\n"
                    << "directory where the dreamfall-extractor.exe is located\n"
                    << "\n"
                    << "For questions and comments mail Ingo Ruhnke <grumbel@gmx.de>\n"
                    << "or join IRC, server irc.rizon.net, channel #Ragnar\n"
                    << "\n"
                    << "Full source code is available on request.\n"
                    << std::endl;
        }
      else if (strcmp(argv[1], "--inject") == 0)
        {
          if (argc == 6)
            {
              inject(argv[2], atoi(argv[3]), argv[4], argv[5]);
            }
          else
            {
              std::cout << "Usage: extract --inject PAK OFFSET FILE OUTFILE" << std::endl;
            }
        }
      else
        {
          for(int i = 1; i < argc; ++i)
            {
              std::ifstream in(argv[i], std::ios::binary);
              if (!in)
                {
                  std::cerr << "Error: Couldn't open " << argv[i] << std::endl;
                }
              else
                {
                  TLJPak pak(argv[i]);
      
                  std::string outfile = directory_name_from_pak(tolowercase(argv[i]));
                  if (0)
                    {
                      std::cout << "Filename:      " << argv[i] << std::endl;
                      std::cout << "Outfile:       " << outfile << std::endl;
                    }
                  pak.print_info();
                  // std::cout << "-----------------------------------\n" << std::endl;      
                         
                  
                  extract(pak, in, outfile);

                  in.close();
                }
            }
        }
    } 
  catch(std::exception& err) 
    {
      std::cout << "Error: " << err.what() << std::endl;
    }

#ifndef __linux__
      std::cout << "\nPress Enter to continue..." << std::endl;
      getchar();
#endif
}

/* EOF */

