
#region ================== Copyright (c) 2007 Pascal vd Heiden

/*
 * Copyright (c) 2007 Pascal vd Heiden, www.codeimp.com
 * This program is released under GNU General Public License
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 */

#endregion

#region ================== Namespaces

using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.IO;

#endregion

namespace CodeImp.DoomBuilder.IO
{
	internal class WAD : IDisposable
	{
		#region ================== Constants

		// WAD types
		private const string TYPE_IWAD = "IWAD";
		private const string TYPE_PWAD = "PWAD";
		
		// Encoder
		public static readonly Encoding ENCODING = Encoding.ASCII;

		//mxd. Official IWAD MD5 hashes
		private static readonly HashSet<string> IWAD_HASHES = new HashSet<string>
		{
			////// DOOM IWADS //////
			"d9153ced9fd5b898b36cc5844e35b520",			// DOOM2 1.666g MD5
			"30e3c2d0350b67bfbf47271970b74b2f",			// DOOM2 1.666 MD5
			"ea74a47a791fdef2e9f2ea8b8a9da13b",			// DOOM2 1.7 MD5
			"d7a07e5d3f4625074312bc299d7ed33f",			// DOOM2 1.7a MD5
			"c236745bb01d89bbb866c8fed81b6f8c",			// DOOM2 1.8 MD5
			"25e1459ca71d321525f84628f45ca8cd",			// DOOM2 1.9 MD5
			"3cb02349b3df649c86290907eed64e7b",			// DOOM2 French MD5
			"c3bea40570c23e511a7ed3ebcd9865f7",			// BFG DOOM2 MD5

			"981b03e6d1dc033301aa3095acc437ce",			// DOOM 1.1 MD5
			"792fd1fea023d61210857089a7c1e351",			// DOOM 1.2 MD5
			"464e3723a7e7f97039ac9fd057096adb",			// DOOM 1.6b MD5
			"54978d12de87f162b9bcc011676cb3c0",			// DOOM 1.666 MD5
			"11e1cd216801ea2657723abc86ecb01f",			// DOOM 1.8 MD5
			"1cd63c5ddff1bf8ce844237f580e9cf3",			// DOOM 1.9 MD5
			"fb35c4a5a9fd49ec29ab6e900572c524",			// BFG DOOM MD5

			"c4fe9fd920207691a9f493668e0a2083",			// ULTIMATE DOOM MD5

			"75c8cf89566741fa9d22447604053bd7",			// PLUTONIA MD5
			"3493be7e1e2588bc9c8b31eab2587a04",			// PLUTONIA RARE MD5

			"4e158d9953c79ccf97bd0663244cc6b6",			// TNT MD5
			"1d39e405bf6ee3df69a8d2646c8d5c49",			// TNT Fixed MD5
			"be626c12b7c9d94b1dfb9c327566b4ff",			// PSN TNT MD5

			////// HERETIC IWADS //////
			"3117e399cdb4298eaa3941625f4b2923",			// HERETIC 1.0 MD5
			"1e4cb4ef075ad344dd63971637307e04",			// HERETIC 1.2 MD5
			"66d686b1ed6d35ff103f15dbd30e0341",			// HERETIC 1.3 MD5

			////// HEXEN IWADS //////
			"c88a2bb3d783e2ad7b599a8e301e099e",			// HEXEN Beta MD5
			"b2543a03521365261d0a0f74d5dd90f0",			// HEXEN 1.0 MD5
			"abb033caf81e26f12a2103e1fa25453f",			// HEXEN 1.1 MD5
			"1077432e2690d390c256ac908b5f4efa",			// HEXEN DK 1.0 MD5
			"78d5898e99e220e4de64edaa0e479593",			// HEXEN DK 1.1 MD5

			////// STRIFE IWADS //////
			"8f2d3a6a289f5d2f2f9c1eec02b47299",			// STRIFE 1.0 MD5
			"2fed2031a5b03892106e0f117f17901f",			// STRIFE 1.2 MD5
		};                                                      
		
		#endregion

		#region ================== Variables

		// File objects
		private string filename;
		private FileStream file;
		private BinaryReader reader;
		private BinaryWriter writer;
		
		// Header
		private int numlumps;
		private int lumpsoffset;
		private bool isiwad; //mxd
		private bool isofficialiwad; //mxd
		
		// Lumps
		private List<Lump> lumps;
		
		// Status
		private bool isreadonly;
		private bool isdisposed;

		#endregion

		#region ================== Properties

		public string Filename { get { return filename; } }
		public Encoding Encoding { get { return ENCODING; } }
		public bool IsReadOnly { get { return isreadonly; } }
		public bool IsDisposed { get { return isdisposed; } }
		public bool IsIWAD { get { return isiwad; } set { isiwad = value; } } //mxd
		public bool IsOfficialIWAD { get { return isofficialiwad; } } //mxd
		public List<Lump> Lumps { get { return lumps; } }

		#endregion

		#region ================== Constructor / Disposer

		// Constructor to open or create a WAD file
		public WAD(string pathfilename)
		{
			// Initialize
			this.isreadonly = false;
			this.Open(pathfilename);
		}

		// Constructor to open or create a WAD file
		public WAD(string pathfilename, bool openreadonly)
		{
			// Initialize
			this.isreadonly = openreadonly;
			this.Open(pathfilename);
		}

		// Destructor
		/*~WAD()
		{
			// Make sure everything is disposed
			this.Dispose();
		}*/
		
		// Disposer
		public void Dispose()
		{
			// Not already disposed?
			if(!isdisposed)
			{
				// Only possible when not read-only
				if(!isreadonly)
				{
					// Flush writing changes
					if(writer != null) writer.Flush();
					if(file != null) file.Flush();
				}
				
				// Clean up
				if(lumps != null) foreach(Lump l in lumps) l.Dispose();
				if(writer != null) writer.Close();
				if(reader != null) reader.Close();
				if(file != null) file.Dispose();
				
				// Done
				isdisposed = true;
				GC.SuppressFinalize(this); //mxd
			}
		}

		#endregion

		#region ================== IO

		// Open a WAD file
		private void Open(string pathfilename)
		{
			FileAccess access;
			FileShare share;

			// Keep filename
			filename = pathfilename;

			//mxd
			CheckHash();
			
			// Determine if opening for read only
			if(isreadonly)
			{
				// Read only
				access = FileAccess.Read;
				share = FileShare.ReadWrite;
			}
			else
			{
				// Private access
				access = FileAccess.ReadWrite;
				share = FileShare.Read;
			}
			
			// Open the file stream
			file = File.Open(pathfilename, FileMode.OpenOrCreate, access, share);

			// Create file handling tools
			reader = new BinaryReader(file, ENCODING);
			if(!isreadonly) writer = new BinaryWriter(file, ENCODING);

			// Is the WAD file zero length?
			if(file.Length < 4)
			{
				// Create the headers in file
				CreateHeaders();
			}
			else
			{
				// Read information from file
				ReadHeaders();
			}
		}

		// This creates new file headers
		private void CreateHeaders()
		{
			// Default settings
			isiwad = false; //mxd
			isofficialiwad = false; //mxd
			lumpsoffset = 12;

			// New lumps array
			lumps = new List<Lump>(numlumps);
			
			// Write the headers
			if(!isreadonly) WriteHeaders();
		}
		
		// This reads the WAD header and lumps table
		private void ReadHeaders()
		{
			// Make sure the write is finished writing
			if(!isreadonly) writer.Flush();

			// Seek to beginning
			file.Seek(0, SeekOrigin.Begin);

			// Read WAD type
			isiwad = (ENCODING.GetString(reader.ReadBytes(4)) == TYPE_IWAD); //mxd
			
			// Number of lumps
			numlumps = reader.ReadInt32();
			if(numlumps < 0) throw new IOException("Invalid number of lumps in wad file.");

			// Lumps table offset
			lumpsoffset = reader.ReadInt32();
			if(lumpsoffset < 0) throw new IOException("Invalid lumps offset in wad file.");

			// Seek to the lumps table
			file.Seek(lumpsoffset, SeekOrigin.Begin);
			
			// Dispose old lumps and create new list
			if(lumps != null) foreach(Lump l in lumps) l.Dispose();
			lumps = new List<Lump>(numlumps);

			// Go for all lumps
			for(int i = 0; i < numlumps; i++)
			{
				// Read lump information
				int offset = reader.ReadInt32();
				int length = reader.ReadInt32();
				byte[] fixedname = reader.ReadBytes(8);

				// Create the lump
				lumps.Add(new Lump(file, this, fixedname, offset, length));
			}
		}

		// This writes the WAD header and lumps table
		public void WriteHeaders()
		{
			// Seek to beginning
			file.Seek(0, SeekOrigin.Begin);

			// Write WAD type
			writer.Write(ENCODING.GetBytes(isiwad ? TYPE_IWAD : TYPE_PWAD));

			// Number of lumps
			writer.Write(numlumps);

			// Lumps table offset
			writer.Write(lumpsoffset);

			// Seek to the lumps table
			file.Seek(lumpsoffset, SeekOrigin.Begin);

			// Go for all lumps
			for(int i = 0; i < lumps.Count; i++)
			{
				// Write lump information
				writer.Write(lumps[i].Offset);
				writer.Write(lumps[i].Length);
				writer.Write(lumps[i].FixedName);
			}
		}

		//mxd
		private void CheckHash()
		{
			// Open the file stream
			FileStream fs = File.Open(filename, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite);
			
			// Empty file can't be official iwad
			if(fs.Length > 4)
			{
				BinaryReader r = new BinaryReader(fs, ENCODING);

				// Read WAD type
				if(ENCODING.GetString(r.ReadBytes(4)) == TYPE_IWAD)
				{
					// Rewind
					r.BaseStream.Position = 0;
					
					// Check hash
					MD5 hasher = MD5.Create();
					byte[] data = hasher.ComputeHash(r.BaseStream);

					// Create a new Stringbuilder to collect the bytes and create a string.
					StringBuilder hash = new StringBuilder();

					// Loop through each byte of the hashed data and format each one as a hexadecimal string.
					for(int i = 0; i < data.Length; i++)
					{
						hash.Append(data[i].ToString("x2"));
					}

					isofficialiwad = IWAD_HASHES.Contains(hash.ToString());
					if(!isreadonly && isofficialiwad) isreadonly = true;
				}

				// Close the reader
				r.Close();
			}
			else
			{
				// Close the file
				fs.Dispose();
			}
		}
		
		// This flushes writing changes
		/*public void Flush()
		{
			// Only possible when not read-only
			if(!isreadonly)
			{
				// Flush writing changes
				if(writer != null) writer.Flush();
				if(file != null) file.Flush();
			}
		}*/
		
		#endregion
		
		#region ================== Lumps

		// This creates a new lump in the WAD file
		public Lump Insert(string name, int position, int datalength) { return Insert(name, position, datalength, true); } //mxd
		public Lump Insert(string name, int position, int datalength, bool writeheaders)
		{
			// We will be adding a lump
			numlumps++;
			
			// Extend the file
			file.SetLength(file.Length + datalength + 16);
			
			// Create the lump
			Lump lump = new Lump(file, this, Lump.MakeFixedName(name, ENCODING), lumpsoffset, datalength);
			lumps.Insert(position, lump);
			
			// Advance lumps table offset
			lumpsoffset += datalength;

			// Write the new headers
			if(writeheaders) WriteHeaders();

			// Return the new lump
			return lump;
		}

		// This removes a lump from the WAD file by index
		public void RemoveAt(int index)
		{
			// Remove from list
			Lump l = lumps[index];
			lumps.RemoveAt(index);
			l.Dispose();
			numlumps--;
			
			// Write the new headers
			WriteHeaders();
		}
		
		// This removes a lump from the WAD file
		public void Remove(Lump lump)
		{
			// Remove from list
			lumps.Remove(lump);
			lump.Dispose();
			numlumps--;
			
			// Write the new headers
			WriteHeaders();
		}
		
		// This finds a lump by name, returns null when not found
		public Lump FindLump(string name)
		{
			int index = FindLumpIndex(name);
			return (index == -1 ? null : lumps[index]);
		}

		// This finds a lump by name, returns null when not found
		public Lump FindLump(string name, int start)
		{
			int index = FindLumpIndex(name, start);
			return (index == -1 ? null : lumps[index]);
		}

		// This finds a lump by name, returns null when not found
		public Lump FindLump(string name, int start, int end)
		{
			int index = FindLumpIndex(name, start, end);
			return (index == -1 ? null : lumps[index]);
		}

		// This finds a lump by name, returns -1 when not found
		public int FindLumpIndex(string name)
		{
			// Do search
			return FindLumpIndex(name, 0, lumps.Count - 1);
		}

		// This finds a lump by name, returns -1 when not found
		public int FindLumpIndex(string name, int start)
		{
			// Do search
			return FindLumpIndex(name, start, lumps.Count - 1);
		}
		
		// This finds a lump by name, returns -1 when not found
		public int FindLumpIndex(string name, int start, int end)
		{
			if(name.Length > 8 || lumps.Count == 0 || start > lumps.Count - 1)	return -1;//mxd. Can't be here. Go away!
			
			long longname = Lump.MakeLongName(name);

			// Fix start/end when they exceed safe bounds
			start = Math.Max(start, 0);
			end = General.Clamp(end, 0, lumps.Count - 1);

			// Loop through the lumps
			for (int i = start; i <= end; i++)
			{
				// Check if the lump name matches
				if(lumps[i].LongName == longname)
				{
					// Found the lump!
					return i;
				}
			}

			// Nothing found
			return -1;
		}

		#endregion
	}
}
