import os
import abc
import sys
import numpy as np
from struct import *
from enum import Enum
from urllib.request import urlopen
from time import sleep
from kivy.uix.button import Button
from kivy.uix.widget import Widget
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout
from kivy.app import App
from kivy.graphics import Color, Rectangle
from random import random as r
from kivy.clock import Clock
from threading import Thread

from e_drive.drone import *


# Firmware Start


class FirmwareHeader():

    def __init__(self):
        self.modelNumber        = 0
        self.version            = 0
        self.length             = 0

        self.year               = 0
        self.month              = 0
        self.day                = 0

        self.versionMajor       = 0
        self.versionMinor       = 0
        self.versionBuild       = 0


    @classmethod
    def getSize(cls):
        return 16


    @classmethod
    def parse(cls, dataArray):
        data = FirmwareHeader()
        
        if len(dataArray) != cls.getSize():
            return None
        
        data.modelNumber, data.version, data.length, data.year, data.month, data.day = unpack('<IIIHBB', dataArray)

        data.modelNumber = ModelNumber(data.modelNumber)

        data.versionMajor = (data.version >> 24) & 0xFF
        data.versionMinor = (data.version >> 16) & 0xFF
        data.versionBuild = (data.version & 0xFFFF)

        return data



class Firmware():

    def __init__(self, url = None):

        if url == None:
            self.url            = 0         # 펌웨어 URL
            self.resource       = None      # 펌웨어 전체 파일
            self.header         = None      # 펌웨어 헤더
            self.length         = 0         # 펌웨어 전체 파일의 길이

            self.rawHeader      = None      # 헤더 배열
            self.stringHeader   = None      # 헤더 배열을 HEX 문자열로 변환한 것
            self.imageType      = ImageType.None_
        else:
            self.open(url)


    def open(self, url):

        self.url = url
        
        with urlopen(self.url) as res:
            self.resource   = res.read()
            self.length     = len(self.resource)

            # 세번째 블록의 데이터가 [01 02 03 04 05 06 07 08 09 0a 0b] 인지 확인하여
            # 맞는 경우 2번째 블럭(CC2541)을, 아니면 1번째 블럭(STM32F4)을 펌웨어 헤더로 사용
            flagCheck3rdBlock = True
            for i in range(11):
                if self.resource[32 + i] != i:
                    flagCheck3rdBlock = False
                    break
            
            if flagCheck3rdBlock == True:
                self.rawHeader  = self.resource[16:32]
                self.header     = FirmwareHeader.parse(self.rawHeader)
                self.header.length = self.length
                self.imageType  = ImageType(self.resource[15])
            else:
                self.rawHeader  = self.resource[0:16]
                self.header     = FirmwareHeader.parse(self.rawHeader)
                self.imageType  = ImageType.RawImageSingle
            
            self.stringHeader = ""
            for data in self.rawHeader:
                self.stringHeader += "{0:02X} ".format(data)

            print(Fore.CYAN + "  - {0}".format(self.header.modelNumber) + Style.RESET_ALL)
            print("    Header Hex : {0}".format(self.stringHeader))
            print("     File Size : {0} bytes".format(self.length))
            print("       Version : {0}.{1}.{2}".format(self.header.versionMajor, self.header.versionMinor, self.header.versionBuild))
            print("          Date : {0}.{1}.{2}".format(self.header.year, self.header.month, self.header.day))
            print("        Length : {0}\n".format(self.header.length))


# Firmware End




# Updater Start

class Updater:


    def __init__(self):

        self.modelNumber        = None
        self.deviceType         = None
        self.imageType          = None
        self.modeUpdate         = ModeUpdate.None_
        self.indexBlockNext     = 0
        self.flagUpdated        = False
        self.flagUpdateComplete = False



    def _eventInformation(self, information):

        if      isinstance(information, Information):
            self.modeUpdate         = information.modeUpdate
            self.flagUpdated        = True 
            self.modelNumber        = information.modelNumber
            self.deviceType         = DeviceType(((self.modelNumber.value >> 8) & 0xFF))
            self.imageType          = ImageType.RawImageSingle
            
            if information.modeUpdate == ModeUpdate.Complete:
                self.flagUpdateComplete = True
            else:
                print(Fore.YELLOW + "* Connected Device : {0}".format(self.deviceType) + Style.RESET_ALL)
                print("  Model Number : {0}".format(information.modelNumber))
                print("       Version : {0}.{1}.{2} ({3} / 0x{3:08X})".format(information.version.major, information.version.minor, information.version.build, information.version.v))
                print("  Release Date : {0}.{1}.{2}".format(information.year, information.month, information.day))
                print("   Mode Update : {0}\n".format(information.modeUpdate))

        elif    isinstance(information, InformationCC2541):
            self.modeUpdate         = information.modeUpdate
            self.flagUpdated        = True 
            self.modelNumber        = information.modelNumber
            self.deviceType         = DeviceType(((self.modelNumber.value >> 8) & 0xFF))
            self.imageType          = information.imageType
            
            if information.modeUpdate == ModeUpdate.Complete:
                self.flagUpdateComplete = True
            else:
                print(Fore.YELLOW + "* Connected Device : {0}".format(self.deviceType) + Style.RESET_ALL)
                print("  Model Number : {0}".format(information.modelNumber))
                print("    Image Type : {0}".format(information.imageType))
                print("       Version : {0}.{1}.{2} ({3} / 0x{3:08X})".format(information.version.major, information.version.minor, information.version.build, information.version.v))
                print("  Release Date : {0}.{1}.{2}".format(information.year, information.month, information.day))
                print("   Mode Update : {0}\n".format(information.modeUpdate))




    def _eventUpdateLocation(self, updateLocation):

        self.flagUpdated        = True
        self.indexBlockNext     = updateLocation.indexBlockNext
        #print("* eventUpdateLocation({0})\n".format(updateLocation.indexBlockNext))



    def start(self):

        colorama.init()

        self._flagThreadRun = True
        self._thread = Thread(target=self._update, args=(), daemon=True)
        self._thread.start()



    def _update(self):
        
        # 펌웨어 업데이트
        print(Back.WHITE + Fore.BLUE + " FIRMWARE UPGRADE " + Style.RESET_ALL)
        print("")

        print(Fore.YELLOW + "* Firmware loading." + Style.RESET_ALL)
        print("")
        
        firmware = []
        firmware.append(Firmware("https://s3.ap-northeast-2.amazonaws.com/byrobot/fw_drone_7_drone_p1_latest.eb"))
        firmware.append(Firmware("https://s3.ap-northeast-2.amazonaws.com/byrobot/fw_drone_7_bleserver_p1_latest.eb"))
        #firmware.append(Firmware("file:///firmware/fw_drone_7_drone_p1_latest.eb"))
        #firmware.append(Firmware("file:///firmware/fw_drone_7_bleserver_p1_a_latest.eb"))
        #firmware.append(Firmware("file:///firmware/fw_drone_7_bleserver_p1_b_latest.eb"))

        drone = Drone()
        if drone.open() == False:
            print(Fore.RED + "* Error : Unable to open serial port." + Style.RESET_ALL)
            sys.exit(1)


        # 이벤트 핸들링 함수 등록
        drone.setEventHandler(DataType.Information, self._eventInformation)
        drone.setEventHandler(DataType.UpdateLocation, self._eventUpdateLocation)


        # 변수
        flagRun             = True
        countError          = 0
        timeTransferNext    = 0
        timeDrawNext        = 0     # 업데이트 상태 다음 갱신 시각


        # 연결된 장치 확인
        drone.sendRequest(DeviceType.Drone, DataType.Information)
        sleep(0.2)

        drone.sendRequest(DeviceType.Controller, DataType.Information)
        sleep(0.2)


        if self.deviceType == None:
            print(Fore.RED + "* Error : No Answer." + Style.RESET_ALL)
            sys.exit(1)


        # 헤더 만들기
        header = Header()
        header.dataType = DataType.Update
        header.length   = 18
        header.from_    = DeviceType.Updater
        header.to_      = self.deviceType


        # 업데이트 위치 요청
        drone.sendRequest(header.to_, DataType.UpdateLocation)
        sleep(0.1)

        drone.sendRequest(header.to_, DataType.UpdateLocation)
        sleep(0.1)

        while self._flagThreadRun:
            for fw in firmware:

                if self.modelNumber == fw.header.modelNumber:

                    # 펌웨어의 모델 번호와 일치하는 드론이 있는 경우

                    # 펌웨어 이미지 확인
                    flagImageTypePass = False

                    if      self.imageType == ImageType.ImageSingle and fw.imageType == ImageType.RawImageSingle:
                        flagImageTypePass = True
                        
                    elif    self.imageType == ImageType.ImageA and fw.imageType == ImageType.RawImageB:
                        flagImageTypePass = True
                        
                    elif    self.imageType == ImageType.ImageB and fw.imageType == ImageType.RawImageA:
                        flagImageTypePass = True

                    if flagImageTypePass == True:

                        if self.modeUpdate == ModeUpdate.Ready or self.modeUpdate == ModeUpdate.Update:

                            while flagRun:

                                sleep(0.001)
                                now = time.time() * 1000

                                if (self.flagUpdated == True) or (timeTransferNext < now):

                                    if self.indexBlockNext == 0:
                                        timeTransferNext = now + 2400
                                    else:
                                        timeTransferNext = now + 100

                                    # 에러 카운트
                                    if self.flagUpdated == False:

                                        countError = countError + 1

                                        # 오류가 과도하게 누적된 경우 업데이트 취소
                                        if countError > 30:
                                            print(Fore.RED + "* Error : No response." + Style.RESET_ALL)
                                            flagRun = False
                                    
                                    else:
                                        countError = 0


                                    index = self.indexBlockNext * 16

                                    # 업데이트 할 위치를 넘어서는 경우 종료
                                    if index + 16 > fw.length:
                                        print(Fore.RED + "* Error : Index Over." + Style.RESET_ALL)
                                        flagRun = False
                                        break

                                    # 업데이트가 완료된 경우 종료
                                    if self.flagUpdateComplete == True:
                                        sleep(1)
                                        print("\n\n" + Fore.GREEN + "  Update Complete." + Style.RESET_ALL)
                                        flagRun = False
                                        break

                                    data = bytearray(2)
                                    data[0] = self.indexBlockNext & 0xFF
                                    data[1] = (self.indexBlockNext >> 8) & 0xFF
                                    data.extend(fw.resource[index : index + 16])
                                    drone.transfer(header, data)
                                    
                                    self.flagUpdated = False

                                    # 진행률 표시
                                    if (timeDrawNext < now) or (fw.length - index < 128):

                                        timeDrawNext    = now + 73
                                        percentage      = index * 100 / fw.length
                                        print(Fore.YELLOW + "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b{0:8.1f}%".format(percentage) + Style.RESET_ALL, end='')
                        else:
                            print(Fore.RED + "* Error : Firmware update is not available." + Style.RESET_ALL)

                        break

            drone.close()


# Updater End




# Main Start


if __name__ == '__main__':

    updater = Updater()

    updater.start()


# Main End

