﻿// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Org.Apache.REEF.Tang.Annotations;
using Org.Apache.REEF.Tang.Exceptions;
using Org.Apache.REEF.Utilities.Logging;

namespace Org.Apache.REEF.Tang.Util
{
    // TODO[REEF-842] Act on the obsoletes
    public static class ReflectionUtilities
    {
        private static readonly Logger LOGGER = Logger.GetLogger(typeof(ReflectionUtilities));

        public static readonly string Regexp = "[\\.\\+]";

        /// <summary>
        /// Gets the AssemblyQualifiedName from the Type. This name is used in ClassHierarchy
        /// as a key when add a node as a child to parent. THe name is used as FullName in a Node
        /// </summary>
        /// <param name="name">The name.</param>
        /// <returns></returns>
        /// <exception cref="System.ArgumentException">null is passed in FullName() in ReflectionUtilities</exception>
        public static string GetAssemblyQualifiedName(Type name)
        {
            if (name == null)
            {
                Utilities.Diagnostics.Exceptions.Throw(new ArgumentException("null is passed in FullName() in ReflectionUtilities"), LOGGER);
            }

            Type t = EnsureInterfaceType(name);

            if (t.AssemblyQualifiedName == null && t.Name == null && t.FullName == null)
            {
                LOGGER.Log(Level.Warning, "The type's name is null: " + t.ToString());
            }

            if (t.AssemblyQualifiedName == null && t.Name != null)
            {
                    return t.Name;
            }

            return t.AssemblyQualifiedName;
        }

        /// <summary>
        /// It returns Type.FullName. This name is used as Name in a Node. 
        /// It is not unique for a generic type with different type of arguments.
        /// It is used for toString or debug info as AssemblyQualifiedName is really long
        /// </summary>
        /// <param name="name">The name.</param>
        /// <returns></returns>
        public static string GetName(Type name)
        {
            if (name.FullName != null)
            {
                return name.FullName;
            }

            // The following lines should be not reached by C# syntax definition. However, it happens  for some generic type such as AbstractObserver<T>
            // It results in name as null. When null name in the class node gets deserialized, as name is required filed in class hierarchy proto buffer schema,
            // it causes exception during deserialization. The code below is to use first portion of AssemblyQualifiedName for the name of the node node in case type.name is null. 
            string[] parts = GetAssemblyQualifiedName(name).Split(',');
            return parts[0];
        }

        /// <summary>
        /// Gets the interface target.
        /// Foo<T> ,  given Foo<T> and Foo return T
        /// example class Foo : Bar<U>, Bas<T>
        /// iface: Bar, type: Foo, return U
        /// iface: Bas, type: Foo, return T
        /// class ACons implements IExternalConstructor<A> 
        /// iface: IExternalConstructor<>, type: ACons return A
        /// </summary>
        /// <param name="iface">The iface.</param>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        public static Type GetInterfaceTarget(Type iface, Type type)
        {
            foreach (Type t in ReflectionUtilities.ClassAndAncestors(type))
            {
                if (IsGenericTypeof(iface, t))
                {
                    return t.GetGenericArguments()[0]; // verify it
                }
            }
            return null;
        }

        public static Type GetInterfaceTargetForType(Type iface, Type type)
        {
            if (IsGenericTypeof(iface, type))
            {
                return type.GetGenericArguments()[0]; // verify it
            }
            return null;
        }

        /// <summary>
        /// Determines whether [is generic typeof] [the specified iface].
        /// </summary>
        /// <param name="iface">The iface.</param>
        /// <param name="type">The type.</param>
        /// <returns>
        ///   <c>true</c> if [is generic typeof] [the specified iface]; otherwise, <c>false</c>.
        /// </returns>
        /// <exception cref="System.ApplicationException"></exception>
        public static bool IsGenericTypeof(Type iface, Type type)
        {
            if (iface == null || type == null)
            {
                var ex = new ApplicationException(string.Format(CultureInfo.CurrentCulture,
                                                             "The type passed in IsGenericTypeof is null: iface : {0} type: {1}. ",
                                                             iface, type));
                Utilities.Diagnostics.Exceptions.Throw(ex, LOGGER);
            }
            if (type.IsGenericType)
            {
                if (GetAssemblyQualifiedName(iface).Equals(GetAssemblyQualifiedNameForGeneric(type)))
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Classes the and ancestors.
        /// </summary>
        /// <param name="c">The c.</param>
        /// <returns></returns>
        public static IEnumerable<Type> ClassAndAncestors(Type c)
        {
            List<Type> workQueue = new List<Type>();
            workQueue.Add(c); // including itself

            foreach (Type t in c.GetInterfaces())
            {
                workQueue.Add(t);
            }

            Type b = c.BaseType;
            while (b != null)
            {
                workQueue.Add(b);
                b = b.BaseType;
            }

            if (c.IsInterface)
            {
                workQueue.Add(typeof(object));
            }

            return workQueue;
        }

        public static IEnumerable<Type> ClassAndAncestorsExcludeSelf(Type c)
        {
            List<Type> workQueue = new List<Type>();
            //// workQueue.Add(c); // including itself

            foreach (Type t in c.GetInterfaces())
            {
                workQueue.Add(t);
            }

            Type b = c.BaseType;
            while (b != null)
            {
                workQueue.Add(b);
                b = b.BaseType;
            }

            if (c.IsInterface)
            {
                workQueue.Add(typeof(object));
            }

            return workQueue;
        }

        /// <summary>
        /// Boxes the class.
        /// </summary>
        /// <param name="c">The c.</param>
        /// <returns></returns>
        /// <exception cref="System.NotSupportedException">Encountered unknown primitive type!</exception>
        public static Type BoxClass(Type c)
        {
            if (c.IsPrimitive && c != typeof(Type))
            {
                if (c == typeof(bool))
                {
                    return typeof(bool);
                }
                else if (c == typeof(byte))
                {
                    return typeof(byte);
                }
                else if (c == typeof(char))
                {
                    return typeof(char);
                }
                else if (c == typeof(short))
                {
                    return typeof(short);
                }
                else if (c == typeof(int))
                {
                    return typeof(int);
                }
                else if (c == typeof(long))
                {
                    return typeof(long);
                }
                else if (c == typeof(float))
                {
                    return typeof(float);
                }
                else if (c == typeof(double))
                {
                    return typeof(double);
                }
                else
                {
                    Utilities.Diagnostics.Exceptions.Throw(new NotSupportedException(
                        "Encountered unknown primitive type!"), LOGGER);
                    return c;
                }
            }
            else
            {
                return c;
            }
        }

        /// <summary>
        /// Determines whether the specified to is coercable.
        //  public static boolean isCoercable(Class<?> to, Class<?> from) castable from to both are numbers and from has a few bits or subclass relationship
        /// </summary>
        /// <param name="to">To.</param>
        /// <param name="from">From.</param>
        /// <returns>
        ///   <c>true</c> if the specified to is coercable; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsCoercable(Type to, Type from)
        {
            to = BoxClass(to);
            from = BoxClass(from);
            ////TODO
            ////if (Number.class.isAssignableFrom(to)
            ////   && Number.class.isAssignableFrom(from)) {
            ////return sizeof.get(from) <= sizeof.get(to);
            return to.IsAssignableFrom(from);
            //// return IsAssignableFromIgnoreGeneric(to, from);
        }

        /// <summary>
        /// Determines whether [is assignable from ignore generic] [the specified to].
        /// </summary>
        /// <param name="to">To.</param>
        /// <param name="from">From.</param>
        /// <returns>
        ///   <c>true</c> if [is assignable from ignore generic] [the specified to]; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsAssignableFromIgnoreGeneric(Type to, Type from)
        {
            var f = ClassAndAncestors(from);
            foreach (Type t in f)
            {
                if (GetAssemblyQualifiedName(to).Equals(GetAssemblyQualifiedNameForGeneric(t)))
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Ensures the type of the interface. For generic types, full name could be null. In this case, we need to 
        /// get GetGenericTypeDefinition for the type so that to retain all teh type information
        /// </summary>
        /// <param name="interf">The interf.</param>
        /// <returns></returns>
        public static Type EnsureInterfaceType(Type interf)
        {
            if (interf != null && interf.IsGenericType && null == interf.FullName)
            {
                return interf.GetGenericTypeDefinition(); // this is to test if this line is ever reached
            }
            return interf;
        }

        /// <summary>
        /// Gets the assembly qualified name for generic.
        /// </summary>
        /// <param name="t">The t.</param>
        /// <returns></returns>
        public static string GetAssemblyQualifiedNameForGeneric(Type t)
        {
            Type t1 = t;
            if (t.IsGenericType)
            {
                t1 = t.GetGenericTypeDefinition();
            }
            return t1.AssemblyQualifiedName;
        }

        /// <summary>
        /// Determines whether [is instance of generic] [the specified p].
        /// </summary>
        /// <param name="p">The p.</param>
        /// <param name="t">The t.</param>
        /// <returns>
        ///   <c>true</c> if [is instance of generic] [the specified p]; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsInstanceOfGeneric(object p, Type t)
        {
            foreach (var g in ReflectionUtilities.ClassAndAncestors(p.GetType()))
            {
                if (GetAssemblyQualifiedNameForGeneric(t).Equals(GetAssemblyQualifiedNameForGeneric(g)))
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Gets the name of the type by.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <returns></returns>
        /// <exception cref="System.ApplicationException">Not able to get Type from the name provided:  +  name</exception>
        public static Type GetTypeByName(string name)
        {
            Type t = null;

            t = Type.GetType(name);

            if (t == null)
            {
                foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
                {
                    t = a.GetType(name);
                    if (t != null)
                    {
                        break;
                    }
                }
            }
            if (t == null)
            {
                Utilities.Diagnostics.Exceptions.Throw(
                    new ApplicationException("Not able to get Type from the name provided: " + name), LOGGER);
            }

            return t;
        }

        /// <summary>
        /// Gets the enclosing classes.
        /// </summary>
        /// <param name="t">The t.</param>
        /// <returns></returns>
        public static Type[] GetEnclosingClasses(Type t)
        {
            IList<Type> l = new List<Type>();
            l.Add(t);
            Type current = t.DeclaringType;
            while (current != null)
            {
                l.Add(current);
                current = current.DeclaringType;
            }
            return l.Reverse().ToArray();
        }

        /// <summary>
        /// Gets the enclosing class names.
        /// </summary>
        /// <param name="t">The t.</param>
        /// <returns></returns>
        /// <exception cref="System.ApplicationException">The Type passed to GetEnclosingClassShortNames is null</exception>
        public static string[] GetEnclosingClassNames(Type t)
        {
            if (t == null)
            {
                Utilities.Diagnostics.Exceptions.Throw(new ApplicationException("The Type passed to GetEnclosingClassShortNames is null"), LOGGER);
            }
            Type[] ts = GetEnclosingClasses(t);
            string[] result = new string[ts.Length];
            for (int i = 0; i < ts.Length; i++)
            {
                result[i] = GetAssemblyQualifiedName(ts[i]);
            }

            return result;
        }

        /// <summary>
        /// Gets the enclosing class names.
        /// </summary>
        /// <param name="fullName">The full name.</param>
        /// <returns></returns>
        /// <exception cref="System.ApplicationException">The name passed to GetEnclosingClassShortNames is null</exception>
        public static string[] GetEnclosingClassNames(string fullName)
        {
            if (fullName == null)
            {
                Utilities.Diagnostics.Exceptions.Throw(new ApplicationException("The name passed to GetEnclosingClassShortNames is null"), LOGGER);
            }
            Type t = ReflectionUtilities.GetTypeByName(fullName);
            return GetEnclosingClassNames(t);
        }

        /// <summary>
        /// Gets the named parameter target or null.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        /// <exception cref="ClassHierarchyException">Named parameter  + GetName(type) +  implements 
        /// + multiple interfaces.  It is only allowed to implement Name</exception>
        public static Type GetNamedParameterTargetOrNull(Type type)
        {
            var npAnnotation = type.GetCustomAttribute<NamedParameterAttribute>();
            if (npAnnotation != null)
            {
                Type[] intfs = type.GetInterfaces();
                if (intfs.Length > 1)
                {
                    var ex = new ClassHierarchyException("Named parameter " + GetName(type) + " implements "
                                  + "multiple interfaces.  It is only allowed to implement Name<T>");
                    Utilities.Diagnostics.Exceptions.Throw(ex, LOGGER);
                }
                else if (intfs.Length == 0 || !IsName(intfs[0]))
                {
                    var ex = new ClassHierarchyException("Found illegal [NamedParameter " + GetName(type)
                                  + " does not implement Name<T>");
                    Utilities.Diagnostics.Exceptions.Throw(ex, LOGGER);
                }
                Type[] args = intfs[0].GetGenericArguments();
                if (args.Length > 1)
                {
                    var ex = new ClassHierarchyException("Found illegal [NamedParameter " + GetName(type)
                        + " that has more than one arguments");
                    Utilities.Diagnostics.Exceptions.Throw(ex, LOGGER);
                }
                if (args.Length == 0)
                {
                    var ex = new ClassHierarchyException("Found illegal [NamedParameter " + GetName(type)
                        + " that has no argument");
                    Utilities.Diagnostics.Exceptions.Throw(ex, LOGGER);
                }
                if (HasConstructor(type) || HasInjectableConstructor(type))
                {
                    var ex = new ClassHierarchyException("Named parameter " + GetName(type) + " has "
                                  + (HasInjectableConstructor(type) ? "an injectable" : "a") + " constructor. "
                                  + " Named parameters must not declare any constructors.");
                    Utilities.Diagnostics.Exceptions.Throw(ex, LOGGER);
                }

                return args[0];
            }

            if (ImplementName(type))
            {
                // Implement Name<> but no  [NamedParameter] attribute
                var ex = new ClassHierarchyException("Named parameter " + GetName(type)
                                  + " is missing its [NamedParameter] attribute.");
                Utilities.Diagnostics.Exceptions.Throw(ex, LOGGER);
            }
            return null;
        }

        public static IEnumerable<Type> GetInterfaces(Type type, bool includeInherited)
        {
            if (includeInherited || type.BaseType == null)
            {
                return type.GetInterfaces();
            }
            else
            {
                return type.GetInterfaces().Except(type.BaseType.GetInterfaces());
            }
        }

        // Here is a more elaborate hack to test for anonymous type:
        // http://stackoverflow.com/questions/2483023/how-to-test-if-a-type-is-anonymous
        // compiler generated classes are always recreatable and need not additional references to check for.
        [Obsolete("Deprecated in 0.14, please use IsAnonymousType instead.")]
        public static bool IsAnnonymousType(Type type)
        {
            return IsAnonymousType(type);
        }

        public static bool IsAnonymousType(Type type)
        {
            if (type != null)
            {
                // HACK: The only way to detect anonymous types right now.
                return Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false)
                       && type.IsGenericType && type.Name.Contains("AnonymousType")
                       && (type.Name.StartsWith("<>", true, CultureInfo.CurrentCulture) || type.Name.StartsWith("VB$", true, CultureInfo.CurrentCulture))
                       && (type.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic;
            }
            return false;
        }

        private static bool ImplementName(Type type)
        {
            foreach (Type t in type.GetInterfaces())
            {
                if (IsName(t))
                {
                    return true;
                }
            }
            return false;
        }

        private static bool IsName(Type t)
        {
            if (t.IsGenericType)
            {
                return t.GetGenericTypeDefinition().AssemblyQualifiedName.Equals(typeof(Name<>).AssemblyQualifiedName);
            }
            return false;
        }

        private static bool HasConstructor(Type type)
        {
            bool hasConstructor = false;

            ConstructorInfo[] constructors =
                type.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

            if (constructors.Length > 1)
            {
                hasConstructor = true;
            }

            if (constructors.Length == 1)
            {
                ConstructorInfo c = constructors[0];
                ParameterInfo[] p = c.GetParameters();
                if (p.Length > 1)
                {
                    // Multiple args. Definitely not implicit.
                    hasConstructor = true;
                }
                else if (p.Length == 1)
                {
                    // One arg. Could be an inner class, in which case the compiler
                    // included an implicit one parameter constructor that takes the
                    // enclosing type.
                    if (p[0].ParameterType != type.DeclaringType)
                    {
                        hasConstructor = true;
                    }
                }
            }
            return hasConstructor;
        }

        private static bool HasInjectableConstructor(Type type)
        {
            bool isInjectable = false;

            ConstructorInfo[] constructors =
                type.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

            foreach (ConstructorInfo c in constructors)
            {
                foreach (Attribute a in c.GetCustomAttributes())
                {
                    if (a is InjectAttribute)
                    {
                        isInjectable = true;
                    }
                }
            }
            return isInjectable;
        }
    }
}