#!/usr/bin/python3
# Learn more about how this works by visiting: https://github.com/SurpriseDog/EasyArgs

import os
import re
import sys
from argparse import ArgumentParser, SUPPRESS

from sd.common import list_get
from sd.columns import auto_cols
from sd.common import undent


def easy_parse(optionals_list, pos_list=None, **kargs):
	'''
	Simpler way to pass arguments to ArgMaster class.
	All kargs are passed to ArgMaster. See the actual implementation for details.
	'''
	am = ArgMaster(**kargs)
	if pos_list:
		am.update(pos_list, title="Positional Arguments", positionals=True)
	am.update(optionals_list, title="Optional Arguments")
	return am.parse()


def show_args(uargs):
	"Show the arguments returned"
	if not uargs:
		return
	auto_cols(sorted([[key, repr(val)] for key, val in (vars(uargs)).items()]))
	print()


class ArgMaster():
	'''
	A wrapper class for ArgumentParser with easy to use arguments. See update_parser() for details.
	sortme will sort all arguments by name (except positionals)
	other arguments are passed onto the ArgumentParser constructor
	'''

	def __init__(self, sortme=True, allow_abbrev=True, usage=None, description=None, newline='\n',
				 verbose=False, exit=True, **kargs):

		# Parsing
		self.verbose = verbose			# Print what each argument does
		self.dashes = ('-', '--')		# - or -- before argument
		self.exit = exit				# Quit on error

		# Help Formatting:
		self.sortme = sortme            # Sort all non positionals args
		self.usage = usage				# Usage message
		self.description = description
		self.newline = newline			# Newlines in print_help
		self.autoformat = True			# Capitalize sentences and add a period

		# Internal
		self.parser = ArgumentParser(allow_abbrev=allow_abbrev, add_help=False, usage=SUPPRESS, **kargs)
		self.groups = []                # List of all groups

		# Allow optionals before positionals:
		self.intermixed = hasattr(self.parser, 'parse_intermixed_args')

	def parse(self, args=None, am_help=True, **kargs):
		"Parse the args and return them"
		if not args:
			args = sys.argv[1:]

		if not am_help:
			self.parser.add_help = True
			return self.parser.parse_args(args)

		# Match help
		for arg in args:
			if re.match('--*h$|--*help$', arg):
				self.print_help(**kargs)
				if self.exit:
					sys.exit(0)
				return None
		try:
			if self.intermixed:
				return self.parser.parse_intermixed_args(args)
			else:
				return self.parser.parse_args(args)
		except SystemExit:
			self.print_help(**kargs)
			if self.exit:
				sys.exit(0)


	def print_help(self, show_type=True, wrap=-4, tab='  '):
		'''Print a custom help message using only ArgMaster args
		show_type = append the variable type expected after each optional argument.
		--arg <int> <int> will expect 2 integers after the arg
		wrap = word wrap instead of using full terminal. 0 = Terminal width
		sort = sort alphabetically. Positional variables are never sorted.
		To sort individual groups, add a special key: group.sortme = True

		Warning: If your variable is not in a group, it will not be shown!'''

		if self.description:
			print('\n' + self.description)

		if self.usage:
			name = os.path.basename(sys.argv[0])
			print('\n' + "Usage:", name, self.usage)
		final = []
		width = 0                       # Max width of the variables column
		for group in self.groups:
			out = []
			for args in group['args']:
				msg = args['msg']
				if msg == SUPPRESS:
					continue
				alias = args['alias']
				if show_type:
					if args['typ'] and args['typ'] != bool:
						if args['typ'] == list:
							typ = '...'
						else:
							typ = '<' + str(args['typ']).replace('class ', '')[2:-2] + '>'
						alias += ' ' + typ
				if len(alias) > width:
					width = len(alias)
				out.append([alias, msg])

			if group['sortme'] is not None:
				sortme = group['sortme']
			else:
				sortme = self.sortme
			if sortme:
				# Sort the group while mainting the order of positional arguments at the top
				positionals = [out.pop(line) for line in range(len(out) - 1, -1, -1) if out[line][0].startswith('<')]
				out.sort()
				out = list(reversed(positionals)) + out
			final.append(out)

		for index, out in enumerate(final):
			group = self.groups[index]
			if out:
				print(self.newline, end='')
				for line, _ in enumerate(out):
					out[line][0] = tab + out[line][0].ljust(width)
				if 'title' in group:
					print(group['title'])	#.rstrip(':') + ':')
				if 'description' in group:
					auto_cols([[tab + group['description']]], wrap=wrap)
				auto_cols(out, wrap=wrap)
		print()


	def update(self, args, title=None, sortme=None, **kargs):
		"Pass list to update_parser() and append result to parser"
		group = self.parser.add_argument_group(title)
		args = self.update_parser(args, group, **kargs)
		self.groups.append(dict(args=args, title=title, sortme=sortme))


	def update_parser(self, lines, parser=None, hidden=False, positionals=False):
		'''
		A more intuitive method for adding arguments
		parser can be empty to return a new parser or a parser argument group
		hidden = Suppress arguments from showing up in help
		positionals = Make group positional arguments
		verbose = Show verbosely how each line in the array is added to argparse


		Format:
			Pass an array with lines in the format:

				('alias', 'variable_name', type, default),
				"help string",

			You only need to include the fields required, but you can't skip over any.
				('alias', '',)        # okay
				('alias', type,)      # not okay

			Substitute the word list with a number like "2" to get that number of args required.
				('list-args, '', 2)

			Positional arguments are optional by default, but you can specify a number to make them required.
			To use them, make sure to pass: positionals=True to update_parser

				('pos-arg', '', 1)

		See what your arguments are producing by passing verbose=True or running easy_args.show_args(args)
		'''

		# Make sure the loop ends on a help string
		if not isinstance(lines[-1], str):
			lines.append("")

		alias = None        # --variable name
		varname = None      # Variable Name
		default = None      # Default value
		out = []

		def update():
			nonlocal alias
			"# Update argument to parser:"
			if parser:
				if positionals:
					parser.add_argument(varname, default=default, nargs=nargs, help=msg)
				else:
					# alias = '--' + alias
					aliases = [d + alias for d in self.dashes]
					if typ == bool:
						parser.add_argument(*aliases, dest=varname, default=default, action=action, help=msg)
					else:
						parser.add_argument(*aliases, dest=varname, default=default, type=typ,
											nargs=nargs, help=msg, metavar='')
				out.append(dict(alias=alias, dest=varname, typ=typ, default=default, msg=msg))
				if self.verbose:
					print('alias  :', alias)
					print('dest   :', varname)
					print('default:', default)
					print('type   :', typ)
					print('nargs  :', nargs, '\n\n')

		for index, args in enumerate(lines):

			# Add help if available
			if isinstance(args, str):
				msg = undent(args.strip())
				if self.autoformat:
					msg = msg.title()
					if msg and not msg.endswith('.'):
						last = msg.split()[-1]
						if last[-1].isalnum() and not last.startswith('-'):
							msg = msg + '.'
				if default:
					msg += "  Default: " + str(default)

			if hidden:
				# Hide the help text:
				msg = SUPPRESS

			# If on a new tuple line, add_argument
			if alias or varname:
				update()
				alias = None
				varname = None
				msg = ""

			# Continue if not on a new tuple line
			if isinstance(args, str):
				continue

			# Read the values from the tuple:
			alias = args[0].lstrip('-')

			# Variable Name
			varname = list_get(args, 1)
			if not varname:
				varname = alias

			# Type
			typ = list_get(args, 2, str)


			# Default value
			if typ == list or type(typ) == int:
				default = list_get(args, 3, [])
			else:
				default = list_get(args, 3, '')


			# Argument Type and number required
			if typ == list:
				nargs = '*'
				typ = str
			elif isinstance(typ, int):
				if positionals and typ == 1:
					nargs = None
				else:
					nargs = typ
				typ = str
			else:
				nargs = '?'

			# Special handing for booleans
			if typ == bool:
				if default:
					action = 'store_false'
				else:
					action = 'store_true'
					default = False
			if index == len(lines) - 1:
				update()

		return out


def help_parser(parser, show_type=True, sortme=True, wrap=100, tab='  '):
	"Standalone version of help that only needs an argparse parser"
	'''Print a custom help message from the ArgumentParser class
		show_type = append the variable type expected after each optional argument.
		--arg <int> <int> will expect 2 integers after the arg
		wrap = word wrap instead of using full terminal. 0 = disable
		sort = sort alphabetically. Positional variables are never sorted.
		To sort individual groups, add a special key: group['sortme'] = True

		Warning: If your variable is not in a group, it will not be shown!
	'''
	def alias(action):
		"Return string representing the variable for the leftmost column"
		if action.option_strings:
			text = ', '.join(action.option_strings)
		else:
			text = action.dest
		if show_type:
			if not action.option_strings:
				text = '<' + text + '>'
			if action.type and action.type != bool:
				if action.type == list:
					typ = '...'
				else:
					typ = '<' + str(action.type).replace('class ', '')[2:-2] + '>'

				if isinstance(action.nargs, int):
					count = action.nargs
				else:
					count = 1
				text += ' ' + ' '.join([typ] * count)
		return text

	final = []
	width = 0
	for group in parser._action_groups:         # pylint: disable=W0212
		out = []
		for action in group._group_actions:     # pylint: disable=W0212
			if action.help and action.help == SUPPRESS:
				continue
			msg = list(action.help)
			msg[0] = msg[0].title()
			variable = alias(action)
			out.append([variable, ''.join(msg)])
			if len(variable) > width:
				width = len(variable)
		if sortme or group.__dict__.get('sortme', False):
			# Sort the group while mainting the order of positional arguments at the top
			positionals = [out.pop(line) for line in range(len(out) - 1, -1, -1) if out[line][0].startswith('<')]
			out.sort()
			out = list(reversed(positionals)) + out
		final.append(out)

	for group in parser._action_groups:         # pylint: disable=W0212
		out = final.pop(0)
		if out:
			for line, _ in enumerate(out):
				out[line][0] = tab + out[line][0].ljust(width)

			print()
			if group.title:
				print(group.title + ':')
			if group.description:
				auto_cols([[tab + group.description]])
			auto_cols(out, wrap=wrap)
#!/usr/bin/python3
# Learn more about how this works by visiting: https://github.com/SurpriseDog/EasyArgs

import os
import re
import sys
from argparse import ArgumentParser, SUPPRESS

from sd.common import list_get
from sd.columns import auto_cols
from sd.common import undent


def easy_parse(optionals_list, pos_list=None, **kargs):
	'''
	Simpler way to pass arguments to ArgMaster class.
	All kargs are passed to ArgMaster. See the actual implementation for details.
	'''
	am = ArgMaster(**kargs)
	if pos_list:
		am.update(pos_list, title="Positional Arguments", positionals=True)
	am.update(optionals_list, title="Optional Arguments")
	return am.parse()


def show_args(uargs):
	"Show the arguments returned"
	if not uargs:
		return
	auto_cols(sorted([[key, repr(val)] for key, val in (vars(uargs)).items()]))
	print()


class ArgMaster():
	'''
	A wrapper class for ArgumentParser with easy to use arguments. See update_parser() for details.
	sortme will sort all arguments by name (except positionals)
	other arguments are passed onto the ArgumentParser constructor
	'''

	def __init__(self, sortme=True, allow_abbrev=True, usage=None, description=None, newline='\n',
				 verbose=False, exit=True, **kargs):

		# Parsing
		self.verbose = verbose			# Print what each argument does
		self.dashes = ('-', '--')		# - or -- before argument
		self.exit = exit				# Quit on error

		# Help Formatting:
		self.sortme = sortme            # Sort all non positionals args
		self.usage = usage				# Usage message
		self.description = description
		self.newline = newline			# Newlines in print_help
		self.autoformat = True			# Capitalize sentences and add a period

		# Internal
		self.parser = ArgumentParser(allow_abbrev=allow_abbrev, add_help=False, usage=SUPPRESS, **kargs)
		self.groups = []                # List of all groups

		# Allow optionals before positionals:
		self.intermixed = hasattr(self.parser, 'parse_intermixed_args')

	def parse(self, args=None, am_help=True, **kargs):
		"Parse the args and return them"
		if not args:
			args = sys.argv[1:]

		if not am_help:
			self.parser.add_help = True
			return self.parser.parse_args(args)

		# Match help
		for arg in args:
			if re.match('--*h$|--*help$', arg):
				self.print_help(**kargs)
				if self.exit:
					sys.exit(0)
				return None
		try:
			if self.intermixed:
				return self.parser.parse_intermixed_args(args)
			else:
				return self.parser.parse_args(args)
		except SystemExit:
			self.print_help(**kargs)
			if self.exit:
				sys.exit(0)


	def print_help(self, show_type=True, wrap=-4, tab='  '):
		'''Print a custom help message using only ArgMaster args
		show_type = append the variable type expected after each optional argument.
		--arg <int> <int> will expect 2 integers after the arg
		wrap = word wrap instead of using full terminal. 0 = Terminal width
		sort = sort alphabetically. Positional variables are never sorted.
		To sort individual groups, add a special key: group.sortme = True

		Warning: If your variable is not in a group, it will not be shown!'''

		if self.description:
			print('\n' + self.description)

		if self.usage:
			name = os.path.basename(sys.argv[0])
			print('\n' + "Usage:", name, self.usage)
		final = []
		width = 0                       # Max width of the variables column
		for group in self.groups:
			out = []
			for args in group['args']:
				msg = args['msg']
				if msg == SUPPRESS:
					continue
				alias = args['alias']
				if show_type:
					if args['typ'] and args['typ'] != bool:
						if args['typ'] == list:
							typ = '...'
						else:
							typ = '<' + str(args['typ']).replace('class ', '')[2:-2] + '>'
						alias += ' ' + typ
				if len(alias) > width:
					width = len(alias)
				out.append([alias, msg])

			if group['sortme'] is not None:
				sortme = group['sortme']
			else:
				sortme = self.sortme
			if sortme:
				# Sort the group while mainting the order of positional arguments at the top
				positionals = [out.pop(line) for line in range(len(out) - 1, -1, -1) if out[line][0].startswith('<')]
				out.sort()
				out = list(reversed(positionals)) + out
			final.append(out)

		for index, out in enumerate(final):
			group = self.groups[index]
			if out:
				print(self.newline, end='')
				for line, _ in enumerate(out):
					out[line][0] = tab + out[line][0].ljust(width)
				if 'title' in group:
					print(group['title'])	#.rstrip(':') + ':')
				if 'description' in group:
					auto_cols([[tab + group['description']]], wrap=wrap)
				auto_cols(out, wrap=wrap)
		print()


	def update(self, args, title=None, sortme=None, **kargs):
		"Pass list to update_parser() and append result to parser"
		group = self.parser.add_argument_group(title)
		args = self.update_parser(args, group, **kargs)
		self.groups.append(dict(args=args, title=title, sortme=sortme))


	def update_parser(self, lines, parser=None, hidden=False, positionals=False):
		'''
		A more intuitive method for adding arguments
		parser can be empty to return a new parser or a parser argument group
		hidden = Suppress arguments from showing up in help
		positionals = Make group positional arguments
		verbose = Show verbosely how each line in the array is added to argparse


		Format:
			Pass an array with lines in the format:

				('alias', 'variable_name', type, default),
				"help string",

			You only need to include the fields required, but you can't skip over any.
				('alias', '',)        # okay
				('alias', type,)      # not okay

			Substitute the word list with a number like "2" to get that number of args required.
				('list-args, '', 2)

			Positional arguments are optional by default, but you can specify a number to make them required.
			To use them, make sure to pass: positionals=True to update_parser

				('pos-arg', '', 1)

		See what your arguments are producing by passing verbose=True or running easy_args.show_args(args)
		'''

		# Make sure the loop ends on a help string
		if not isinstance(lines[-1], str):
			lines.append("")

		alias = None        # --variable name
		varname = None      # Variable Name
		default = None      # Default value
		out = []

		def update():
			nonlocal alias
			"# Update argument to parser:"
			if parser:
				if positionals:
					parser.add_argument(varname, default=default, nargs=nargs, help=msg)
				else:
					# alias = '--' + alias
					aliases = [d + alias for d in self.dashes]
					if typ == bool:
						parser.add_argument(*aliases, dest=varname, default=default, action=action, help=msg)
					else:
						parser.add_argument(*aliases, dest=varname, default=default, type=typ,
											nargs=nargs, help=msg, metavar='')
				out.append(dict(alias=alias, dest=varname, typ=typ, default=default, msg=msg))
				if self.verbose:
					print('alias  :', alias)
					print('dest   :', varname)
					print('default:', default)
					print('type   :', typ)
					print('nargs  :', nargs, '\n\n')

		for index, args in enumerate(lines):

			# Add help if available
			if isinstance(args, str):
				msg = undent(args.strip())
				if self.autoformat:
					msg = msg.title()
					if msg and not msg.endswith('.'):
						last = msg.split()[-1]
						if last[-1].isalnum() and not last.startswith('-'):
							msg = msg + '.'
				if default:
					msg += "  Default: " + str(default)

			if hidden:
				# Hide the help text:
				msg = SUPPRESS

			# If on a new tuple line, add_argument
			if alias or varname:
				update()
				alias = None
				varname = None
				msg = ""

			# Continue if not on a new tuple line
			if isinstance(args, str):
				continue

			# Read the values from the tuple:
			alias = args[0].lstrip('-')

			# Variable Name
			varname = list_get(args, 1)
			if not varname:
				varname = alias

			# Type
			typ = list_get(args, 2, str)


			# Default value
			if typ == list or type(typ) == int:
				default = list_get(args, 3, [])
			else:
				default = list_get(args, 3, '')


			# Argument Type and number required
			if typ == list:
				nargs = '*'
				typ = str
			elif isinstance(typ, int):
				if positionals and typ == 1:
					nargs = None
				else:
					nargs = typ
				typ = str
			else:
				nargs = '?'

			# Special handing for booleans
			if typ == bool:
				if default:
					action = 'store_false'
				else:
					action = 'store_true'
					default = False
			if index == len(lines) - 1:
				update()

		return out


def help_parser(parser, show_type=True, sortme=True, wrap=100, tab='  '):
	"Standalone version of help that only needs an argparse parser"
	'''Print a custom help message from the ArgumentParser class
		show_type = append the variable type expected after each optional argument.
		--arg <int> <int> will expect 2 integers after the arg
		wrap = word wrap instead of using full terminal. 0 = disable
		sort = sort alphabetically. Positional variables are never sorted.
		To sort individual groups, add a special key: group['sortme'] = True

		Warning: If your variable is not in a group, it will not be shown!
	'''
	def alias(action):
		"Return string representing the variable for the leftmost column"
		if action.option_strings:
			text = ', '.join(action.option_strings)
		else:
			text = action.dest
		if show_type:
			if not action.option_strings:
				text = '<' + text + '>'
			if action.type and action.type != bool:
				if action.type == list:
					typ = '...'
				else:
					typ = '<' + str(action.type).replace('class ', '')[2:-2] + '>'

				if isinstance(action.nargs, int):
					count = action.nargs
				else:
					count = 1
				text += ' ' + ' '.join([typ] * count)
		return text

	final = []
	width = 0
	for group in parser._action_groups:         # pylint: disable=W0212
		out = []
		for action in group._group_actions:     # pylint: disable=W0212
			if action.help and action.help == SUPPRESS:
				continue
			msg = list(action.help)
			msg[0] = msg[0].title()
			variable = alias(action)
			out.append([variable, ''.join(msg)])
			if len(variable) > width:
				width = len(variable)
		if sortme or group.__dict__.get('sortme', False):
			# Sort the group while mainting the order of positional arguments at the top
			positionals = [out.pop(line) for line in range(len(out) - 1, -1, -1) if out[line][0].startswith('<')]
			out.sort()
			out = list(reversed(positionals)) + out
		final.append(out)

	for group in parser._action_groups:         # pylint: disable=W0212
		out = final.pop(0)
		if out:
			for line, _ in enumerate(out):
				out[line][0] = tab + out[line][0].ljust(width)

			print()
			if group.title:
				print(group.title + ':')
			if group.description:
				auto_cols([[tab + group.description]])
			auto_cols(out, wrap=wrap)
#!/usr/bin/python3 # Learn more about how this works by visiting: https://github.com/SurpriseDog/EasyArgs Simpler way to pass arguments to ArgMaster class. 	All kargs are passed to ArgMaster. See the actual implementation for details. A wrapper class for ArgumentParser with easy to use arguments. See update_parser() for details. 	sortme will sort all arguments by name (except positionals) 	other arguments are passed onto the ArgumentParser constructor # Parsing # Print what each argument does # - or -- before argument # Quit on error # Help Formatting: # Sort all non positionals args # Usage message # Newlines in print_help # Capitalize sentences and add a period # Internal # List of all groups # Allow optionals before positionals: # Match help Print a custom help message using only ArgMaster args 		show_type = append the variable type expected after each optional argument. 		--arg <int> <int> will expect 2 integers after the arg 		wrap = word wrap instead of using full terminal. 0 = Terminal width 		sort = sort alphabetically. Positional variables are never sorted. 		To sort individual groups, add a special key: group.sortme = True  		Warning: If your variable is not in a group, it will not be shown! # Max width of the variables column # Sort the group while mainting the order of positional arguments at the top #.rstrip(':') + ':') A more intuitive method for adding arguments 		parser can be empty to return a new parser or a parser argument group 		hidden = Suppress arguments from showing up in help 		positionals = Make group positional arguments 		verbose = Show verbosely how each line in the array is added to argparse   		Format: 			Pass an array with lines in the format:  				('alias', 'variable_name', type, default), 				"help string",  			You only need to include the fields required, but you can't skip over any. 				('alias', '',)        # okay 				('alias', type,)      # not okay  			Substitute the word list with a number like "2" to get that number of args required. 				('list-args, '', 2)  			Positional arguments are optional by default, but you can specify a number to make them required. 			To use them, make sure to pass: positionals=True to update_parser  				('pos-arg', '', 1)  		See what your arguments are producing by passing verbose=True or running easy_args.show_args(args) # Make sure the loop ends on a help string # --variable name # Variable Name # Default value # alias = '--' + alias # Add help if available # Hide the help text: # If on a new tuple line, add_argument # Continue if not on a new tuple line # Read the values from the tuple: # Variable Name # Type # Default value # Argument Type and number required # Special handing for booleans Print a custom help message from the ArgumentParser class 		show_type = append the variable type expected after each optional argument. 		--arg <int> <int> will expect 2 integers after the arg 		wrap = word wrap instead of using full terminal. 0 = disable 		sort = sort alphabetically. Positional variables are never sorted. 		To sort individual groups, add a special key: group['sortme'] = True  		Warning: If your variable is not in a group, it will not be shown! # pylint: disable=W0212 # pylint: disable=W0212 # Sort the group while mainting the order of positional arguments at the top # pylint: disable=W0212