using ICSharpCode.SharpZipLib.Zip.Compression;
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;

namespace PbpCreator
{
  public class PbpManager
  {
    #region Private fields

    // Current folder
    private static readonly string _ApplicationPath = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);

    #endregion

    #region Private structures

    class WorkerData
    {
      public byte[] InputData;
      public byte[] OutputData;
    }

    #endregion

    #region Private structures

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    struct SFOHeader
    {
      public uint signature;
      public uint version;
      public uint fields_table_offs;
      public uint values_table_offs;
      public int nitems;
    };

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    struct SFODir
    {
      public ushort field_offs;
      public byte unk;
      public byte type; // 0x2 -> string, 0x4 -> number
      public uint length;
      public uint size;
      public ushort val_offs;
      public ushort unk4;
    };

    #endregion

    #region Private methods

    private void WriteInteger(Stream output, uint value, int count)
    {
      for (; --count >= 0;)
      {
        output.WriteByte((byte)value);
        output.WriteByte((byte)(value >> 8));
        output.WriteByte((byte)(value >> 16));
        output.WriteByte((byte)(value >> 24));
      }
    }

    private void WriteByte(Stream output, byte value, int count)
    {
      for (; --count >= 0;)
        output.WriteByte(value);
    }

    private void WriteRandom(Stream output, int count)
    {
      Random r = new Random((int)new DateTime().Ticks);
      for (count *= 4; --count >= 0;)
        output.WriteByte((byte)r.Next());
    }

    private void WriteASCII(Stream output, string value)
    {
      foreach (char c in value)
        output.WriteByte((byte)c);
    }

    private static unsafe long IndexOf(byte[] haystack, byte[] needle, long startOffset = 0)
    {
      fixed (byte* h = haystack) fixed (byte* n = needle)
      {
        for (byte* hNext = h + startOffset, hEnd = h + haystack.LongLength + 1 - needle.LongLength, nEnd = n + needle.LongLength; hNext < hEnd; hNext++)
          for (byte* hInc = hNext, nInc = n; *nInc == *hInc; hInc++)
            if (++nInc == nEnd)
              return hNext - h;
        return -1;
      }
    }

    private string ToDiscID(string source)
    {
      int p = source.IndexOf(';');
      if (p >= 0) source = source.Remove(p);
      p = source.LastIndexOf('\\');
      if (p >= 0) source = source.Remove(0, p);
      source = source.Replace('\\', '.').Replace('-', '.').Replace(';', '.').Replace('_', '.').Replace(".", "").ToUpperInvariant();
      // Special case of 5Letters+5Digits (Dragon quest VII US)
      if (source.Length > 9) if (char.IsLetter(source[4])) source = source.Remove(4, 1);
      // Remove trail trash
      if (source.Length > 9) source = source.Remove(9);
      if (source.Length != 9) return null;
      // Control
      for (int i = 3; --i >= 0;) if (!char.IsLetter(source[i])) return null;
      if (!char.IsLetterOrDigit(source[3])) return null; // Some rare games have 3 letter code and more digits
      for (int i = 5; --i >= 0;) if (!char.IsDigit(source[i + 4])) return null;
      return source;
    }

    private string GetDiscIDFromBin(string filename, IReporting reporter)
    {
      byte[] _Boot = new byte[] { (byte)'c', (byte)'d', (byte)'r', (byte)'o', (byte)'m', (byte)':' };
      try
      {
        using (FileStream fs = new FileStream(filename, FileMode.Open, FileAccess.Read))
        {
          byte[] buffer = new byte[_Boot.Length + (10 << 20)];
          do
          {
            int count = fs.Read(buffer, 0, buffer.Length);
            if (count <= _Boot.Length)
            {
              reporter.Log(ConsoleColor.Red, "CANNOT LOCATE DISC_ID!");
              return "UNKN00001";
            }
            int p = -1;
            do
            {
              p = (int)IndexOf(buffer, _Boot, p + 1);
              if (p >= 0)
              {
                string result = ToDiscID(Encoding.ASCII.GetString(buffer, p + _Boot.Length, 40));
                if (!string.IsNullOrEmpty(result)) return result;
              }
            }
            while (p >= 0);

            fs.Seek(-_Boot.Length, SeekOrigin.Current);
          }
          while (true);
        }
      }
      catch(Exception)
      {
        return "UNKN00000";
      }
    }

    private byte[] _BaseHeader = null;
    private byte[] _DataPsp = null;
    private byte[] _Icon0 = null;
    private byte[] _Pic0 = null;
    private byte[] _Pic1 = null;

    private byte[] GetBaseHeader()
    {
      if (_BaseHeader != null) return _BaseHeader;
      string baseFile = Path.Combine(_ApplicationPath, "Resources", "BASE.PBP");
      return _BaseHeader = File.ReadAllBytes(baseFile);
    }

    private byte[] GetDataPsp()
    {
      if (_DataPsp != null) return _DataPsp;
      string baseFile = Path.Combine(_ApplicationPath, "Resources", "DATA.PSP");
      return _DataPsp = File.ReadAllBytes(baseFile);
    }

    private byte[] GetIcon0()
    {
      if (_Icon0 != null) return _Icon0;
      string baseFile = Path.Combine(_ApplicationPath, "Resources", "ICON0.PNG");
      return _Icon0 = File.ReadAllBytes(baseFile);
    }

    private byte[] GetPic0()
    {
      if (_Pic0 != null) return _Pic0;
      string baseFile = Path.Combine(_ApplicationPath, "Resources", "PIC0.PNG");
      return _Pic0 = File.ReadAllBytes(baseFile);
    }

    private byte[] GetPic1()
    {
      if (_Pic1 != null) return _Pic1;
      string baseFile = Path.Combine(_ApplicationPath, "Resources", "PIC1.PNG");
      return _Pic1 = File.ReadAllBytes(baseFile);
    }

    private int AlignUp(int v, int a)
    {
      return ((v + (a - 1)) / a) * a;
    }

    private long AlignUp(long v, long a)
    {
      return ((v + (a - 1)) / a) * a;
    }

    private unsafe void SetSFOTitle(byte[] buffer, int start, string title)
    {
      fixed (byte* sfo = &buffer[start])
      {
        SFOHeader* header = (SFOHeader*)sfo;
        SFODir* entries = (SFODir*)(sfo + 0x14);
        int i;

        for (i = 0; i < header->nitems; i++)
        {
          byte* fieldName = sfo + header->fields_table_offs + entries[i].field_offs;
          if (fieldName[0] == 'T' && fieldName[1] == 'I' && fieldName[2] == 'T' && fieldName[3] == 'L' && fieldName[4] == 'E')
          {
            byte* fieldValue = sfo + header->values_table_offs + entries[i].val_offs;
            int fieldSize = (int)entries[i].size;
            for (int j = fieldSize; --j >= 0;) fieldValue[j] = 0;
            for (int j = Math.Min(fieldSize - 1, title.Length) ; --j >= 0; ) fieldValue[j] = (byte)title[j];
            entries[i].length = (uint)Math.Min(fieldSize, title.Length + 1);
          }
        }
      }
    }

    private unsafe void SetSFOCode(byte[] buffer, int start, string discid)
    {
      fixed (byte* sfo = &buffer[start])
      {
        SFOHeader* header = (SFOHeader*)sfo;
        SFODir* entries = (SFODir*)(sfo + 0x14);
        int i;

        for (i = 0; i < header->nitems; i++)
        {
          byte* fieldName = sfo + header->fields_table_offs + entries[i].field_offs;
          if (fieldName[0] == 'D' && fieldName[1] == 'I' && fieldName[2] == 'S' && fieldName[3] == 'C' && fieldName[4] == '_' && fieldName[5] == 'I' && fieldName[6] == 'D')
          {
            byte* fieldValue = sfo + header->values_table_offs + entries[i].val_offs;
            int fieldSize = (int)entries[i].size;
            for (int j = fieldSize; --j >= 0;) fieldValue[j] = 0;
            for (int j = Math.Min(fieldSize - 1, discid.Length); --j >= 0;) fieldValue[j] = (byte)discid[j];
            entries[i].length = (uint)Math.Min(fieldSize, discid.Length + 1);
          }
        }
      }
    }
    #endregion

    #region Public methods

    public bool ToPbp(Configuration conf, IReporting report, bool verbose)
    {
      if (conf == null) { report?.Log(ConsoleColor.Red, "Invalid Configuration object"); return false; }
      if (conf.Sources == null) { report?.Log(ConsoleColor.Red, "Invalid source files"); return false; }
      if (conf.Sources.Count == 0) { report?.Log(ConsoleColor.Red, "No source files"); return false; }
      if (conf.DestinationFile == null) { report?.Log(ConsoleColor.Red, "Invalid destination files"); return false; }
      if (conf.GameTitle == null) { report?.Log(ConsoleColor.Red, "Invalid game title"); return false; }

      Stopwatch watch = new Stopwatch();
      watch.Start();

      report?.Log(ConsoleColor.Yellow, "\r\nConversion of {0} CD into a single PBP file started", conf.Sources.Count);
      int cd = 0;
      foreach (string s in conf.Sources)
      {
        report?.Log(ConsoleColor.Gray, "  CD {0} : {1}", cd, conf.Sources[cd]);
        cd++;
      }
      report?.Log(ConsoleColor.White, "To file: {0}", conf.DestinationFile);

      // Get PBP base header
      report?.Log(ConsoleColor.Gray, "Loading required files.");
      byte[] baseHeader = GetBaseHeader();
      byte[] dataPsp = GetDataPsp();
      byte[] icon0 = GetIcon0();
      byte[] pic0 = GetPic0();
      byte[] pic1 = GetPic1();

      using (FileStream output = File.Create(conf.DestinationFile))
        unsafe
        {
          // Create output MemoryStream
          /*const long maxSize = (700 * 5) << 20;
          IntPtr memIntPtr = Marshal.AllocHGlobal((IntPtr)maxSize);
          byte* memBytePtr = (byte*)memIntPtr.ToPointer();
          UnmanagedMemoryStream output = new UnmanagedMemoryStream(memBytePtr, maxSize);*/

          try
          {
            report?.Log(ConsoleColor.Gray, "Scanning file for main DiscID...");
            string DiscID = GetDiscIDFromBin(conf.Sources[0], report);
            if (verbose) report?.Log(ConsoleColor.Gray, "DiscID: {0}", DiscID);
            int sfoStart = 0;
            int sfoSize = 0;
            int curoffs = 40;

            // Build header
            if (verbose) report?.Log(ConsoleColor.Gray, "Writing header.");
            fixed (byte* baseHeaderByte = &baseHeader[0])
            {
              int* baseHeaderInt = (int*)baseHeaderByte;
              if (baseHeaderInt[0] != 0x50425000)
              {
                report?.Log(ConsoleColor.Red, "Bad header file");
                return false;
              }

              sfoStart = baseHeaderInt[2];
              sfoSize = baseHeaderInt[3] - baseHeaderInt[2];
            }

            // create header
            int[] Header = new int[10];
            Header[0] = 0x50425000;
            Header[1] = 0x10000;
            Header[2] = curoffs; curoffs += sfoSize;
            Header[3] = curoffs; curoffs += icon0.Length;
            Header[4] = curoffs; curoffs += 0 /*icon1_size*/;
            Header[5] = curoffs; curoffs += pic0.Length;
            Header[6] = curoffs; curoffs += pic1.Length;
            Header[7] = curoffs; curoffs += 0 /*snd_size*/;
            Header[8] = curoffs; curoffs += dataPsp.Length; ;
            Header[9] = (curoffs = AlignUp(curoffs, 0x10000));

            // Write header
            foreach (int value in Header)
              WriteInteger(output, (uint)value, 1);

            if (verbose) report?.Log(ConsoleColor.Gray, "Writing SFO.");
            SetSFOTitle(baseHeader, sfoStart, conf.GameTitle);
            SetSFOCode(baseHeader, sfoStart, DiscID);
            // Write SFO
            output.Write(baseHeader, sfoStart, sfoSize);

            // Write icon/img/snd
            output.Write(icon0, 0, icon0.Length);
            output.Write(pic0, 0, pic0.Length);
            output.Write(pic1, 0, pic1.Length);

            // Write DATA.PSP
            if (verbose) report?.Log(ConsoleColor.Gray, "Writing DATA.PSP.");
            output.Write(dataPsp, 0, dataPsp.Length);

            // Alignment
            WriteByte(output, 0, curoffs - (int)output.Position);

            long mOffset = 0;
            long p1Offset = 0;
            long p2Offset = 0;
            if (conf.Sources.Count > 1)
            {
              // Write PSTITLE header
              if (verbose) report?.Log(ConsoleColor.Gray, "Writing PS Title.");

              //long offset = output.Position;
              WriteASCII(output, "PSTITLEIMG000000");
              p1Offset = output.Position;
              WriteInteger(output, 0, 2);
              WriteInteger(output, 0x2CC9C5BC, 1);
              WriteInteger(output, 0x33B5A90F, 1);
              WriteInteger(output, 0x06F6B4B3, 1);
              WriteInteger(output, 0xB25945BA, 1);
              WriteInteger(output, 0, 0x76);
              mOffset = output.Position;

              WriteInteger(output, 0, 5); // Placeholder for iso offsets
              WriteRandom(output, 12);
              WriteInteger(output, 0, 8);
              WriteByte(output, (byte)'_', 1);
              WriteASCII(output, DiscID.Substring(0, 4));
              WriteByte(output, (byte)'_', 1);
              WriteASCII(output, DiscID.Substring(4, 5));
              WriteByte(output, 0, 0x15);
              p2Offset = output.Position;
              WriteInteger(output, 0, 2);
              output.Write(Data3.Data, 0, Data3.Data.Length);
              WriteASCII(output, conf.GameTitle);
              WriteByte(output, 0, 0x80 - conf.GameTitle.Length);
              WriteInteger(output, 7, 1);
              WriteInteger(output, 0, 0x1C);
            }

            // Get total input size
            long totalSize = 0;
            foreach (string filename in conf.Sources)
              totalSize += new FileInfo(filename).Length;
            if (verbose) report?.Log(ConsoleColor.Gray, "Total source size: {0}mb", totalSize >> 20);

            // Run through ISO
            int isoNumber = -1;
            foreach (string filename in conf.Sources)
            {
              isoNumber++;
              if (verbose) report?.Log(ConsoleColor.Gray, "Starting CD #{0} :", isoNumber);

              if (isoNumber != 0)
              {
                report?.Log(ConsoleColor.Gray, "  Scanning file for DiscID...");
                DiscID = GetDiscIDFromBin(filename, report);
              }
              if (verbose) report?.Log(ConsoleColor.Gray, "  DiscID: {0}", DiscID);

              long realIsoSize = new FileInfo(filename).Length;
              long isoSize = AlignUp(realIsoSize, 0x9300);
              if ((output.Position & 0x7FFF) != 0)
                WriteByte(output, 0, 0x8000 - (int)(output.Position & 0x7FFF));

              long isoPosition = output.Position - curoffs;
              if (conf.Sources.Count > 1)
              {
                // Write ISO position
                if (verbose) report?.Log(ConsoleColor.Gray, "  Writing ISO offset: {0:X}", isoPosition);
                output.Position = mOffset + sizeof(int) * isoNumber;
                WriteInteger(output, (uint)isoPosition, 1);
                output.Seek(0, SeekOrigin.End);
              }

              if (verbose) report?.Log(ConsoleColor.Gray, "  Writing ISO header.");
              WriteASCII(output, "PSISOIMG0000");
              if (conf.Sources.Count == 1)
                p1Offset = output.Position;
              WriteInteger(output, (uint)(isoSize + 0x100000), 1);
              WriteInteger(output, 0, 0xFC);
              for (int i = 4; --i >= 0;) Data1.Data[i + 1] = (byte)DiscID[i];
              for (int i = 5; --i >= 0;) Data1.Data[i + 6] = (byte)DiscID[i + 4];
              output.Write(Data1.Data, 0, Data1.Data.Length);
              if (conf.Sources.Count == 1)
              {
                p2Offset = output.Position;
                WriteInteger(output, (uint)(isoSize + 0x100000 + 0x2d31), 1);
              }
              else
                WriteInteger(output, 0, 1);
              string CDTitle = Path.GetFileNameWithoutExtension(filename);
              for (int i = CDTitle.Length; --i >= 0;) Data2.Data[i + 8] = (byte)CDTitle[i];
              output.Write(Data2.Data, 0, Data2.Data.Length);

              int chunkCount = (int)(isoSize / 0x9300);
              long indexOffset = output.Position; // Placeholder for chunk offsets
              if (verbose) report?.Log(ConsoleColor.Gray, "  Writing Index placeholders.");
              for (int i = chunkCount; --i >= 0;)
                WriteInteger(output, 0, 8); // Write dummy indexes
              WriteByte(output, 0, (int)((isoPosition + curoffs + 0x100000) - output.Position));

              int[] chunkIndexes = new int[chunkCount];
              int[] chunkLength = new int[chunkCount];

              report?.Log(ConsoleColor.Gray, "  Deflating...");
              ThreadPool.SetMaxThreads(Environment.ProcessorCount * 2, Environment.ProcessorCount * 2);
              ManualResetEvent Locker = new ManualResetEvent(false);
              WorkerData[] AllWorkers = new WorkerData[chunkCount];
              int Counter = chunkCount;
              using (FileStream file = File.OpenRead(filename))
              {
                for (int chunk = 0; file.Position < file.Length; chunk++)
                {
                  int sizeRead = Math.Min((int)(file.Length - file.Position), 0x9300);
                  byte[] uncompressed = new byte[0x9300];
                  file.Read(uncompressed, 0, sizeRead);

                  WorkerData workerData = AllWorkers[chunk] = new WorkerData()
                  {
                    InputData = uncompressed,
                    OutputData = null,
                  };

                  ThreadPool.QueueUserWorkItem(new WaitCallback((o) =>
                  {
                    WorkerData ws = o as WorkerData;
                    ws.OutputData = new byte[ws.InputData.Length * 4];
                    Deflater zdeflater = new Deflater(Deflater.BEST_COMPRESSION, true);
                    zdeflater.SetInput(ws.InputData, 0, ws.InputData.Length);
                    zdeflater.Finish();
                    int sizeDeflated = zdeflater.Deflate(ws.OutputData);
                    zdeflater.Reset();
                    if (sizeDeflated >= ws.InputData.Length)
                      ws.OutputData = ws.InputData;
                    else
                      Array.Resize(ref ws.OutputData, sizeDeflated);
                    ws.InputData = null;
                    if (Interlocked.Decrement(ref Counter) == 0) Locker.Set();
                    report?.Progress(isoNumber, chunkCount - Counter, chunkCount);
                  }), workerData);
                }
              }
              Locker.WaitOne();
              report?.Log(ConsoleColor.Gray, "");

              // Write back chunk
              report?.Log(ConsoleColor.Gray, "  Writting deflated data...");
              for (int i = 0; i < chunkCount; i++)
              {
                output.Write(AllWorkers[i].OutputData, 0, AllWorkers[i].OutputData.Length);
                report?.Progress(isoNumber, i + 1, chunkCount);
              }
              report?.Log(ConsoleColor.Gray, "");

              // Write back indexes
              if (verbose) report?.Log(ConsoleColor.Gray, "  Updating indexes.");
              output.Position = indexOffset;
              int chunkOffset = 0;
              for (int i = 0; i < chunkCount; i++)
              {
                int length = AllWorkers[i].OutputData.Length;
                WriteInteger(output, (uint)chunkOffset, 1);
                WriteInteger(output, (uint)length, 1);
                WriteInteger(output, 0, 6); // Write spaces
                chunkOffset += length;
              }
              output.Seek(0, SeekOrigin.End);
            }


            // Get End position
            if (verbose) report?.Log(ConsoleColor.Gray, "  Writing end data.");
            long endPosition = AlignUp(output.Position, 0x10);
            if (endPosition != output.Position)
              WriteByte(output, 0, (int)(endPosition - output.Position));
            endPosition -= curoffs;

            if (verbose) report?.Log(ConsoleColor.Gray, "  Writing special data.");
            fixed (byte* baseHeaderByte = &baseHeader[0])
            {
              int* baseHeaderInt = (int*)baseHeaderByte;
              baseHeaderInt = (int*)&baseHeaderByte[baseHeaderInt[9] + 12];
              int p = baseHeaderInt[0] + 0x50000;

              if (baseHeader[p] != 'S' || baseHeader[p + 1] != 'T' || baseHeader[p + 2] != 'A' || baseHeader[p + 3] != 'R' || baseHeader[p + 4] != 'T' || baseHeader[p + 5] != 'D' || baseHeader[p + 6] != 'A' || baseHeader[p + 7] != 'T')
                throw new Exception("Error analying BASE.");

              baseHeaderInt = (int*)&baseHeaderByte[p + 16];
              int size1 = baseHeaderInt[0];
              int size2 = baseHeaderInt[1];
              output.Write(baseHeader, p, size1 + size2);
              output.Write(baseHeader, p + size1 + size2, baseHeader.Length - (p + size1 + size2)); // ?! 
            }

            output.Position = p1Offset;
            WriteInteger(output, (uint)endPosition, 1);
            output.Position = p2Offset;
            WriteInteger(output, (uint)endPosition + 0x2d31, 1);

            // Save output file
            if (verbose) report?.Log(ConsoleColor.Gray, "Saved PBP file: {0}mb ({1}% of the original size)", output.Length >> 20, (output.Length * 100) / totalSize);
          }
          catch (Exception e)
          {
            report?.Log(ConsoleColor.Red, "Exception: {0}", e.Message);
            report?.Log(ConsoleColor.Red, "Stack trace: {0}", e.StackTrace);
            return false;
          }
        }

      watch.Stop();
      if (verbose) report?.Log(ConsoleColor.Gray, "Completed game in {0}", watch.Elapsed.ToString(@"mm\:ss"));

      return true;
    }

    #endregion
  }
}
