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

namespace LeafWeb.Core.Utility
{
	public static class ParsedObjectFactory<T> 
		where T : new()
	{
		static ParsedObjectFactory()
		{
			// register this for usage by TypeDescriptor.GetConverter
			BoolTypeConverter.Register();
        }

		/// <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 = new ParseInfoPropertyMatcherWithCache<T>().ParseInfoProperties;
			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($"Cannot convert value '{value}' for {property.Name} at line number {lineNumber}");
					property.Set(obj, convertedVal);
				}
			}
			return obj;
		}

		public static T[] Create(string[] titles, string[][] valueArrays)
		{
			var matcher = new ParseInfoPropertyMatcherWithCache<T>();
			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 = matcher.MatchProperty(title, position);

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

		public static T Create(Tuple<string, string>[] titleValues)
		{
			var matcher = new ParseInfoPropertyMatcherWithCache<T>();
			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 = matcher.MatchProperty(title, position);

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

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

		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;
				value = HandleBoolEncodedProperty(property, value);
				//  convertedValue = Convert.ChangeType(value, t);
				var converter = TypeDescriptor.GetConverter(t);
				convertedValue = converter.ConvertTo(value, t);
			}
			catch (Exception)
			{
				convertedValue = null;
				return false;
			}
			return true;
		}

		private static object HandleBoolEncodedProperty(PropertyInfo property, object value)
		{
			var boolEncodedPosition = property.Attribute<ParseInfoAttribute>().BoolEncodedPosition;
			if (boolEncodedPosition == null) return value;

			var v = (value as string)?.Substring(boolEncodedPosition.Value -1, 1);
			switch (v)
			{
				// these values are False=1, True=2
				case "1":
					value = false;
					break;
				case "2":
					value = true;
					break;
				default:
					throw new ArgumentOutOfRangeException($"{property.Name} {boolEncodedPosition} {value} {v}");
			}
			return value;
		}
	}
}