// SemiTwist Library
// Written in the D programming language.

/** 
Author:
$(WEB www.semitwist.com, Nick Sabalausky)
*/

module semitwist.cmdlineparser;

import tango.core.Array;
import tango.io.Stdout;
import tango.math.Math;
import tango.text.Util;

import semitwist.util;

/**
Usage:

void main(char[][] args)
{
	bool help;
	bool detailhelp;
	int myInt = 2; // Default value == 2
	bool myBool;   // Default value == bool.init (ie, false)
	char[] myStr;  // Default value == (char[]).init (ie, "")

	auto cmd = new CmdLineParser();
	mixin(defineArg!(cmd, help,       "Displays a help summary and exits" ));
	mixin(defineArg!(cmd, detailhelp, "Displays a detailed help message and exits" ));
	mixin(defineArg!(cmd, myInt,  "An integer"));
	mixin(defineArg!(cmd, myBool, "A flag"));
	mixin(defineArg!(cmd, myStr,  "A string"));
	
	if(!cmd.parse(args) || help)
	{
		Stdout.format("{}", cmd.getUsage());
		return;
	}
	if(detailhelp)
	{
		Stdout.format("{}", cmd.getDetailedUsage());
		return;
	}

	Stdout.formatln("myInt:  {}", myInt);
	Stdout.formatln("myBool: {}", myBool);
	Stdout.formatln("myStr:  {}", myStr);
}

Sample Command Lines:
> myApp.exe /myInt:5 /myBool /myStr:blah
> myApp.exe -myInt=5 -myBool:true -myStr:blah
> myApp.exe --myInt=5 /myBool+ "-myStr:blah"
myInt:  5
myBool: true
myStr:  blah

> myApp.exe "/myStr:Hello World"
myInt:  2
myBool: false
myStr:  Hello World

> myApp.exe /foo
Unknown switch: "/foo"
Switches: (prefixes can be '/', '-' or '--')
  /help               Displays a help summary and exits
  /detailhelp         Displays a detailed help message and exits
  /myInt:<int>        An integer (default: 2)
  /myBool             A flag
  /myStr:<char[]>     A string
  
*/

template defineArg(alias cmdLineParser, alias var, char[] desc)
{
	const char[] defineArg =
		"auto _cmdarg_refbox_"~var.stringof~" = new RefBox!("~typeof(var).stringof~")(&"~var.stringof~");"~
		"auto _cmdarg_"~var.stringof~" = new Arg(_cmdarg_refbox_"~var.stringof~`, "`~var.stringof~`", "`~desc~`");`~
		cmdLineParser.stringof~".addArg(_cmdarg_"~var.stringof~");";
	//pragma(msg, "defineArg: " ~ defineArg);
}

//TODO: Add float, double, byte, short, long, and unsigned of each.

// A boxable wrapper useful for variables of primitive types.
class RefBox(T)
{
	private T* val;
	private T optionalValSrc;
	
	this()
	{
		this.val = &optionalValSrc;
	}
	
	this(T* val)
	{
		this.val = val;
	}
	
	T opCall()
	{
		return *val;
	}
	
	void opAssign(T val)
	{
		*this.val = val;
	}
	
	bool opEquals(RefBox!(T) val)
	{
		return *this.val == *val.val;
	}
	
	bool opEquals(T val)
	{
		return *this.val == val;
	}
	
	RefBox!(T) dup()
	{
		auto newBox = new RefBox!(T)();
		newBox.optionalValSrc = *this.val;
		return newBox;
	}
}

char[] getRefBoxTypeName(Object o)
{
	char[] head = "RefBox!(";
	char[] unknown = "{unknown}";
	char[] typeName = o.classinfo.name;
	
	auto start = locatePatternPrior(typeName, head);
	if(start == typeName.length)
		return unknown;
	start += head.length;
		
	auto end = locate(typeName, ')', start);
	if(end == typeName.length)
		return unknown;
	
	// Nested () inside "RefBox!(...)" is not currently supported
	if(tango.text.Util.contains(typeName[start..end], '('))
		return unknown;
		
	return typeName[start..end];
}

class Arg
{
	char[]  name;
	char[]  altName;
	char[]  desc;

	bool isDefault     = false;
	bool isRequired    = false;
	bool arrayMultiple = false;
	bool arrayUnique   = false;
	
	private Object value;
	private Object defaultValue;
	
	this(Object value, char[] name, char[] desc)
	{
		mixin(initMember!(value, name, desc));
		ensureValid();
	}
	
	private void genDefaultValue()
	{
		auto valAsInt  = cast(RefBox!(int   ))value;
		auto valAsBool = cast(RefBox!(bool  ))value;
		auto valAsStr  = cast(RefBox!(char[]))value;

		if     (valAsInt)  defaultValue = valAsInt.dup();
		else if(valAsBool) defaultValue = valAsBool.dup();
		else if(valAsStr)  defaultValue = valAsStr.dup();
	}
	
	void ensureValid()
	{
		//TODO: arrayMultiple and arrayUnique cannot both be set
			
		if( !cast(RefBox!(int   ))value &&
		    !cast(RefBox!(bool  ))value &&
		    !cast(RefBox!(char[]))value )
		{
			throw new Exception("Param to Arg contructor myst be RefBox!(T), where T is int, bool or char[]");
		}
		
		void ensureValidName(char[] name)
		{
			if(!CmdLineParser.isValidArgName(name))
				throw new Exception(stformat(`Tried to define an invalid arg name: "{}". Arg names must be "[a-zA-Z0-9_?]+"`, name));
		}
		ensureValidName(name);
		ensureValidName(altName);
		
		if(name == "")
			throw new Exception(`Tried to define a blank arg name`);
	}
}

class CmdLineParser
{
	private Arg[] args;
	private Arg[char[]] argLookup;
	
	private enum Prefix
	{
		Invalid, DoubleDash, SingleDash, Slash
	}
	
	enum ParseArgResult
	{
		Done, NotFound, Error
	}
	
	static bool isValidArgName(char[] name)
	{
		foreach(char c; name)
		{
			if(!isAlphaNumeric(c) && c != '_' && c != '?')
				return false;
		}
		return true;
	}
	
	private void ensureValid()
	{
		foreach(Arg arg; args)
		{
			arg.ensureValid();
		}
	}
	
	private void populateLookup()
	{
		foreach(Arg arg; args)
		{
			addToArgLookup(arg.name, arg);
			
			if(arg.altName != "")
				addToArgLookup(arg.altName, arg);
		}
	}

	private void genDefaultValues()
	{
		foreach(Arg arg; args)
		{
			arg.genDefaultValue();
		}
	}

	public void addArg(Arg arg)
	{
		args ~= arg;
	}
	
	private void addToArgLookup(char[] name, Arg argDef)
	{
		if(name in argLookup)
			throw new Exception(stformat(`Argument name "{}" defined more than once.`, name));

		argLookup[name] = argDef;
	}
	
	private void splitArg(char[] fullArg, out Prefix prefix, out char[] name, out char[] suffix)
	{
		char[] argNoPrefix;

		// Get prefix
		if(fullArg.length > 2 && fullArg[0..2] == "--")
		{
			argNoPrefix = fullArg[2..$];
			prefix = Prefix.DoubleDash;
		}
		else if(fullArg.length > 1)
		{
			argNoPrefix = fullArg[1..$];
			
			if(fullArg[0] == '-')
				prefix = Prefix.SingleDash;
			else if(fullArg[0] == '/')
				prefix = Prefix.Slash;
			else
			{
				prefix = Prefix.Invalid;
				argNoPrefix = fullArg;
			}
		}
		
		// Get suffix and arg name
		auto suffixIndex = min( tango.core.Array.find(argNoPrefix, ':'),
								tango.core.Array.find(argNoPrefix, '+'),
								tango.core.Array.find(argNoPrefix, '-') );
		name = argNoPrefix[0..suffixIndex];
		suffix = suffixIndex < argNoPrefix.length ?
				 argNoPrefix[suffixIndex..$] : "";
	}

	private ParseArgResult parseArg(char[] cmdArg, Prefix prefix, char[] cmdName, char[] suffix)
	{
		ParseArgResult ret = ParseArgResult.Error;
		
		void HandleMalformedArgument()
		{
			Stdout.formatln(`Invalid value: "{}"`, cmdArg);
			ret = ParseArgResult.Error;
		}
		
		if(cmdName in argLookup)
		{
			auto argDef = argLookup[cmdName];
			auto valAsIntBox  = cast(RefBox!(int   ))argDef.value;
			auto valAsBoolBox = cast(RefBox!(bool  ))argDef.value;
			auto valAsStrBox  = cast(RefBox!(char[]))argDef.value;
			
			ret = ParseArgResult.Done;
			if(valAsBoolBox)
			{
				switch(suffix)
				{
				case "":
				case "+":
				case ":true":
					valAsBoolBox = true;
					break;
				case "-":
				case ":false":
					valAsBoolBox = false;
					break;
				default:
					HandleMalformedArgument();
					break;
				}
			}
			else if(valAsStrBox)
			{
				if(suffix.length > 1 && suffix[0] == ':')
					valAsStrBox = trim(suffix[1..$]);
				else
					HandleMalformedArgument();
			}
			else
				throw new Exception("Internal Error: Failed to process an Arg.value type that hasn't been set as unsupported.");
		}
		else
		{
			Stdout.formatln(`Unknown switch: "{}"`, cmdArg);
			ret = ParseArgResult.NotFound;
		}
		
		return ret;
	}

	//TODO: check for response file

	public bool parse(char[][] args)
	{
		bool error=false;
		
		ensureValid();
		populateLookup();
		genDefaultValues();
		
		foreach(char[] argStr; args[1..$])
		{
			char[] suffix;
			char[] argName;
			Prefix prefix;
			
			splitArg(argStr, prefix, argName, suffix);
			if(prefix == Prefix.Invalid)
			{
				Stdout.formatln(`Unexpected value: "{}"`, argStr);
				error = true;
				continue;
			}
			//mixin(traceVal!("argStr ", "prefix ", "argName", "suffix "));
			
			auto result = parseArg(argStr, prefix, argName, suffix);
			switch(result)
			{
			case ParseArgResult.Done:
				continue;
				
			case ParseArgResult.Error:
			case ParseArgResult.NotFound:
				error = true;
				break;
				
			default:
				throw new Exception(
					stformat("Unexpected ParseArgResult: ({})", result));
			}
		}
		
		return !error;
	}
	
	//TODO: Make function to get the maximum length of the arg names

	private char[] switchTypesMsg =
`Switch types:
  bool (default):
    Set s to true: /s /s+ /s:true
    Set s to false: /s- /s:false
    Default value: false (unless otherwise noted)
				  
  char[]:
    Set s to "text": /s:text
    Default value: "" (unless otherwise noted)
				  
  int:
    Set s to 3: /s:3
    Default value: 0 (unless otherwise noted)
`;

	char[] getUsage(int nameColumnWidth=20)
	{
		char[] ret;
		char[] indent = "  ";
		
		ret ~= "Switches: (prefixes can be '/', '-' or '--')\n";
		foreach(Arg arg; args)
		{
			auto valAsIntBox  = cast(RefBox!(int   ))arg.defaultValue;
			auto valAsBoolBox = cast(RefBox!(bool  ))arg.defaultValue;
			auto valAsStrBox  = cast(RefBox!(char[]))arg.defaultValue;

			char[] defaultVal;
			if(valAsIntBox)
				defaultVal = stformat("{}", valAsIntBox());
			else if(valAsBoolBox)
				defaultVal = valAsBoolBox() ? "true" : "";
			else if(valAsStrBox)
				defaultVal = valAsStrBox() == "" ? "" : stformat(`"{}"`, valAsStrBox());
			
			char[] defaultValStr = defaultVal == "" ?
				"" : stformat(" (default: {})", defaultVal);
				
			char[] argSuffix = valAsBoolBox ? "" : ":<"~getRefBoxTypeName(arg.value)~">" ;

			char[] argName = "/"~arg.name~argSuffix;
			if(arg.altName != "")
				argName ~= ", /"~arg.altName~argSuffix;
	
			char[] nameColumnWidthStr = stformat("{}", nameColumnWidth);
			ret ~= stformat("{}{,-"~nameColumnWidthStr~"}{}{}\n",
			                indent, argName~" ", arg.desc, defaultValStr);
		}
		return ret;
	}

	char[] getDetailedUsage()
	{
		char[] ret;
		char[] indent = "  ";
		
		ret ~= "Switches: (prefixes can be '/', '-' or '--')\n";
		foreach(Arg arg; args)
		{
			char[] argName = "/"~arg.name;
			if(arg.altName != "")
				argName ~= ", /"~arg.altName;
	
			auto valAsIntBox  = cast(RefBox!(int   ))arg.defaultValue;
			auto valAsBoolBox = cast(RefBox!(bool  ))arg.defaultValue;
			auto valAsStrBox  = cast(RefBox!(char[]))arg.defaultValue;

			char[] defaultVal;
			if(valAsIntBox)
				defaultVal = stformat("{}", valAsIntBox());
			else if(valAsBoolBox)
				defaultVal = stformat("{}", valAsBoolBox());
			else if(valAsStrBox)
				defaultVal = stformat(`"{}"`, valAsStrBox());
			
			ret ~= "\n";
			ret ~= stformat("{} ({}), default: {}\n",
			                argName, getRefBoxTypeName(arg.value), defaultVal);
			ret ~= stformat("{}\n", arg.desc);
		}
		ret ~= "\n";
		ret ~= switchTypesMsg;
		return ret;
	}
}
