
"""
The MIT License (MIT)
Copyright © 2019 Jean-Christophe Bos & HC² (www.hc2.fr)
"""

from   .              import *
from   .httpResponse  import HttpResponse
from   binascii       import a2b_base64
import json

# ============================================================================
# ===( HttpRequest )==========================================================
# ============================================================================

class HttpRequest :

    MAX_RECV_HEADER_LINES = 100

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

    def __init__(self, microWebSrv2, xasCli) :
        self._mws2   = microWebSrv2
        self._xasCli = xasCli
        self._waitForRecvRequest()

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

    def _recvLine(self, onRecv) :
        self._xasCli.AsyncRecvLine(onLineRecv=onRecv, timeoutSec=self._mws2._timeoutSec)

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

    def _waitForRecvRequest(self) :
        self._httpVer  = ''
        self._method   = ''
        self._path     = ''
        self._headers  = { }
        self._content  = None
        self._response = HttpResponse(self._mws2, self)
        self._recvLine(self._onFirstLineRecv)

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

    def _onFirstLineRecv(self, xasCli, line, arg) :
        try :
            elements = line.strip().split()
            if len(elements) == 3 :
                self._httpVer     = elements[2].upper()
                self._method      = elements[0].upper()
                elements          = elements[1].split('?', 1)
                self._path        = UrlUtils.UnquotePlus(elements[0])
                self._queryString = (elements[1] if len(elements) > 1 else '')
                self._queryParams = { }
                if self._queryString :
                    elements = self._queryString.split('&')
                    for s in elements :
                        p = s.split('=', 1)
                        if len(p) > 0 :
                            v = (UrlUtils.Unquote(p[1]) if len(p) > 1 else '')
                            self._queryParams[UrlUtils.Unquote(p[0])] = v
                self._recvLine(self._onHeaderLineRecv)
            else :
                self._response.ReturnBadRequest()
        except :
            self._response.ReturnBadRequest()

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

    def _onHeaderLineRecv(self, xasCli, line, arg) :
        try :
            elements = line.strip().split(':', 1)
            if len(elements) == 2 :
                if len(self._headers) < HttpRequest.MAX_RECV_HEADER_LINES :
                    self._headers[elements[0].strip().lower()] = elements[1].strip()
                    self._recvLine(self._onHeaderLineRecv)
                else :
                    self._response.ReturnEntityTooLarge()
            elif len(elements) == 1 and len(elements[0]) == 0 :
                self._processRequest()
            else :
                self._response.ReturnBadRequest()
        except :
            self._response.ReturnBadRequest()

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

    def _processRequest(self) :
        if not self._processRequestModules() :
            if not self.IsUpgrade :
                if not self._processRequestRoutes() :
                    if self._method in ('GET', 'HEAD') :
                        filename = self._mws2.ResolvePhysicalPath(self._path)
                        if filename :
                            ct = self._mws2.GetMimeTypeFromFilename(filename)
                            if ct :
                                self._response.AllowCaching = True
                                self._response.ContentType  = ct
                                self._response.ReturnFile(filename)
                            else :
                                self._response.ReturnForbidden()
                        else :
                            self._response.ReturnNotFound()
                    elif self._method == 'OPTIONS' :
                        if self._mws2.CORSAllowAll :
                            self._response.SetHeader( 'Access-Control-Allow-Methods',     '*'     )
                            self._response.SetHeader( 'Access-Control-Allow-Headers',     '*'     )
                            self._response.SetHeader( 'Access-Control-Allow-Credentials', 'true'  )
                            self._response.SetHeader( 'Access-Control-Max-Age',           '86400' )
                        self._response.ReturnOk()
                    else :
                        self._response.ReturnMethodNotAllowed()
            else :
                self._response.ReturnNotImplemented()

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

    def _processRequestModules(self) :
        for modName, modInstance in self._mws2._modules.items() :
            try :
                modInstance.OnRequest(self._mws2, self)
                if self._response.HeadersSent :
                    return True
            except Exception as ex :
                self._mws2.Log( 'Exception in request handler of module "%s" (%s).'
                                % (modName, ex),
                                self._mws2.ERROR )
        return False

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

    def _processRequestRoutes(self) :
        self._routeResult = ResolveRoute(self._method, self._path)
        if self._routeResult :
            cntLen = self.ContentLength
            if not cntLen :
                self._routeRequest()
            elif self._method != 'GET' and self._method != 'HEAD' :
                if cntLen <= self._mws2._maxContentLen :
                    def onContentRecv(xasCli, content, arg) :
                        self._content = content
                        self._routeRequest()
                        self._content = None
                    try :
                        self._xasCli.AsyncRecvData( size       = cntLen,
                                                    onDataRecv = onContentRecv,
                                                    timeoutSec = self._mws2._timeoutSec )
                    except :
                        self._mws2.Log( 'Not enough memory to read a content of %s bytes.' % cntLen,
                                        self._mws2.ERROR )
                        self._response.ReturnServiceUnavailable()
                else :
                    self._response.ReturnEntityTooLarge()
            else :
                self._response.ReturnBadRequest()
            return True
        return False

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

    def _routeRequest(self) :
        try :
            if self._routeResult.Args :
                self._routeResult.Handler(self._mws2, self, self._routeResult.Args)
            else :
                self._routeResult.Handler(self._mws2, self)
            if not self._response.HeadersSent :
                self._mws2.Log( 'No response was sent from route %s.'
                                % self._routeResult,
                                self._mws2.WARNING )
                self._response.ReturnNotImplemented()
        except Exception as ex :
            self._mws2.Log( 'Exception raised from route %s: %s'
                            % (self._routeResult, ex),
                            self._mws2.ERROR )
            self._response.ReturnInternalServerError()

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

    def GetPostedURLEncodedForm(self) :
        res = { }
        if self.ContentType.lower() == 'application/x-www-form-urlencoded' :
            try :
                elements = bytes(self._content).decode('UTF-8').split('&')
                for s in elements :
                    p = s.split('=', 1)
                    if len(p) > 0 :
                        v = (UrlUtils.UnquotePlus(p[1]) if len(p) > 1 else '')
                        res[UrlUtils.UnquotePlus(p[0])] = v
            except :
                pass
        return res

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

    def GetPostedJSONObject(self) :
        if self.ContentType.lower() == 'application/json' :
            try :
                s = bytes(self._content).decode('UTF-8')
                return json.loads(s)
            except :
                pass
        return None

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

    def GetHeader(self, name) :
        if not isinstance(name, str) or len(name) == 0 :
            raise ValueError('"name" must be a not empty string.')
        return self._headers.get(name.lower(), '')

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

    def CheckBasicAuth(self, username, password) :
        if not isinstance(username, str) :
            raise ValueError('"username" must be a string.')
        if not isinstance(password, str) :
            raise ValueError('"password" must be a string.')
        auth = self.Authorization
        if auth :
            try :
                auth = auth.split()
                if len(auth) == 2 and auth[0].lower() == 'basic' :
                    auth = a2b_base64(auth[1].encode()).decode()
                    auth = auth.split(':')
                    return ( auth[0].lower() == username.lower() and \
                             auth[1] == password )
            except :
                pass
        return False

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

    def CheckBearerAuth(self, token) :
        if not isinstance(token, str) :
            raise ValueError('"token" must be a string.')
        auth = self.Authorization
        if auth :
            try :
                auth = auth.split()
                return ( len(auth) == 2 and \
                         auth[0].lower() == 'bearer' and \
                         auth[1] == token )
            except :
                pass
        return False

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

    @property
    def UserAddress(self) :
        return self._xasCli.CliAddr

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

    @property
    def IsSSL(self) :
        return self._xasCli.IsSSL

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

    @property
    def HttpVer(self) :
        return self._httpVer

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

    @property
    def Method(self) :
        return self._method

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

    @property
    def Path(self) :
        return self._path

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

    @property
    def QueryString(self) :
        return self._queryString

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

    @property
    def QueryParams(self) :
        return self._queryParams

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

    @property
    def Host(self) :
        return self._headers.get('host', '')

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

    @property
    def Accept(self) :
        s = self._headers.get('accept', None)
        if s :
            return [x.strip() for x in s.split(',')]
        return [ ]

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

    @property
    def AcceptEncodings(self) :
        s = self._headers.get('accept-encoding', None)
        if s :
            return [x.strip() for x in s.split(',')]
        return [ ]

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

    @property
    def AcceptLanguages(self) :
        s = self._headers.get('accept-language', None)
        if s :
            return [x.strip() for x in s.split(',')]
        return [ ]

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

    @property
    def Cookies(self) :
        s = self._headers.get('cookie', None)
        if s :
            return [x.strip() for x in s.split(';')]
        return [ ]

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

    @property
    def CacheControl(self) :
        return self._headers.get('cache-control', '')

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

    @property
    def Referer(self) :
        return self._headers.get('referer', '')

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

    @property
    def ContentType(self) :
        return self._headers.get('content-type', '').split(';', 1)[0].strip()

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

    @property
    def ContentLength(self) :
        try :
            return int(self._headers.get('content-length', 0))
        except :
            return 0

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

    @property
    def UserAgent(self) :
        return self._headers.get('user-agent', '')

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

    @property
    def Authorization(self) :
        return self._headers.get('authorization', '')

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

    @property
    def Origin(self) :
        return self._headers.get('origin', '')

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

    @property
    def IsKeepAlive(self) :
        return ('keep-alive' in self._headers.get('connection', '').lower())

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

    @property
    def IsUpgrade(self) :
        return ('upgrade' in self._headers.get('connection', '').lower())

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

    @property
    def Upgrade(self) :
        return self._headers.get('upgrade', '')

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

    @property
    def Content(self) :
        return self._content

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

    @property
    def Response(self) :
        return self._response

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

    @property
    def XAsyncTCPClient(self) :
        return self._xasCli

# ============================================================================
# ============================================================================
# ============================================================================
