/* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
/*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*
*  Licensed to Elasticsearch B.V. under one or more contributor
*  license agreements. See the NOTICE file distributed with
*  this work for additional information regarding copyright
*  ownership. Elasticsearch B.V. 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.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace OpenSearch.Client
{
	internal static class TypeExtensions
	{
		internal static readonly MethodInfo GetActivatorMethodInfo =
			typeof(TypeExtensions).GetMethod(nameof(GetActivator), BindingFlags.Static | BindingFlags.NonPublic);

		private static readonly ConcurrentDictionary<string, ObjectActivator<object>> CachedActivators =
			new ConcurrentDictionary<string, ObjectActivator<object>>();

		private static readonly ConcurrentDictionary<string, Type> CachedGenericClosedTypes =
			new ConcurrentDictionary<string, Type>();

		private static readonly ConcurrentDictionary<Type, List<PropertyInfo>> CachedTypePropertyInfos =
			new ConcurrentDictionary<Type, List<PropertyInfo>>();

		internal static object CreateGenericInstance(this Type t, Type closeOver, params object[] args) =>
			t.CreateGenericInstance(new[] { closeOver }, args);

		internal static object CreateGenericInstance(this Type t, Type[] closeOver, params object[] args)
		{
			var key = closeOver.Aggregate(new StringBuilder(t.FullName), (sb, gt) =>
			{
				sb.Append("--");
				return sb.Append(gt.FullName);
			}, sb => sb.ToString());
			if (!CachedGenericClosedTypes.TryGetValue(key, out var closedType))
			{
				closedType = t.MakeGenericType(closeOver);
				CachedGenericClosedTypes.TryAdd(key, closedType);
			}
			return closedType.CreateInstance(args);
		}

		internal static T CreateInstance<T>(this Type t, params object[] args) => (T)t.CreateInstance(args);

		internal static object CreateInstance(this Type t, params object[] args)
		{
			var key = t.FullName;
			var argKey = args.Length;
			if (args.Length > 0)
				key = argKey + "--" + key;
			if (CachedActivators.TryGetValue(key, out var activator))
				return activator(args);

			var generic = GetActivatorMethodInfo.MakeGenericMethod(t);
			var constructors = from c in t.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
				let p = c.GetParameters()
				where p.Length == args.Length
				select c;

			var ctor = constructors.FirstOrDefault();
			if (ctor == null)
				throw new Exception($"Cannot create an instance of {t.FullName} because it has no constructor taking {args.Length} arguments");

			activator = (ObjectActivator<object>)generic.Invoke(null, new object[] { ctor });
			CachedActivators.TryAdd(key, activator);
			return activator(args);
		}

		//do not remove this is referenced through GetActivatorMethod
		internal static ObjectActivator<T> GetActivator<T>(ConstructorInfo ctor)
		{
			var paramsInfo = ctor.GetParameters();

			//create a single param of type object[]
			var param = Expression.Parameter(typeof(object[]), "args");

			var argsExp = new Expression[paramsInfo.Length];

			//pick each arg from the params array
			//and create a typed expression of them
			for (var i = 0; i < paramsInfo.Length; i++)
			{
				var index = Expression.Constant(i);
				var paramType = paramsInfo[i].ParameterType;
				var paramAccessorExp = Expression.ArrayIndex(param, index);
				var paramCastExp = Expression.Convert(paramAccessorExp, paramType);
				argsExp[i] = paramCastExp;
			}

			//make a NewExpression that calls the
			//ctor with the args we just created
			var newExp = Expression.New(ctor, argsExp);

			//create a lambda with the New
			//Expression as body and our param object[] as arg
			var lambda = Expression.Lambda(typeof(ObjectActivator<T>), newExp, param);

			//compile it
			var compiled = (ObjectActivator<T>)lambda.Compile();
			return compiled;
		}

		internal static List<PropertyInfo> GetAllProperties(this Type type)
		{
			if (CachedTypePropertyInfos.TryGetValue(type, out var propertyInfos))
				return propertyInfos;

			var properties = new Dictionary<string, PropertyInfo>();
			GetAllPropertiesCore(type, properties);
			propertyInfos = properties.Values.ToList();
			CachedTypePropertyInfos.TryAdd(type, propertyInfos);
			return propertyInfos;
		}

		/// <summary>
		/// Returns inherited properties with reflectedType set to base type
		/// </summary>
		private static void GetAllPropertiesCore(Type type, Dictionary<string, PropertyInfo> collectedProperties)
		{
			foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
			{
				if (collectedProperties.TryGetValue(property.Name, out var existingProperty))
				{
					if (IsHidingMember(property))
						collectedProperties[property.Name] = property;
					else if (!type.IsInterface && property.IsVirtual())
					{
						var propertyDeclaringType = property.GetDeclaringType();

						if (!(existingProperty.IsVirtual() && existingProperty.GetDeclaringType().IsAssignableFrom(propertyDeclaringType)))
						{
							collectedProperties[property.Name] = property;
						}
					}
				}
				else
					collectedProperties.Add(property.Name, property);
			}

			if (type.BaseType != null)
				GetAllPropertiesCore(type.BaseType, collectedProperties);
		}

		/// <summary>
		/// Determines if a <see cref="PropertyInfo"/> is hiding/shadowing a
		/// <see cref="PropertyInfo"/> from a base type
		/// </summary>
		private static bool IsHidingMember(PropertyInfo propertyInfo)
		{
			var baseType = propertyInfo.DeclaringType?.BaseType;
			var baseProperty = baseType?.GetProperty(propertyInfo.Name);
			if (baseProperty == null)
				return false;

			var derivedGetMethod = propertyInfo.GetBaseDefinition();
			return derivedGetMethod?.ReturnType != propertyInfo.PropertyType;
		}

		private static Type GetDeclaringType(this PropertyInfo propertyInfo) =>
			propertyInfo.GetBaseDefinition()?.DeclaringType ?? propertyInfo.DeclaringType;

		private static MethodInfo GetBaseDefinition(this PropertyInfo propertyInfo)
		{
			var m = propertyInfo.GetMethod;
			return m != null
				? m.GetBaseDefinition()
				: propertyInfo.SetMethod?.GetBaseDefinition();
		}

		/// <summary>
		/// Determines if a <see cref="PropertyInfo"/> is virtual
		/// </summary>
		private static bool IsVirtual(this PropertyInfo propertyInfo)
		{
			var methodInfo = propertyInfo.GetMethod;
			if (methodInfo != null && methodInfo.IsVirtual)
				return true;

			methodInfo = propertyInfo.SetMethod;
			return methodInfo != null && methodInfo.IsVirtual;
		}

		internal delegate T ObjectActivator<out T>(params object[] args);

		private static readonly Assembly OscAssembly = typeof(TypeExtensions).Assembly;

		public static bool IsOpenSearchClientType(this Type type) => type.Assembly == OscAssembly;
	}
}