//
// Copyright (C) 2010 Novell Inc. http://novell.com
// Copyright (C) 2012 Xamarin Inc. http://xamarin.com
//
// 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.
//
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Windows.Markup;
using System.Xaml;
using System.Xaml.Schema;
using System.Xml;
using System.Xml.Serialization;

// To use this under .NET, compile sources as:
//
//	dmcs -d:DOTNET -r:System.Xaml -debug System.Xaml/XamlObjectWriter.cs System.Xaml/XamlWriterInternalBase.cs System.Xaml/TypeExtensionMethods.cs System.Xaml/XamlWriterStateManager.cs System.Xaml/XamlNameResolver.cs System.Xaml/PrefixLookup.cs System.Xaml/ValueSerializerContext.cs ../../build/common/MonoTODOAttribute.cs Test/System.Xaml/TestedTypes.cs

/*

State transition:

* StartObject or GetObject
	These start a new object instance, either by creating new or getting
	from parent.
* Value
	This either becomes an entire property value, or an item of current
	collection, or a key or a value item of current dictionary, or an
	entire object if it is either Initialization.
* EndObject
	Almost the same as Value. Though the it is likely already instantiated.
* StartMember
	Indicates a new property as current.
* EndMember
	It accompanies a property value (might be lacking), or ends a
	collection (including those for PositionalParameters), or ends a key
	property of a dictionary element (if it is Key), or ends an entire
	value of current object if it is Initialization.


*/

#if DOTNET
namespace Mono.Xaml
#else
namespace System.Xaml
#endif
{
	public class XamlObjectWriter : XamlWriter, IXamlLineInfoConsumer
	{
		public XamlObjectWriter (XamlSchemaContext schemaContext)
			: this (schemaContext, null)
		{
		}

		public XamlObjectWriter (XamlSchemaContext schemaContext, XamlObjectWriterSettings settings)
		{
			if (schemaContext == null)
				throw new ArgumentNullException ("schemaContext");
			this.sctx = schemaContext;
			this.settings = settings ?? new XamlObjectWriterSettings ();
			var manager = new XamlWriterStateManager<XamlObjectWriterException, XamlObjectWriterException> (false);
			intl = new XamlObjectWriterInternal (this, sctx, manager);
		}

		XamlSchemaContext sctx;
		XamlObjectWriterSettings settings;

		XamlObjectWriterInternal intl;

		//int line, column;
		bool lineinfo_was_given;

		internal XamlObjectWriterSettings Settings {
			get { return settings; }
		}

		public virtual object Result {
			get { return intl.Result; }
		}

		public INameScope RootNameScope {
			get { return intl.NameScope; }
		}

		public override XamlSchemaContext SchemaContext {
			get { return sctx; }
		}

		public bool ShouldProvideLineInfo {
			get { return lineinfo_was_given; }
		}

		public void SetLineInfo (int lineNumber, int linePosition)
		{
//			line = lineNumber;
//			column = linePosition;
			lineinfo_was_given = true;
		}
		
		public void Clear ()
		{
			throw new NotImplementedException ();
		}

		protected override void Dispose (bool disposing)
		{
			if (!disposing)
				return;

			intl.CloseAll ();
		}

		protected internal virtual void OnAfterBeginInit (object value)
		{
			if (settings.AfterBeginInitHandler != null)
				settings.AfterBeginInitHandler (this, new XamlObjectEventArgs (value));
		}

		protected internal virtual void OnAfterEndInit (object value)
		{
			if (settings.AfterEndInitHandler != null)
				settings.AfterEndInitHandler (this, new XamlObjectEventArgs (value));
		}

		protected internal virtual void OnAfterProperties (object value)
		{
			if (settings.AfterPropertiesHandler != null)
				settings.AfterPropertiesHandler (this, new XamlObjectEventArgs (value));
		}

		protected internal virtual void OnBeforeProperties (object value)
		{
			if (settings.BeforePropertiesHandler != null)
				settings.BeforePropertiesHandler (this, new XamlObjectEventArgs (value));
		}

		protected internal virtual bool OnSetValue (object eventSender, XamlMember member, object value)
		{
			if (settings.XamlSetValueHandler != null) {
				var args = new XamlSetValueEventArgs (member, value);
				settings.XamlSetValueHandler (eventSender, args);
				return args.Handled;
			}
			return false;
		}

		public override void WriteGetObject ()
		{
			intl.WriteGetObject ();
		}

		public override void WriteNamespace (NamespaceDeclaration namespaceDeclaration)
		{
			intl.WriteNamespace (namespaceDeclaration);
		}

		public override void WriteStartObject (XamlType xamlType)
		{
			intl.WriteStartObject (xamlType);
		}
		
		public override void WriteValue (object value)
		{
			intl.WriteValue (value);
		}
		
		public override void WriteStartMember (XamlMember property)
		{
			intl.WriteStartMember (property);
		}
		
		public override void WriteEndObject ()
		{
			intl.WriteEndObject ();
		}

		public override void WriteEndMember ()
		{
			intl.WriteEndMember ();
		}
	}

	// specific implementation
	class XamlObjectWriterInternal : XamlWriterInternalBase
	{
		const string Xmlns2000Namespace = "http://www.w3.org/2000/xmlns/";

		public XamlObjectWriterInternal (XamlObjectWriter source, XamlSchemaContext schemaContext, XamlWriterStateManager manager)
			: base (schemaContext, manager)
		{
			this.source = source;
			this.sctx = schemaContext;
			var ext = source.Settings.ExternalNameScope;
			name_scope = ext != null && source.Settings.RegisterNamesOnExternalNamescope ? ext : new NameScope (ext);
		}
		
		XamlObjectWriter source;
		XamlSchemaContext sctx;
		INameScope name_scope;
		List<NameFixupRequired> pending_name_references = new List<NameFixupRequired> ();
		AmbientProvider ambient_provider = new AmbientProvider ();

		public INameScope NameScope {
			get { return name_scope; }
		}

		public object Result { get; set; }
		
		protected override void OnWriteStartObject ()
		{
			var state = object_states.Pop ();
			if (object_states.Count > 0) {
				var pstate = object_states.Peek ();
				if (CurrentMemberState.Value != null)
					throw new XamlDuplicateMemberException (String.Format ("Member '{0}' is already written to current type '{1}'", CurrentMember, pstate.Type));
			} else {
				var obj = source.Settings.RootObjectInstance;
				if (obj != null) {
					if (state.Type.UnderlyingType != null && !state.Type.UnderlyingType.IsAssignableFrom (obj.GetType ()))
						throw new XamlObjectWriterException (String.Format ("RootObjectInstance type '{0}' is not assignable to '{1}'", obj.GetType (), state.Type));
					state.Value = obj;
					state.IsInstantiated = true;
				}
				root_state = state;
			}
			object_states.Push (state);
			if (!state.Type.IsContentValue (service_provider))
				InitializeObjectIfRequired (true);

			state.IsXamlWriterCreated = true;
			source.OnBeforeProperties (state.Value);
		}

		protected override void OnWriteGetObject ()
		{
			var state = object_states.Pop ();
			var xm = CurrentMember;
			var instance = xm.Invoker.GetValue (object_states.Peek ().Value);
			if (instance == null)
				throw new XamlObjectWriterException (String.Format ("The value  for '{0}' property is null", xm.Name));
			state.Value = instance;
			state.IsInstantiated = true;
			object_states.Push (state);
		}

		protected override void OnWriteEndObject ()
		{
			InitializeObjectIfRequired (false); // this is required for such case that there was no StartMember call.

			var state = object_states.Pop ();
			var obj = state.Value;
			
			if (obj is MarkupExtension) {
				try {
					obj = ((MarkupExtension) obj).ProvideValue (service_provider);
				} catch (XamlObjectWriterException) {
					throw;
				} catch (Exception ex) {
					throw new XamlObjectWriterException ("An error occured on getting provided value", ex);
				}
			}
			
			// call this (possibly) before the object is added to parent collection. (bug #3003 also expects this)
			if (state.IsXamlWriterCreated)
				source.OnAfterProperties (obj);
			
			var nfr = obj as NameFixupRequired;
			if (nfr != null && object_states.Count > 0) { // IF the root object to be written is x:Reference, then the Result property will become the NameFixupRequired. That's what .NET also does.
				// actually .NET seems to seek "parent" object in its own IXamlNameResolver implementation.
				var pstate = object_states.Peek ();
				nfr.ParentType = pstate.Type;
				nfr.ParentMember = CurrentMember; // Note that it is a member of the pstate.
				nfr.ParentValue = pstate.Value;
				pending_name_references.Add ((NameFixupRequired) obj);
			}
			else
				StoreAppropriatelyTypedValue (obj, state.KeyValue);
			
			if (state.Type.IsAmbient)
				ambient_provider.Pop ();
			else
				HandleEndInit (obj);
			
			object_states.Push (state);
			if (object_states.Count == 1) {
				Result = obj;
				ResolvePendingReferences ();
			}
		}

		Stack<object> escaped_objects = new Stack<object> ();

		protected override void OnWriteStartMember (XamlMember property)
		{
			if (property == XamlLanguage.PositionalParameters ||
			    property == XamlLanguage.Arguments) {
				var state = object_states.Peek ();
				escaped_objects.Push (state.Value);
				state.Value = new List<object> ();
			}

			// FIXME: this condition needs to be examined. What is known to be prevented are: PositionalParameters, Initialization and Base (the last one sort of indicates there's a lot more).
			else if (!(property is XamlDirective))
				InitializeObjectIfRequired (false);
		}

		static readonly BindingFlags static_flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;

		protected override void OnWriteEndMember ()
		{
			var xm = CurrentMember;
			var state = object_states.Peek ();

			if (xm == XamlLanguage.PositionalParameters) {
				var l = (List<object>) state.Value;
				state.Value = escaped_objects.Pop ();
				state.IsInstantiated = true;
				PopulateObject (true, l);
				return;
			} else if (xm == XamlLanguage.Arguments) {
				if (state.FactoryMethod != null) {
					var contents = (List<object>) state.Value;
					var mi = state.Type.UnderlyingType.GetMethods (static_flags).FirstOrDefault (mii => mii.Name == state.FactoryMethod && mii.GetParameters ().Length == contents.Count);
					if (mi == null)
						throw new XamlObjectWriterException (String.Format ("Specified static factory method '{0}' for type '{1}' was not found", state.FactoryMethod, state.Type));
					state.Value = mi.Invoke (null, contents.ToArray ());
				}
				else
					PopulateObject (false, (List<object>) state.Value);
				state.IsInstantiated = true;
				escaped_objects.Pop ();
			} else if (xm == XamlLanguage.Initialization) {
				// ... and no need to do anything. The object value to pop *is* the return value.
			} else if (xm == XamlLanguage.Name || xm == state.Type.GetAliasedProperty (XamlLanguage.Name)) {
				string name = (string) CurrentMemberState.Value;
				name_scope.RegisterName (name, state.Value);
			} else {
				if (xm.IsEvent)
					SetEvent (xm, (string) CurrentMemberState.Value);
				else if (!xm.IsReadOnly) // exclude read-only object such as collection item.
					SetValue (xm, CurrentMemberState.Value);
			}
		}

		void SetEvent (XamlMember member, string value)
		{
			if (member.UnderlyingMember == null)
				throw new XamlObjectWriterException (String.Format ("Event {0} has no underlying member to attach event", member));

			int idx = value.LastIndexOf ('.');
			var xt = idx < 0 ? root_state.Type : ResolveTypeFromName (value.Substring (0, idx));
			if (xt == null)
				throw new XamlObjectWriterException (String.Format ("Referenced type {0} in event {1} was not found", value, member));
			if (xt.UnderlyingType == null)
				throw new XamlObjectWriterException (String.Format ("Referenced type {0} in event {1} has no underlying type", value, member));
			string mn = idx < 0 ? value : value.Substring (idx + 1);
			var ev = (EventInfo) member.UnderlyingMember;
			// get an appropriate MethodInfo overload whose signature matches the event's handler type.
			// FIXME: this may need more strict match. RuntimeBinder may be useful here.
			var eventMethodParams = ev.EventHandlerType.GetMethod ("Invoke").GetParameters ();
			
			var target = root_state.Value;
			var mi = target.GetType().GetMethod (mn, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, null, (from pi in eventMethodParams select pi.ParameterType).ToArray (), null);
			if (mi == null)
				throw new XamlObjectWriterException (String.Format ("Referenced value method {0} in type {1} indicated by event {2} was not found", mn, value, member));
			var obj = object_states.Peek ().Value;
			ev.AddEventHandler (obj, Delegate.CreateDelegate (ev.EventHandlerType, target, mi));
		}

		void SetValue (XamlMember member, object value)
		{
			if (member == XamlLanguage.FactoryMethod)
				object_states.Peek ().FactoryMethod = (string) value;
			else if (member.IsDirective)
				return;
			else
				SetValue (member, object_states.Peek ().Value, value);
		}
		
		void SetValue (XamlMember member, object target, object value)
		{
			if (!source.OnSetValue (target, member, value))
				member.Invoker.SetValue (target, value);
		}

		void PopulateObject (bool considerPositionalParameters, IList<object> contents)
		{
			var state = object_states.Peek ();

			var args = state.Type.GetSortedConstructorArguments ().ToArray ();
			var argt = args != null ? (IList<XamlType>) (from arg in args select arg.Type).ToArray () : considerPositionalParameters ? state.Type.GetPositionalParameters (contents.Count) : null;

			var argv = new object [argt.Count];
			for (int i = 0; i < argv.Length; i++)
				argv [i] = GetCorrectlyTypedValue (args [i], argt [i], contents [i]);
			state.Value = state.Type.Invoker.CreateInstance (argv);
			state.IsInstantiated = true;
			if (state.Type.IsAmbient)
				ambient_provider.Push (new AmbientPropertyValue (CurrentMember, state.Value));
			HandleBeginInit (state.Value);
		}

		protected override void OnWriteValue (object value)
		{
			if (CurrentMemberState.Value != null)
				throw new XamlDuplicateMemberException (String.Format ("Member '{0}' is already written to current type '{1}'", CurrentMember, object_states.Peek ().Type));
			StoreAppropriatelyTypedValue (value, null);
		}

		protected override void OnWriteNamespace (NamespaceDeclaration nd)
		{
			// nothing to do here.
		}
		
		void StoreAppropriatelyTypedValue (object obj, object keyObj)
		{
			var ms = CurrentMemberState; // note that this retrieves parent's current property for EndObject.
			if (ms != null) {
				var state = object_states.Peek ();
				var parent = state.Value;
				var xt = state.Type;
				var xm = ms.Member;
				if (xm == XamlLanguage.Initialization) {
					state.Value = GetCorrectlyTypedValue (null, xt, obj);
					state.IsInstantiated = true;
				} else if (xm.IsEvent) {
					ms.Value = (string) obj; // save name of value delegate (method).
					state.IsInstantiated = true;
				} else if (xm.Type.IsXData) {
					var xdata = (XData) obj;
					var ixser = xm.Invoker.GetValue (state.Value) as IXmlSerializable;
					if (ixser != null)
						ixser.ReadXml ((XmlReader) xdata.XmlReader);
				}
				else if (xm == XamlLanguage.Base)
					ms.Value = GetCorrectlyTypedValue (null, xm.Type, obj);
				else if (xm == XamlLanguage.Name || xm == xt.GetAliasedProperty (XamlLanguage.Name))
					ms.Value = GetCorrectlyTypedValue (xm, XamlLanguage.String, obj);
				else if (xm == XamlLanguage.Key)
					state.KeyValue = GetCorrectlyTypedValue (null, xt.KeyType, obj);
				else {
					if (!AddToCollectionIfAppropriate (xt, xm, parent, obj, keyObj)) {
						if (!xm.IsReadOnly)
							ms.Value = GetCorrectlyTypedValue (xm, xm.Type, obj);
					}
				}
			}
		}

		bool AddToCollectionIfAppropriate (XamlType xt, XamlMember xm, object parent, object obj, object keyObj)
		{
			var mt = xm.Type;
			if (xm == XamlLanguage.Items ||
			    xm == XamlLanguage.PositionalParameters ||
			    xm == XamlLanguage.Arguments) {
				if (xt.IsDictionary)
					mt.Invoker.AddToDictionary (parent, GetCorrectlyTypedValue (null, xt.KeyType, keyObj), GetCorrectlyTypedValue (null, xt.ItemType, obj));
				else // collection. Note that state.Type isn't usable for PositionalParameters to identify collection kind.
					mt.Invoker.AddToCollection (parent, GetCorrectlyTypedValue (null, xt.ItemType, obj));
				return true;
			}
			else
				return false;
		}

		object GetCorrectlyTypedValue (XamlMember xm, XamlType xt, object value)
		{
			try {
				return DoGetCorrectlyTypedValue (xm, xt, value);
			} catch (XamlObjectWriterException) {
				throw;
			} catch (Exception ex) {
				// For + ex.Message, the runtime should print InnerException message like .NET does.
				throw new XamlObjectWriterException (String.Format ("Could not convert object \'{0}' (of type {1}) to {2}: ", value, value != null ? (object) value.GetType () : "(null)", xt)  + ex.Message, ex);
			}
		}

		// It expects that it is not invoked when there is no value to 
		// assign.
		// When it is passed null, then it returns a default instance.
		// For example, passing null as Int32 results in 0.
		// But do not immediately try to instantiate with the type, since the type might be abstract.
		object DoGetCorrectlyTypedValue (XamlMember xm, XamlType xt, object value)
		{
			if (value == null) {
				if (xt.IsContentValue (service_provider)) // it is for collection/dictionary key and item
					return null;
				else
					return xt.IsNullable ? null : xt.Invoker.CreateInstance (new object [0]);
			}
			if (xt == null)
				return value;

			// Not sure if this is really required though...
			var vt = sctx.GetXamlType (value.GetType ());
			if (vt.CanAssignTo (xt))
				return value;

			// FIXME: this could be generalized by some means, but I cannot find any.
			if (xt.UnderlyingType == typeof (XamlType) && value is string)
				value = ResolveTypeFromName ((string) value);

			// FIXME: this could be generalized by some means, but I cannot find any.
			if (xt.UnderlyingType == typeof (Type))
				value = new TypeExtension ((string) value).ProvideValue (service_provider);
			if (xt == XamlLanguage.Type && value is string)
				value = new TypeExtension ((string) value);
			
			if (IsAllowedType (xt, value))
				return value;

			var xtc = (xm != null ? xm.TypeConverter : null) ?? xt.TypeConverter;
			if (xtc != null && value != null) {
				var tc = xtc.ConverterInstance;
				if (tc != null && tc.CanConvertFrom (value.GetType ()))
					value = tc.ConvertFrom (value);
				return value;
			}

			throw new XamlObjectWriterException (String.Format ("Value '{0}' (of type {1}) is not of or convertible to type {0} (member {3})", value, value != null ? (object) value.GetType () : "(null)", xt, xm));
		}

		XamlType ResolveTypeFromName (string name)
		{
			var nsr = (IXamlNamespaceResolver) service_provider.GetService (typeof (IXamlNamespaceResolver));
			return sctx.GetXamlType (XamlTypeName.Parse (name, nsr));
		}

		bool IsAllowedType (XamlType xt, object value)
		{
			return  xt == null ||
				xt.UnderlyingType == null ||
				xt.UnderlyingType.IsInstanceOfType (value) ||
				value == null && xt == XamlLanguage.Null ||
				xt.IsMarkupExtension && IsAllowedType (xt.MarkupExtensionReturnType, value);
		}
		
		void InitializeObjectIfRequired (bool waitForParameters)
		{
			var state = object_states.Peek ();
			if (state.IsInstantiated)
				return;

			if (waitForParameters && (state.Type.ConstructionRequiresArguments || state.Type.HasPositionalParameters (service_provider)))
				return;

			// FIXME: "The default techniques in absence of a factory method are to attempt to find a default constructor, then attempt to find an identified type converter on type, member, or destination type."
			// http://msdn.microsoft.com/en-us/library/system.xaml.xamllanguage.factorymethod%28VS.100%29.aspx
			object obj;
			if (state.FactoryMethod != null) // FIXME: it must be implemented and verified with tests.
				throw new NotImplementedException ();
			else
				obj = state.Type.Invoker.CreateInstance (null);
			state.Value = obj;
			state.IsInstantiated = true;
			if (state.Type.IsAmbient)
				ambient_provider.Push (new AmbientPropertyValue (CurrentMember, obj));
			else
				HandleBeginInit (obj);
		}

		internal IXamlNameResolver name_resolver {
			get { return (IXamlNameResolver) service_provider.GetService (typeof (IXamlNameResolver)); }
		}

		internal override IAmbientProvider AmbientProvider {
			get { return ambient_provider; }
		}

		void ResolvePendingReferences ()
		{
			foreach (var fixup in pending_name_references) {
				foreach (var name in fixup.Names) {
					bool isFullyInitialized;
					// FIXME: sort out relationship between name_scope and name_resolver. (unify to name_resolver, probably)
					var obj = name_scope.FindName (name) ?? name_resolver.Resolve (name, out isFullyInitialized);
					if (obj == null)
						throw new XamlObjectWriterException (String.Format ("Unresolved object reference '{0}' was found", name));
					if (!AddToCollectionIfAppropriate (fixup.ParentType, fixup.ParentMember, fixup.ParentValue, obj, null)) // FIXME: is keyObj always null?
						SetValue (fixup.ParentMember, fixup.ParentValue, obj);
				}
			}
		}
		
		void HandleBeginInit (object value)
		{
			var si = value as ISupportInitialize;
			if (si == null)
				return;
			si.BeginInit ();
			source.OnAfterBeginInit (value);
		}
		
		void HandleEndInit (object value)
		{
			var si = value as ISupportInitialize;
			if (si == null)
				return;
			si.EndInit ();
			source.OnAfterEndInit (value);
		}
	}
}
