using System;
using System.Reflection;
using System.Collections;
using Server;
using Server.Targeting;
using Server.Items;
using Server.Gumps;
using CPA = Server.CommandPropertyAttribute;
using Server.Commands;
using Server.Commands.Generic;

namespace Server.Commands
{
	public enum PropertyAccess
	{
		Read		= 0x01,
		Write		= 0x02,
		ReadWrite	= Read | Write
	}

	public class Properties
	{
		public static void Initialize()
		{
			CommandSystem.Register( "Props", AccessLevel.Counselor, new CommandEventHandler( Props_OnCommand ) );
		}

		private class PropsTarget : Target
		{
			public PropsTarget() : base( -1, true, TargetFlags.None )
			{
			}

			protected override void OnTarget( Mobile from, object o )
			{
				if ( !BaseCommand.IsAccessible( from, o ) )
					from.SendMessage( "That is not accessible." );
				else
					from.SendGump( new PropertiesGump( from, o ) );
			}
		}

		[Usage( "Props [serial]" )]
		[Description( "Opens a menu where you can view and edit all properties of a targeted (or specified) object." )]
		private static void Props_OnCommand( CommandEventArgs e )
		{
			if ( e.Length == 1 )
			{
				IEntity ent = World.FindEntity( e.GetInt32( 0 ) );

				if ( ent == null )
					e.Mobile.SendMessage( "No object with that serial was found." );
				else if ( !BaseCommand.IsAccessible( e.Mobile, ent ) )
					e.Mobile.SendMessage( "That is not accessible." );
				else
					e.Mobile.SendGump( new PropertiesGump( e.Mobile, ent ) );
			}
			else
			{
				e.Mobile.Target = new PropsTarget();
			}
		}

		private static bool CIEqual( string l, string r )
		{
			return Insensitive.Equals( l, r );
		}

		private static Type typeofCPA = typeof( CPA );

		public static CPA GetCPA( PropertyInfo p )
		{
			object[] attrs = p.GetCustomAttributes( typeofCPA, false );

			if ( attrs.Length == 0 )
				return null;

			return attrs[0] as CPA;
		}

		public static PropertyInfo[] GetPropertyInfoChain( Mobile from, Type type, string propertyString, PropertyAccess endAccess, ref string failReason )
		{
			string[] split = propertyString.Split( '.' );

			if ( split.Length == 0 )
				return null;

			PropertyInfo[] info = new PropertyInfo[split.Length];

			for ( int i = 0; i < info.Length; ++i )
			{
				string propertyName = split[i];

				if ( CIEqual( propertyName, "current" ) )
					continue;

				PropertyInfo[] props = type.GetProperties( BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public );

				bool isFinal = ( i == (info.Length - 1) );

				PropertyAccess access = endAccess;

				if ( !isFinal )
					access |= PropertyAccess.Read;

				for ( int j = 0; j < props.Length; ++j )
				{
					PropertyInfo p = props[j];

					if ( CIEqual( p.Name, propertyName ) )
					{
						CPA attr = GetCPA( p );

						if ( attr == null )
						{
							failReason = String.Format( "Property '{0}' not found.", propertyName );
							return null;
						}
						else if ( (access & PropertyAccess.Read) != 0 && from.AccessLevel < attr.ReadLevel )
						{
							failReason = String.Format( "You must be at least {0} to get the property '{1}'.",
								Mobile.GetAccessLevelName( attr.ReadLevel ), propertyName );

							return null;
						}
						else if ( (access & PropertyAccess.Write) != 0 && from.AccessLevel < attr.WriteLevel )
						{
							failReason = String.Format( "You must be at least {0} to set the property '{1}'.",
								Mobile.GetAccessLevelName( attr.WriteLevel ), propertyName );

							return null;
						}
						else if ( (access & PropertyAccess.Read) != 0 && !p.CanRead )
						{
							failReason = String.Format( "Property '{0}' is write only.", propertyName );
							return null;
						}
						else if ( (access & PropertyAccess.Write) != 0 && (!p.CanWrite || attr.ReadOnly) && isFinal )
						{
							failReason = String.Format( "Property '{0}' is read only.", propertyName );
							return null;
						}

						info[i] = p;
						type = p.PropertyType;
						break;
					}
				}

				if ( info[i] == null )
				{
					failReason = String.Format( "Property '{0}' not found.", propertyName );
					return null;
				}
			}

			return info;
		}

		public static PropertyInfo GetPropertyInfo( Mobile from, ref object obj, string propertyName, PropertyAccess access, ref string failReason )
		{
			PropertyInfo[] chain = GetPropertyInfoChain( from, obj.GetType(), propertyName, access, ref failReason );

			if ( chain == null )
				return null;

			return GetPropertyInfo( ref obj, chain, ref failReason );
		}

		public static PropertyInfo GetPropertyInfo( ref object obj, PropertyInfo[] chain, ref string failReason )
		{
			if ( chain == null || chain.Length == 0 )
			{
				failReason = "Property chain is empty.";
				return null;
			}

			for ( int i = 0; i < chain.Length - 1; ++i )
			{
				if ( chain[i] == null )
					continue;

				obj = chain[i].GetValue( obj, null );

				if ( obj == null )
				{
					failReason = String.Format( "Property '{0}' is null.", chain[i] );
					return null;
				}
			}

			return chain[chain.Length-1];
		}

		public static string GetValue( Mobile from, object o, string name )
		{
			string failReason = "";

			PropertyInfo[] chain = GetPropertyInfoChain( from, o.GetType(), name, PropertyAccess.Read, ref failReason );

			if ( chain == null || chain.Length == 0 )
				return failReason;

			PropertyInfo p = GetPropertyInfo( ref o, chain, ref failReason );

			if ( p == null )
				return failReason;

			return InternalGetValue( o, p, chain );
		}

		public static string IncreaseValue( Mobile from, object o, string[] args )
		{
			Type type = o.GetType();

			object[] realObjs = new object[args.Length/2];
			PropertyInfo[] realProps = new PropertyInfo[args.Length/2];
			int[] realValues = new int[args.Length/2];

			bool positive = false, negative = false;

			for ( int i = 0; i < realProps.Length; ++i )
			{
				string name = args[i*2];

				try
				{
					string valueString = args[1 + (i*2)];

					if ( valueString.StartsWith( "0x" ) )
					{
						realValues[i] = Convert.ToInt32( valueString.Substring( 2 ), 16 );
					}
					else
					{
						realValues[i] = Convert.ToInt32( valueString );
					}
				}
				catch
				{
					return "Offset value could not be parsed.";
				}

				if ( realValues[i] > 0 )
					positive = true;
				else if ( realValues[i] < 0 )
					negative = true;
				else
					return "Zero is not a valid value to offset.";

				string failReason = null;
				realObjs[i] = o;
				realProps[i] = GetPropertyInfo( from, ref realObjs[i], name, PropertyAccess.ReadWrite, ref failReason );

				if ( failReason != null )
					return failReason;

				if ( realProps[i] == null )
					return "Property not found.";
			}

			for ( int i = 0; i < realProps.Length; ++i )
			{
				object obj = realProps[i].GetValue( realObjs[i], null );

				if( !( obj is IConvertible ) )
					return "Property is not IConvertable.";

				try
				{

				long v = (long)Convert.ChangeType( obj, TypeCode.Int64 );
				v += realValues[i];

				realProps[i].SetValue( realObjs[i], Convert.ChangeType( v, realProps[i].PropertyType ), null );
				}
				catch
				{
					return "Value could not be converted";
				}
			}

			if ( realProps.Length == 1 )
			{
				if ( positive )
					return "The property has been increased.";

				return "The property has been decreased.";
			}

			if ( positive && negative )
				return "The properties have been changed.";

			if ( positive )
				return "The properties have been increased.";

			return "The properties have been decreased.";
		}

		private static string InternalGetValue( object o, PropertyInfo p )
		{
			return InternalGetValue( o, p, null );
		}

		private static string InternalGetValue( object o, PropertyInfo p, PropertyInfo[] chain )
		{
			Type type = p.PropertyType;

			object value = p.GetValue( o, null );
			string toString;

			if ( value == null )
				toString = "null";
			else if ( IsNumeric( type ) )
				toString = String.Format( "{0} (0x{0:X})", value );
			else if ( IsChar( type ) )
				toString = String.Format( "'{0}' ({1} [0x{1:X}])", value, (int) value );
			else if ( IsString( type ) )
				toString = ( (string) value == "null" ? @"@""null""" : String.Format( "\"{0}\"", value ) );
			else
				toString = value.ToString();

			if ( chain == null )
				return String.Format( "{0} = {1}", p.Name, toString );

			string[] concat = new string[chain.Length*2+1];

			for ( int i = 0; i < chain.Length; ++i )
			{
				concat[(i*2)+0] = chain[i].Name;
				concat[(i*2)+1] = ( i < (chain.Length - 1) ) ? "." : " = ";
			}

			concat[concat.Length-1] = toString;

			return String.Concat( concat );
		}

		public static string SetValue( Mobile from, object o, string name, string value )
		{
			object logObject = o;

			string failReason = "";
			PropertyInfo p = GetPropertyInfo( from, ref o, name, PropertyAccess.Write, ref failReason );

			if ( p == null )
				return failReason;

			return InternalSetValue( from, logObject, o, p, name, value, true );
		}

		private static Type typeofSerial = typeof( Serial );

		private static bool IsSerial( Type t )
		{
			return ( t == typeofSerial );
		}

		private static Type typeofType = typeof( Type );

		private static bool IsType( Type t )
		{
			return ( t == typeofType );
		}

		private static Type typeofChar = typeof( Char );

		private static bool IsChar( Type t )
		{
			return ( t == typeofChar );
		}

		private static Type typeofString = typeof( String );

		private static bool IsString( Type t )
		{
			return ( t == typeofString );
		}

		private static bool IsEnum( Type t )
		{
			return t.IsEnum;
		}

		private static Type typeofTimeSpan = typeof( TimeSpan );
		private static Type typeofParsable = typeof( ParsableAttribute );

		private static bool IsParsable( Type t )
		{
			return ( t == typeofTimeSpan || t.IsDefined( typeofParsable, false ) );
		}

		private static Type[] m_ParseTypes = new Type[]{ typeof( string ) };
		private static object[] m_ParseParams = new object[1];

		private static object Parse( object o, Type t, string value )
		{
			MethodInfo method = t.GetMethod( "Parse", m_ParseTypes );

			m_ParseParams[0] = value;

			return method.Invoke( o, m_ParseParams );
		}

		private static Type[] m_NumericTypes = new Type[]
			{
				typeof( Byte ), typeof( SByte ),
				typeof( Int16 ), typeof( UInt16 ),
				typeof( Int32 ), typeof( UInt32 ),
				typeof( Int64 ), typeof( UInt64 )
			};

		private static bool IsNumeric( Type t )
		{
			return ( Array.IndexOf( m_NumericTypes, t ) >= 0 );
		}

		public static string ConstructFromString( Type type, object obj, string value, ref object constructed )
		{
			object toSet;
			bool isSerial = IsSerial( type );

			if ( isSerial ) // mutate into int32
				type = m_NumericTypes[4];

			if ( value == "(-null-)" && !type.IsValueType )
				value = null;

			if ( IsEnum( type ) )
			{
				try
				{
					toSet = Enum.Parse( type, value, true );
				}
				catch
				{
					return "That is not a valid enumeration member.";
				}
			}
			else if ( IsType( type ) )
			{
				try
				{
					toSet = ScriptCompiler.FindTypeByName( value );

					if ( toSet == null )
						return "No type with that name was found.";
				}
				catch
				{
					return "No type with that name was found.";
				}
			}
			else if ( IsParsable( type ) )
			{
				try
				{
					toSet = Parse( obj, type, value );
				}
				catch
				{
					return "That is not properly formatted.";
				}
			}
			else if ( value == null )
			{
				toSet = null;
			}
			else if ( value.StartsWith( "0x" ) && IsNumeric( type ) )
			{
				try
				{
					toSet = Convert.ChangeType( Convert.ToUInt64( value.Substring( 2 ), 16 ), type );
				}
				catch
				{
					return "That is not properly formatted.";
				}
			}
			else
			{
				try
				{
					toSet = Convert.ChangeType( value, type );
				}
				catch
				{
					return "That is not properly formatted.";
				}
			}

			if ( isSerial ) // mutate back
				toSet = (Serial)((Int32)toSet);

			constructed = toSet;
			return null;
		}

		public static string SetDirect( Mobile from, object logObject, object obj, PropertyInfo prop, string givenName, object toSet, bool shouldLog )
		{
			try
			{
				if ( toSet is AccessLevel )
				{
					AccessLevel newLevel = (AccessLevel) toSet;
					AccessLevel reqLevel = AccessLevel.Administrator;

					if ( newLevel == AccessLevel.Administrator )
						reqLevel = AccessLevel.Developer;
					else if ( newLevel >= AccessLevel.Developer )
						reqLevel = AccessLevel.Owner;

					if ( from.AccessLevel < reqLevel )
						return "You do not have access to that level.";
				}

				if ( shouldLog )
					CommandLogging.LogChangeProperty( from, logObject, givenName, toSet == null ? "(-null-)" : toSet.ToString() );

				prop.SetValue( obj, toSet, null );
				return "Property has been set.";
			}
			catch
			{
				return "An exception was caught, the property may not be set.";
			}
		}

		public static string SetDirect( object obj, PropertyInfo prop, object toSet )
		{
			try
			{
				if ( toSet is AccessLevel )
				{
					return "You do not have access to that level.";
				}

				prop.SetValue( obj, toSet, null );
				return "Property has been set.";
			}
			catch
			{
				return "An exception was caught, the property may not be set.";
			}
		}

		public static string InternalSetValue( Mobile from, object logobj, object o, PropertyInfo p, string pname, string value, bool shouldLog )
		{
			object toSet = null;
			string result = ConstructFromString( p.PropertyType, o, value, ref toSet );

			if ( result != null )
				return result;

			return SetDirect( from, logobj, o, p, pname, toSet, shouldLog );
		}

		public static string InternalSetValue( object o, PropertyInfo p, string value )
		{
			object toSet = null;
			string result = ConstructFromString( p.PropertyType, o, value, ref toSet );

			if ( result != null )
				return result;

			return SetDirect( o, p, toSet );
		}
	}
}

namespace Server
{
	public abstract class PropertyException : ApplicationException
	{
		protected Property m_Property;

		public Property Property
		{
			get { return m_Property; }
		}

		public PropertyException( Property property, string message )
			: base( message )
		{
			m_Property = property;
		}
	}

	public abstract class BindingException : PropertyException
	{
		public BindingException( Property property, string message )
			: base( property, message )
		{
		}
	}

	public sealed class NotYetBoundException : BindingException
	{
		public NotYetBoundException( Property property )
			: base( property, String.Format( "Property has not yet been bound." ) )
		{
		}
	}

	public sealed class AlreadyBoundException : BindingException
	{
		public AlreadyBoundException( Property property )
			: base( property, String.Format( "Property has already been bound." ) )
		{
		}
	}

	public sealed class UnknownPropertyException : BindingException
	{
		public UnknownPropertyException( Property property, string current )
			: base( property, String.Format( "Property '{0}' not found.", current ) )
		{
		}
	}

	public sealed class ReadOnlyException : BindingException
	{
		public ReadOnlyException( Property property )
			: base( property, "Property is read-only." )
		{
		}
	}

	public sealed class WriteOnlyException : BindingException
	{
		public WriteOnlyException( Property property )
			: base( property, "Property is write-only." )
		{
		}
	}

	public abstract class AccessException : PropertyException
	{
		public AccessException( Property property, string message )
			: base( property, message )
		{
		}
	}

	public sealed class InternalAccessException : AccessException
	{
		public InternalAccessException( Property property )
			: base( property, "Property is internal." )
		{
		}
	}

	public abstract class ClearanceException : AccessException
	{
		protected AccessLevel m_PlayerAccess;
		protected AccessLevel m_NeededAccess;

		public AccessLevel PlayerAccess
		{
			get { return m_PlayerAccess; }
		}

		public AccessLevel NeededAccess
		{
			get { return m_NeededAccess; }
		}

		public ClearanceException( Property property, AccessLevel playerAccess, AccessLevel neededAccess, string accessType )
			: base( property, string.Format(
				"You must be at least {0} to {1} this property.",
				Mobile.GetAccessLevelName( neededAccess ),
				accessType
			) )
		{
		}
	}

	public sealed class ReadAccessException : ClearanceException
	{
		public ReadAccessException( Property property, AccessLevel playerAccess, AccessLevel neededAccess )
			: base( property, playerAccess, neededAccess, "read" )
		{
		}
	}

	public sealed class WriteAccessException : ClearanceException
	{
		public WriteAccessException( Property property, AccessLevel playerAccess, AccessLevel neededAccess )
			: base( property, playerAccess, neededAccess, "write" )
		{
		}
	}

	public sealed class Property
	{
		private string m_Binding;

		private PropertyInfo[] m_Chain;
		private PropertyAccess m_Access;

		public string Binding
		{
			get { return m_Binding; }
		}

		public bool IsBound
		{
			get { return ( m_Chain != null ); }
		}

		public PropertyAccess Access
		{
			get { return m_Access; }
		}

		public PropertyInfo[] Chain
		{
			get
			{
				if ( !IsBound )
					throw new NotYetBoundException( this );

				return m_Chain;
			}
		}

		public Type Type
		{
			get
			{
				if ( !IsBound )
					throw new NotYetBoundException( this );

				return m_Chain[m_Chain.Length - 1].PropertyType;
			}
		}

		public bool CheckAccess( Mobile from )
		{
			if ( !IsBound )
				throw new NotYetBoundException( this );

			for ( int i = 0; i < m_Chain.Length; ++i )
			{
				PropertyInfo prop = m_Chain[i];

				bool isFinal = ( i == ( m_Chain.Length - 1 ) );

				PropertyAccess access = m_Access;

				if ( !isFinal )
					access |= PropertyAccess.Read;

				CPA security = Properties.GetCPA( prop );

				if ( security == null )
					throw new InternalAccessException( this );

				if ( ( access & PropertyAccess.Read ) != 0 && from.AccessLevel < security.ReadLevel )
					throw new ReadAccessException( this, from.AccessLevel, security.ReadLevel );

				if ( ( access & PropertyAccess.Write ) != 0 && (from.AccessLevel < security.WriteLevel || security.ReadOnly) )
					throw new WriteAccessException( this, from.AccessLevel, security.ReadLevel );
			}

			return true;
		}

		public void BindTo( Type objectType, PropertyAccess desiredAccess )
		{
			if ( IsBound )
				throw new AlreadyBoundException( this );

			string[] split = m_Binding.Split( '.' );

			PropertyInfo[] chain = new PropertyInfo[split.Length];

			for ( int i = 0; i < split.Length; ++i )
			{
				bool isFinal = ( i == ( chain.Length - 1 ) );

				chain[i] = objectType.GetProperty( split[i], BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase );

				if ( chain[i] == null )
					throw new UnknownPropertyException( this, split[i] );

				objectType = chain[i].PropertyType;

				PropertyAccess access = desiredAccess;

				if ( !isFinal )
					access |= PropertyAccess.Read;

				if ( ( access & PropertyAccess.Read ) != 0 && !chain[i].CanRead )
					throw new WriteOnlyException( this );

				if ( ( access & PropertyAccess.Write ) != 0 && !chain[i].CanWrite )
					throw new ReadOnlyException( this );
			}

			m_Access = desiredAccess;
			m_Chain = chain;
		}

		public Property( string binding )
		{
			m_Binding = binding;
		}

		public Property( PropertyInfo[] chain )
		{
			m_Chain = chain;
		}

		public override string ToString()
		{
			if ( !IsBound )
				return m_Binding;

			string[] toJoin = new string[m_Chain.Length];

			for ( int i = 0; i < toJoin.Length; ++i )
				toJoin[i] = m_Chain[i].Name;

			return string.Join( ".", toJoin );
		}

		public static Property Parse( Type type, string binding, PropertyAccess access )
		{
			Property prop = new Property( binding );

			prop.BindTo( type, access );

			return prop;
		}
	}
}