# This source file is part of the Swift.org open source project
#
# Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See https://swift.org/LICENSE.txt for license information
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors


"""
Extensions to the standard argparse ArgumentParer class to support multiple
destination actions as well as a new builder DSL for declaratively
constructing complex parsers.
"""

import argparse
from contextlib import contextmanager

from . import Namespace, SUPPRESS, actions
from .actions import Action


__all__ = [
    'ArgumentParser',
]


# -----------------------------------------------------------------------------

class _ActionContainer(object):
    """Container object holding partially applied actions used as a part of the
    builder DSL.
    """

    def __init__(self):
        self.append = _PartialAction(actions.AppendAction)
        self.custom_call = _PartialAction(actions.CustomCallAction)
        self.store = _PartialAction(actions.StoreAction)
        self.store_int = _PartialAction(actions.StoreIntAction)
        self.store_true = _PartialAction(actions.StoreTrueAction)
        self.store_false = _PartialAction(actions.StoreFalseAction)
        self.store_path = _PartialAction(actions.StorePathAction)
        self.toggle_true = _PartialAction(actions.ToggleTrueAction)
        self.toggle_false = _PartialAction(actions.ToggleFalseAction)
        self.unsupported = _PartialAction(actions.UnsupportedAction)


class _CompoundAction(Action):
    """Action composed of multiple actions. Default attributes are derived
    from the first action.
    """

    def __init__(self, actions, **kwargs):
        _actions = []
        for action in actions:
            _actions.append(action(**kwargs))

        kwargs.setdefault('nargs', kwargs[0].nargs)
        kwargs.setdefault('metavar', kwargs[0].metavar)
        kwargs.setdefault('choices', kwargs[0].choices)

        super(_CompoundAction, self).__init__(**kwargs)

        self.actions = _actions

    def __call__(self, *args):
        for action in self.actions:
            action(*args)


class _PartialAction(Action):
    """Action that is partially applied, creating a factory closure used to
    defer initialization of actions in the builder DSL.
    """

    def __init__(self, action_class):
        self.action_class = action_class

    def __call__(self, dests=None, *call_args, **call_kwargs):
        def factory(**kwargs):
            kwargs.update(call_kwargs)
            if dests is not None:
                return self.action_class(dests=dests, *call_args, **kwargs)
            return self.action_class(*call_args, **kwargs)

        return factory


# -----------------------------------------------------------------------------

class _Builder(object):
    """Builder object for constructing complex ArgumentParser instances with
    a more friendly and descriptive DSL.
    """

    def __init__(self, parser, **kwargs):
        assert isinstance(parser, ArgumentParser)

        self._parser = parser
        self._current_group = self._parser
        self._defaults = dict()

        self.actions = _ActionContainer()

    def build(self):
        self._parser.set_defaults(**self._defaults)
        return self._parser

    def _add_argument(self, names, *actions, **kwargs):
        # Unwrap partial actions
        _actions = []
        for action in actions:
            if isinstance(action, _PartialAction):
                action = action()
            _actions.append(action)

        if len(_actions) == 0:
            # Default to store action
            action = actions.StoreAction
        elif len(_actions) == 1:
            action = _actions[0]
        else:
            def thunk(**kwargs):
                return _CompoundAction(_actions, **kwargs)
            action = thunk

        return self._current_group.add_argument(
            *names, action=action, **kwargs)

    def add_positional(self, dests, action=None, **kwargs):
        if isinstance(dests, (str,)):
            dests = [dests]

        if any(dest.startswith('-') for dest in dests):
            raise ValueError("add_positional can't add optional arguments")

        if action is None:
            action = actions.StoreAction

        return self._add_argument(dests, action, **kwargs)

    def add_option(self, option_strings, *actions, **kwargs):
        if isinstance(option_strings, (str,)):
            option_strings = [option_strings]

        if not all(opt.startswith('-') for opt in option_strings):
            raise ValueError("add_option can't add positional arguments")

        return self._add_argument(option_strings, *actions, **kwargs)

    def set_defaults(self, *args, **kwargs):
        if len(args) == 1:
            raise TypeError('set_defaults takes at least 2 arguments')

        if len(args) >= 2:
            dests, value = args[:-1], args[-1]
            for dest in dests:
                kwargs[dest] = value

        self._defaults.update(**kwargs)

    def in_group(self, description):
        self._current_group = self._parser.add_argument_group(description)
        return self._current_group

    def reset_group(self):
        self._current_group = self._parser

    @contextmanager
    def argument_group(self, description):
        previous_group = self._current_group
        self._current_group = self._parser.add_argument_group(description)
        yield self._current_group
        self._current_group = previous_group

    @contextmanager
    def mutually_exclusive_group(self, **kwargs):
        previous_group = self._current_group
        self._current_group = previous_group \
            .add_mutually_exclusive_group(**kwargs)
        yield self._current_group
        self._current_group = previous_group


# -----------------------------------------------------------------------------

class ArgumentParser(argparse.ArgumentParser):
    """A thin extension class to the standard ArgumentParser which includes
    methods to interact with a builder instance.
    """

    @classmethod
    def builder(cls, **kwargs):
        """Create a new builder instance using this parser class.
        """

        return _Builder(parser=cls(**kwargs))

    def to_builder(self):
        """Construct and return a builder instance with this parser.
        """

        return _Builder(parser=self)

    def parse_known_args(self, args=None, namespace=None):
        """Thin wrapper around parse_known_args which shims-in support for
        actions with multiple destinations.
        """

        if namespace is None:
            namespace = Namespace()

        # Add action defaults not present in namespace
        for action in self._actions:
            if not hasattr(action, 'dests'):
                continue

            for dest in action.dests:
                if hasattr(namespace, dest):
                    continue
                if action.default is SUPPRESS:
                    continue

                setattr(namespace, dest, action.default)

        return super(ArgumentParser, self).parse_known_args(args, namespace)
