# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# https://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import copy

from boto3.compat import collections_abc
from boto3.docs.utils import DocumentModifiedShape
from boto3.dynamodb.conditions import ConditionBase, ConditionExpressionBuilder
from boto3.dynamodb.types import TypeDeserializer, TypeSerializer


def register_high_level_interface(base_classes, **kwargs):
    base_classes.insert(0, DynamoDBHighLevelResource)


def copy_dynamodb_params(params, **kwargs):
    return copy.deepcopy(params)


class DynamoDBHighLevelResource:
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Apply handler that creates a copy of the user provided dynamodb
        # item such that it can be modified.
        self.meta.client.meta.events.register(
            'provide-client-params.dynamodb',
            copy_dynamodb_params,
            unique_id='dynamodb-create-params-copy',
        )

        self._injector = TransformationInjector()
        # Apply the handler that generates condition expressions including
        # placeholders.
        self.meta.client.meta.events.register(
            'before-parameter-build.dynamodb',
            self._injector.inject_condition_expressions,
            unique_id='dynamodb-condition-expression',
        )

        # Apply the handler that serializes the request from python
        # types to dynamodb types.
        self.meta.client.meta.events.register(
            'before-parameter-build.dynamodb',
            self._injector.inject_attribute_value_input,
            unique_id='dynamodb-attr-value-input',
        )

        # Apply the handler that deserializes the response from dynamodb
        # types to python types.
        self.meta.client.meta.events.register(
            'after-call.dynamodb',
            self._injector.inject_attribute_value_output,
            unique_id='dynamodb-attr-value-output',
        )

        # Apply the documentation customizations to account for
        # the transformations.
        attr_value_shape_docs = DocumentModifiedShape(
            'AttributeValue',
            new_type='valid DynamoDB type',
            new_description=(
                '- The value of the attribute. The valid value types are '
                'listed in the '
                ':ref:`DynamoDB Reference Guide<ref_valid_dynamodb_types>`.'
            ),
            new_example_value=(
                '\'string\'|123|Binary(b\'bytes\')|True|None|set([\'string\'])'
                '|set([123])|set([Binary(b\'bytes\')])|[]|{}'
            ),
        )

        key_expression_shape_docs = DocumentModifiedShape(
            'KeyExpression',
            new_type=(
                'condition from :py:class:`boto3.dynamodb.conditions.Key` '
                'method'
            ),
            new_description=(
                'The condition(s) a key(s) must meet. Valid conditions are '
                'listed in the '
                ':ref:`DynamoDB Reference Guide<ref_dynamodb_conditions>`.'
            ),
            new_example_value='Key(\'mykey\').eq(\'myvalue\')',
        )

        con_expression_shape_docs = DocumentModifiedShape(
            'ConditionExpression',
            new_type=(
                'condition from :py:class:`boto3.dynamodb.conditions.Attr` '
                'method'
            ),
            new_description=(
                'The condition(s) an attribute(s) must meet. Valid conditions '
                'are listed in the '
                ':ref:`DynamoDB Reference Guide<ref_dynamodb_conditions>`.'
            ),
            new_example_value='Attr(\'myattribute\').eq(\'myvalue\')',
        )

        self.meta.client.meta.events.register(
            'docs.*.dynamodb.*.complete-section',
            attr_value_shape_docs.replace_documentation_for_matching_shape,
            unique_id='dynamodb-attr-value-docs',
        )

        self.meta.client.meta.events.register(
            'docs.*.dynamodb.*.complete-section',
            key_expression_shape_docs.replace_documentation_for_matching_shape,
            unique_id='dynamodb-key-expression-docs',
        )

        self.meta.client.meta.events.register(
            'docs.*.dynamodb.*.complete-section',
            con_expression_shape_docs.replace_documentation_for_matching_shape,
            unique_id='dynamodb-cond-expression-docs',
        )


class TransformationInjector:
    """Injects the transformations into the user provided parameters."""

    def __init__(
        self,
        transformer=None,
        condition_builder=None,
        serializer=None,
        deserializer=None,
    ):
        self._transformer = transformer
        if transformer is None:
            self._transformer = ParameterTransformer()

        self._condition_builder = condition_builder
        if condition_builder is None:
            self._condition_builder = ConditionExpressionBuilder()

        self._serializer = serializer
        if serializer is None:
            self._serializer = TypeSerializer()

        self._deserializer = deserializer
        if deserializer is None:
            self._deserializer = TypeDeserializer()

    def inject_condition_expressions(self, params, model, **kwargs):
        """Injects the condition expression transformation into the parameters

        This injection includes transformations for ConditionExpression shapes
        and KeyExpression shapes. It also handles any placeholder names and
        values that are generated when transforming the condition expressions.
        """
        self._condition_builder.reset()
        generated_names = {}
        generated_values = {}

        # Create and apply the Condition Expression transformation.
        transformation = ConditionExpressionTransformation(
            self._condition_builder,
            placeholder_names=generated_names,
            placeholder_values=generated_values,
            is_key_condition=False,
        )
        self._transformer.transform(
            params, model.input_shape, transformation, 'ConditionExpression'
        )

        # Create and apply the Key Condition Expression transformation.
        transformation = ConditionExpressionTransformation(
            self._condition_builder,
            placeholder_names=generated_names,
            placeholder_values=generated_values,
            is_key_condition=True,
        )
        self._transformer.transform(
            params, model.input_shape, transformation, 'KeyExpression'
        )

        expr_attr_names_input = 'ExpressionAttributeNames'
        expr_attr_values_input = 'ExpressionAttributeValues'

        # Now that all of the condition expression transformation are done,
        # update the placeholder dictionaries in the request.
        if expr_attr_names_input in params:
            params[expr_attr_names_input].update(generated_names)
        else:
            if generated_names:
                params[expr_attr_names_input] = generated_names

        if expr_attr_values_input in params:
            params[expr_attr_values_input].update(generated_values)
        else:
            if generated_values:
                params[expr_attr_values_input] = generated_values

    def inject_attribute_value_input(self, params, model, **kwargs):
        """Injects DynamoDB serialization into parameter input"""
        self._transformer.transform(
            params,
            model.input_shape,
            self._serializer.serialize,
            'AttributeValue',
        )

    def inject_attribute_value_output(self, parsed, model, **kwargs):
        """Injects DynamoDB deserialization into responses"""
        if model.output_shape is not None:
            self._transformer.transform(
                parsed,
                model.output_shape,
                self._deserializer.deserialize,
                'AttributeValue',
            )


class ConditionExpressionTransformation:
    """Provides a transformation for condition expressions

    The ``ParameterTransformer`` class can call this class directly
    to transform the condition expressions in the parameters provided.
    """

    def __init__(
        self,
        condition_builder,
        placeholder_names,
        placeholder_values,
        is_key_condition=False,
    ):
        self._condition_builder = condition_builder
        self._placeholder_names = placeholder_names
        self._placeholder_values = placeholder_values
        self._is_key_condition = is_key_condition

    def __call__(self, value):
        if isinstance(value, ConditionBase):
            # Create a conditional expression string with placeholders
            # for the provided condition.
            built_expression = self._condition_builder.build_expression(
                value, is_key_condition=self._is_key_condition
            )

            self._placeholder_names.update(
                built_expression.attribute_name_placeholders
            )
            self._placeholder_values.update(
                built_expression.attribute_value_placeholders
            )

            return built_expression.condition_expression
        # Use the user provided value if it is not a ConditonBase object.
        return value


class ParameterTransformer:
    """Transforms the input to and output from botocore based on shape"""

    def transform(self, params, model, transformation, target_shape):
        """Transforms the dynamodb input to or output from botocore

        It applies a specified transformation whenever a specific shape name
        is encountered while traversing the parameters in the dictionary.

        :param params: The parameters structure to transform.
        :param model: The operation model.
        :param transformation: The function to apply the parameter
        :param target_shape: The name of the shape to apply the
            transformation to
        """
        self._transform_parameters(model, params, transformation, target_shape)

    def _transform_parameters(
        self, model, params, transformation, target_shape
    ):
        type_name = model.type_name
        if type_name in ('structure', 'map', 'list'):
            getattr(self, f'_transform_{type_name}')(
                model, params, transformation, target_shape
            )

    def _transform_structure(
        self, model, params, transformation, target_shape
    ):
        if not isinstance(params, collections_abc.Mapping):
            return
        for param in params:
            if param in model.members:
                member_model = model.members[param]
                member_shape = member_model.name
                if member_shape == target_shape:
                    params[param] = transformation(params[param])
                else:
                    self._transform_parameters(
                        member_model,
                        params[param],
                        transformation,
                        target_shape,
                    )

    def _transform_map(self, model, params, transformation, target_shape):
        if not isinstance(params, collections_abc.Mapping):
            return
        value_model = model.value
        value_shape = value_model.name
        for key, value in params.items():
            if value_shape == target_shape:
                params[key] = transformation(value)
            else:
                self._transform_parameters(
                    value_model, params[key], transformation, target_shape
                )

    def _transform_list(self, model, params, transformation, target_shape):
        if not isinstance(params, collections_abc.MutableSequence):
            return
        member_model = model.member
        member_shape = member_model.name
        for i, item in enumerate(params):
            if member_shape == target_shape:
                params[i] = transformation(item)
            else:
                self._transform_parameters(
                    member_model, params[i], transformation, target_shape
                )
