using System;
using System.Linq;
using System.Reflection;
using Fasterflect;

namespace LeafWeb.Core.Services
{
	public static class ParsedObjectFactory<T> where T : new()
	{
		private static PropertyInfo[] GetProperties()
		{
			var propertyInfos = typeof(T).Properties();
			return propertyInfos.Where(p => p.HasAttribute<ParseInfoAttribute>()).ToArray();
		}

		/// <summary>
		/// Create an object type T filling properties from the given title values
		/// </summary>
		/// <param name="titleValues">Colon separated title: values</param>
		public static T Create(string[] titleValues)
		{
			var properties = GetProperties();
			var obj = new T();
			// take each of the 
			for (var index = 0; index < titleValues.Length; index++)
			{
				PropertyInfo property = null;
				string value = null;

				var lineNumber = index + 1;
				var row = titleValues[index];
				var split = row.Split(':');
				if (split.Length > 1)
				{
					// handles case for "Title : Value"
					var title = split[0].Trim();
					value = split[1].Trim();
					property =
						properties
						.FirstOrDefault(p => p.Attribute<ParseInfoAttribute>().IsTitleMatch(title));
				}
				if (property == null)
				{
					// handles case for row number, i.e. "Value"
					value = row.Trim();
					property =
						properties
						.FirstOrDefault(p => p.Attribute<ParseInfoAttribute>().IsPositionMatch(lineNumber));
				}

				if (property != null)
				{
					object convertedVal;
					if (!TryConvertValue(property, value, out convertedVal))
						throw new ParseException(string.Format("Cannot convert value '{0}' for {1} at line number {2}", value, property.Name, lineNumber));
					property.Set(obj, convertedVal);
				}
			}
			return obj;
		}

		public static T[] Create(string[] titles, string[][] valueArrays)
		{
			var properties = GetProperties();
			var objs = new T[valueArrays.Length];

			for (var vIndex = 0; vIndex < valueArrays.Length; vIndex++)
			{
				var obj = new T();
				var values = valueArrays[vIndex];
				for (var tIndex = 0; tIndex < titles.Length; tIndex++)
				{
					var title = titles[tIndex];
					var value = values[tIndex];
					var position = tIndex + 1;

					if (IsMissingValue(value))
						continue;

					var property = MatchProperty(properties, title, position);

					if (property != null)
					{
						object convertedVal;
						if (!TryConvertValue(property, value, out convertedVal))
							throw new ParseException(string.Format("Cannot convert value '{0}' for {1} in position {2}", value, property.Name, position));
						property.Set(obj, convertedVal);
					}
				}
				objs[vIndex] = obj;
			}
			return objs;
		}

		public static T Create(Tuple<string, string>[] titleValues)
		{
			var properties = GetProperties();
			var obj = new T();

			for (var index = 0; index < titleValues.Length; index++)
			{
				var item = titleValues[index];
				var position = index + 1;
				var title = item.Item1.Trim();
				var value = item.Item2.Trim();

				if (IsMissingValue(value))
					continue;

				var property = MatchProperty(properties, title, position);

				if (property != null)
				{
					object convertedVal;
					if (!TryConvertValue(property, value, out convertedVal))
						throw new ParseException(string.Format("Cannot convert value '{0}' for {1} in position {2}", value, property.Name, position));
					property.Set(obj, convertedVal);
				}
			}
			return obj;
		}

		private static bool IsMissingValue(string value)
		{
			return string.IsNullOrEmpty(value) || value == "NA" || value == "-9999";
		}

		private static PropertyInfo MatchProperty(PropertyInfo[] properties, string title, int position)
		{
			var property =
				properties
				.FirstOrDefault(p => p.Attribute<ParseInfoAttribute>().IsTitleMatch(title));

			if (property == null)
			{
				property =
					properties
					.FirstOrDefault(p => p.Attribute<ParseInfoAttribute>().IsPositionMatch(position));
			}
			return property;
		}

		private static bool TryConvertValue(PropertyInfo property, object value, out object convertedValue)
		{
			try
			{
				// http://stackoverflow.com/questions/3531318/convert-changetype-fails-on-nullable-types
				var t = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
				convertedValue = Convert.ChangeType(value, t);
			}
			catch (Exception)
			{
				convertedValue = null;
				return false;
			}
			return true;
		}
	}
}