import argparse
from collections import OrderedDict
import os
import re
import sys
import types

if sys.version_info >= (3, 0):
    from io import StringIO
else:
    from StringIO import StringIO

__version__ = "0.9"


ACTION_TYPES_THAT_DONT_NEED_A_VALUE = {argparse._StoreTrueAction,
    argparse._StoreFalseAction, argparse._CountAction,
    argparse._StoreConstAction, argparse._AppendConstAction}


# global ArgumentParser instances
_parsers = {}

def initArgumentParser(name=None, **kwargs):
    """Creates a global ArgumentParser instance with the given name,
    passing any args other than "name" to the ArgumentParser constructor.
    This instance can then be retrieved using getArgumentParser(..)
    """

    if name is None:
        name = "default"

    if name in _parsers:
        raise ValueError(("kwargs besides 'name' can only be passed in the"
            " first time. '%s' ArgumentParser already exists: %s") % (
            name, _parsers[name]))

    kwargs.setdefault('formatter_class', argparse.ArgumentDefaultsHelpFormatter)
    kwargs.setdefault('conflict_handler', 'resolve')
    _parsers[name] = ArgumentParser(**kwargs)


def getArgumentParser(name=None, **kwargs):
    """Returns the global ArgumentParser instance with the given name. The 1st
    time this function is called, a new ArgumentParser instance will be created
    for the given name, and any args other than "name" will be passed on to the
    ArgumentParser constructor.
    """
    if name is None:
        name = "default"

    if len(kwargs) > 0 or name not in _parsers:
        initArgumentParser(name, **kwargs)

    return _parsers[name]


class ArgumentParser(argparse.ArgumentParser):
    """Drop-in replacement for argparse.ArgumentParser that adds support for
    environment variables and .ini or .yaml-style config files.
    """

    def __init__(self,
        prog=None,
        usage=None,
        description=None,
        epilog=None,
        version=None,
        parents=[],
        formatter_class=argparse.HelpFormatter,
        prefix_chars='-',
        fromfile_prefix_chars=None,
        argument_default=None,
        conflict_handler='error',
        add_help=True,

        add_config_file_help=True,
        add_env_var_help=True,

        default_config_files=[],
        allow_unknown_config_file_keys=False,

        args_for_setting_config_path=[],
        config_arg_is_required=False,
        config_arg_help_message="config file path",
        ):

        """Supports all the same args as the argparse.ArgumentParser
        constructor, as well as these additional args.

        Additional Args:
            add_config_file_help: Whether to add a description of config file
                syntax to the help message.
            add_env_var_help: Whether to add something to the help message for
                args that can be set through environment variables.
            default_config_files: When specified, this list of config files will
                be parsed in order, with the values from each config file
                taking precedence over pervious ones. This allows an application
                to look for config files in multiple standard locations such as
                the install directory, home directory, and current directory:
                ["<install dir>/app_config.ini",
                "~/.my_app_config.ini",
                "./app_config.txt"]
            allow_unknown_config_file_keys: Whether unknown config file keys
                should be ignored or whether it should be an error.
            args_for_setting_config_path: A list of one or more command line
                args that would allow a user to provide a config file path
                (eg. ["-c", "--config-file"]). Default: []
            config_arg_is_required: when args_for_setting_config_path is set,
                set this to True to always require users to provide a config path.
            config_arg_help_message: when args_for_setting_config_path is set,
                this will be the help message for the config_file_args.
        """
        self._add_config_file_help = add_config_file_help
        self._add_env_var_help = add_env_var_help

        # extract kwargs that can be passed to the super constructor
        kwargs_for_super = {k: v for k, v in locals().items() if k in [
            "prog", "usage", "description", "epilog", "version", "parents",
            "formatter_class", "prefix_chars", "fromfile_prefix_chars",
            "argument_default", "conflict_handler", "add_help" ]}
        if sys.version_info >= (3, 3) and "version" in kwargs_for_super:
            del kwargs_for_super["version"]  # version arg deprecated in v3.3

        argparse.ArgumentParser.__init__(self, **kwargs_for_super)

        # parse the additionial args
        self._default_config_files = default_config_files
        self._allow_unknown_config_file_keys = allow_unknown_config_file_keys
        if args_for_setting_config_path:
            self.add_argument(*args_for_setting_config_path, dest="config_file",
                required=config_arg_is_required, help=config_arg_help_message,
                is_config_file=True)


    def parse_args(self, args = None, namespace = None,
                   config_file_contents = None, env_vars = os.environ):
        """Supports all the same args as the ArgumentParser.parse_args(..),
        as well as these additional args.

        Additional Args:
            config_file_contents: String. Used for testing.
            env_vars: Dictionary. Used for testing.
        """
        args, argv = self.parse_known_args(args = args,
            namespace = namespace,
            config_file_contents = config_file_contents,
            env_vars = env_vars)
        if argv:
            self.error('unrecognized arguments: %s' % ' '.join(argv))
        return args


    def parse_known_args(self, args = None, namespace = None,
                         config_file_contents = None, env_vars = os.environ):
        """Supports all the same args as the ArgumentParser.parse_args(..),
        as well as these additional args.

        Additional Args:
            config_file_contents: String. Used for testing.
            env_vars: Dictionary. Used for testing.
        """

        if args is None:
            args = sys.argv[1:]
        elif type(args) == str:
            args = args.split()
        else:
            args = list(args)

        # maps string describing the source (eg. env var) to a settings dict
        # to keep track of where values came from (used by print_values())
        self._source_to_settings = OrderedDict()
        self._command_line_args_string = ' '.join(args)
        if args:
            self._source_to_settings["Command Line Args: "] = {
                '': self._command_line_args_string}

        # add env var settings to the command line that aren't there already
        env_var_args = []
        actions_with_env_var_values = [a for a in self._actions
            if a.option_strings and a.env_var
                and a.env_var in env_vars
                and not any(opt in args for opt in a.option_strings)]
        for a in actions_with_env_var_values:
            key = a.env_var
            value = env_vars[key]
            env_var_args += self.convert_setting_to_command_line_arg(
                a, key, value)

        args = env_var_args + args

        if env_var_args:
            self._source_to_settings["Environment Variables:\n"] = OrderedDict(
                [(a.env_var, env_vars[a.env_var])
                    for a in actions_with_env_var_values])


        # read settings from config file(s)
        if config_file_contents:
            stream = StringIO(config_file_contents)
            stream.name = "method arg"
            config_streams = [stream]
        else:
            config_streams = self._get_config_file_streams(args)

        # add config file settings to the command line that aren't there already

        # for each action, add its possible config keys to a dict
        possible_config_keys = {config_key: action for action in self._actions
            for config_key in self.get_possible_config_keys(action)}

        # parse each config file
        for stream in config_streams[::-1]:
            try:
                config_settings = self.parse_config_file(stream)
            finally:
                if hasattr(stream, "close"):
                    stream.close()
            # make sure config file doesn't use any unknown keys
            if not self._allow_unknown_config_file_keys:
                invalid_keys = list(
                    set(config_settings.keys()) - set(possible_config_keys.keys()))
                if invalid_keys:
                    self.error(("%s contains unknown config key(s): %s") % (
                        stream.name, ", ".join(invalid_keys)))

            # add config settings to the command line if they aren't there already
            config_args = []
            for key, value in config_settings.items():
                if key in possible_config_keys:
                    action = possible_config_keys[key]
                    already_on_command_line = any(
                        arg in args for arg in action.option_strings)