using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;

namespace ReScene
{
	struct RarBlock
	{
		public byte Type;
		public ushort Flags;
		public byte[] Data;
	}

	struct StoreBlock  // 0x6A
	{
		public string FileName;
		public ushort FileOffset;
		public uint FileLength;

		public StoreBlock(RarBlock block)
		{
			BinaryReader br = new BinaryReader(new MemoryStream(block.Data), Encoding.ASCII);

			// skip 7-byte block header
			br.BaseStream.Position += 7;

			// 4 bytes for file length
			FileLength = br.ReadUInt32();

			// 2 bytes for name length, then the name
			FileName = new string(br.ReadChars(br.ReadUInt16()));
			FileOffset = (ushort)br.BaseStream.Position;
		}
	}

	struct SrrBlock // 0x71
	{
		public string FileName;

		public SrrBlock(RarBlock block)
		{
			BinaryReader br = new BinaryReader(new MemoryStream(block.Data), Encoding.ASCII);

			// skip 7-byte block header
			br.BaseStream.Position += 7;

			// 2 bytes for name length, then the name
			FileName = new string(br.ReadChars(br.ReadUInt16()));
		}
	}

	struct FileBlock // 0x74
	{
		public byte CompressionMethod;
		public ulong PackedSize;
		public ulong UnpackedSize;
		public string FileName;

		public FileBlock(RarBlock block)
		{
			BinaryReader br = new BinaryReader(new MemoryStream(block.Data), Encoding.ASCII);

			// skip 7-byte block header
			br.BaseStream.Position += 7;

			// 4 bytes for packed size, 4 for unpacked
			PackedSize = br.ReadUInt32();
			UnpackedSize = br.ReadUInt32();

			// skip 1 byte for OS, 4 for crc, 4 for file date/time , and 1 for required RAR version
			br.BaseStream.Position += 10;

			// 1 byte for compression method, then 2 for filename length
			CompressionMethod = br.ReadByte();
			ushort nameLength = br.ReadUInt16();

			// skip 4 bytes for file attributes
			br.BaseStream.Position += 4;

			// if large file flag is set, next are 4 bytes each for high order bits of file sizes
			if ((block.Flags & 0x0100) == 0x0100)
			{
				PackedSize += br.ReadUInt32() * 0x100000000ul;
				UnpackedSize += br.ReadUInt32() * 0x100000000ul;
			}

			// and finally, the file name
			FileName = new string(br.ReadChars(nameLength));
		}
	}

	class Program
	{
		static void ReportError(string msg)
		{
			ConsoleColor normalColor = Console.ForegroundColor;
			Console.ForegroundColor = ConsoleColor.Red;
			Console.WriteLine(msg);
			Console.ForegroundColor = normalColor;
		}

		static void DisplayUsage()
		{
			Console.WriteLine("\nUsage:");
			Console.WriteLine("\tTo create a reconstruction file (SRR), use the release SFV file.\n\tAll files referenced by the SFV must be in the same folder as the SFV.\n\t\tex: srr example.sfv");
			Console.WriteLine("\tTo reconstruct a release, use the SRR file created from the release.\n\tAll files to be archived must be in the same folder as the SRR file.\n\t\tex: srr example.srr");
			Console.WriteLine("\tUse -v switch for verbose output.\n\t\tex: srr example.sfv -v");
		}

		static bool CheckOverwrite(string filePath)
		{
			if (File.Exists(filePath))
			{
				Console.WriteLine("Warning: File {0} already exists.  Do you wish to continue? (Y/N)", filePath);
				char res = Console.ReadKey(true).KeyChar;
				if (res != 'y' && res != 'Y')
					return false;
			}

			return true;
		}

		static IList<string> GetSfvFileList(byte[] sfvData)
		{
			// get list of files referenced by the SFV.  Make sure RAR files are sorted so we get the data in the right place
			SortedList<int, string> files = new SortedList<int, string>();

			StreamReader sr = new StreamReader(new MemoryStream(sfvData));
			Regex newNameFormat = new Regex(@"\.part(\d+)\.rar$", RegexOptions.IgnoreCase);
			Regex oldNameFormat = new Regex(@"\.([rs])(ar|\d{2})$", RegexOptions.IgnoreCase);

			string line;
			while ((line = sr.ReadLine()) != null)
			{
				if (line.StartsWith(";"))
					continue;

				line = line.Substring(0, line.Length - 9).Trim();
				if (newNameFormat.IsMatch(line))
				{
					files.Add(int.Parse(newNameFormat.Match(line).Groups[1].Value), line);
				}
				else if (oldNameFormat.IsMatch(line))
				{
					Match match = oldNameFormat.Match(line);
					files.Add((match.Groups[1].Value.ToLower() == "s" ? 100 : 0) + (match.Groups[2].Value.ToLower() != "ar" ? int.Parse(match.Groups[2].Value) : -1), line);
				}
				else
				{
					ReportError(string.Format("Warning: Non-RAR file referenced in SFV.  This file cannot be recreated:\n\t{0}", line));
				}
			}

			return files.Values;
		}
	
		static IList<RarBlock> GetRarBlocks(string fileName, bool srrMode)
		{
			List<RarBlock> blocks = new List<RarBlock>();

			using (FileStream fs = new FileStream(fileName, FileMode.Open))
			{
				BinaryReader br = new BinaryReader(fs, Encoding.ASCII);

				while (br.BaseStream.Position <= br.BaseStream.Length - 7)
				{
					// block header is always 7 bytes.  2 for crc, 1 for block type, 2 for flags, and 2 for block length
					ushort crc = br.ReadUInt16();
					byte blockType = br.ReadByte();
					ushort flags = br.ReadUInt16();
					ushort length = br.ReadUInt16();

					int addlLength = 0;
					if ((flags & 0x8000) == 0x8000) // additional data following block header
					{
						// if ADD_SIZE flag is set, next 4 bytes are additional data size
						addlLength = br.ReadInt32();
						br.BaseStream.Position -= 4;
					}

					// reset position to the beginning we can read the entire block
					br.BaseStream.Position -= 7;

					byte[] buff;
					if (blockType == 0x74) // file block
					{
						// for file blocks, skip the actual data, but read the rest of the block
						buff = new byte[length];
						br.Read(buff, 0, buff.Length);
						if (!srrMode)  // dont't skip if we're reading a .srr file because the data isn't there
							br.BaseStream.Seek(addlLength, SeekOrigin.Current);
					}
					else
					{
						// for all other blocks, read everything
						buff = new byte[length + addlLength];
						br.Read(buff, 0, buff.Length);
					}

					blocks.Add(new RarBlock() {Type = blockType, Flags = flags, Data = buff});
				}
			}

			return blocks;
		}

		static int CreateReconstructionFile(FileInfo sfvFileInfo)
		{
			string srrName = Path.Combine(sfvFileInfo.DirectoryName, Path.GetFileNameWithoutExtension(sfvFileInfo.Name) + ".srr");
			using (FileStream srrfs = new FileStream(srrName, FileMode.Create))
			{
				BinaryWriter bw = new BinaryWriter(srrfs, Encoding.ASCII);

				// fixed header sequence based on RAR block format.  Block type is 0x69.  We don't use crc for blocks (as of now), so crc value is set to 0x6969
				//  There are currently no flags defined and length of the block is always 7.
				//  See http://datacompression.info/ArchiveFormats/RAR202.txt for more details on RAR file format
				bw.Write(new byte[] { 0x69, 0x69, 0x69, 0x00, 0x00, 0x07, 0x00 });

				// we store a copy of the the SFV file in the .srr using a "store block".  any other file(s) could be stored the same way.
				byte[] sfvBuff;
				using (FileStream sfvfs = sfvFileInfo.Open(FileMode.Open, FileAccess.Read))
				{
					sfvBuff = new byte[sfvfs.Length];
					sfvfs.Read(sfvBuff, 0, sfvBuff.Length);
				}

				// store block (type 0x6A) has the 0x8000 flag set to indicate there is additional data following the block.
				// format is 7 byte header followed by 4 byte file size, 2 byte file name length, and file name
				bw.Write(new byte[] { 0x6A, 0x6A, 0x6A, 0x00, 0x80 });
				bw.Write((ushort)(7 + 4 + 2 + sfvFileInfo.Name.Length));
				bw.Write((uint)sfvBuff.Length);
				bw.Write((ushort)sfvFileInfo.Name.Length);
				bw.Write(sfvFileInfo.Name.ToCharArray());

				// then the file data
				bw.Write(sfvBuff);

				foreach (string file in GetSfvFileList(sfvBuff))
				{
					Console.WriteLine("Processing file: {0}", file);

					string fileName = Path.Combine(sfvFileInfo.DirectoryName, file);
					if (!File.Exists(fileName))
					{
						ReportError(string.Format("Referenced file not found: {0}", fileName));
						srrfs.Close();
						File.Delete(srrName);
						return 2;
					}

					// we create one SRR block (type 0x71) for each RAR file.
					//  it has 7 byte header, 2 bytes for file name length, then file name
					bw.Write(new byte[] { 0x71, 0x71, 0x71, 0x00, 0x00 });
					bw.Write((ushort)(9 + file.Length));
					bw.Write((ushort)file.Length);
					bw.Write(file.ToCharArray());

					foreach (RarBlock block in GetRarBlocks(fileName, false))
					{
						if (Verbose)
						{
							Console.WriteLine("\tBlock Type: 0x{0:x2}", block.Type);
							Console.WriteLine("\tBlock Size: {0}", block.Data.Length);
						}

						if (block.Type == 0x74) // file block
						{
							FileBlock fileData = new FileBlock(block);

							if (Verbose)
							{
								Console.WriteLine("\t\tCompression Type: 0x{0:x2}", fileData.CompressionMethod);
								Console.WriteLine("\t\tPacked Data Size: {0:n0}", fileData.PackedSize);
								Console.WriteLine("\t\tFile Size: {0:n0}", fileData.UnpackedSize);
								Console.WriteLine("\t\tFile Name: {0}", fileData.FileName);
							}

							if (fileData.CompressionMethod != 0x30)
							{
								ReportError(string.Format("Archive uses unsupported compression method: {0}", fileName));
								srrfs.Close();
								File.Delete(srrName);
								return 3;
							}
						}

						// store the raw data for any blocks found
						bw.Write(block.Data);
					}
				}
			}

			Console.WriteLine("\nReconstruction file successfully created: {0}", srrName);

			return 0;
		}

		static int Reconstruct(FileInfo srrFileInfo)
		{
			string rarName = null, srcName = null;
			FileStream rarfs = null, srcfs = null;
			byte[] copyBuff = new byte[65536];

			foreach (RarBlock block in GetRarBlocks(srrFileInfo.FullName, true))
			{
				if (Verbose)
				{
					Console.WriteLine("\tBlock Type: 0x{0:x2}", block.Type);
					Console.WriteLine("\tBlock Size: {0}", block.Data.Length);
				}

				if (block.Type == 0x69) // sfv block
				{
					// file header block.  nothing useful here yet.
				}
				else if (block.Type == 0x6A) // store block
				{
					// There is a file stored within the .srr.  extract it.
					StoreBlock sb = new StoreBlock(block);
					string fileName = Path.Combine(srrFileInfo.DirectoryName, sb.FileName);
					if (CheckOverwrite(fileName))
					{
						Console.WriteLine("Re-creating stored file: {0}", sb.FileName);
						using (FileStream sffs = new FileStream(fileName, FileMode.Create))
						{
							sffs.Write(block.Data, sb.FileOffset, (int)sb.FileLength);
						}
					}
					else
					{
						ReportError("Operation aborted.");
						return -1;
					}
				}
				/*
				else if (block.Type >= 0x6B && block.Type <= 70)
				{
					// reserved for future use
				}
				*/
				else if (block.Type == 0x71) // srr block
				{
					// for each SRR block, we need to create a RAR file.  get the stored name and create it.
					SrrBlock srrBlock = new SrrBlock(block);

					if (rarName != srrBlock.FileName)
					{
						rarName = srrBlock.FileName;
						if (rarfs != null)
							rarfs.Close();

						if (CheckOverwrite(Path.Combine(srrFileInfo.DirectoryName, rarName)))
						{
							rarfs = new FileStream(Path.Combine(srrFileInfo.DirectoryName, rarName), FileMode.Create);
							Console.WriteLine("Re-creating RAR file: {0}", srrBlock.FileName);
						}
						else
						{
							ReportError("Operation aborted.");
							return -1;
						}
					}
				}
				else if (block.Type == 0x74) // file data
				{
					// this is the one RAR block we treat differently.  We removed the data when storing it, so we need to get the data back from the extracted file
					FileBlock fileData = new FileBlock(block);

					if (Verbose)
					{
						Console.WriteLine("\t\tPacked Data Size: {0:n0}", fileData.PackedSize);
						Console.WriteLine("\t\tFile Name: {0}", fileData.FileName);
					}

					if (srcName != fileData.FileName)
					{
						srcName = fileData.FileName;
						if (srcfs != null)
							srcfs.Close();

						FileInfo srcInfo = new FileInfo(Path.Combine(srrFileInfo.DirectoryName, srcName));
						if (!srcInfo.Exists)
						{
							ReportError(string.Format("Could not locate data file: {0}", srcInfo.FullName));
							return 4;
						}
						if ((ulong)srcInfo.Length != fileData.UnpackedSize)
						{
							ReportError(string.Format("Data file is not the correct size: {0}\n\tFound: {1:n0}\n\tExpected: {2:n0}", srcInfo.FullName, srcInfo.Length, fileData.UnpackedSize));
							return 5;
						}

						srcfs = new FileStream(srcInfo.FullName, FileMode.Open);
					}

					// write the block contents from the .srr file
					rarfs.Write(block.Data, 0, block.Data.Length);

					// then grab the correct amount of data from the extracted file
					ulong bytesCopied = 0;
					while (bytesCopied < fileData.PackedSize)
					{
						ulong bytesToCopy = fileData.PackedSize - bytesCopied;
						if (bytesToCopy > (ulong)copyBuff.Length)
							bytesToCopy = (ulong)copyBuff.Length;

						srcfs.Read(copyBuff, 0, (int)bytesToCopy);
						rarfs.Write(copyBuff, 0, (int)bytesToCopy);
						bytesCopied += bytesToCopy;
					}
				}
				else if (block.Type >= 0x72) // rar block
				{
					// copy any other rar blocks to the destination unmodified
					rarfs.Write(block.Data, 0, block.Data.Length);
				}
				else
				{
					ReportError(string.Format("Warning: Unknown block type ({0:x2}) encountered in SRR file, consisting of {1:n0} bytes.  This block will be skipped.", block.Type, block.Data.Length));
				}
			}

			if (rarfs != null)
				rarfs.Close();

			Console.WriteLine("\nRelease successfully reconstructed.  Please re-check files against the SFV to verify before using.");

			return 0;
		}

		static bool Verbose = false;

		static int Main(string[] args)
		{
			try
			{
				if (args.Length < 1 || args[0] == "-?" || args[0] == "/?")
				{
					ReportError("No input file specified");
					DisplayUsage();
					return -1;
				}
				if (args.Length == 2 && args[1] == "-v")
				{
					Verbose = true;
				}

				FileInfo inputFileInfo = new FileInfo(args[0]);
				if (!inputFileInfo.Exists)
				{
					ReportError(string.Format("Input file not found: {0}", inputFileInfo.FullName));
					DisplayUsage();
					return 1;
				}
				else if (inputFileInfo.Extension.ToLower() == ".sfv")
				{
					return CreateReconstructionFile(inputFileInfo);
				}
				else if (inputFileInfo.Extension.ToLower() == ".srr")
				{
					return Reconstruct(inputFileInfo);
				}
				else
				{
					ReportError(string.Format("Input file type not recognized: {0}", inputFileInfo.Extension));
					DisplayUsage();
					return -1;
				}
			}
			catch (Exception ex)
			{
				System.Windows.Forms.MessageBox.Show(ex.ToString(), "Unexpected Error", System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Error);
				return 99;
			}
		}
	}
}
