import sys
platform = sys.platform
# platform :    ESP32 => 'esp32'
#               Linux => 'linux'

import utime as time
import hmac
import ubinascii as binascii
import usocket as socket
import ussl as ssl
try:
    import uhashlib as hashlib      # for Linux
except:
    import hashlib                  # for ESP32
import ujson as json
import ntptime

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# ---- percent encoding
def RFC3986_encode(s):
    ret = ''
    bts = s.encode('utf-8')
    for c in bts :
        if c in range(0x30, 0x39 + 1) or \
           c in range(0x41, 0x5a + 1) or \
           c in range(0x61, 0x7a + 1) or \
           c in (0x2d, 0x2e, 0x5f, 0x7e):
            ret += chr(c)
        else :
            ret += '%%%02X' % (c)
    return ret

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def current_time():
    #t = 1502156468
#    t = int(time.time())
    t = int(ntptime.time())
    return t
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def getTimeString() :
    # return("Tue Aug 8 2017 10:41:08 GMT+0900")
    wday_str = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]    # wday=0は月曜
    d = time.localtime(ntptime.time())
    # d[0] : year     d[1] : month     d[2] : day
    # d[3] : hour     d[4] : min       d[5] : sec
    # d[6] : wday     d[7] : yday      d[8] : isDST   (unix only)
    return  ("%04d/%02d/%02d[%s] %02d:%02d:%02d" % (d[0], d[1], d[2], wday_str[d[6]], d[3], d[4], d[5]))

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class tiny_twitter :
    def __init__(self, consumer_key, consumer_secret, access_token, access_secret, callback, debug=False, blocking=True) :
        # パラメータチェック
        if not callable(callback) :
            raise ValueError("callback must be function")
        if type(consumer_key) is not str or type(consumer_secret) is not str or type(access_token) is not str or type(access_secret) is not str:
            raise ValueError("each key must be string")
        if platform == 'linux' and not blocking :
            raise ValueError("Linux not support non-blocking mode")

        self.consumer_key      = consumer_key
        self.consumer_secret   = consumer_secret
        self.access_token      = access_token
        self.access_secret     = access_secret
        self.callback          = callback
        self.__DEBUG__         = debug
        self.__ssl_sock        = None
        self.__rcv_line        = b''
        self.__blocking        = blocking

    def __debug_print(self, str) :
        if self.__DEBUG__ :
            print(str)

    def __makeRequestMessage(self, method, host, query_params={}, message=None) :
        method = method.upper()                                             # to upper
        url    = 'https://' + host['host'] + '/1.1/' + host['endpoint']
        time = current_time()                                               # current time

        # make body_params (if message exist)
        body_params = {}
        if message is not None :
            if len(message) > 0 :
                # add timestamp
                message += '\n('+ getTimeString() + ')'
                body_params["status"] = RFC3986_encode(message)

        # encode query_params
        for key in query_params.keys() :
            query_params[key] = RFC3986_encode(query_params[key])

        # make aouth header =====================================================================================================
        tstamp = str(time)
        oauth_params = {
            'oauth_consumer_key'        : RFC3986_encode(self.consumer_key),         \
            'oauth_nonce'               : RFC3986_encode('nonce' + tstamp),     \
            'oauth_signature_method'    : 'HMAC-SHA1',                          \
            'oauth_timestamp'           : RFC3986_encode(tstamp),               \
            'oauth_token'               : RFC3986_encode(self.access_token),         \
            'oauth_version'             : '1.0',                                \
        }

        # concatenate oauth_params, query_params and body_params
        params = {}
        params.update(query_params)
        params.update(oauth_params)
        params.update(body_params)

        # make parameter string
        param_str = ''
        for key in sorted(params.keys()) :
            param_str += '%s=%s&' % (key, params[key])
        param_str = param_str[:-1]
        self.__debug_print('#### param_str: %s' % (param_str))

        # make signature base string
        sigbase_str = method + '&' + RFC3986_encode(url) + '&' + RFC3986_encode(param_str)
        self.__debug_print('#### sigbase_str: %s' % (sigbase_str))

        # make signature
        signing_key = RFC3986_encode(self.consumer_secret) + '&' + RFC3986_encode(self.access_secret)
        hash = hmac.new(bytearray(signing_key), msg=sigbase_str, digestmod=hashlib.sha1)
        sig = binascii.b2a_base64(hash.digest()).rstrip().decode('utf-8')
        self.__debug_print('#### sig: %s' % (sig))

        oauth_head  = '        OAuth oauth_consumer_key="'     + oauth_params['oauth_consumer_key']     + '",\n' \
                    + '              oauth_nonce="'            + oauth_params['oauth_nonce']            + '",\n' \
                    + '              oauth_signature="'        + RFC3986_encode(sig)                    + '",\n' \
                    + '              oauth_signature_method="' + oauth_params['oauth_signature_method'] + '",\n' \
                    + '              oauth_timestamp="'        + oauth_params['oauth_timestamp']        + '",\n' \
                    + '              oauth_token="'            + oauth_params['oauth_token']            + '",\n' \
                    + '              oauth_version="'          + oauth_params['oauth_version']          + '"'
        self.__debug_print('### oauth_head:\n%s' % (oauth_head))
        # ========================================================================================================================

        # make request
        request = method + ' ' + url + '?'
        for key in query_params.keys() :
            request += key + '=' + query_params[key] + '&'
        # delete last '&' and add 'HTTP/1.1'
        request = request[:-1] + ' HTTP/1.1\n'

        # make message body(if exist)
        body = ''
        for key in body_params.keys() :
            body += key + '=' + body_params[key]

        # make message header
        header  = 'Accept: */*\n'                                           \
                    + 'Host: ' + host["host"] + '\n'                        \
                    + 'Connection: close\n'                                 \
                    + 'User-Agent: espruino tweet Bot v0.1\n'               \
                    + 'Content-Type: application/x-www-form-urlencoded\n'   \
                    + 'Authorization: \n'                                   \
                    + oauth_head + '\n'
        if len(body) > 0 :
            header += 'Content-Length: ' + str(len(body)) + '\n'

        # request message
        ret = request + header + '\n'
        if len(body) > 0 :
            ret += body + '\n'

        return(ret)

    def __sendmessage(self, host, msg) :
        sock = socket.socket()
        addr = socket.getaddrinfo(host['host'], host['port'])[0][-1]

        # connect socket
        sock.connect(addr)
        try :
            # SSL wrap
            ssl_sock = ssl.wrap_socket(sock)

            # send data
            ssl_sock.write(msg)

            # 受信データの最初の1行
            rcv_line = ssl_sock.readline()
            protover, status, msg = rcv_line.split(None, 2)
            # self.__debug_print('%s::::%s::::%s' % (protover, status, msg))
            # status 200以外はエラー
            if status != b"200":
                raise ValueError(status)
            # それ以外のレスポンスヘッダを読む
            while True:
                rcv_line = ssl_sock.readline()
                # self.__debug_print(rcv_line)
                if not rcv_line:
                    # なんらかの異常なレスポンス(ヘッダが終わる前にデータがなくなった)
                    raise ValueError("Unexpected EOF in HTTP headers")
                if rcv_line == b'\r\n':
                    # 空行でヘッダ終了
                    break
        except Exception as e:
            # エラーが発生したらクローズして上位へ例外通知
            ssl_sock.close()
            self.__ssl_sock = None
            raise e

        self.__ssl_sock = ssl_sock
        self.__rcv_line = b''
        if self.__blocking :
            # blocking mode
            while True :
                if self.recieve() < 0 :
                    break
        else :
            # non-blocking mode
            ssl_sock.setblocking(False)

    # ######## recieve ################################
    def recieve(self) :
        # self.__debug_print("==== START ====")
        if self.__ssl_sock is None :
            raise  ValueError("socket not opend")
        try :
            l = self.__ssl_sock.readline()
        except Exception as e:
            # エラーが発生したらクローズして上位へ例外通知
            self.close()
            raise e

        if l == b'':
            # 相手からクローズされたときはb''が返ってくるらしい
            self.close()
            return(-1)  # クローズされた
        if not l :
            # 受信データなし
            return(0)   # 正常終了
        if len(l) < 10 :
            # 文字列長が小さいときは空行
            # or データバイト数行(Transfer-Encoding: Chunked) (これがあるのはStreaming APIのみ)
            # とみなして捨てる
            # Streaming APIで{"delimited":"length"}; を指定してもここで捨てられる(^^;;;
            # if not l == b'\r\n' :
            #     self.__debug_print("<<<<" + str(l) + ">>>>")
            return(0)   # 正常終了
        if l[-2:] == b'\r\n' :              # 行末が\r\nだったら
            l = l[:-2]                      # それを取り除く
        self.__rcv_line += l
        # self.__debug_print("@@@@" + str(self.__rcv_line))

        # 長すぎるデータはどこかでミスっていると判断して捨てる。
        # サイズは適当に決めたので、変更必要かも。
        if len(self.__rcv_line) > 16*1024 :
            self.__rcv_line = b''
            self.__debug_print("**** Too long data  deleted ****")
            return(0)   # 正常終了

        if not (self.__rcv_line[0:1] == b'{' or self.__rcv_line[0:1] == b'[') :
            # JSONデータとして認識できない(途中から始まっている)データなので捨てる
            # user_timelineはJSONデータのリストが送られてくるので、'['で始まる
            self.__rcv_line = b''
            self.__debug_print("**** incomplete data  deleted ****")
            return(0)   # 正常終了

        try :
            j = json.loads(self.__rcv_line)
        except :
            # 途中までしか受信していないので続きを受信する
            return(0)   # 正常終了

        ### self.__debug_print("====" + str(self.__rcv_line))
        self.__rcv_line = b''      # 使い終わったので破棄

        # callback function 呼び出し
        if type(j) is list :
            # 受信データはJSONデータのリスト
            # user_timelineはJSONデータのリストが送られてくる
            for j1 in j :
                self.callback(j1)
        else :
            # 受信データはJSONデータ
            self.callback(j)
        # self.__debug_print("==== END ====")
        return(0)   # 正常終了

    # ######## close ################################
    def close(self) :
        # close socket
        self.__debug_print("close!!")
        self.__ssl_sock.close()
        self.__ssl_sock = None

    # ######## Tweet API ################################
    def tweet(self, msg, query_params={}) :
        host = { 'host': 'api.twitter.com', 'port': 443, 'endpoint': 'statuses/update.json'}

        reqMessage = self.__makeRequestMessage('POST', host, query_params, msg)
        self.__debug_print("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%")
        self.__debug_print(reqMessage)
        self.__debug_print("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%")

        self.__sendmessage(host, reqMessage)

    # ######## GetTimeline API ################################
    def getTimeline(self, user, count=1, query_params={}) :
        query_params["screen_name"] = user
        query_params["count"]       = str(count)
        host = { 'host': 'api.twitter.com', 'port': 443, 'endpoint': 'statuses/user_timeline.json'};

        reqMessage = self.__makeRequestMessage('GET', host, query_params);
        self.__debug_print("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%")
        self.__debug_print(reqMessage)
        self.__debug_print("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%")

        self.__sendmessage(host, reqMessage)

    # ######## user stream API ################################
    def userstream(self, query_params={}) :
        host = { 'host': 'userstream.twitter.com', 'port': 443, 'endpoint': 'user.json'}

        reqMessage = self.__makeRequestMessage('GET', host, query_params)
        self.__debug_print('%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%')
        self.__debug_print(reqMessage)
        self.__debug_print('%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%')

        self.__sendmessage(host, reqMessage)

    # ######## sample stream API ################################
    def samplestream(self, query_params={}) :
        host = { 'host': 'stream.twitter.com', 'port': 443, 'endpoint': 'statuses/sample.json'};

        reqMessage = self.__makeRequestMessage('GET', host, query_params);
        self.__debug_print('%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%')
        self.__debug_print(reqMessage)
        self.__debug_print('%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%')

        self.__sendmessage(host, reqMessage)

    # ######## filter stream API ################################
    def filterstream(self, query_params={}) :
        host = { 'host': 'stream.twitter.com', 'port': 443, 'endpoint': 'statuses/filter.json'};

        reqMessage = self.__makeRequestMessage('POST', host, query_params);
        self.__debug_print('%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%')
        self.__debug_print(reqMessage)
        self.__debug_print('%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%')

        self.__sendmessage(host, reqMessage)
# ====== END of Class =================================================================================
