#region Copyright & License Information
/*
 * Copyright 2007-2015 The OpenRA Developers (see AUTHORS)
 * This file is part of OpenRA, which is free software. It is made
 * available to you under the terms of the GNU General Public License
 * as published by the Free Software Foundation. For more information,
 * see COPYING.
 */
#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using ICSharpCode.SharpZipLib.Zip.Compression;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;

namespace OpenRA.FileSystem
{
	public class InstallShieldCABExtractor : IDisposable, IFolder
	{
		const uint FileSplit = 0x1;
		const uint FileObfuscated = 0x2;
		const uint FileCompressed = 0x4;
		const uint FileInvalid = 0x8;

		const uint LinkPrev = 0x1;
		const uint LinkNext = 0x2;
		const uint MaxFileGroupCount = 71;

#region Nested Structs

		struct FileGroup
		{
			public readonly string Name;
			public readonly uint FirstFile;
			public readonly uint LastFile;

			public FileGroup(Stream reader, long offset)
			{
				var nameOffset = reader.ReadUInt32();
				/*   unknown  */ reader.ReadBytes(18);
				FirstFile = reader.ReadUInt32();
				LastFile = reader.ReadUInt32();

				reader.Seek(offset + (long)nameOffset, SeekOrigin.Begin);
				Name = reader.ReadASCIIZ();
			}
		}

		struct VolumeHeader
		{
			public readonly uint DataOffset;
			public readonly uint DataOffsetHigh;
			public readonly uint FirstFileIndex;
			public readonly uint LastFileIndex;

			public readonly uint FirstFileOffset;
			public readonly uint FirstFileOffsetHigh;
			public readonly uint FirstFileSizeExpanded;
			public readonly uint FirstFileSizeExpandedHigh;

			public readonly uint FirstFileSizeCompressed;
			public readonly uint FirstFileSizeCompressedHigh;
			public readonly uint LastFileOffset;
			public readonly uint LastFileOffsetHigh;

			public readonly uint LastFileSizeExpanded;
			public readonly uint LastFileSizeExpandedHigh;
			public readonly uint LastFileSizeCompressed;
			public readonly uint LastFileSizeCompressedHigh;

			public VolumeHeader(Stream reader)
			{
				DataOffset = reader.ReadUInt32();
				DataOffsetHigh = reader.ReadUInt32();

				FirstFileIndex = reader.ReadUInt32();
				LastFileIndex = reader.ReadUInt32();
				FirstFileOffset = reader.ReadUInt32();
				FirstFileOffsetHigh = reader.ReadUInt32();

				FirstFileSizeExpanded = reader.ReadUInt32();
				FirstFileSizeExpandedHigh = reader.ReadUInt32();
				FirstFileSizeCompressed = reader.ReadUInt32();
				FirstFileSizeCompressedHigh = reader.ReadUInt32();

				LastFileOffset = reader.ReadUInt32();
				LastFileOffsetHigh = reader.ReadUInt32();
				LastFileSizeExpanded = reader.ReadUInt32();
				LastFileSizeExpandedHigh = reader.ReadUInt32();

				LastFileSizeCompressed = reader.ReadUInt32();
				LastFileSizeCompressedHigh = reader.ReadUInt32();
			}
		}

		struct CommonHeader
		{
			public const long Size = 16;
			public readonly uint Version;
			public readonly uint VolumeInfo;
			public readonly long CabDescriptorOffset;
			public readonly uint CabDescriptorSize;

			public CommonHeader(Stream reader)
			{
				Version = reader.ReadUInt32();
				VolumeInfo = reader.ReadUInt32();
				CabDescriptorOffset = (long)reader.ReadUInt32();
				CabDescriptorSize = reader.ReadUInt32();
			}
		}

		struct CabDescriptor
		{
			public readonly long FileTableOffset;
			public readonly uint FileTableSize;
			public readonly uint FileTableSize2;
			public readonly uint DirectoryCount;

			public readonly uint FileCount;
			public readonly long FileTableOffset2;

			public CabDescriptor(Stream reader, CommonHeader commonHeader)
			{
				reader.Seek(commonHeader.CabDescriptorOffset + 12, SeekOrigin.Begin);
				FileTableOffset = (long)reader.ReadUInt32();
				/*    unknown  */ reader.ReadUInt32();
				FileTableSize = reader.ReadUInt32();

				FileTableSize2 = reader.ReadUInt32();
				DirectoryCount = reader.ReadUInt32();
				/*   unknown  */ reader.ReadBytes(8);
				FileCount = reader.ReadUInt32();

				FileTableOffset2 = (long)reader.ReadUInt32();
			}
		}

		struct FileDescriptor
		{
			public readonly ushort	Flags;
			public readonly uint	ExpandedSize;
			public readonly uint	CompressedSize;
			public readonly uint	DataOffset;

			public readonly byte[]	MD5;
			public readonly uint	NameOffset;
			public readonly ushort	DirectoryIndex;
			public readonly uint	LinkToPrevious;

			public readonly uint	LinkToNext;
			public readonly byte	LinkFlags;
			public readonly ushort	Volume;
			public readonly string	Filename;

			public FileDescriptor(Stream reader, long tableOffset)
			{
				Flags = reader.ReadUInt16();
				ExpandedSize = reader.ReadUInt32();
				/*    unknown   */ reader.ReadUInt32();
				CompressedSize = reader.ReadUInt32();

				/*  unknown */ reader.ReadUInt32();
				DataOffset   = reader.ReadUInt32();
				/*  unknown */ reader.ReadUInt32();
				MD5          = reader.ReadBytes(16);

				/*   unknown  */ reader.ReadBytes(16);
				NameOffset     = reader.ReadUInt32();
				DirectoryIndex = reader.ReadUInt16();
				/*   unknown  */ reader.ReadBytes(12);
				LinkToPrevious = reader.ReadUInt32();
				LinkToNext = reader.ReadUInt32();

				LinkFlags = reader.ReadBytes(1)[0];
				Volume = reader.ReadUInt16();
				var posSave = reader.Position;

				reader.Seek(tableOffset + NameOffset, SeekOrigin.Begin);
				Filename = reader.ReadASCIIZ();
				reader.Seek(posSave, SeekOrigin.Begin);
			}
		}

		class CabReader : IDisposable
		{
			readonly FileDescriptor fileDes;
			public uint RemainingArchiveStream;
			public uint RemainingFileStream;
			readonly uint index;
			readonly string commonName;
			ushort volumeNumber;
			Stream cabFile;

			public CabReader(FileDescriptor fileDes, uint index, string commonName)
			{
				this.fileDes = fileDes;
				this.index = index;
				this.commonName = commonName;
				volumeNumber = (ushort)((uint)fileDes.Volume - 1u);
				RemainingArchiveStream = 0;
				if ((fileDes.Flags & FileCompressed) > 0)
					RemainingFileStream = fileDes.CompressedSize;
				else
					RemainingFileStream = fileDes.ExpandedSize;

				cabFile = null;
				NextFile();
			}

			public void CopyTo(Stream dest)
			{
				if ((fileDes.Flags & FileCompressed) != 0)
				{
					var inf = new Inflater(true);
					var buffer = new byte[165535];
					do
					{
						var bytesToExtract = cabFile.ReadUInt16();
						RemainingArchiveStream -= 2u;
						RemainingFileStream -= 2u;
						inf.SetInput(GetBytes(bytesToExtract));
						RemainingFileStream -= bytesToExtract;
						while (!inf.IsNeedingInput)
						{
							var inflated = inf.Inflate(buffer);
							dest.Write(buffer, 0, inflated);
						}

						inf.Reset();
					}
					while (RemainingFileStream > 0);
				}
				else
				{
					do
					{
						RemainingFileStream -= RemainingArchiveStream;
						dest.Write(GetBytes(RemainingArchiveStream), 0, (int)RemainingArchiveStream);
					}
					while (RemainingFileStream > 0);
				}
			}

			public byte[] GetBytes(uint count)
			{
				if (count < RemainingArchiveStream)
				{
					RemainingArchiveStream -= count;
					return cabFile.ReadBytes((int)count);
				}
				else
				{
					var outArray = new byte[count];
					var read = cabFile.Read(outArray, 0, (int)RemainingArchiveStream);
					if (RemainingFileStream > RemainingArchiveStream)
					{
						NextFile();
						RemainingArchiveStream -= (uint)cabFile.Read(outArray, read, (int)count - read);
					}

					return outArray;
				}
			}

			public void Dispose()
			{
				cabFile.Dispose();
			}

			public void NextFile()
			{
				if (cabFile != null)
					cabFile.Dispose();

				++volumeNumber;
				cabFile = GlobalFileSystem.Open("{0}{1}.cab".F(commonName, volumeNumber));
				if (cabFile.ReadUInt32() != 0x28635349)
					throw new InvalidDataException("Not an Installshield CAB package");

				uint fileOffset;
				if ((fileDes.Flags & FileSplit) != 0)
				{
					cabFile.Seek(CommonHeader.Size, SeekOrigin.Current);
					var head = new VolumeHeader(cabFile);
					if (index == head.LastFileIndex)
					{
						if ((fileDes.Flags & FileCompressed) != 0)
							RemainingArchiveStream = head.LastFileSizeCompressed;
						else
							RemainingArchiveStream = head.LastFileSizeExpanded;

						fileOffset = head.LastFileOffset;
					}
					else if (index == head.FirstFileIndex)
					{
						if ((fileDes.Flags & FileCompressed) != 0)
							RemainingArchiveStream = head.FirstFileSizeCompressed;
						else
							RemainingArchiveStream = head.FirstFileSizeExpanded;

						fileOffset = head.FirstFileOffset;
					}
					else
						throw new Exception("Cannot Resolve Remaining Stream");
				}
				else
				{
					if ((fileDes.Flags & FileCompressed) != 0)
						RemainingArchiveStream = fileDes.CompressedSize;
					else
						RemainingArchiveStream = fileDes.ExpandedSize;

					fileOffset = fileDes.DataOffset;
				}

				cabFile.Seek(fileOffset, SeekOrigin.Begin);
			}
		}
#endregion

		readonly Stream hdrFile;
		readonly CommonHeader commonHeader;
		readonly CabDescriptor cabDescriptor;
		readonly List<uint> directoryTable;
		readonly Dictionary<uint, string> directoryNames = new Dictionary<uint, string>();
		readonly Dictionary<uint, FileDescriptor> fileDescriptors = new Dictionary<uint, FileDescriptor>();
		readonly Dictionary<string, uint> fileLookup = new Dictionary<string, uint>();
		int priority;
		string commonName;
		public int Priority { get { return priority; } }

		public string Name { get { return commonName; } }

		public InstallShieldCABExtractor(string hdrFilename, int priority = -1)
		{
			var fileGroups = new List<FileGroup>();
			var fileGroupOffsets = new List<uint>();

			this.priority = priority;
			hdrFile = GlobalFileSystem.Open(hdrFilename);

			// Strips archive number AND file extension
			commonName = Regex.Replace(hdrFilename, @"\d*\.[^\.]*$", "");
			var signature = hdrFile.ReadUInt32();

			if (signature != 0x28635349)
				throw new InvalidDataException("Not an Installshield CAB package");

			commonHeader = new CommonHeader(hdrFile);
			cabDescriptor = new CabDescriptor(hdrFile, commonHeader);
			/*    unknown   */ hdrFile.ReadBytes(14);

			for (var i = 0U; i < MaxFileGroupCount; ++i)
				fileGroupOffsets.Add(hdrFile.ReadUInt32());

			hdrFile.Seek(commonHeader.CabDescriptorOffset + cabDescriptor.FileTableOffset, SeekOrigin.Begin);
			directoryTable = new List<uint>();

			for (var i = 0U; i < cabDescriptor.DirectoryCount; ++i)
				directoryTable.Add(hdrFile.ReadUInt32());

			foreach (var offset in fileGroupOffsets)
			{
				var nextOffset = offset;
				while (nextOffset != 0)
				{
					hdrFile.Seek((long)nextOffset + 4 + commonHeader.CabDescriptorOffset, SeekOrigin.Begin);
					var descriptorOffset = hdrFile.ReadUInt32();
					nextOffset = hdrFile.ReadUInt32();
					hdrFile.Seek((long)descriptorOffset + commonHeader.CabDescriptorOffset, SeekOrigin.Begin);

					fileGroups.Add(new FileGroup(hdrFile, commonHeader.CabDescriptorOffset));
				}
			}

			hdrFile.Seek(commonHeader.CabDescriptorOffset + cabDescriptor.FileTableOffset + cabDescriptor.FileTableOffset2, SeekOrigin.Begin);
			foreach (var fileGroup in fileGroups)
			{
				for (var index = fileGroup.FirstFile; index <= fileGroup.LastFile; ++index)
				{
					AddFileDescriptorToList(index);
					var fileDescriptor = fileDescriptors[index];
					var fullFilePath   = "{0}\\{1}\\{2}".F(fileGroup.Name, DirectoryName((uint)fileDescriptor.DirectoryIndex), fileDescriptor.Filename);
					fileLookup.Add(fullFilePath, index);
				}
			}
		}

		public string DirectoryName(uint index)
		{
			if (directoryNames.ContainsKey(index))
				return directoryNames[index];

			hdrFile.Seek(commonHeader.CabDescriptorOffset +
					cabDescriptor.FileTableOffset +
					directoryTable[(int)index],
					SeekOrigin.Begin);

			var test = hdrFile.ReadASCIIZ();
			return test;
		}

		public bool Exists(string filename)
		{
			return fileLookup.ContainsKey(filename);
		}

		public uint DirectoryCount()
		{
			return cabDescriptor.DirectoryCount;
		}

		public string FileName(uint index)
		{
			if (!fileDescriptors.ContainsKey(index))
				AddFileDescriptorToList(index);

			return fileDescriptors[index].Filename;
		}

		void AddFileDescriptorToList(uint index)
		{
			hdrFile.Seek(commonHeader.CabDescriptorOffset +
					cabDescriptor.FileTableOffset +
					cabDescriptor.FileTableOffset2 +
					index * 0x57,
					SeekOrigin.Begin);

			var fd = new FileDescriptor(hdrFile,
				commonHeader.CabDescriptorOffset + cabDescriptor.FileTableOffset);

			fileDescriptors.Add(index, fd);
		}

		public uint FileCount()
		{
			return cabDescriptor.FileCount;
		}

		public void ExtractFile(uint index, string fileName)
		{
			using (var destfile = File.Open(fileName, FileMode.Create))
				GetContentById(index, destfile);
		}

		public void Write(Dictionary<string, byte[]> input)
		{
			throw new NotImplementedException("Cannot Add Files To Cab");
		}

		public IEnumerable<uint> ClassicHashes()
		{
			return fileLookup.Keys.Select(k => PackageEntry.HashFilename(k, PackageHashType.Classic));
		}

		public Stream GetContentById(uint index)
		{
			var fileDes = fileDescriptors[index];
			if ((fileDes.Flags & FileInvalid) != 0)
				throw new Exception("File Invalid");

			if ((fileDes.LinkFlags & LinkPrev) != 0)
				return GetContentById(fileDes.LinkToPrevious);

			if ((fileDes.Flags & FileObfuscated) != 0)
				throw new NotImplementedException("Haven't implemented obfustcated files");

			var output = new MemoryStream((int)fileDes.ExpandedSize);

			using (var reader = new CabReader(fileDes, index, commonName))
				reader.CopyTo(output);

			if (output.Length != fileDes.ExpandedSize)
				throw new Exception("Did not fully extract Expected = {0}, Got = {1}".F(fileDes.ExpandedSize, output.Length));

			output.Position = 0;
			return output;
		}

		public void GetContentById(uint index, Stream output)
		{
			var fileDes = fileDescriptors[index];
			if ((fileDes.Flags & FileInvalid) != 0)
				throw new Exception("File Invalid");

			if ((fileDes.LinkFlags & LinkPrev) != 0)
			{
				GetContentById(fileDes.LinkToPrevious, output);
				return;
			}

			if ((fileDes.Flags & FileObfuscated) != 0)
				throw new NotImplementedException("Haven't implemented obfustcated files");

			using (var reader = new CabReader(fileDes, index, commonName))
				reader.CopyTo(output);

			if (output.Length != fileDes.ExpandedSize)
				throw new Exception("Did not fully extract Expected = {0}, Got = {1}".F(fileDes.ExpandedSize, output.Length));
		}

		public Stream GetContent(string fileName)
		{
			return GetContentById(fileLookup[fileName]);
		}

		public IEnumerable<uint> CrcHashes()
		{
			yield break;
		}

		public IEnumerable<string> AllFileNames()
		{
			return fileLookup.Keys;
		}

		public void Dispose()
		{
			hdrFile.Dispose();
		}
	}
}
