//Copyright 2010 Microsoft Corporation
//
//Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
//You may obtain a copy of the License at 
//
//http://www.apache.org/licenses/LICENSE-2.0 
//
//Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an 
//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
//See the License for the specific language governing permissions and limitations under the License.


namespace System.Data.Services.Client
{
    #region Namespaces.

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.Xml;

    #endregion Namespaces.

    internal static class ClientConvert
    {
#if !ASTORIA_LIGHT        
        private const string SystemDataLinq = "System.Data.Linq, Version=" + FX35Assembly.Version + ", Culture=neutral, PublicKeyToken=" + AssemblyRef.EcmaPublicKeyToken;
#endif

        private static readonly Type[] knownTypes = CreateKnownPrimitives();

        private static readonly Dictionary<string, Type> namedTypesMap = CreateKnownNamesMap();

#if !ASTORIA_LIGHT       
        private static bool needSystemDataLinqBinary = true;
#endif

        internal enum StorageType
        {
            Boolean,

            Byte,

            ByteArray,

            Char,

            CharArray,

            DateTime,

            DateTimeOffset,

            Decimal,

            Double,

            Guid,

            Int16,

            Int32,

            Int64,

            Single,

            String,

            SByte,

            TimeSpan,

            Type,

            UInt16,

            UInt32,

            UInt64,

            Uri,

            XDocument,

            XElement,

#if !ASTORIA_LIGHT            
            Binary,
#endif
        }

        internal static object ChangeType(string propertyValue, Type propertyType)
        {
            Debug.Assert(null != propertyValue, "should never be passed null");
            try
            {
                switch ((StorageType)IndexOfStorage(propertyType))
                {
                    case StorageType.Boolean:
                        return XmlConvert.ToBoolean(propertyValue);
                    case StorageType.Byte:
                        return XmlConvert.ToByte(propertyValue);
                    case StorageType.ByteArray:
                        return Convert.FromBase64String(propertyValue);
                    case StorageType.Char:
                        return XmlConvert.ToChar(propertyValue);
                    case StorageType.CharArray:
                        return propertyValue.ToCharArray();
                    case StorageType.DateTime:
                        return XmlConvert.ToDateTime(propertyValue, XmlDateTimeSerializationMode.RoundtripKind);
                    case StorageType.DateTimeOffset:
                        return XmlConvert.ToDateTimeOffset(propertyValue);
                    case StorageType.Decimal:
                        return XmlConvert.ToDecimal(propertyValue);
                    case StorageType.Double:
                        return XmlConvert.ToDouble(propertyValue);
                    case StorageType.Guid:
                        return new Guid(propertyValue);
                    case StorageType.Int16:
                        return XmlConvert.ToInt16(propertyValue);
                    case StorageType.Int32:
                        return XmlConvert.ToInt32(propertyValue);
                    case StorageType.Int64:
                        return XmlConvert.ToInt64(propertyValue);
                    case StorageType.Single:
                        return XmlConvert.ToSingle(propertyValue);
                    case StorageType.String:
                        return propertyValue;
                    case StorageType.SByte:
                        return XmlConvert.ToSByte(propertyValue);
                    case StorageType.TimeSpan:
                        return XmlConvert.ToTimeSpan(propertyValue);
                    case StorageType.Type:
                        return Type.GetType(propertyValue, true);
                    case StorageType.UInt16:
                        return XmlConvert.ToUInt16(propertyValue);
                    case StorageType.UInt32:
                        return XmlConvert.ToUInt32(propertyValue);
                    case StorageType.UInt64:
                        return XmlConvert.ToUInt64(propertyValue);
                    case StorageType.Uri:
                        return Util.CreateUri(propertyValue, UriKind.RelativeOrAbsolute);
                    case StorageType.XDocument:
                        return (0 < propertyValue.Length ? System.Xml.Linq.XDocument.Parse(propertyValue) : new System.Xml.Linq.XDocument());
                    case StorageType.XElement:
                        return System.Xml.Linq.XElement.Parse(propertyValue);
#if !ASTORIA_LIGHT                    
                    case StorageType.Binary:
                        Debug.Assert(null != knownTypes[(int)StorageType.Binary], "null typeof(System.Data.Linq.Binary)");
                        return Activator.CreateInstance(knownTypes[(int)StorageType.Binary], Convert.FromBase64String(propertyValue));
#endif
                    default:
                        Debug.Assert(false, "new StorageType without update to knownTypes");
                        return propertyValue;
                }
            }
            catch (FormatException ex)
            {
                propertyValue = (0 == propertyValue.Length ? "String.Empty" : "String");
                throw Error.InvalidOperation(Strings.Deserialize_Current(propertyType.ToString(), propertyValue), ex);
            }
            catch (OverflowException ex)
            {
                propertyValue = (0 == propertyValue.Length ? "String.Empty" : "String");
                throw Error.InvalidOperation(Strings.Deserialize_Current(propertyType.ToString(), propertyValue), ex);
            }
        }

#if !ASTORIA_LIGHT
        internal static bool IsBinaryValue(object value)
        {
            Debug.Assert(value != null, "value != null");
            return StorageType.Binary == (StorageType)IndexOfStorage(value.GetType());
        }

        internal static bool TryKeyBinaryToString(object binaryValue, out string result)
        {
            Debug.Assert(binaryValue != null, "binaryValue != null");
            Debug.Assert(IsBinaryValue(binaryValue), "IsBinaryValue(binaryValue) - otherwise TryKeyBinaryToString shouldn't have been called.");
            const System.Reflection.BindingFlags Flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.InvokeMethod;
            byte[] bytes = (byte[])binaryValue.GetType().InvokeMember("ToArray", Flags, null, binaryValue, null, System.Globalization.CultureInfo.InvariantCulture);
            return System.Data.Services.Parsing.WebConvert.TryKeyPrimitiveToString(bytes, out result);
        }
#endif

        internal static bool TryKeyPrimitiveToString(object value, out string result)
        {
            Debug.Assert(value != null, "value != null");
#if !ASTORIA_LIGHT
            if (IsBinaryValue(value))
            {
                return TryKeyBinaryToString(value, out result);
            }
#endif
            return System.Data.Services.Parsing.WebConvert.TryKeyPrimitiveToString(value, out result);
        }

        internal static bool ToNamedType(string typeName, out Type type)
        {
            type = typeof(string);
            return String.IsNullOrEmpty(typeName) || ClientConvert.namedTypesMap.TryGetValue(typeName, out type);
        }

        internal static string ToTypeName(Type type)
        {
            Debug.Assert(type != null, "type != null");
            foreach (var pair in ClientConvert.namedTypesMap)
            {
                if (pair.Value == type)
                {
                    return pair.Key;
                }
            }

            return type.FullName; 
        }

        internal static string ToString(object propertyValue, bool atomDateConstruct)
        {
            Debug.Assert(null != propertyValue, "null should be handled by caller");
            switch ((StorageType)IndexOfStorage(propertyValue.GetType()))
            {
                case StorageType.Boolean:
                    return XmlConvert.ToString((bool)propertyValue);
                case StorageType.Byte:
                    return XmlConvert.ToString((byte)propertyValue);
                case StorageType.ByteArray:
                    return Convert.ToBase64String((byte[])propertyValue);
                case StorageType.Char:
                    return XmlConvert.ToString((char)propertyValue);
                case StorageType.CharArray:
                    return new String((char[])propertyValue);
                case StorageType.DateTime:                  
                    DateTime dt = (DateTime)propertyValue;
                    return XmlConvert.ToString(dt.Kind == DateTimeKind.Unspecified && atomDateConstruct ? new DateTime(dt.Ticks, DateTimeKind.Utc) : dt, XmlDateTimeSerializationMode.RoundtripKind); 
                case StorageType.DateTimeOffset:
                    return XmlConvert.ToString((DateTimeOffset)propertyValue);
                case StorageType.Decimal:
                    return XmlConvert.ToString((Decimal)propertyValue);
                case StorageType.Double:
                    return XmlConvert.ToString((Double)propertyValue);
                case StorageType.Guid:
                    return ((Guid)propertyValue).ToString();
                case StorageType.Int16:
                    return XmlConvert.ToString((Int16)propertyValue);
                case StorageType.Int32:
                    return XmlConvert.ToString((Int32)propertyValue);
                case StorageType.Int64:
                    return XmlConvert.ToString((Int64)propertyValue);
                case StorageType.Single:
                    return XmlConvert.ToString((Single)propertyValue);
                case StorageType.String:
                    return (String)propertyValue;
                case StorageType.SByte:
                    return XmlConvert.ToString((SByte)propertyValue);
                case StorageType.TimeSpan:
                    return XmlConvert.ToString((TimeSpan)propertyValue);
                case StorageType.Type:
                    return ((Type)propertyValue).AssemblyQualifiedName;
                case StorageType.UInt16:
                    return XmlConvert.ToString((UInt16)propertyValue);
                case StorageType.UInt32:
                    return XmlConvert.ToString((UInt32)propertyValue);
                case StorageType.UInt64:
                    return XmlConvert.ToString((UInt64)propertyValue);
                case StorageType.Uri:
                    return ((Uri)propertyValue).ToString();
                case StorageType.XDocument:
                    return ((System.Xml.Linq.XDocument)propertyValue).ToString();
                case StorageType.XElement:
                    return ((System.Xml.Linq.XElement)propertyValue).ToString();
#if !ASTORIA_LIGHT                
                case StorageType.Binary:
                    Debug.Assert(null != knownTypes[(int)StorageType.Binary], "null typeof(System.Data.Linq.Binary)");
                    Debug.Assert(knownTypes[(int)StorageType.Binary].IsInstanceOfType(propertyValue), "not IsInstanceOfType System.Data.Linq.Binary");
                    return propertyValue.ToString();
#endif
                default:
                    Debug.Assert(false, "new StorageType without update to knownTypes");
                    return propertyValue.ToString();
            }
        }

        internal static bool IsKnownType(Type type)
        {
            return (0 <= IndexOfStorage(type));
        }

        internal static bool IsKnownNullableType(Type type)
        {
            return IsKnownType(Nullable.GetUnderlyingType(type) ?? type);
        }

        internal static bool IsSupportedPrimitiveTypeForUri(Type type)
        {
            return Util.ContainsReference(namedTypesMap.Values.ToArray(), type);
        }

        internal static string GetEdmType(Type propertyType)
        {
            switch ((StorageType)IndexOfStorage(propertyType))
            {
                case StorageType.Boolean:
                    return XmlConstants.EdmBooleanTypeName;
                case StorageType.Byte:
                    return XmlConstants.EdmByteTypeName;
#if !ASTORIA_LIGHT                
                case StorageType.Binary:
#endif
                case StorageType.ByteArray:
                    return XmlConstants.EdmBinaryTypeName;
                case StorageType.DateTime:
                    return XmlConstants.EdmDateTimeTypeName;
                case StorageType.Decimal:
                    return XmlConstants.EdmDecimalTypeName;
                case StorageType.Double:
                    return XmlConstants.EdmDoubleTypeName;
                case StorageType.Guid:
                    return XmlConstants.EdmGuidTypeName;
                case StorageType.Int16:
                    return XmlConstants.EdmInt16TypeName;
                case StorageType.Int32:
                    return XmlConstants.EdmInt32TypeName;
                case StorageType.Int64:
                    return XmlConstants.EdmInt64TypeName;
                case StorageType.Single:
                    return XmlConstants.EdmSingleTypeName;
                case StorageType.SByte:
                    return XmlConstants.EdmSByteTypeName;
                case StorageType.DateTimeOffset:
                case StorageType.TimeSpan:
                case StorageType.UInt16:
                case StorageType.UInt32:
                case StorageType.UInt64:
                    throw new NotSupportedException(Strings.ALinq_CantCastToUnsupportedPrimitive(propertyType.Name));
                case StorageType.Char:
                case StorageType.CharArray:
                case StorageType.String:
                case StorageType.Type:
                case StorageType.Uri:
                case StorageType.XDocument:
                case StorageType.XElement:
                    return null;                default:
                    Debug.Assert(false, "knowntype without reverse mapping");
                    return null;
            }
        }

        private static Type[] CreateKnownPrimitives()
        {
#if !ASTORIA_LIGHT            
            Type[] types = new Type[1 + (int)StorageType.Binary];
#else
            Type[] types = new Type[1 + (int)StorageType.XElement];
#endif
            types[(int)StorageType.Boolean] = typeof(Boolean);
            types[(int)StorageType.Byte] = typeof(Byte);
            types[(int)StorageType.ByteArray] = typeof(Byte[]);
            types[(int)StorageType.Char] = typeof(Char);
            types[(int)StorageType.CharArray] = typeof(Char[]);
            types[(int)StorageType.DateTime] = typeof(DateTime);
            types[(int)StorageType.DateTimeOffset] = typeof(DateTimeOffset);
            types[(int)StorageType.Decimal] = typeof(Decimal);
            types[(int)StorageType.Double] = typeof(Double);
            types[(int)StorageType.Guid] = typeof(Guid);
            types[(int)StorageType.Int16] = typeof(Int16);
            types[(int)StorageType.Int32] = typeof(Int32);
            types[(int)StorageType.Int64] = typeof(Int64);
            types[(int)StorageType.Single] = typeof(Single);
            types[(int)StorageType.String] = typeof(String);
            types[(int)StorageType.SByte] = typeof(SByte);
            types[(int)StorageType.TimeSpan] = typeof(TimeSpan);
            types[(int)StorageType.Type] = typeof(Type);
            types[(int)StorageType.UInt16] = typeof(UInt16);
            types[(int)StorageType.UInt32] = typeof(UInt32);
            types[(int)StorageType.UInt64] = typeof(UInt64);
            types[(int)StorageType.Uri] = typeof(Uri);
            types[(int)StorageType.XDocument] = typeof(System.Xml.Linq.XDocument);
            types[(int)StorageType.XElement] = typeof(System.Xml.Linq.XElement);
#if !ASTORIA_LIGHT            
            types[(int)StorageType.Binary] = null;
#endif
            return types;
        }

        private static Dictionary<string, Type> CreateKnownNamesMap()
        {
            Dictionary<string, Type> named = new Dictionary<string, Type>(EqualityComparer<String>.Default);

            named.Add(XmlConstants.EdmStringTypeName, typeof(string));
            named.Add(XmlConstants.EdmBooleanTypeName, typeof(Boolean));
            named.Add(XmlConstants.EdmByteTypeName, typeof(Byte));
            named.Add(XmlConstants.EdmDateTimeTypeName, typeof(DateTime));
            named.Add(XmlConstants.EdmDecimalTypeName, typeof(Decimal));
            named.Add(XmlConstants.EdmDoubleTypeName, typeof(Double));
            named.Add(XmlConstants.EdmGuidTypeName, typeof(Guid));
            named.Add(XmlConstants.EdmInt16TypeName, typeof(Int16));
            named.Add(XmlConstants.EdmInt32TypeName, typeof(Int32));
            named.Add(XmlConstants.EdmInt64TypeName, typeof(Int64));
            named.Add(XmlConstants.EdmSByteTypeName, typeof(SByte));
            named.Add(XmlConstants.EdmSingleTypeName, typeof(Single));
            named.Add(XmlConstants.EdmBinaryTypeName, typeof(byte[]));
            return named;
        }

        private static int IndexOfStorage(Type type)
        {
            int index = Util.IndexOfReference(ClientConvert.knownTypes, type);
#if !ASTORIA_LIGHT            
            if ((index < 0) && needSystemDataLinqBinary && (type.Name == "Binary"))
            {
                return LoadSystemDataLinqBinary(type);
            }
#endif
            return index;
        }

#if !ASTORIA_LIGHT        
        private static int LoadSystemDataLinqBinary(Type type)
        {
            if ((type.Namespace == "System.Data.Linq") &&
                (System.Reflection.AssemblyName.ReferenceMatchesDefinition(
                    type.Assembly.GetName(), new System.Reflection.AssemblyName(SystemDataLinq))))
            {
                ClientConvert.knownTypes[(int)StorageType.Binary] = type;
                needSystemDataLinqBinary = false;
                return (int)StorageType.Binary;
            }

            return -1;
        }
#endif
    }
}
