# coding: utf8
from __future__ import unicode_literals
from flask import Request
from flask_api.negotiation import DefaultNegotiation
from flask_api.settings import default_settings
from werkzeug.datastructures import MultiDict
from werkzeug.urls import url_decode_stream
from werkzeug.wsgi import get_content_length
from werkzeug._compat import to_unicode
import io


class APIRequest(Request):
    parser_classes = default_settings.DEFAULT_PARSERS
    renderer_classes = default_settings.DEFAULT_RENDERERS
    negotiator_class = DefaultNegotiation
    empty_data_class = MultiDict

    # Request parsing...

    @property
    def data(self):
        if not hasattr(self, '_data'):
            self._parse()
        return self._data

    @property
    def form(self):
        if not hasattr(self, '_form'):
            self._parse()
        return self._form

    @property
    def files(self):
        if not hasattr(self, '_files'):
            self._parse()
        return self._files

    def _parse(self):
        """
        Parse the body of the request, using whichever parser satifies the
        client 'Content-Type' header.
        """
        if not self.content_type or not self.content_length:
            self._set_empty_data()
            return

        negotiator = self.negotiator_class()
        parsers = [parser_cls() for parser_cls in self.parser_classes]
        options = self._get_parser_options()
        try:
            parser, media_type = negotiator.select_parser(parsers)
            ret = parser.parse(self.stream, media_type, **options)
        except:
            # Ensure that accessing `request.data` again does not reraise
            # the exception, so that eg exceptions can handle properly.
            self._set_empty_data()
            raise

        if parser.handles_file_uploads:
            assert isinstance(ret, tuple) and len(ret) == 2, 'Expected a two-tuple of (data, files)'
            self._data, self._files = ret
        else:
            self._data = ret
            self._files = self.empty_data_class()

        self._form = self._data if parser.handles_form_data else self.empty_data_class()

    def _get_parser_options(self):
        """
        Any additional information to pass to the parser.
        """
        return {'content_length': self.content_length}

    def _set_empty_data(self):
        """
        If the request does not contain data then return an empty representation.
        """
        self._data = self.empty_data_class()
        self._form = self.empty_data_class()
        self._files = self.empty_data_class()

    # Content negotiation...

    @property
    def accepted_renderer(self):
        if not hasattr(self, '_accepted_renderer'):
            self._perform_content_negotiation()
        return self._accepted_renderer

    @property
    def accepted_media_type(self):
        if not hasattr(self, '_accepted_media_type'):
            self._perform_content_negotiation()
        return self._accepted_media_type

    def _perform_content_negotiation(self):
        """
        Determine which of the available renderers should be used for
        rendering the response content, based on the client 'Accept' header.
        """
        negotiator = self.negotiator_class()
        renderers = [renderer() for renderer in self.renderer_classes]
        self._accepted_renderer, self._accepted_media_type = negotiator.select_renderer(renderers)

    # Method and content type overloading.

    @property
    def method(self):
        if not hasattr(self, '_method'):
            self._perform_method_overloading()
        return self._method

    @property
    def content_type(self):
        if not hasattr(self, '_content_type'):
            self._perform_method_overloading()
        return self._content_type

    @property
    def content_length(self):
        if not hasattr(self, '_content_length'):
            self._perform_method_overloading()
        return self._content_length

    @property
    def stream(self):
        if not hasattr(self, '_stream'):
            self._perform_method_overloading()
        return self._stream

    def _perform_method_overloading(self):
        """
        Perform method and content type overloading.

        Provides support for browser PUT, PATCH, DELETE & other requests,
        by specifing a '_method' form field.

        Also provides support for browser non-form requests (eg JSON),
        by specifing '_content' and '_content_type' form fields.
        """
        self._method = super(APIRequest, self).method
        self._stream = super(APIRequest, self).stream
        self._content_type = self.headers.get('Content-Type')
        self._content_length = get_content_length(self.environ)

        if (self._method == 'POST' and self._content_type == 'application/x-www-form-urlencoded'):
            # Read the request data, then push it back onto the stream again.
            body = self.get_data()
            data = url_decode_stream(io.BytesIO(body))
            self._stream = io.BytesIO(body)
            if '_method' in data:
                # Support browser forms with PUT, PATCH, DELETE & other methods.
                self._method = data['_method']
            if '_content' in data and '_content_type' in data:
                # Support browser forms with non-form data, such as JSON.
                body = data['_content'].encode('utf8')
                self._stream = io.BytesIO(body)
                self._content_type = data['_content_type']
                self._content_length = len(body)

    # Misc...

    @property
    def full_path(self):
        """
        Werzueg's full_path implementation always appends '?', even when the
        query string is empty.  Let's fix that.
        """
        if not self.query_string:
            return self.path
        return self.path + u'?' + to_unicode(self.query_string, self.url_charset)

    # @property
    # def auth(self):
    #     if not has_attribute(self, '_auth'):
    #         self._authenticate()
    #     return self._auth

    # def _authenticate(self):
    #     for authentication_class in self.authentication_classes:
    #         authenticator = authentication_class()
    #         try:
    #             auth = authenticator.authenticate(self)
    #         except exceptions.APIException:
    #             self._not_authenticated()
    #             raise

    #         if not auth is None:
    #             self._authenticator = authenticator
    #             self._auth = auth
    #             return

    #     self._not_authenticated()

    # def _not_authenticated(self):
    #     self._authenticator = None
    #     self._auth = None
