﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using EasyApi.Reflection;
using EasyApi.Utils;
using EasyApi.Values;

namespace EasyApi.Metadata.Providers
{
	public class ServiceMetadataProvider : IServiceMetadataProvider
	{
		private static IEnumerable<ExtraProperty> GetGlobalMetadataProperties(GlobalMetadataConfiguration globalMetadataConfiguration)
		{
			Argument.IsNotNull(globalMetadataConfiguration, nameof(globalMetadataConfiguration));

			return globalMetadataConfiguration.Properties;
		}

		private readonly IServiceMetadataReader[] _readers;
		private readonly IActionParameterMetadataProvider _actionParameterMetadataProvider;
		private readonly IActionMetadataProvider _actionMetadataProvider;
		private readonly GlobalMetadata _globalMetadata;

		public ServiceMetadataProvider(IEnumerable<IServiceMetadataReader> metadataReaders, GlobalMetadataConfiguration globalMetadataConfiguration)
			: this(metadataReaders, GetGlobalMetadataProperties(globalMetadataConfiguration))
		{
		}

		public ServiceMetadataProvider(IEnumerable<IServiceMetadataReader> metadataReaders, IEnumerable<ExtraProperty> globalMetadataProperties)
		{
			Argument.IsNotNull(metadataReaders, nameof(metadataReaders));
			Argument.IsNotNull(globalMetadataProperties, nameof(globalMetadataProperties));

			_readers = metadataReaders.ToArray();
			Argument.IsNotEmpty(_readers, nameof(metadataReaders));
			Argument.ItemsAreNotNull(_readers, nameof(metadataReaders));

			_actionParameterMetadataProvider = new ActionParameterMetadataProvider(this);
			_actionMetadataProvider = new ActionMetadataProvider(this);
			_globalMetadata = new GlobalMetadata(globalMetadataProperties);
		}

		private bool IgnoreAction(ActionMethod method, ServiceMetadata serviceMetadata)
		{
			var ignoreFlag = _readers
				.Select(r => r.IgnoreAction(method, serviceMetadata))
				.FirstOrDefault(f => f != null);

			return ignoreFlag ?? false;
		}

		private IExtraPropertyCollection ReadProperties(Action<IServiceMetadataReader, WritableExtraPropertyCollection> readAction)
		{
			var properties = new WritableExtraPropertyCollection();
			for (var i = _readers.Length - 1; i >= 0; i--)
			{
				readAction(_readers[i], properties);
			}

			return properties;
		}

		public IServiceMetadata GetMetadata(Type serviceType)
		{
			Argument.IsNotNull(serviceType, nameof(serviceType));
			Argument.IsPublicInterface(serviceType, nameof(serviceType));
			Argument.IsNotGenericType(serviceType, nameof(serviceType));

			return new ServiceMetadata(
				_globalMetadata,
				serviceType,
				ReadProperties((r, ps) => r.ReadServiceProperties(ps, serviceType)),
				_actionMetadataProvider
			);
		}

		#region CLASSES

		private class ActionParameterMetadataProvider : IActionParameterMetadataProvider
		{
			private readonly ServiceMetadataProvider _metadataProvider;

			public ActionParameterMetadataProvider(ServiceMetadataProvider metadataProvider)
			{
				_metadataProvider = metadataProvider;
			}

			public IEnumerable<ActionParameterMetadata> GetMetadata(ActionMetadata actionMetadata)
			{
				foreach (var parameterInfo in actionMetadata.ReflectedMethod.MethodInfo.GetParameters())
				{
					var properties = _metadataProvider.ReadProperties(
						(r, ps) => r.ReadParameterProperties(ps, parameterInfo, actionMetadata)
					);

					yield return new ActionParameterMetadata(
						actionMetadata,
						parameterInfo,
						properties
					);
				}
			}
		}

		private class ActionMetadataProvider : IActionMetadataProvider
		{
			private readonly ServiceMetadataProvider _metadataProvider;

			public ActionMetadataProvider(ServiceMetadataProvider metadataProvider)
			{
				_metadataProvider = metadataProvider;
			}

			public IEnumerable<ActionMetadata> GetMetadata(ServiceMetadata serviceMetadata)
			{
				var serviceType = serviceMetadata.ReflectedType;
				var allServiceInterfaces = serviceType
					.GetInterfaces()
					.Concat(serviceType)
					.Where(i => i != typeof(IDisposable))
					.ToArray();

				var methods = allServiceInterfaces
					.SelectMany(i => i
						.GetMethods()
						.Where(m => !m.IsSpecialName) // filtering properties
						.Select(ActionMethod.Common))
					.Distinct()
					.ToList();

				foreach (var propertyInfo in allServiceInterfaces.SelectMany(i => i.GetProperties()).Distinct(MemberInfoEqualityComparer<PropertyInfo>.DefaultInstance))
				{
					var getMethodInfo = ActionMethod.PropertyGetter(propertyInfo);
					if (getMethodInfo != null)
					{
						methods.Add(getMethodInfo);
					}

					var setMethodInfo = ActionMethod.PropertySetter(propertyInfo);
					if (setMethodInfo != null)
					{
						methods.Add(setMethodInfo);
					}
				}

				foreach (var method in methods)
				{
					if (!_metadataProvider.IgnoreAction(method, serviceMetadata))
					{
						var properties = _metadataProvider.ReadProperties(
							(r, ps) => r.ReadActionProperties(ps, method, serviceMetadata)
						);

						yield return new ActionMetadata(
							serviceMetadata,
							method,
							properties,
							_metadataProvider._actionParameterMetadataProvider
						);
					}
				}
			}
		}

		#endregion CLASSES
	}
}