// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// Copyright (c) 2004-2005 Novell, Inc.
//
// Authors:
// 	Duncan Mak	duncan@ximian.com
//	Nick Drochak	ndrochak@gol.com
//	Paolo Molaro	lupus@ximian.com
//	Peter Bartok	pbartok@novell.com
//

// COMPLETE

using System;
using System.Collections;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Globalization;
using System.IO;
using System.Resources;
using System.Runtime.Serialization.Formatters.Binary;
using System.Xml;

namespace System.Resources
{
	public class ResXResourceReader : IResourceReader, IDisposable
	{
		#region Local Variables
		private Stream			stream;
		private XmlTextReader		reader;
		private Hashtable		hasht;
		private ITypeResolutionService	typeresolver;
		#endregion	// Local Variables

		#region Constructors & Destructor
		public ResXResourceReader (Stream stream)
		{
			if (stream == null)
				throw new ArgumentNullException ("Value cannot be null.");
			
			if (!stream.CanRead)
				throw new ArgumentException ("Stream was not readable.");

			this.stream = stream;
			basic_setup ();
		}

		public ResXResourceReader (Stream stream, ITypeResolutionService typeresolver) : this(stream) {
			this.typeresolver = typeresolver;
		}
		
		public ResXResourceReader (string fileName)
		{
			stream = File.OpenRead (fileName);
			basic_setup ();
		}

		public ResXResourceReader (string fileName, ITypeResolutionService typeresolver) : this(fileName) {
			this.typeresolver = typeresolver;
		}

		public ResXResourceReader(TextReader reader) {
			this.reader = new XmlTextReader(reader);
			this.hasht = new Hashtable();

			load_data();
		}

		public ResXResourceReader (TextReader reader, ITypeResolutionService typeresolver) : this(reader) {
			this.typeresolver = typeresolver;
		}

		~ResXResourceReader() {
			Dispose(false);
		}
		#endregion	// Constructors & Destructor


		#region Private Methods
		void basic_setup () {
			reader = new XmlTextReader (stream);
			hasht = new Hashtable ();

			if (!IsStreamValid()){
				throw new ArgumentException("Stream is not a valid .resx file!  It was possibly truncated.");
			}
			load_data ();
		}
		
		static string get_attr (XmlTextReader reader, string name) {
			if (!reader.HasAttributes)
				return null;
			for (int i = 0; i < reader.AttributeCount; i++) {
				reader.MoveToAttribute (i);
				if (String.Compare (reader.Name, name, true) == 0) {
					string v = reader.Value;
					reader.MoveToElement ();
					return v;
				}
			}
			reader.MoveToElement ();
			return null;
		}


		static string get_value (XmlTextReader reader, string name)
		{
			return get_value (reader, name, true);
		}

		// Returns the value of the next XML element with the specified
		// name from the reader. canBeCdata == true specifies that
		// the element may be a CDATA node as well.
		static string get_value (XmlTextReader reader, string name, bool canBeCdata) {
			bool gotelement = false;
			while (reader.Read ()) {
				if (reader.NodeType == XmlNodeType.Element && String.Compare (reader.Name, name, true) == 0) {
					gotelement = true;
					break;
				}
				if (canBeCdata && reader.NodeType == XmlNodeType.CDATA)
				   return reader.Value;
			}
			if (!gotelement)
				return null;
			while (reader.Read ()) {
				if (reader.NodeType == XmlNodeType.Text || reader.NodeType == XmlNodeType.CDATA) {
					string v = reader.Value;
					return v;
				}
				else if (reader.NodeType == XmlNodeType.EndElement && reader.Value == string.Empty)
				{
					string v = reader.Value;
					return v;
				}
			}
			return null;
		}

		private bool IsStreamValid() {
			bool gotroot = false;
			bool gotmime = false;
			
			while (reader.Read ()) {
				if (reader.NodeType == XmlNodeType.Element && String.Compare (reader.Name, "root", true) == 0) {
					gotroot = true;
					break;
				}
			}
			if (!gotroot)
				return false;
			while (reader.Read ()) {
				if (reader.NodeType == XmlNodeType.Element && String.Compare (reader.Name, "resheader", true) == 0) {
					string v = get_attr (reader, "name");
					if (v != null && String.Compare (v, "resmimetype", true) == 0) {
						v = get_value (reader, "value");
						if (String.Compare (v, "text/microsoft-resx", true) == 0) {
							gotmime = true;
							break;
						}
					}
				} else if (reader.NodeType == XmlNodeType.Element && String.Compare (reader.Name, "data", true) == 0) {
					/* resheader apparently can appear anywhere, so we collect
					 * the data even if we haven't validated yet.
					 */
					parse_data_node ();
				}
			}
			return gotmime;
		}

		private void load_data ()
		{
			while (reader.Read ()) {
				if (reader.NodeType == XmlNodeType.Element && String.Compare (reader.Name, "data", true) == 0)
					parse_data_node ();

			}
		}

		void parse_data_node ()
		{
			string name = get_attr (reader, "name");
			string type_name = get_attr (reader, "type");
			string mime_type = get_attr (reader, "mimetype");

			if (name == null)
				return;

			Type type = type_name == null ? null : ResolveType (type_name);

			if (type_name != null && type == null)
				throw new ArgumentException (String.Format (
					"The type '{0}' of the element '{1}' could not be resolved.", type_name, name));

			if (type == typeof (ResXNullRef)) {
				hasht [name] = null;
				return;
			}

			string value = get_value (reader, "value");
			object obj = null;

			if (type != null) {
				TypeConverter c = TypeDescriptor.GetConverter (type);

				if (c == null) {
					obj = null;
				} else if (c.CanConvertFrom (typeof (string))) {
					obj = c.ConvertFromInvariantString (value);
				} else if (c.CanConvertFrom (typeof (byte[]))) {
					obj = c.ConvertFrom (Convert.FromBase64String (value));
				} else {
					// the type must be a byte[]
					obj  = Convert.FromBase64String(value);
				}
			} else if (mime_type != null && mime_type != String.Empty) {
				if (mime_type == ResXResourceWriter.BinSerializedObjectMimeType) {
					byte [] data = Convert.FromBase64String (value);
					BinaryFormatter f = new BinaryFormatter ();
					using (MemoryStream s = new MemoryStream (data)) {
						obj = f.Deserialize (s);
					}
				} else {
					// invalid mime type
					obj = null;
				}
			} else {
				obj = value;
			}
			hasht [name] = obj;
		}

		private Type ResolveType (string type) {
			if (typeresolver == null) {
				return Type.GetType(type);
			} else {
				return typeresolver.GetType(type);
			}
		}
		#endregion	// Private Methods

		#region Public Methods
		public void Close ()
		{
			if (stream != null) {
				stream.Close ();
				stream = null;
			}

			if (reader != null) {
				reader.Close();
				reader = null;
			}
		}
		
		public IDictionaryEnumerator GetEnumerator () {
			if (null == stream){
				throw new InvalidOperationException("ResourceReader is closed.");
			}
			else {
				return hasht.GetEnumerator ();
			}
		}
		
		IEnumerator IEnumerable.GetEnumerator ()
		{
			return ((IResourceReader) this).GetEnumerator();
		}
		
		void IDisposable.Dispose ()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		protected virtual void Dispose(bool disposing) {
			if (disposing) {
				Close();
			}
		}

		public static ResXResourceReader FromFileContents(string fileContents) {
			return new ResXResourceReader(new StringReader(fileContents));
		}

		public static ResXResourceReader FromFileContents(string fileContents, ITypeResolutionService typeResolver) {
			return new ResXResourceReader(new StringReader(fileContents), typeResolver);
		}

		#endregion	// Public Methods
		
	}  // public sealed class ResXResourceReader
} // namespace System.Resources
