# install_twisted_rector must be called before importing the reactor
from __future__ import unicode_literals

from asyncio import sleep
from datetime import datetime
# from time import sleep

# fix for PyInstaller packages app to avoid ReactorAlreadyInstalledError
import sys
if 'twisted.internet.reactor' in sys.modules:
    del sys.modules['twisted.internet.reactor']

from kivy.uix.stacklayout import StackLayout

from kivy.support import install_twisted_reactor
# from kivy.uix.button import Button
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
# from kivy.uix.label import Label
from kivy.uix.popup import Popup
# from kivy.uix.textinput import TextInput
from kivy.uix.boxlayout import BoxLayout

# Import Smart Card functionality
# from smartcard.Exceptions import NoCardException, CardRequestTimeoutException
from smartcard.util import toHexString, HexListToBinString, BinStringToHexList
# from twisted.internet.error import getConnectError, ConnectError

from configManager import iConfig
from scProcessor import sCard
install_twisted_reactor()
from twisted.internet import reactor, protocol

class EchoClient(protocol.Protocol):
    def connectionMade(self):
        self.factory.app.on_connection(self.transport)
        # try to athrozie crutch!!!!!!!
        self.factory.app.authorize()

    def dataReceived(self, data):
        parser = bytearray(data)
        # self.factory.app.print_message('Server', parser)

        if parser[0] == 0x40:
            # Ruptela
            device_type = 1

            # print('Ruptela device')
        else:
            # Teltonika
            device_type = 2
            # print('Teltonika device')

        # get atr 4040 0001 01
        if parser[4] == 0x00:
            self.factory.app.print_message('Server', 'Login confirmation received. Ready to send query DDD command')

        elif parser[4] == 0x01:
            # Need to request ATR from sCard
            atr = self.factory.app.scObj.getAtr()
            self.factory.app.tcProtocol_sendData(atr, 1)

        elif parser[4] == 0x02:
            # # it is APDU from device
            # 4040 0008 02 B4144B26E9D7F0

            apdu_from_device = data[5:]

            if device_type == 1:
                # self.factory.app.print_message('Server', 'APDU(Encoded): ' + str(apdu_from_device))
                decoded_apdu_from_device = self.factory.app.ruptelaCrypt(apdu_from_device, 0)
                # self.factory.app.print_message('Server', 'APDU(Decoded): ' + toHexString(decoded_apdu_from_device))
            else:
                decoded_apdu_from_device = list(apdu_from_device)

            # send APDU to SC
            apduAck = self.factory.app.scObj.transmitAPDU(decoded_apdu_from_device)
            # self.factory.app.print_message('TCA', '(APDU: ' + toHexString(apduAck))

            if device_type == 1:
                encoded_apdu_from_tca = self.factory.app.ruptelaCrypt(apduAck, 1)
                # print('encoded_apdu_from_tca:', encoded_apdu_from_tca, ' type is:', type(encoded_apdu_from_tca))
                # self.factory.app.print_message('TCA', 'APDU(Encoded): ' + toHexString(encoded_apdu_from_tca))
            else:
                encoded_apdu_from_tca = list(apduAck)
            # check later
            # binstring = HexListToBinString(test_data)
            # hexlist = BinStringToHexList(binstring)

            # send data to server
            self.factory.app.tcProtocol_sendData(encoded_apdu_from_tca, 2)

        elif parser[4] == 0x03:
            if parser[5] != 0:
                self.factory.app.print_message('Server', 'APDU/ATR reply error')

        elif parser[4] == 0x04:
            self.factory.app.print_message('Server', 'Communication error')

        elif parser[4] == 0x05:
            self.factory.app.print_message('Server', 'Authorization finished successful')
            # reconnecto to SC to reset EF dir
            # self.factory.app.scObj.scDisconnect()
            # print('scObj connect =', self.factory.app.scObj.scConnect())
            self.factory.app.disconnect_from_server(self.transport)

        elif parser[4] == 0x06:
            self.factory.app.print_message('Server', 'Status message: ' + str(data[5:]))

        else:
            self.factory.app.print_message('Warning', 'Incoming message with unknown type: ')

class EchoClientFactory(protocol.ClientFactory):
    protocol = EchoClient

    def __init__(self, app):
        self.app = app

    def startedConnecting(self, connector):
        # self.app.print_message('TCA', 'Started to connect.')
        print('Started to connect.')

    def clientConnectionLost(self, connector, reason):
        self.app.print_message('TCA', 'Lost connection & try reconnect.')
        # null scObj
        self.app.scObj = None
        # Connect again
        self.app.connect_to_server()
        # Init SC
        # self.app.authorize()

    def clientConnectionFailed(self, connector, reason):
        self.app.print_message('TCA', 'Connection failed.')


# main Layout class. see .kv file
class RootLayout(BoxLayout):
    def popup_change_log(self):
        show = ChangeLog()

        # setup server settings window
        logWindow = Popup(title='Change log',
                            content=show,
                            size_hint=(0.6, 0.7))
        # start popup new window with server settings
        logWindow.open()

    def popup_server_settings(self):
        show = ServerSettings()

        # setup server settings window
        settingsWindow = Popup(title='Server settings',
                            content=show,
                            size_hint=(None, None),
                            size=(400, 400))
        # start popup new window with server settings
        settingsWindow.open()

# Server settings poupup window. Description in .kv file
class ServerSettings(FloatLayout):
    configObj = iConfig()

    def showInitIp(self):
        return self.configObj.getServerAddress('IP')

    def showInitPort(self):
        return self.configObj.getServerAddress('PORT')

    def updateConfig(self):
        # self.ipinput.text = 'Hello new text'

        ip, port = self.configObj.getServerAddress('ip and port')
        # print('Saved server settings. ' + 'ip:', ip, 'port:', port)

        # get from user fields
        user_ip = self.ipinput.text
        user_port = self.portinput.text
        print('Saved server settings. ' + 'ip:', user_ip, 'port:', user_port)

        if user_ip != ip or user_port != port:
            self.configObj.setServerAddress(user_ip, user_port)

        self.btnupdate.text = 'Settings are saved'

        self.popup_server_settings_notification()

    def popup_server_settings_notification(self):
        show = ServerSettingsNotification()

        # setup server settings window
        settingsWindow = Popup(title='Server settings notification',
                            content=show,
                            size_hint=(None, None),
                            size=(400, 200))
        # start popup new window with server settings
        # settingsWindow.open()


# Server settings poupup window. Description in .kv file
class ServerSettingsNotification(FloatLayout):
    configObj = iConfig()

    def Cancel(self):
        # Close the popup window without doing anything else
        self.dismiss()


# Server settings poupup window. Description in .kv file
class ChangeLog(FloatLayout):
    log = {'v0.2.2': 'Added viewing changes as "Change log"',
           'v0.2.3': 'Added authorization for the second generation of tachographs. Only for Ruptela.',
           'v0.2.4': '(Only for Ruptela and Wialon Hosting)'
                    '\n- Added a mechanism for displaying authorization and download status.\n'
                    '- Fixed sending multiple commands at once. Until the first is completed, the second cannot be sent.',
           'v0.2.5': 'Bug fix. Update stability.',
           'v0.2.6': 'Fixed bug when authorizing different objects.',
           'v0.2.7': 'Fixed issue of restarting the app after successful authorization'}

    def updateLog(self):
        text = ''

        for key, value in self.log.items():
            text += key + ': ' + value + '\n'

        return text

class TCAClientApp(App):
    connection = None
    textbox = None
    # label = None
    scObj = None
    configObj = iConfig()

    # current app version need to change here
    version = 'v0.2.7 (Beta)'

    def build(self):
        layObj = RootLayout()

        layObj.ids.app_ver.title = "Tachograph Card Authorizer " + self.version

        # root = self.setup_gui()
        self.connect_to_server()
        # return root

        print('start RootLayout')
        return layObj

    # def setup_gui(self):
    #     self.textbox = TextInput(size_hint_y=.8, multiline=False)
    #     # self.textbox.bind(on_text_validate=self.send_message)
    #     # self.label = Label(size_hint_y=.4, text='connecting with manually enter...\n\n')
    #     self.button = Button(text='Authorize', size_hint_y=.1, on_release=self.authorize)
    #
    #     layout = BoxLayout(orientation='vertical')
    #     # layout.add_widget(self.label)
    #     layout.add_widget(self.textbox)
    #     layout.add_widget(self.button)
    #     return layout

    def connect_to_server(self):
        ip, port = self.configObj.getServerAddress('ip and port')
        reactor.connectTCP(ip, int(port), EchoClientFactory(self))

    def disconnect_from_server(self):
        self.connection.disconnect()

    def on_connection(self, connection):
        self.print_message('TCA', 'Connected successfully!')
        self.connection = connection

    def send_message(self, *args):
        msg = self.root.ids.textbox.text
        self.print_message('TCA', msg)
        if msg and self.connection:
            self.connection.write(msg.encode('utf-8'))
            self.root.ids.textbox.text = ""

    def authorize(self, *args):
        # Check connection and reconnecti if it is needed.
        if not self.connection:
            # do not authorize without connection
            self.connect_to_server()
            
            # if self.connection is None:
            #     self.print_message('TCA', 'Remote server is not avalible to connect')
            #     return

        if self.scObj is None:
            # self.print_message('TCA', 'Try to authorize Smart Card')

            try:
                # Please Note to this part of code !! til the &@3 symbol
                # init SC obj
                self.scObj = sCard(self)

                # check MasterFile of Company Card
                ####### DANGER #######
                self.scObj.checkIsTachographCard()
                ######################
                self.scObj.scDisconnect()

                # Reinig SC object cause i don't know how to close selected file!!!! Need to fix in the future!
                self.scObj = sCard(self)
                # &@3 symbol

                self.print_message('TCA', 'Smart Card is Authorized')

                # Read ICC file
                icc = self.scObj.readBinaryEF()
                # send ICC to the server
                self.print_message('TCA', 'Company card SN is ' + toHexString(icc).replace(' ', ''))

                if icc and self.connection:
                    self.connection.write(self.tcProtocol_sendICC(icc))
                    # just print
                    # self.print_message('TCA', 'Send to server: ' + toHexString(icc))

            # isn't using CardRequestTimeoutException cause kivy doesn't want to import this module
            # except CardRequestTimeoutException:
            except Exception:
                self.print_message('TCA',
                                   'Smart card waiting limit has been exceeded. The card is either not connected or has problems connecting to a computer.')

        else:
            self.print_message('TCA', 'has been already authorized')

    def print_message(self, obj, msg):
        if obj == 'TCA':
            # self.label.text += datetime.strftime(datetime.now(), '%H:%M:%S') + ': ' + "*TCA*: {}\n".format(msg)
            self.root.ids.textbox.text += datetime.strftime(datetime.now(), '%H:%M:%S') + ': ' + "*TCA*: {}\n".format(msg)

        elif obj == "Server":
            # self.label.text += datetime.strftime(datetime.now(), '%H:%M:%S') + ': ' + "*Server*: {}\n".format(msg)
            self.root.ids.textbox.text += datetime.strftime(datetime.now(), '%H:%M:%S') + ': ' + "*Server*: {}\n".format(msg)

        elif obj == "Warning":
            self.root.ids.textbox.text += datetime.strftime(datetime.now(), '%H:%M:%S') + ': ' + "*Warning*: {}\n".format(msg)

        else:
            self.root.ids.textbox.text += datetime.strftime(datetime.now(), '%H:%M:%S') + ': ' + "*Trace*: {}\n".format(msg)

        print(datetime.strftime(datetime.now(), '%H:%M:%S') + ': ' + "*", obj, "*: {}".format(msg))


    def tcProtocol_sendICC(self, data):
        # create header for TC Ruptela only
        # tachograph card header 2424 000A 01 00 0047063B06170469
        tch = [0x24, 0x24, 0x00, 0x0A, 0x01, 0x00]
        # make APDU
        apdu = tch + data

        return bytearray(apdu)

    def tcProtocol_sendData(self, data, cmd_type):
        # cmd_type - (ATR - 1, APDU - 2)

        if cmd_type == 1:
            data_type = 'ATR'
        else:
            data_type = 'APDU'

        # self.print_message('TCA', 'APDU >>: ' + toHexString(data))
        # print('test', hex(test[0]), test[1])

        # tachograph card header 2424 000A 01 00 0047063B06170469
        # header for tacho card

        tch = [0x24, 0x24, 0x00, len(data) + 2, 0x01, cmd_type]
        # compose apdu with header
        apdu = tch + data
        # print('apdu:', apdu, ' type is:', type(apdu))

        self.connection.write(bytearray(apdu))
        # just print
        # self.print_message('TCA', data_type + ' send: ' + toHexString(apdu))

    def ruptelaCrypt(self, data, revers):
        data_len = len(data)

        crc_table_dec = [-74, -80, 73, 42, -21, -41, -16, -15, -30, -6, -33, 50, 102, 108, 66, 127, 97, 114, -87, 21, -70,
                     32, -7, -17, 116, 90, 57, -85, 8, -53, -97, -117, 63, -60, -66, -43, 88, -86, 57, 89, 49, 27, 36,
                     -17, -44, 79, 38, 117, -51, -23, 26, 79, 56, 122, 96, -112, 10, 47, 96, 97, 60, 82, 113, 29, 18,
                     91, -80, -110, -80, 78, 16, 127, -97, -119, 23, 100, 21, -28, 36, -100, -10, -43, -100, -94, -43,
                     -82, 102, -78, -76, 65, 79, 18, -14, 19, 81, 23, -128, -45, -85, -57, -68, -25, 51, -22, 89, -3,
                     127, -78, 86, -66, -16, -61, 63, 2, 61, 76, 93, -88, -17, 117, 8, -56, -63, -99, 124, -70, -115,
                     -54, -55, 83, 121, 12, -76, -73, -82, 10, -100, -12, -86, -17, -122, -92, 11, -54, -89, 47, -77,
                     -55, -75, -24, 64, -39, -114, -77, -50, -105, -54, 45, -127, -103, 78, -72, 110, -34, -44, 2, -85,
                     -63, 29, 74, 71, -16, 40, 13, 62, -81, 120, -109, 105, -98, 25, -124, 85, -110, -56, 52, 69, -100,
                     49, 4, 36, 3, 27, -44, 80, 81, 75, 107, 100, 19, -3, -30, 37, 84, 98, 108, 5, 36, -26, 84, 72,
                     -120, 109, 87, -98, 35, -83, -113, -88, 127, -30, 58, 15, -65, -109, 64, 61, -75, 103, 94, -29,
                     -70, -96, 59, -103, -19, 122, 86, 93, -116, 91, -87, -41, 99, 105, 7, 70, 29, -79, -112, 53, 69,
                     102, 73, -115, 16]

        # make one byte value
        crc_table = []
        for index in range(len(crc_table_dec)):
            crc_table.append(crc_table_dec[index] & 0xFF)

        # revers data for next encryption to send apdu
        if revers == 1:
            # revers first & last bytes
            byte0 = data[0]
            bytelast = data[len(data) - 1]
            data[len(data) - 1] = byte0
            data[0] = bytelast

        # crypt data
        crypted_data = []
        for index in range(data_len):
            byte = data[index]
            ind1 = index % len(crc_table)
            crypted_data.append(byte ^ crc_table[ind1])


        # decrypt data for incomming apdu
        if revers == 0:
            # revers first & last bytes
            byte0 = crypted_data[0]
            bytelast = crypted_data[len(crypted_data) - 1]
            crypted_data[len(crypted_data) - 1] = byte0
            crypted_data[0] = bytelast

        return crypted_data

if __name__ == '__main__':
    TCAClientApp().run()