//
// OptionDetails.cs
//
// Author: Rafael Teixeira (rafaelteixeirabr@hotmail.com)
//
// (C) 2002 Rafael Teixeira
//

//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

using System;
using System.Collections;
using System.IO;
using System.Reflection;

namespace Mono.GetOptions
{
	public enum WhatToDoNext
	{
		AbandonProgram,
		GoAhead
	}
	
	internal enum OptionProcessingResult
	{
		NotThisOption,
		OptionAlone,
		OptionConsumedParameter
	}

	internal class OptionDetails : IComparable
	{
		public string ShortForm;
		public string LongForm;
		public string AlternateForm;
		public string ShortDescription;
		public bool NeedsParameter;
		public int MaxOccurs; // negative means there is no limit
		public int Occurs;
		public bool BooleanOption;
		public Options OptionBundle;
		public MemberInfo MemberInfo;
		public ArrayList Values;
		public System.Type ParameterType;
		public string paramName = null;
		public bool VBCStyleBoolean;
		public bool SecondLevelHelp;
		public bool Hidden;
		
		public OptionDetails NextAlternate  = null;

		private string ExtractParamName(string shortDescription)
		{
			int whereBegins = shortDescription.IndexOf("{");
			if (whereBegins < 0)
				paramName = "PARAM";
			else {
				int whereEnds = shortDescription.IndexOf("}");
				if (whereEnds < whereBegins)
					whereEnds = shortDescription.Length+1;
						
				paramName = shortDescription.Substring(whereBegins + 1, whereEnds - whereBegins - 1);
				shortDescription = 
					shortDescription.Substring(0, whereBegins) + 
					paramName +
					shortDescription.Substring(whereEnds + 1);
			}
			return shortDescription;
		}

		public string ParamName { get { return paramName; } }
				
		private bool verboseParsing { get { return OptionBundle.VerboseParsingOfOptions || OptionBundle.DebuggingOfOptions; } }

//		private bool debugOptions { get { return OptionBundle.DebuggingOfOptions; } }

		private OptionsParsingMode parsingMode { get { return OptionBundle.ParsingMode; } } 
		
		private bool useGNUFormat { get { return (parsingMode & OptionsParsingMode.GNU_DoubleDash) == OptionsParsingMode.GNU_DoubleDash; } } 
		
		private bool dontSplitOnCommas { get { return OptionBundle.DontSplitOnCommas; } } 

		private string linuxLongPrefix {
			get { 
				return (useGNUFormat? "--":"-"); 
			} 
		}
		
		public string DefaultForm
		{
			get {
				string shortPrefix = "-";
				string longPrefix = linuxLongPrefix;
				if (parsingMode == OptionsParsingMode.Windows) {
					shortPrefix = "/";
					longPrefix = "/";
				}
				if (this.ShortForm != string.Empty)
					return shortPrefix+this.ShortForm;
				else
					return longPrefix+this.LongForm;
			}
		}

		private string optionHelp = null;
		
		public override string ToString()
		{
			if 	(optionHelp == null)
			{
				string shortPrefix;
				string longPrefix;
				bool hasLongForm = (this.LongForm != null && this.LongForm != string.Empty);
				if (this.OptionBundle.ParsingMode == OptionsParsingMode.Windows) {
					shortPrefix = "/";
					longPrefix = "/";
				} else {
					shortPrefix = "-";
					longPrefix = linuxLongPrefix;
				}
				optionHelp = "  ";
				optionHelp += (this.ShortForm != string.Empty) ? shortPrefix+this.ShortForm+" " : "   ";
				optionHelp += hasLongForm ? longPrefix+this.LongForm : "";
				if (NeedsParameter)	{
					if (hasLongForm)
						optionHelp += ":"; 
					optionHelp += ParamName; 
				} else if (BooleanOption && VBCStyleBoolean) {
					optionHelp += "[+|-]";
				}
				optionHelp += "\t";
				if (this.AlternateForm != string.Empty && this.AlternateForm != null)
					optionHelp += "Also "+ shortPrefix + this.AlternateForm + (NeedsParameter?(":"+ParamName):"") +". ";
				optionHelp += this.ShortDescription;
			}
			return optionHelp;
		}

		private static System.Type TypeOfMember(MemberInfo memberInfo)
		{
			if ((memberInfo.MemberType == MemberTypes.Field && memberInfo is FieldInfo))
				return ((FieldInfo)memberInfo).FieldType;

			if ((memberInfo.MemberType == MemberTypes.Property && memberInfo is PropertyInfo))
				return ((PropertyInfo)memberInfo).PropertyType;

			if ((memberInfo.MemberType == MemberTypes.Method && memberInfo is MethodInfo))
			{
				if (((MethodInfo)memberInfo).ReturnType.FullName != typeof(WhatToDoNext).FullName)
					throw new NotSupportedException("Option method must return '" + typeof(WhatToDoNext).FullName + "'");

				ParameterInfo[] parameters = ((MethodInfo)memberInfo).GetParameters();
				if ((parameters == null) || (parameters.Length == 0))
					return null;
				else
					return parameters[0].ParameterType;
			}

			throw new NotSupportedException("'" + memberInfo.MemberType + "' memberType is not supported");
		}

		public OptionDetails(MemberInfo memberInfo, OptionAttribute option, Options optionBundle)
		{
			this.ShortForm = ("" + option.ShortForm).Trim();
			if (option.LongForm == null)
				this.LongForm = string.Empty;
			else
				this.LongForm = (option.LongForm == string.Empty)? memberInfo.Name:option.LongForm;
			this.AlternateForm = option.AlternateForm;
			this.ShortDescription = ExtractParamName(option.ShortDescription);
			this.Occurs = 0;
			this.OptionBundle = optionBundle; 
			this.BooleanOption = false;
			this.MemberInfo = memberInfo;
			this.NeedsParameter = false;
			this.Values = null;
			this.MaxOccurs = 1;
			this.VBCStyleBoolean = option.VBCStyleBoolean;
			this.SecondLevelHelp = option.SecondLevelHelp;
			this.Hidden = false; // TODO: check other attributes
			
			this.ParameterType = TypeOfMember(memberInfo);

			if (this.ParameterType != null)
			{
				if (this.ParameterType.FullName != "System.Boolean")
				{
					if (this.LongForm.IndexOf(':') >= 0)
						throw new InvalidOperationException("Options with an embedded colon (':') in their visible name must be boolean!!! [" + 
									this.MemberInfo.ToString() + " isn't]");
				
					this.NeedsParameter = true;

					if (option.MaxOccurs != 1)
					{
						if (this.ParameterType.IsArray)
						{
							this.Values = new ArrayList();
							this.MaxOccurs = option.MaxOccurs;
						}
						else
						{
							if (this.MemberInfo is MethodInfo || this.MemberInfo is PropertyInfo)
								this.MaxOccurs = option.MaxOccurs;
							else
								throw new InvalidOperationException("MaxOccurs set to non default value (" + option.MaxOccurs + ") for a [" + 
											this.MemberInfo.ToString() + "] option");
						}
					}
				}
				else
				{
					this.BooleanOption = true;
					if (option.MaxOccurs != 1)
					{			
						if (this.MemberInfo is MethodInfo || this.MemberInfo is PropertyInfo)
							this.MaxOccurs = option.MaxOccurs;
						else
							throw new InvalidOperationException("MaxOccurs set to non default value (" + option.MaxOccurs + ") for a [" + 
										this.MemberInfo.ToString() + "] option");
					}
				}
			}
		}

		internal string Key
		{
			get { 
				if (useGNUFormat) {				
					string ShortID = this.ShortForm.ToUpper();
					if (ShortID == string.Empty)
						ShortID = "ZZ";
					return  ShortID + " " + this.LongForm; 
				} else
					return this.LongForm + " " + this.ShortForm; 
			}
		}

		int IComparable.CompareTo(object other)
		{
			return Key.CompareTo(((OptionDetails)other).Key);
		}

		public void TransferValues()
		{
			if (Values != null)
			{
				if (MemberInfo is FieldInfo)
				{
					((FieldInfo)MemberInfo).SetValue(OptionBundle, Values.ToArray(ParameterType.GetElementType()));
					return;
				}

				if (MemberInfo is PropertyInfo) 
				{
					((PropertyInfo)MemberInfo).SetValue(OptionBundle, Values.ToArray(ParameterType.GetElementType()), null);
					return;
				}

				if ((WhatToDoNext)((MethodInfo)MemberInfo).Invoke(OptionBundle, new object[] { Values.ToArray(ParameterType.GetElementType()) }) == WhatToDoNext.AbandonProgram)
					System.Environment.Exit(1);
			}
		}

		private int HowManyBeforeExceedingMaxOccurs(int howMany)
		{
			if (MaxOccurs > 0 && (Occurs + howMany) > MaxOccurs) {
				System.Console.Error.WriteLine("Option " + LongForm + " can be used at most " + MaxOccurs + " times. Ignoring extras...");
				howMany = MaxOccurs - Occurs;
			}
			Occurs += howMany;
			return howMany;
		}
		
		private bool AddingOneMoreExceedsMaxOccurs { get { return HowManyBeforeExceedingMaxOccurs(1) < 1; } }

		private void DoIt(bool setValue)
		{
			if (AddingOneMoreExceedsMaxOccurs) 
				return;

			if (verboseParsing)
				Console.WriteLine("<{0}> set to [{1}]", this.LongForm, setValue);

			if (MemberInfo is FieldInfo)
			{
				((FieldInfo)MemberInfo).SetValue(OptionBundle, setValue);
				return;
			}
			if (MemberInfo is PropertyInfo)
			{
				((PropertyInfo)MemberInfo).SetValue(OptionBundle, setValue, null);
				return;
			}
			if ((WhatToDoNext)((MethodInfo)MemberInfo).Invoke(OptionBundle, null) == WhatToDoNext.AbandonProgram)
				System.Environment.Exit(1);
		}
		
		private void DoIt(string parameterValue)
		{
			if (parameterValue == null)
				parameterValue = "";

			string[] parameterValues;
			
			if (dontSplitOnCommas || MaxOccurs == 1)
				parameterValues = new string[] { parameterValue };
			else
				parameterValues = parameterValue.Split(',');

			int waitingToBeProcessed = HowManyBeforeExceedingMaxOccurs(parameterValues.Length);

			foreach (string parameter in parameterValues)
			{
				if (waitingToBeProcessed-- <= 0)
					break;
					
				object convertedParameter = null;

				if (verboseParsing)
					Console.WriteLine("<" + this.LongForm + "> set to [" + parameter + "]");

				if (Values != null && parameter != null) {
					try {
						convertedParameter = Convert.ChangeType(parameter, ParameterType.GetElementType());
					} catch (Exception ex) {
						Console.WriteLine(String.Format("The value '{0}' is not convertible to the appropriate type '{1}' for the {2} option", parameter, ParameterType.GetElementType().Name, DefaultForm));						
					}
					Values.Add(convertedParameter);
					continue;
				}

				if (parameter != null) {	
					try {
						convertedParameter = Convert.ChangeType(parameter, ParameterType);
					} catch (Exception ex) {
						Console.WriteLine(String.Format("The value '{0}' is not convertible to the appropriate type '{1}' for the {2} option", parameter, ParameterType.Name, DefaultForm));												
						continue;
					}
				}

				if (MemberInfo is FieldInfo) {
					((FieldInfo)MemberInfo).SetValue(OptionBundle, convertedParameter);
					continue;
				}

				if (MemberInfo is PropertyInfo) {
					((PropertyInfo)MemberInfo).SetValue(OptionBundle, convertedParameter, null);
					continue;
				}

				if ((WhatToDoNext)((MethodInfo)MemberInfo).Invoke(OptionBundle, new object[] { convertedParameter }) == WhatToDoNext.AbandonProgram)
					System.Environment.Exit(1);
			}
		}

		private bool IsThisOption(string arg)
		{
			if (arg != null && arg != string.Empty)
			{
				arg = arg.TrimStart('-', '/');			
				if (VBCStyleBoolean)
					arg = arg.TrimEnd('-', '+');	
				return (arg == ShortForm || arg == LongForm || arg == AlternateForm);
			}
			return false;
		}

		public static void LinkAlternatesInsideList(ArrayList list)
		{
			Hashtable baseForms = new Hashtable(list.Count);
			foreach (OptionDetails option in list) {
				if (option.LongForm != null && option.LongForm.Trim().Length > 0) {
					string[] parts = option.LongForm.Split(':');
					if (parts.Length < 2) {
						baseForms.Add(option.LongForm, option);
					} else {
						OptionDetails baseForm = (OptionDetails)baseForms[parts[0]];
						if (baseForm != null) {
							// simple linked list
							option.NextAlternate = baseForm.NextAlternate;
							baseForm.NextAlternate = option;
						}
					}
				}
			}
		}

		private bool IsAlternate(string compoundArg)
		{
			OptionDetails next = NextAlternate;
			while (next != null) {
				if (next.IsThisOption(compoundArg))
					return true;
				next = next.NextAlternate;
			}
			return false;
		}

		public OptionProcessingResult ProcessArgument(string arg, string nextArg)
		{
			if (IsAlternate(arg + ":" + nextArg))
				return OptionProcessingResult.NotThisOption;
				
			if (IsThisOption(arg))
			{
				if (!NeedsParameter)
				{
					if (VBCStyleBoolean && arg.EndsWith("-"))
						DoIt(false);
					else
						DoIt(true);
					return OptionProcessingResult.OptionAlone;
				}
				else
				{
					DoIt(nextArg);
					return OptionProcessingResult.OptionConsumedParameter;
				}
			}

			if (IsThisOption(arg + ":" + nextArg))
			{
				DoIt(true);
				return OptionProcessingResult.OptionConsumedParameter;
			}

			return OptionProcessingResult.NotThisOption;
		}
	}
}
