# language governing permissions and limitations under the License.
"""
Top Level Boolean Parameters
----------------------------

This customization will take a parameter that has
a structure of a single boolean element and allow the argument
to be specified without a value.

Instead of having to say::

    --ebs-optimized '{"Value": true}'
    --ebs-optimized '{"Value": false}'

You can instead say `--ebs-optimized/--no-ebs-optimized`.


"""

import logging
from functools import partial


from awscli.argprocess import detect_shape_structure
from awscli import arguments
from awscli.customizations.utils import validate_mutually_exclusive_handler
from awscli.customizations.exceptions import ParamValidationError


LOG = logging.getLogger(__name__)
# This sentinel object is used to distinguish when
# a parameter is not specified vs. specified with no value
# (a value of None).
_NOT_SPECIFIED = object()


def register_bool_params(event_handler):
    event_handler.register(
        'building-argument-table.ec2.*',
        partial(pull_up_bool, event_handler=event_handler),
    )


def _qualifies_for_simplification(arg_model):
    if detect_shape_structure(arg_model) == 'structure(scalar)':
        members = arg_model.members
        if (
            len(members) == 1
            and list(members.keys())[0] == 'Value'
            and list(members.values())[0].type_name == 'boolean'
        ):
            return True
    return False


def pull_up_bool(argument_table, event_handler, **kwargs):
    # List of tuples of (positive_bool, negative_bool)
    # This is used to validate that we don't specify
    # an --option and a --no-option.
    boolean_pairs = []
    event_handler.register(
        'operation-args-parsed.ec2.*',
        partial(validate_boolean_mutex_groups, boolean_pairs=boolean_pairs),
    )
    for value in list(argument_table.values()):
        if hasattr(value, 'argument_model'):
            arg_model = value.argument_model
            if _qualifies_for_simplification(arg_model):
                # Swap out the existing CLIArgument for two args:
                # one that supports --option and --option <some value>
                # and another arg of --no-option.
                new_arg = PositiveBooleanArgument(
                    value.name,
                    arg_model,
                    value._operation_model,
                    value._event_emitter,
                    group_name=value.name,
                    serialized_name=value._serialized_name,
                )
                argument_table[value.name] = new_arg
                negative_name = 'no-%s' % value.name
                negative_arg = NegativeBooleanParameter(
                    negative_name,
                    arg_model,
                    value._operation_model,
                    value._event_emitter,
                    action='store_true',
                    dest='no_%s' % new_arg.py_name,
                    group_name=value.name,
                    serialized_name=value._serialized_name,
                )
                argument_table[negative_name] = negative_arg
                # If we've pulled up a structure(scalar) arg
                # into a pair of top level boolean args, we need
                # to validate that a user only provides the argument
                # once.  They can't say --option/--no-option, nor
                # can they say --option --option Value=false.
                boolean_pairs.append((new_arg, negative_arg))


def validate_boolean_mutex_groups(boolean_pairs, parsed_args, **kwargs):
    # Validate we didn't pass in an --option and a --no-option.
    for positive, negative in boolean_pairs:
        if (
            getattr(parsed_args, positive.py_name) is not _NOT_SPECIFIED
            and getattr(parsed_args, negative.py_name) is not _NOT_SPECIFIED
        ):
            raise ParamValidationError(
                'Cannot specify both the "%s" option and '
                'the "%s" option.' % (positive.cli_name, negative.cli_name)
            )


class PositiveBooleanArgument(arguments.CLIArgument):
    def __init__(
        self,
        name,
        argument_model,
        operation_model,
        event_emitter,
        serialized_name,
        group_name,
    ):
        super(PositiveBooleanArgument, self).__init__(
            name,
            argument_model,
            operation_model,
            event_emitter,
            serialized_name=serialized_name,
        )
        self._group_name = group_name

    @property
    def group_name(self):
        return self._group_name

    def add_to_parser(self, parser):
        # We need to support three forms:
        # --option-name
        # --option-name Value=(true|false)
        parser.add_argument(
            self.cli_name,
            help=self.documentation,
            action='store',
            default=_NOT_SPECIFIED,
            nargs='?',
        )

    def add_to_params(self, parameters, value):
        if value is _NOT_SPECIFIED:
            return
        elif value is None:
            # Then this means that the user explicitly
            # specified this arg with no value,
            # e.g. --boolean-parameter
            # which means we should add a true value
            # to the parameters dict.
            parameters[self._serialized_name] = {'Value': True}
        else:
            # Otherwise the arg was specified with a value.
            parameters[self._serialized_name] = self._unpack_argument(value)


class NegativeBooleanParameter(arguments.BooleanArgument):
    def __init__(
        self,
        name,
        argument_model,
        operation_model,
        event_emitter,
        serialized_name,
        action='store_true',
        dest=None,
        group_name=None,
    ):
        super(NegativeBooleanParameter, self).__init__(
            name,
            argument_model,
            operation_model,
            event_emitter,
            default=_NOT_SPECIFIED,
            serialized_name=serialized_name,
        )
        self._group_name = group_name

    def add_to_params(self, parameters, value):
        if value is not _NOT_SPECIFIED and value:
            parameters[self._serialized_name] = {'Value': False}
