# Copyright (c) 2012-2013 Mitch Garnaat http://garnaat.org/
# Copyright 2012-2014 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
#
# http://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 logging
from .exceptions import MissingParametersError
from .exceptions import UnknownParameterError
from .paginate import DeprecatedPaginator
from . import serialize
from . import BotoCoreObject, xform_name

from .validate import ParamValidator
from .exceptions import ParamValidationError


logger = logging.getLogger(__name__)


class Operation(BotoCoreObject):

    _DEFAULT_PAGINATOR_CLS = DeprecatedPaginator

    def __init__(self, service, op_data, model, paginator_cls=None):
        self.input = {}
        self.output = {}
        self._model = model
        BotoCoreObject.__init__(self, **op_data)
        self.service = service
        if self.service:
            self.session = self.service.session
        else:
            self.session = None
        self.type = 'operation'
        self._params = None
        if paginator_cls is None:
            paginator_cls = self._DEFAULT_PAGINATOR_CLS
        self._paginator_cls = paginator_cls

    def __repr__(self):
        return 'Operation:%s' % self.name

    @property
    def model(self):
        return self._model

    @property
    def output_shape(self):
        return self._model.output_shape

    @property
    def signature_version(self):
        return self.service.signature_version

    def call(self, endpoint, **kwargs):
        logger.debug("%s called with kwargs: %s", self, kwargs)
        # It probably seems a little weird to be firing two different
        # events here.  The reason is that the first event is fired
        # with the parameters exactly as supplied.  The second event
        # is fired with the built parameters.  Generally, it's easier
        # to manipulate the former but at times, like with ReST operations
        # that build an XML or JSON payload, you have to wait for
        # build_parameters to do it's job and the latter is necessary.
        event = self.session.create_event('before-parameter-build',
                                          self.service.endpoint_prefix,
                                          self.name)
        self.session.emit(event, endpoint=endpoint,
                          model=self.model,
                          params=kwargs)
        request_dict = self.build_parameters(**kwargs)
        event = self.session.create_event('before-call',
                                          self.service.endpoint_prefix,
                                          self.name)
        # The operation kwargs is being passed in kwargs to support
        # handlers that still depend on this value.  Eventually
        # everything should move over to the model/endpoint args.
        self.session.emit(event, endpoint=endpoint,
                          model=self.model,
                          params=request_dict,
                          operation=self)
        response = endpoint.make_request(self.model, request_dict)
        event = self.session.create_event('after-call',
                                          self.service.endpoint_prefix,
                                          self.name)
        self.session.emit(event,
                          http_response=response[0],
                          model=self.model,
                          operation=self,
                          parsed=response[1])
        return response

    @property
    def pagination(self):
        try:
            return self._load_pagination_config()
        except Exception as e:
            return {}

    @property
    def can_paginate(self):
        try:
            self._load_pagination_config()
        except Exception as e:
            return False
        return True

    def paginate(self, endpoint, **kwargs):
        """Iterate over the responses of an operation.

        This will return an iterator with each element
        being a tuple of (``http_response``, ``parsed_response``).
        If the operation does not paginate, a ``TypeError`` will
        be raised.  You can check if an operation can be paginated
        by using the ``can_paginate`` arg.
        """
        if not self.can_paginate:
            raise TypeError("Operation cannot be paginated: %s" % self)
        config = self._load_pagination_config()
        paginator = self._paginator_cls(self, config)
        return paginator.paginate(endpoint, **kwargs)

    def _load_pagination_config(self):
        loader = self.session.get_component('data_loader')
        api_version = self.service.api_version
        config = loader.load_data('aws/%s/%s.paginators' %
                                  (self.service.service_name, api_version))
        return config['pagination'][self.name]

    @property
    def params(self):
        raise RuntimeError(
            "Attempted to access removed parameter objects in botocore.")
        if self._params is None:
            self._params = self._create_parameter_objects()
        return self._params

    def _create_parameter_objects(self):
        """
        Build the list of Parameter objects for this operation.
        """
        logger.debug("Creating parameter objects for: %s", self)
        params = []
        return params

    def _find_payload(self):
        """
        Searches the parameters for an operation to find the payload
        parameter, if it exists.  Returns that param or None.
        """
        payload = None
        for param in self.params:
            if hasattr(param, 'payload') and param.payload:
                payload = param
                break
        return payload

    def build_parameters(self, **kwargs):
        """
        Returns a dictionary containing the kwargs for the
        given operation formatted as required to pass to the service
        in a request.
        """
        protocol = self._model.metadata['protocol']
        input_shape = self._model.input_shape
        if input_shape is not None:
            self._convert_kwargs_to_correct_casing(kwargs)
            validator = ParamValidator()
            errors = validator.validate(kwargs, self._model.input_shape)
            if errors.has_errors():
                raise ParamValidationError(report=errors.generate_report())
        serializer = serialize.create_serializer(protocol)
        request_dict = serializer.serialize_to_request(kwargs, self._model)
        return request_dict

    def _convert_kwargs_to_correct_casing(self, kwargs):
        # XXX: This will be removed in botocore 1.0, but we should
        # support snake casing for now.
        # First we're going to build a map of snake_casing -> service casing
        actual_casing = list(self._model.input_shape.members)
        mapping = {}
        for key in actual_casing:
            transformed = xform_name(key)
            if key != transformed:
                mapping[xform_name(key)] = key
        # Look for anything in the user provided kwargs that is in the mapping
        # dict and convert appropriately.
        for key in list(kwargs):
            if key in mapping:
                # TODO: add a pending deprecation warning.
                value = kwargs[key]
                kwargs[mapping[key]] = value
                del kwargs[key]

    def _check_for_unknown_params(self, kwargs):
        valid_names = [p.py_name for p in self.params]
        for key in kwargs:
            if key not in valid_names:
                raise UnknownParameterError(name=key, operation=self,
                                            choices=', '.join(valid_names))

    def is_streaming(self):
        # TODO: add deprecation warning
        return self._model.has_streaming_output

    @property
    def has_streaming_output(self):
        return self._model.has_streaming_output
