#! /usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright (c) 2024 Realtek Semiconductor Corp.
# SPDX-License-Identifier: Apache-2.0

from serial.tools.list_ports import comports
import serial
import serial.tools.list_ports
from datetime import datetime

from .rom_handler import *
from .floader_handler import *
from .flash_utils import *
from .device_profile import *
from .json_utils import *
from .rt_settings import *
from .spic_addr_mode import *
from .memory_info import *
from .config_utils import *
from .remote_serial import RemoteSerial
from typing import Optional, Dict, Any

_RTK_USB_VID = "0BDA"

CmdEsc = b'\x1b\r\n'
CmdSetBackupRegister = bytes("EW 0x480003C0 0x200\r\n", encoding="utf-8")
CmdResetIntoDownloadMode = bytes("reboot uartburn\r\n", encoding="utf-8")

OtpSpicAddrModeAddr = 0x02
OtpSpicAddrModeMask = 0x02
OtpSpicAddrModePos = 1
OtpSpicAddrMode4Byte = (1 << OtpSpicAddrModePos)
OtpSpicAddrMode3Byte = (0 << OtpSpicAddrModePos)

OtpSpicAddrModeAddrForAmebaD = 0x0E
OtpSpicAddrModeMaskForAmebaD = 0x40
OtpSpicAddrModePosForAmebaD = 6
OtpSpicAddrMode4ByteForAmebaD = (1 << OtpSpicAddrModePosForAmebaD)
OtpSpicAddrMode3ByteForAmebaD = (0 << OtpSpicAddrModePosForAmebaD)


class Ameba(object):
    def __init__(self,
                 profile: RtkDeviceProfile,
                 serial_port: serial.Serial,
                 baudrate: int,
                 image_path: str,
                 setting: RtSettings,
                 logger,
                 download_img_info=None,
                 chip_erase=False,
                 memory_type=None,
                 erase_info=None,
                 remote_server: Optional[str] = None,
                 remote_port: Optional[int] = None,
                 remote_password: Optional[str] = None):
        self.logger = logger
        self.profile_info = profile
        self.serial_port = None
        self.serial_port_name = serial_port
        self.remote_server = remote_server
        self.remote_port = remote_port
        self.remote_password = remote_password
        self.is_usb = self.is_realtek_usb() if not remote_server else False
        self.initial_serial_port()
        self.baudrate = baudrate
        self.image_path = image_path
        self.download_img_info = download_img_info
        self.chip_erase = chip_erase
        self.memory_type = memory_type
        self.setting = setting
        self.device_info = None
        self.erase_info = erase_info
        self.is_all_ram = True

        self.rom_handler = RomHandler(self)
        self.floader_handler = FloaderHandler(self)

    def __del__(self):
        if self.serial_port:
            if self.serial_port.is_open:
                self.logger.info(f"{self.serial_port.port} try to close.")
                self.serial_port.close()
            while self.serial_port.is_open:
                pass
            self.logger.info(f"{self.serial_port.port} closed.")

    def initial_serial_port(self):
        # initial serial port
        try:
            # determine whether to use a remote serial port
            self.logger.debug(f"Remote server: self.logger type={type(self.logger)}, value={self.logger}")
            if self.remote_server and self.remote_port:
                self.logger.info(f"Connect to remote serial server: {self.remote_server}:{self.remote_port} (Serial port: {self.serial_port_name})")
                # initialize remote serial port
                self.serial_port = RemoteSerial(
                    remote_server=self.remote_server,
                    remote_port=self.remote_port,
                    port=self.serial_port_name,
                    baudrate=self.profile_info.handshake_baudrate,
                    logger=self.logger
                )
                if self.remote_password:
                    self.logger.debug("Remote server: password set, will send validate command")
                    self.serial_port.validate(self.remote_password)
                self.serial_port.open()
            else:
                # initialize local serial port
                if self.is_usb:
                    self.serial_port = serial.Serial(self.serial_port_name,
                                                     baudrate=self.profile_info.handshake_baudrate,
                                                     parity=serial.PARITY_NONE,
                                                     stopbits=serial.STOPBITS_ONE,
                                                     bytesize=serial.EIGHTBITS)
                else:
                    self.serial_port = serial.Serial()
                    self.serial_port.port = self.serial_port_name
                    self.serial_port.baudrate = self.profile_info.handshake_baudrate
                    self.serial_port.parity = serial.PARITY_NONE
                    self.serial_port.stopbits = serial.STOPBITS_ONE
                    self.serial_port.bytesize = serial.EIGHTBITS
                    self.serial_port.dtr = False
                    self.serial_port.rts = False
                    self.serial_port.open()
        except Exception as err:
            self.logger.error(f"Initialize serial port failed: {err}")
            sys.exit(1)

    # --- check if serial port is open (remote/local compatible) ---
    def is_open(self) -> bool:
        if isinstance(self.serial_port, RemoteSerial):
            return self.serial_port.is_open
        elif isinstance(self.serial_port, serial.Serial):
            return self.serial_port.is_open
        return False

    def switch_baudrate(self, baud, delay_s, force=False):
        ret = ErrType.OK

        if (baud == self.serial_port.baudrate) and (not force):
            self.logger.debug(f"Don't need to switch baudrate: {baud}")
            return ret

        self.logger.debug(f"Switch baudrate: {self.serial_port.baudrate} -> {baud}")

        try:
            # remote serial port: close and reopen (no dynamic switching supported yet)
            if isinstance(self.serial_port, RemoteSerial):
                self.serial_port.close()
                #time.sleep(delay_s)
                self.serial_port.baudrate = baud
                self.serial_port.open()
            else:
                # local serial port
                if self.is_usb:
                    for retry in range(10):
                        try:
                            if self.serial_port.is_open:
                                self.serial_port.close()
                            while self.serial_port.is_open:
                                pass
                            ret = ErrType.OK
                            break
                        except:
                            ret = ErrType.SYS_IO
                        time.sleep(0.1)
                    if ret != ErrType.OK:
                        self.logger.warning(f"Close serial port failed")
                    time.sleep(delay_s)

                if self.serial_port.baudrate != baud:
                    self.serial_port.baudrate = baud

                if self.is_usb:
                    for rty in range(10):
                        try:
                            self.serial_port.open()
                            ret = ErrType.OK
                            break
                        except:
                            ret = ErrType.SYS_IO
                        time.sleep(0.1)
        except Exception as e:
            self.logger.error(f"An exception occurs when switching baudrate: {str(e)}")
            ret = ErrType.SYS_IO

        if ret == ErrType.OK:
            self.logger.debug(f"Switch baudrate success: {baud}")
        else:
            self.logger.debug(f"Switch baudrate failed")

        return ret

    def read_bytes(self, timeout_seconds, size=1):
        ret = ErrType.OK
        read_ch = None

        start_time = datetime.now()
        while self.serial_port.inWaiting() < size:
            if self.remote_server:
                time.sleep(0.001) # avoid waiting idly and improve efficiency.
            if (datetime.now() - start_time).seconds >= timeout_seconds:
                return ErrType.DEV_TIMEOUT, None

        try:
            read_ch = self.serial_port.read(size=size)
        except Exception as err:
            self.logger.error(f"[{self.serial_port.port}] read bytes err: {err}")
            ret = ErrType.SYS_IO

        return ret, read_ch

    def write_bytes(self, data_bytes):
        self.serial_port.write(data_bytes)

    def write_string(self, string):
        bytes_array = string.encode("utf-8")
        self.serial_port.write(bytes_array)

    def is_realtek_usb(self):
        if self.remote_server:
            return False
        ports = serial.tools.list_ports.comports()
        for port, desc, hvid in sorted(ports):
            if port == self.serial_port_name:
                # hvid: USB VID:PID=0BDA:8722 SER=5 LOCATION=1-1
                if _RTK_USB_VID in hvid:
                    return True
        else:
            return False

    def switch_baudrate_old(self, baud, delay_s, force=False):
        ret = ErrType.OK

        if (baud == self.serial_port.baudrate) and (not force):
            self.logger.debug(f"Reactive port {self.serial_port.port} ignored, baudrate no change")
            return ret

        if baud != self.serial_port.baudrate:
            self.logger.debug(
                f"Reactive port {self.serial_port.port} with baudrate from {self.serial_port.baudrate} to {baud}")
        else:
            self.logger.debug(
                f"Reactive port {self.serial_port.port} with baudrate {baud}")

        # if uart dtr/rts enable, should skip close/reopen operation
        # if USB port, should close/reopen port when switch baudrate
        if self.is_usb:
            # check if already activated
            for retry in range(10):
                try:
                    if self.serial_port.is_open:
                        self.serial_port.close()

                    while self.serial_port.is_open:
                        pass
                    ret = ErrType.OK
                except:
                    ret = ErrType.SYS_IO

                if ret == ErrType.OK:
                    break

                time.sleep(0.1)

            if ret != ErrType.OK:
                self.logger.warning(f"Failed to close {self.serial_port.port} when reactive it.")

            time.sleep(delay_s)

        if self.serial_port.baudrate != baud:
            self.serial_port.baudrate = baud

        if self.is_usb:
            ret = ErrType.OK
            for rty in range(10):
                try:
                    self.serial_port.open()
                    ret = ErrType.OK
                except:
                    ret = ErrType.SYS_IO

                if ret == ErrType.OK:
                    break

                time.sleep(0.1)

        if ret == ErrType.OK:
            self.logger.debug(f"Reactive port {self.serial_port.port} ok")
        else:
            self.logger.debug(f"Reactive port {self.serial_port.port} fail")

        return ret

    def check_download_mode(self):
        ret = ErrType.SYS_IO
        is_floader = False
        boot_delay = self.setting.usb_rom_boot_delay_in_second if self.profile_info.support_usb_download else self.setting.rom_boot_delay_in_second

        self.logger.debug(f"Check download mode with baudrate {self.serial_port.baudrate}")
        retry = 0
        while retry < 3:
            retry += 1
            try:
                self.logger.debug(f"Check whether in rom download mode")
                ret = self.rom_handler.handshake()
                if ret == ErrType.OK:
                    self.logger.debug(f"Handshake ok, device in rom download mode")
                    break

                if not self.is_usb:
                    self.logger.debug(
                        f'Assume in application or ROM normal mode with baudrate {self.profile_info.log_baudrate}')
                    self.switch_baudrate(self.profile_info.log_baudrate, self.setting.baudrate_switch_delay_in_second)

                    self.logger.debug("Try to reset device...")

                    self.serial_port.flushOutput()

                    self.write_bytes(CmdEsc)
                    time.sleep(0.1)

                    if self.profile_info.is_amebad():
                        self.serial_port.flushOutput()
                        self.write_bytes(CmdSetBackupRegister)
                        time.sleep(0.1)

                    self.serial_port.flushOutput()
                    self.write_bytes(CmdResetIntoDownloadMode)

                    self.switch_baudrate(self.profile_info.handshake_baudrate, boot_delay, True)

                    self.logger.debug(
                        f'Check whether reset in ROM download mode with baudrate {self.profile_info.handshake_baudrate}')

                    ret = self.rom_handler.handshake()
                    if ret == ErrType.OK:
                        self.logger.debug("Handshake ok, device in ROM download mode")
                        break
                    else:
                        self.logger.debug("Handshake fail, cannot enter UART download mode")

                    self.switch_baudrate(self.baudrate, self.setting.baudrate_switch_delay_in_second, True)

                self.logger.debug(f"Check whether in floader with baudrate {self.baudrate}")
                ret, status = self.floader_handler.sense(self.setting.sync_response_timeout_in_second)
                if ret == ErrType.OK:
                    # do not reset floader
                    is_floader = True
                    self.logger.debug("Floader handshake ok")
                    break
                else:
                    self.logger.debug(f"Floader handshake fail: {ret}")
            except Exception as err:
                self.logger.error(f"Check download mode exception: {err}")

        return ret, is_floader

    def prepare(self, show_device_info=True):
        ret = ErrType.OK
        floader_init_baud = self.baudrate if self.is_usb else (self.profile_info.handshake_baudrate if
                                                               (self.setting.switch_baudrate_at_floader == 1) else self.baudrate)
        boot_delay = self.setting.usb_floader_boot_delay_in_second if self.profile_info.support_usb_download else self.setting.floader_boot_delay_in_second

        if (not self.is_usb) and (self.setting.auto_switch_to_download_mode_with_dtr_rts != 0):
            ret = self.auto_enter_download_mode()
            if ret != ErrType.OK:
                self.logger.error(f"Enter download mode by DTR/RTS fail: {ret}")
                return ret

        ret, is_floader = self.check_download_mode()
        if ret != ErrType.OK:
            self.logger.error(f"Enter download mode fail: {ret}")
            return ret

        if not is_floader:
            # download flashloader to RAM
            ret = self.rom_handler.download_floader()
            if ret != ErrType.OK:
                self.rom_handler.abort()
                self.logger.error(f"Flashloader download fail: {ret}")
                return ret

            ret = self.switch_baudrate(floader_init_baud, boot_delay, True)
            if ret != ErrType.OK:
                self.logger.error(f"Flashloader boot fail: {ret}")
                return ret

            ret = self.floader_handler.handshake(self.baudrate)
            if ret != ErrType.OK:
                self.logger.error(f"Flashloader handshake fail: {ret}")
                return ret

        ret, self.device_info = self.floader_handler.query()
        if ret != ErrType.OK:
            self.logger.error(f"Query fail: {ret}")
            return ret

        if not show_device_info:
            return ret

        self.logger.info("Device info:")
        self.logger.info(f'* DID: {hex(self.device_info.did)}')
        self.logger.info(f'* ImageType: 0x{format(self.device_info.image_type, "04x")}')
        self.logger.info(
            f'* CmdSetVersion: {(self.device_info.cmd_set_version >> 8) & 0xFF}.{self.device_info.cmd_set_version & 0xFF}')
        if self.device_info.is_boot_from_nand():
            self.logger.info(f'* MemoryType: NAND')
            self.logger.info(f'* FlashMID: 0x{format(self.device_info.flash_mid, "02X")}')  # customized, do not modify
            if self.device_info.flash_mid == FlashUtils.NandMfgMicron:
                self.logger.info(f'* FlashDID: 0x{format(self.device_info.flash_did, "04X")}')
            else:
                self.logger.info(f'* FlashDID: 0x{format(self.device_info.flash_did, "02X")}')
            self.logger.info(f'* FlashMFG: {self.device_info.flash_mfg}')
            self.logger.info(f'* FlashModel: {self.device_info.flash_model}')
            self.logger.info(
                f'* FlashCapacity: {self.device_info.flash_capacity // 1024 // 1024 // (1024 // 8)}Gb/{self.device_info.flash_capacity // 1024 // 1024}MB')
            self.logger.info(f'* FlashBlockSize: {self.device_info.flash_block_size() // 1024}KB')
            self.logger.info(f'* FlashPageSize: {self.device_info.flash_page_size}B')
            self.logger.info(f'* FlashOobSize: {self.device_info.flash_oob_size}B')
            self.logger.info(f'* FlashPagesPerBlock: {self.device_info.flash_pages_per_block}')
            self.logger.info(f'* FlashBlocksPerLun: {self.device_info.flash_blocks_per_lun}')
            self.logger.info(f'* FlashLunsPerTarget: {self.device_info.flash_luns_per_target}')
            self.logger.info(f'* FlashTargets: {self.device_info.flash_targets}')
            self.logger.info(f'* FlashMaxBadBlocksPerLun: {self.device_info.flash_max_bad_block_per_lun}')
            self.logger.info(f'* FlashReqHostEccLevel: {self.device_info.flash_req_host_ecc_level}')
        else:
            self.logger.info(f'* MemoryType: NOR')
            self.logger.info(f'* FlashMID: {hex(self.device_info.flash_mid)}')
            self.logger.info(f'* FlashDID: {hex(self.device_info.flash_did)}')
            self.logger.info(
                f'* FlashCapacity: {self.device_info.flash_capacity // 1024 // (1024 // 8)}Mb/{self.device_info.flash_capacity // 1024 // 1024}MB')
            self.logger.info(f'* FlashPageSize: {self.device_info.flash_page_size}B')

        self.logger.info(f'* WiFiMAC: {self.device_info.get_wifi_mac_text()}')

        if (self.device_info.did != self.profile_info.device_id) and (self.device_info.did != 0xFFFF):
            self.logger.error("Device ID mismatch:")
            self.logger.error(f'* Device: {hex(self.device_info.did)}')
            self.logger.error(f'* Device Profile: {hex(self.profile_info.device_id)}')
            return ErrType.SYS_PARAMETER

        if self.device_info.memory_type != self.profile_info.memory_type:
            self.logger.error("Flash type mismatch:")
            self.logger.error(f'* Device: {self.device_info.memory_type}')
            self.logger.error(f'* Device Profile: {self.profile_info.memory_type}')
            return ErrType.SYS_PARAMETER

        if self.device_info.is_boot_from_nand():
            if self.device_info.flash_req_host_ecc_level > 0:
                self.logger.error(f"Unsupported NAND flash model without internal ECC")
                return ErrType.SYS_IO
            if self.device_info.flash_pages_per_block != FlashUtils.NandDefaultPagePerBlock.value:
                self.logger.error(
                    f"Unsupported NAND flash model with {self.device_info.flash_pages_per_block} pages per block")
                return ErrType.SYS_IO

        program_param1 = self.setting.program_config1.to_bytes(8, byteorder="little")
        program_param2 = self.setting.program_config2.to_bytes(8, byteorder="little")
        param = [program_param1, program_param2]
        ret = self.floader_handler.config(param)
        if ret != ErrType.OK:
            self.logger.error(f"Config device fail: {ret}")

        return ret

    def check_flash_lock(self, flash_status):
        ret = ErrType.OK

        if self.device_info.is_boot_from_nand():
            cmd = FlashUtils.NandCmdGetFeatures.value
            address = FlashUtils.NandRegProtection.value
            bp_mask = FlashUtils.NandRegProtectionBpMask.value
            if self.device_info.flash_mid == FlashUtils.NandMfgWinbond or self.device_info.flash_mid == FlashUtils.NandMfgMicron:
                bp_mask = FlashUtils.NandRegProtectionBpMaskWinbondMicron
        else:
            cmd = FlashUtils.NorCmdReadStatusReg1.value
            address = 0
            bp_mask = FlashUtils.NorStatusReg1BpMask.value

        ret, protection = self.read_flash_status_register(cmd, address)
        flash_status.protection = protection
        if ret == ErrType.OK:
            if (protection & bp_mask) != 0:
                flash_status.is_locked = True

        return ret

    def check_and_process_flash_lock(self, flash_status):
        follow_up_action = self.setting.flash_protection_process

        ret = self.check_flash_lock(flash_status)
        if ret != ErrType.OK:
            self.logger.error(f"Flash protection check fail: {ret}")

        if flash_status.is_locked:
            self.logger.warning("Flash block protection detected")

            if follow_up_action == RtSettings.FLASH_PROTECTION_PROCESS_PROMPT:
                self.logger.info(f"Follow-up Actions:")
                self.logger.info(f"1: Try operation with block protected(may fail)")
                self.logger.info(f"2: Remove the protection and restore the protection after operation")
                self.logger.info(f"3: Abort the operation")
                retry = 0
                while retry < 3:
                    try:
                        follow_up_action = int(input("Please Input the selected action index: ").strip())
                        if RtSettings.FLASH_PROTECTION_PROCESS_PROMPT < follow_up_action <= RtSettings.FLASH_PROTECTION_PROCESS_ABORT:
                            break
                        else:
                            self.logger.info(f"{follow_up_action} is invalid")
                    except Exception as err:
                        self.logger.error(f"Input is invalid: {err}")
                        continue
                else:
                    return ErrType.SYS_PARAMETER

            if follow_up_action == RtSettings.FLASH_PROTECTION_PROCESS_UNLOCK:
                flash_status.need_unlock = True
                self.logger.info("Remove the flash block protection...")
                ret = self.unlock_flash()
                if ret != ErrType.OK:
                    self.logger.error(f"Fail to remove the flash lock protection: {ret}")
                    return ret
            elif follow_up_action == RtSettings.FLASH_PROTECTION_PROCESS_ABORT:
                self.logger.warning(f"Operation aborted for block protection")
                return ErrType.SYS_CANCEL
            else:
                self.logger.warning(f"Trying to operate with block protection")
                return ErrType.SYS_PARAMETER

        return ret

    def unlock_flash(self):
        return self.lock_flash(0)

    def lock_flash(self, protection):
        if self.device_info.is_boot_from_nand():
            cmd = FlashUtils.NandCmdSetFeatures.value
            address = FlashUtils.NandRegProtection.value
        else:
            cmd = FlashUtils.NorCmdWriteStatusReg1.value
            address = 0

        ret = self.write_flash_status_register(cmd, address, protection)

        return ret

    def read_flash_status_register(self, cmd, address):
        return self.floader_handler.read_status_register(cmd, address)

    def write_flash_status_register(self, cmd, address, status):
        return self.floader_handler.write_status_register(cmd, address, status)

    def dtr_rts_timing_mapping(self, timing_list):
        ret = ErrType.OK

        if not self.serial_port.is_open:
            return ErrType.SYS_IO

        dtr = self.serial_port.dtr
        rts = self.serial_port.rts

        for key_val in timing_list:
            for key, val in key_val.items():
                if key.upper() == "DTR":
                    self.serial_port.dtr = (val != 0)
                elif key.upper() == "RTS":
                    self.serial_port.rts = (val != 0)
                elif key.upper() == "DELAY":
                    time.sleep(val / 1000)
                else:
                    self.logger.error(f"Unsupport DTR/RTS timing type: [{key}: {val}]")

        self.serial_port.dtr = dtr
        self.serial_port.rts = rts

        return ret

    def auto_enter_download_mode(self):
        ret = ErrType.OK

        if not self.serial_port.is_open:
            return ErrType.SYS_IO

        reburn_timing_file = os.path.join(RtkUtils.get_executable_root_path(), self.setting.auto_switch_to_download_mode_with_dtr_rts_file)
        if os.path.exists(reburn_timing_file):
            reburn_timing = ConfigUtils.get_key_value_pairs(self.logger, reburn_timing_file)
            try:
                if reburn_timing:
                    ret = self.dtr_rts_timing_mapping(reburn_timing)
            except Exception as err:
                self.logger.error(f"Fail to auto enter download mode: {err}")
                ret = ErrType.SYS_IO
        else:
            self.logger.debug(f"{reburn_timing_file} is not exists!")

        return ret

    def auto_reset_device(self):
        ret = ErrType.OK

        if not self.serial_port.is_open:
            return ErrType.SYS_IO

        reset_timing_file = os.path.join(RtkUtils.get_executable_root_path(), self.setting.auto_reset_device_with_dtr_rts_file)
        if os.path.exists(reset_timing_file):
            reset_timing = ConfigUtils.get_key_value_pairs(self.logger, reset_timing_file)
            try:
                if reset_timing:
                    ret = self.dtr_rts_timing_mapping(reset_timing)
            except Exception as err:
                self.logger.error(f"Fail to reset device: {err}")
                ret = ErrType.SYS_IO
        else:
            self.logger.debug(f"{reset_timing_file} is not exists!")

        return ret

    def post_process(self):
        ret = ErrType.OK

        post_process_str = self.setting.post_process.strip().upper()
        try:
            next_op = NextOpType[post_process_str]
            self.logger.debug(f"Next option: {next_op}")
        except KeyError:
            self.logger.error(f"No matching enum found for {post_process_str}")
            next_op = NextOpType.NONE

        if next_op != NextOpType.NONE:
            if (next_op == NextOpType.RESET) and (not self.is_usb) and (self.setting.auto_reset_device_with_dtr_rts != 0):
                self.logger.debug(f"Reset device with DTR/RTS...")
                ret = self.auto_reset_device()
                if ret != ErrType.OK:
                    self.logger.warning(f"Reset device with DTR/RTS fail: {ret}")
            else:
                if next_op == NextOpType.RESET:
                    self.logger.info(f"Reset device without DTR/RTS")

                ret = self.floader_handler.next_operation(next_op, 0)
                if ret != ErrType.OK:
                    self.logger.warning(f"Next option {next_op} fail: {ret}")

        # should close serial port
        if self.serial_port and self.is_open():
            try:
                self.logger.info(f"close {self.serial_port.port}...")
                self.serial_port.close()
                self.logger.info(f"{self.serial_port.port} close done")
            except Exception as e:
                self.logger.error(f"close error: {e}", exc_info=True)
        self.serial_port = None

        return ret

    def check_protocol(self):
        ret = True

        if (not self.is_usb) and self.setting.disable_nand_access_with_uart == 1:
            self.logger.warning(
                f"NAND access via UART is not allowed, please check the COM port and make sure the device profile matches the attached device!")
            ret = False

        return ret

    def check_protocol_for_download(self):
        ret = True
        has_nand = False

        for image_info in self.profile_info.images:
            if (image_info.memory_type == MemoryInfo.MEMORY_TYPE_NAND):
                has_nand = True
                break

        if has_nand:
            ret = self.check_protocol()

        return ret

    def _process_image(self, img_name):
        if img_name.strip().startswith(("A:", "B:")):
            img_name = img_name.split(":")[1].split("(")[0].strip()
        if img_name.endswith(".dtb"):
            img_path_files = os.listdir(self.image_path)
            for img_f in img_path_files:
                self.logger.debug(img_f)
                if img_f.endswith(".dtb") and os.path.isfile(os.path.join(self.image_path, img_f)):
                    img_name = img_f
                    break
            else:
                img_name = None

        return img_name

    def verify_images(self):
        ret = ErrType.OK
        image_selected = False

        all_images = self.profile_info.images
        if self.download_img_info:
            all_images = self.download_img_info

        for image_info in all_images:
            if not image_info.mandatory:
                continue
            if not self.download_img_info:
                image_name = self._process_image(image_info.image_name)
                if image_name is None:
                    self.logger.error(f"Cannot find a valid {image_name} for download")
                    ret = ErrType.SYS_PARAMETER
                    break
                image_path = os.path.realpath(os.path.join(self.image_path, image_name))
            else:
                image_name = os.path.basename(image_info.image_name)
                image_path = image_info.image_name

            if not os.path.exists(image_path):
                self.logger.error(f"Image file {image_name} dose not exist: {image_path}")
                ret = ErrType.SYS_PARAMETER
                break

            if image_info.start_address < 0:
                self.logger.error(f"Start address is not valid specified for image {image_name}")
                ret = ErrType.SYS_PARAMETER
                break
            if image_info.end_address < 0:
                self.logger.error(f"End address is not valid specified for image {image_name}")
                ret = ErrType.SYS_PARAMETER
                break
            if image_info.start_address >= image_info.end_address:
                self.logger.error(
                    f"Invalid address range {image_info.start_address}-{image_info.end_address} for {image_name}")
                ret = ErrType.SYS_PARAMETER
                break
            image_size = os.path.getsize(image_path)
            if image_size > (image_info.end_address - image_info.start_address):
                self.logger.error(
                    f"Image file {image_path} is too large for {image_name}, please adjust the memory layout")
                ret = ErrType.SYS_PARAMETER
                break

            is_start_address_in_ram = self.profile_info.is_ram_address(image_info.start_address)
            is_end_address_in_ram = self.profile_info.is_ram_address(image_info.end_address)
            if (((self.memory_type == MemoryInfo.MEMORY_TYPE_RAM) and (
                    (not is_start_address_in_ram) or (not is_end_address_in_ram))) or
                    ((self.memory_type == MemoryInfo.MEMORY_TYPE_NOR) and (
                            is_start_address_in_ram or is_end_address_in_ram))):
                self.logger.error(
                    f"Invalid address range {image_info.start_address}-{image_info.end_address} for {image_name}")
                ret = ErrType.SYS_PARAMETER
                break
            if not is_start_address_in_ram:
                self.is_all_ram = False

            image_selected = True
        else:
            if not image_selected:
                self.logger.warning(f"No image selected!")
                ret = ErrType.SYS_PARAMETER

        return ret

    def post_verify_images(self):
        ret = ErrType.OK

        max_addr = self.profile_info.flash_start_address + self.device_info.flash_capacity
        image_dowload_list = self.download_img_info if self.download_img_info else self.profile_info.images
        for image_info in image_dowload_list:
            is_ram = self.profile_info.is_ram_address(image_info.start_address)
            if not is_ram:
                if (image_info.start_address > max_addr) or (image_info.end_address > max_addr):
                    self.logger.error(f"Invalid layout, image {image_info.image_name} address overflow")
                    ret = ErrType.SYS_OVERRANGE
                    break
                if (self.device_info.is_boot_from_nand() and
                        (((image_info.start_address % self.device_info.flash_block_size()) != 0) or
                         ((image_info.end_address % self.device_info.flash_block_size()) != 0))):
                    self.logger.error(f"{image_info.image_name} address range not aligned")
                    ret = ErrType.SYS_PARAMETER
                    break
        return ret

    def validate_config_for_erase(self):
        ret = ErrType.OK

        if self.erase_info:
            if ((self.profile_info.memory_type == MemoryInfo.MEMORY_TYPE_NAND) and (
                    self.memory_type == MemoryInfo.MEMORY_TYPE_NOR)) or \
                    ((self.profile_info.memory_type == MemoryInfo.MEMORY_TYPE_NOR) and (
                            self.memory_type == MemoryInfo.MEMORY_TYPE_NAND)):
                self.logger.error("Unsupported memory type.")
                ret = ErrType.SYS_PARAMETER
                return ret

            if (self.memory_type == MemoryInfo.MEMORY_TYPE_RAM) and (not self.profile_info.is_ram_address(self.erase_info.start_address)):
                self.logger.error(f"Invalid RAM start address: {self.erase_info.start_address}")
                ret = ErrType.SYS_PARAMETER
                return ret
            elif (self.memory_type != MemoryInfo.MEMORY_TYPE_RAM) and \
                    (not ((self.profile_info.memory_type == MemoryInfo.MEMORY_TYPE_NAND) or self.profile_info.is_flash_address(self.erase_info.start_address))):
                self.logger.error(f"Invalid start address: {self.erase_info.start_address}")
                ret = ErrType.SYS_PARAMETER
                return ret

            if (self.memory_type == MemoryInfo.MEMORY_TYPE_NOR):
                if not self.is_address_block_aligned(self.erase_info.start_address):
                    self.logger.warning(f"NOR flash start address should be aligned to {FlashUtils.NorDefaultBlockSize.value}B.")
                    ret = ErrType.SYS_PARAMETER
                    return ret
                if (self.erase_info.size_in_kbyte != 0xFFFFFFFF) and (not self.is_address_block_aligned(self.erase_info.size_in_byte())):
                    self.logger.warning(f"NOR flash erase size should be aligned to {FlashUtils.NorDefaultBlockSize.value}B.")
                    ret = ErrType.SYS_PARAMETER
                    return ret
                self.logger.info(f"NOR flash erase: start address={hex(self.erase_info.start_address)}, size={self.erase_info.size_in_kbyte}KB.")
            else:
                if not self.is_address_block_aligned(self.erase_info.start_address):
                    self.logger.warning(f"NAND flash start address should be block size aligned!")
                    ret = ErrType.SYS_PARAMETER
                    return ret
                if not self.is_address_block_aligned(self.erase_info.end_address):
                    self.logger.warning(f"NAND flash end address should be block size aligned!")
                    ret = ErrType.SYS_PARAMETER
                    return ret
                if (self.erase_info.end_address <= self.erase_info.start_address):
                    self.logger.warning(f"NAND flash start address should be less than end address!")
                    ret = ErrType.SYS_PARAMETER
                    return ret

                self.logger.info(f"NAND flash erase: start address={hex(self.erase_info.start_address)}, end address={hex(self.erase_info.end_address)}")

        return ret

    def check_protocol_for_erase(self):
        ret = True

        if (not self.is_usb) and (self.erase_info.memory_type == MemoryInfo.MEMORY_TYPE_NAND) and (
                self.setting.disable_nand_access_with_uart == 1):
            ret = False
            self.logger.warning(
                f"NAND access via UART is not allowed, please check the COM port and make sure the device profile matches the attached device!")
        return ret

    def is_address_block_aligned(self, address):
        ret = False

        if self.device_info:
            block_size = self.device_info.flash_block_size()
            ret = ((address % block_size) == 0)

        return ret

    def post_validate_config_for_erase(self):
        ret = ErrType.OK

        if self.erase_info and (not self.profile_info.is_ram_address(self.erase_info.start_address)):
            if not self.is_address_block_aligned(self.erase_info.start_address):
                ret = ErrType.SYS_PARAMETER
                self.logger.warning(
                    f"Flash start address should be aligned to block size {self.device_info.flash_block_size()}KB")
            if self.erase_info.memory_type == MemoryInfo.MEMORY_TYPE_NAND and (
                    not self.is_address_block_aligned(self.erase_info.end_address)):
                ret = ErrType.SYS_PARAMETER
                self.logger.warning(
                    f"Flash end address should be aligned to block size {self.device_info.flash_block_size()}KB")
            if self.erase_info.memory_type == MemoryInfo.MEMORY_TYPE_NOR and (
                    not self.is_address_block_aligned(self.erase_info.size_in_byte())):
                ret = ErrType.SYS_PARAMETER
                self.logger.warning(
                    f"Flash size should be aligned to block size {self.device_info.flash_block_size()}KB")
        return ret

    def calculate_checksum(self, image):
        with open(image, 'rb') as stream:
            img_content = stream.read()
        img_len = len(img_content)
        img_arr = list(img_content)
        chksum = 0
        offset = 0
        while (img_len - offset) > 3:
            chksum += (img_arr[offset + 0] | (img_arr[offset + 1] << 8) | (img_arr[offset + 2] << 16) | (
                    img_arr[offset + 3] << 24))
            offset += 4

        tmp = 0
        while (img_len - offset - tmp) > 0:
            chksum += img_arr[offset + tmp] << (8 * tmp)
            tmp += 1

        chksum = chksum & 0xffffffff
        return chksum

    def erase_flash_chip(self):
        self.logger.info(f"Chip erase start")  # customized, do not modify
        ret = self.floader_handler.erase_flash(MemoryInfo.MEMORY_TYPE_NOR, RtkDeviceProfile.DEFAULT_FLASH_START_ADDR,
                                               0, 0xFFFFFFFF,
                                               nor_erase_timeout_in_second(0xFFFFFFFF),
                                               sense=True, force=False)
        return ret

    def download_images(self):
        ret = ErrType.OK

        # support chip erase
        if self.chip_erase and (self.memory_type == MemoryInfo.MEMORY_TYPE_NOR):
            ret = self.erase_flash_chip()
            if ret != ErrType.OK:
                self.logger.error(f"Chip erase fail")
                return ret
            self.logger.info(f"Chip erase end")

        if self.download_img_info:
            for image_info in self.download_img_info:
                img_path = image_info.image_name
                img_name = os.path.basename(img_path)
                image_info.image_name = img_name

                self.logger.info(f"{img_name} download...")
                ret = self._download_image(img_path, image_info)
                if ret != ErrType.OK:
                    self.logger.info(f"{img_name} download fail: {ret}")
                    break
        else:
            is_area_A = False
            is_area_B = False
            for image_info in self.profile_info.images:
                is_mandatory = image_info.mandatory
                if not is_mandatory:
                    continue
                img_name = image_info.image_name
                self.logger.info(f"{img_name} download...")
                if img_name.strip().startswith(("A:", "a_")):
                    is_area_A = True
                elif img_name.strip().startswith(("B:", "b_")):
                    is_area_B = True

                if is_area_A and is_area_B:
                    self.logger.error(f"NOT support both A and B download at the same time")
                    ret = ErrType.SYS_PARAMETER
                    break

                img_name = self._process_image(img_name)
                img_path = os.path.realpath(os.path.join(self.image_path, img_name))
                ret = self._download_image(img_path, image_info)
                if ret != ErrType.OK:
                    self.logger.info(f"{img_name} download fail: {ret}")
                    break

        if ret == ErrType.OK:
            self.logger.info("All images download done")

        return ret

    def get_page_alligned_size(self, size, page_size):
        result = size

        if (size % page_size) != 0:
            result = (size // page_size + 1) * page_size

        return result

    def _download_image(self, image_path, image_info):
        ret = ErrType.OK

        page_size = self.device_info.flash_page_size
        block_size = self.device_info.flash_block_size()
        pages_per_block = self.device_info.flash_pages_per_block
        last_erase_addr = 0
        next_erase_addr = 0
        checksum = 0
        write_timeout = 0
        is_ram = (image_info.memory_type == MemoryInfo.MEMORY_TYPE_RAM)
        padding_data = self.setting.ram_download_padding_byte if is_ram else FlashUtils.FlashWritePaddingData.value
        start_time = datetime.now()

        with open(image_path, 'rb') as stream:
            img_content = stream.read()

        img_length = len(img_content)
        aligned_img_length = self.get_page_alligned_size(img_length, page_size)
        self.logger.debug(
            f"Image download size={aligned_img_length}({img_length}), start_addr={hex(image_info.start_address)}, "
            f"end_addr={hex(image_info.end_address)}")

        addr = image_info.start_address
        tx_sum = 0
        if ((image_info.memory_type == MemoryInfo.MEMORY_TYPE_NAND) or (
                is_ram and (self.profile_info.memory_type == MemoryInfo.MEMORY_TYPE_NAND))):
            write_timeout = nand_program_timeout_in_second(block_size,
                                                           page_size) + FlashUtils.NandBlockEraseTimeoutInSeconds.value
            img = io.BytesIO(img_content)
            img_bytes = img.read(img_length)
            data_bytes = img_bytes.ljust(aligned_img_length, padding_data.to_bytes(1, byteorder="little"))

            is_last_page = False
            progress_int = 0
            while not is_last_page:
                if addr >= image_info.end_address:
                    self.logger.debug(f"Overrange target={hex(addr)}, end={hex(image_info.end_address)}")
                    ret = ErrType.SYS_OVERRANGE
                    break

                ret = self.floader_handler.erase_flash(image_info.memory_type, addr, addr + block_size, block_size,
                                                       nand_erase_timeout_in_second(block_size, block_size),
                                                       sense=True)
                if ret == ErrType.DEV_NAND_BAD_BLOCK.value or ret == ErrType.DEV_NAND_WORN_BLOCK.value:
                    self.logger.info(
                        f"{'Bad' if ret == ErrType.DEV_NAND_BAD_BLOCK else 'Worn'} block: 0x{format(addr, '08X')}")
                    addr += self.device_info.flash_block_size()
                    next_erase_addr = addr
                    continue
                elif ret != ErrType.OK:
                    break

                next_erase_addr = addr + self.device_info.flash_block_size()

                i = 0
                while i < pages_per_block:
                    if tx_sum + page_size >= aligned_img_length:
                        is_last_page = True

                    need_sense = (is_last_page or (i == pages_per_block - 1))
                    ret = self.floader_handler.write(image_info.memory_type, data_bytes[tx_sum: tx_sum + page_size],
                                                     page_size, addr, write_timeout, need_sense=need_sense)
                    if ret == ErrType.OK:
                        idx = 0
                        while idx < page_size:
                            checksum += (data_bytes[tx_sum + idx] + (data_bytes[tx_sum + idx + 1] << 8) + (
                                    data_bytes[tx_sum + idx + 2] << 16) + (data_bytes[tx_sum + idx + 3] << 24))
                            idx += 4
                        checksum &= 0xFFFFFFFF
                        addr += page_size
                        tx_sum += page_size
                    else:
                        self.logger.error(f"Write to addr={format(addr, '08x')}, size={page_size} fail: {ret}")
                        break

                    if is_last_page:
                        break

                    i += 1
                progress = int((tx_sum / aligned_img_length) * 100)
                if int((progress) / 10) != progress_int:
                    progress_int += 1
                    self.logger.info(f"Programming progress: {progress}%")  # customized, do not modified

                if ret != ErrType.OK:
                    break

            if ret == ErrType.OK:
                if image_info.full_erase and (next_erase_addr < image_info.end_address):
                    self.logger.debug(
                        f"Erase extra address range: {hex(next_erase_addr)}-{hex(image_info.end_address)}")
                    ret = self.floader_handler.erase_flash(image_info.memory_type, next_erase_addr,
                                                           image_info.end_address,
                                                           (image_info.end_address - next_erase_addr),
                                                           nand_erase_timeout_in_second(
                                                               (image_info.end_address - next_erase_addr), block_size),
                                                           sense=True)
                    if ret == ErrType.DEV_NAND_BAD_BLOCK.value or ret == ErrType.DEV_NAND_WORN_BLOCK.value:
                        self.logger.debug(
                            f"{'Bad' if ret == ErrType.DEV_NAND_BAD_BLOCK else 'Worn'} block: {hex(addr)}")
                        ret = ErrType.OK
                    elif ret != ErrType.OK:
                        self.logger.error(f"Fail to erase block {hex(addr)}:{ret}")

                if tx_sum >= aligned_img_length:
                    if aligned_img_length < 1024:
                        self.logger.debug(f"Image download done: {aligned_img_length}bytes")
                    elif aligned_img_length < 1024 * 1024:
                        self.logger.debug(f"Image download done: {aligned_img_length // 1024}KB")
                    else:
                        self.logger.debug(f"Image download done: {round(aligned_img_length / 1024 / 1024, 2)}MB")
                else:
                    self.logger.warning(f"Image download uncompleted: {tx_sum}/{aligned_img_length}")

                elapse_ms = round((datetime.now() - start_time).total_seconds() * 1000, 0)
                kbps = aligned_img_length * 8 // elapse_ms
                size_kb = aligned_img_length // 1024

                if self.is_usb:
                    self.logger.info(
                        f"{image_info.image_name} download done: {size_kb}KB / {elapse_ms}ms / {kbps / 1000}Mbps")
                else:
                    self.logger.info(f"{image_info.image_name} download done: {size_kb}KB / {elapse_ms}ms / {kbps}Kbps")
        else:
            write_pages = 0
            img = io.BytesIO(img_content)
            img_bytes = img.read(img_length)
            data_bytes = img_bytes.ljust(aligned_img_length, padding_data.to_bytes(1, byteorder="little"))
            progress_int = 0
            while tx_sum < aligned_img_length:
                if write_pages == 0:
                    if (addr % (64 * FlashUtils.NorDefaultPageSize.value)) == 0 and \
                            ((aligned_img_length - tx_sum >= 64 * FlashUtils.NorDefaultPageSize.value)):
                        block_size = 64 * FlashUtils.NorDefaultPageSize.value
                    else:
                        block_size = 4 * FlashUtils.NorDefaultPageSize.value

                    pages_per_block = block_size // page_size
                    erase_addr = addr
                    write_timeout = FlashUtils.NorPageProgramTimeoutInSeconds.value * int(
                        max(self.setting.sense_packet_count, pages_per_block)) + nor_erase_timeout_in_second(
                        divide_then_round_up(block_size, 1024))
                    if erase_addr != last_erase_addr:
                        if (not is_ram) and (erase_addr % block_size) != 0:
                            # error: # customized, do not modify
                            self.logger.error(
                                f"Flash erase address align error: addr {hex(erase_addr)} not aligned to block size {hex(block_size)}")
                            ret = ErrType.SYS_PARAMETER
                            break

                        ret = self.floader_handler.erase_flash(image_info.memory_type, erase_addr,
                                                               erase_addr + block_size, block_size,
                                                               nor_erase_timeout_in_second(
                                                                   divide_then_round_up(block_size, 1024)))
                        if ret != ErrType.OK:
                            break

                        last_erase_addr = erase_addr
                        next_erase_addr = erase_addr + block_size

                need_sense = ((((write_pages + 1) % self.setting.sense_packet_count) == 0) or
                              (write_pages + 1 >= pages_per_block) or
                              (tx_sum + page_size >= aligned_img_length))
                ret = self.floader_handler.write(image_info.memory_type, data_bytes[tx_sum: tx_sum + page_size],
                                                 page_size, addr, write_timeout, need_sense=need_sense)
                if ret != ErrType.OK:
                    self.logger.debug(f"Write to addr={hex(addr)} size={page_size} fail: {ret}")
                    break

                write_pages += 1
                if write_pages >= pages_per_block:
                    write_pages = 0

                idx = 0
                while idx < page_size:
                    checksum += (data_bytes[tx_sum + idx] + (data_bytes[tx_sum + idx + 1] << 8) + (
                            data_bytes[tx_sum + idx + 2] << 16) + (data_bytes[tx_sum + idx + 3] << 24))
                    idx += 4

                checksum &= 0xFFFFFFFF

                addr += page_size
                tx_sum += page_size

                progress = int((tx_sum / aligned_img_length) * 100)
                if int((progress) / 10) != progress_int:
                    progress_int += 1
                    self.logger.info(f"Programming progress: {progress}%")  # customized, do not modified

            if image_info.full_erase and (next_erase_addr < image_info.end_address):
                self.logger.debug(f"Erase extra address range: {hex(next_erase_addr)}-{hex(image_info.end_address)}")
                ret = self.floader_handler.erase_flash(image_info.memory_type, next_erase_addr, image_info.end_address,
                                                       (image_info.end_address - next_erase_addr),
                                                       nor_erase_timeout_in_second(divide_then_round_up(
                                                           (image_info.end_address - next_erase_addr), 1024)),
                                                       sense=True)
                if ret != ErrType.OK:
                    self.logger.warning(
                        f"Fail to extra address range {hex(next_erase_addr)}-{hex(image_info.end_address)}")

            if aligned_img_length < 1024:
                self.logger.debug(f"Image download done: {aligned_img_length}bytes")
            elif aligned_img_length < 1024 * 1024:
                self.logger.debug(f"Image download done: {aligned_img_length // 1024}KB")
            else:
                self.logger.debug(f"Image download done: {round(aligned_img_length / 1024 / 1024, 2)}MB")

            elapse_ms = round((datetime.now() - start_time).total_seconds() * 1000, 0)
            kbps = aligned_img_length * 8 // elapse_ms
            size_kb = aligned_img_length // 1024

            if self.is_usb:
                self.logger.info(
                    f"{image_info.image_name} download done: {size_kb}KB / {elapse_ms}ms / {kbps / 1000}Mbps")
            else:
                self.logger.info(f"{image_info.image_name} download done: {size_kb}KB / {elapse_ms}ms / {kbps}Kbps")

        if ret == ErrType.OK:
            cal_checksum = 0
            if image_info.memory_type == MemoryInfo.MEMORY_TYPE_NAND:
                checksum_timeout = nand_checksum_timeout_in_second(aligned_img_length, None)
            else:
                checksum_timeout = nor_checksum_timeout_in_second(aligned_img_length)
            ret, cal_checksum = self.floader_handler.checksum(image_info.memory_type, image_info.start_address,
                                                              image_info.end_address, aligned_img_length,
                                                              checksum_timeout)
            if ret == ErrType.OK:
                if cal_checksum != checksum:
                    self.logger.debug(f"Checksum fail: expect {hex(checksum)} get {hex(cal_checksum)}")
                    ret = ErrType.SYS_CHECKSUM

        return ret

    def erase_flash(self):
        ret = ErrType.OK

        if self.erase_info.memory_type == MemoryInfo.MEMORY_TYPE_NAND:
            erase_total_size = self.erase_info.end_address - self.erase_info.start_address
            erase_size = 0
            addr = self.erase_info.start_address
            while addr < self.erase_info.end_address:
                ret = self.floader_handler.erase_flash(self.erase_info.memory_type,
                                                       addr,
                                                       addr + self.device_info.flash_block_size(),
                                                       self.device_info.flash_block_size(),
                                                       nand_erase_timeout_in_second(self.device_info.flash_block_size(),
                                                                                    self.device_info.flash_block_size()),
                                                       sense=True)
                if ret == ErrType.OK:
                    erase_size += self.device_info.flash_block_size()
                    # TODO：update erase progress
                    # self.logger.info(f"Erase progress: {int()}")
                    self.logger.debug(
                        f"NAND erase address  ={hex(addr)}, size = {self.device_info.flash_block_size() / 1024}KB OK")
                elif ret == ErrType.DEV_NAND_BAD_BLOCK:
                    self.logger.warning(
                        f"NAND erase address = {hex(addr)} size = {self.device_info.flash_block_size() / 1024}KB skipped: bad block")
                    ret = ErrType.OK
                elif ret == ErrType.DEV_NAND_WORN_BLOCK:
                    self.logger.warning(
                        f"NAND erase address = {hex(addr)} size = {self.device_info.flash_block_size() / 1024}KB failed: mark warning block")
                    ret = ErrType.OK
                else:
                    self.logger.warning(
                        f"NAND erase address = {hex(addr)} size = {self.device_info.flash_block_size() / 1024}KB failed: {ret}")
                    break
                addr += self.device_info.flash_block_size()

            if ret == ErrType.OK:
                self.logger.info(f"Erase nand done")
        elif self.erase_info.memory_type == MemoryInfo.MEMORY_TYPE_NOR:
            if self.setting.erase_by_block != 0:
                addr = self.erase_info.start_address
                size_erased = 0
                while size_erased < self.erase_info.size_in_byte():
                    if ((addr % (64 * FlashUtils.NorDefaultPageSize.value)) == 0) and \
                            ((
                                     self.erase_info.size_in_byte() - size_erased) >= 64 * FlashUtils.NorDefaultPageSize.value):
                        block_size = 64 * FlashUtils.NorDefaultPageSize.value
                    elif ((addr % (32 * FlashUtils.NorDefaultPageSize.value)) == 0) and \
                            ((
                                     self.erase_info.size_in_byte() - size_erased) >= 32 * FlashUtils.NorDefaultPageSize.value):
                        block_size = 32 * FlashUtils.NorDefaultPageSize.value
                    else:
                        block_size = 4 * FlashUtils.NorDefaultPageSize.value

                    need_sense = ((size_erased + block_size) >= self.erase_info.size_in_byte())
                    ret = self.floader_handler.erase_flash(self.erase_info.memory_type, addr, addr + block_size,
                                                           block_size,
                                                           nor_erase_timeout_in_second(
                                                               divide_then_round_up(block_size, 1024)),
                                                           sense=need_sense)
                    if ret != ErrType.OK:
                        break

                    addr += block_size
                    size_erased += block_size
                    # TODO：update erase progress
                    # self.logger.info(f"Erase Progress: {}")
            else:
                ret = self.floader_handler.erase_flash(self.erase_info.memory_type,
                                                       self.erase_info.start_address,
                                                       self.erase_info.end_address,
                                                       self.erase_info.size_in_kbyte,
                                                       nor_erase_timeout_in_second(
                                                           divide_then_round_up(self.erase_info.size_in_byte(), 1024)),
                                                       sense=True)
                if ret == ErrType.OK:
                    self.logger.info(f"Erase nor done")
        elif self.erase_info.memory_type == MemoryInfo.MEMORY_TYPE_RAM:
            # memset 16MB ram, cost time: 0.01s
            ret = self.floader_handler.erase_flash(self.erase_info.memory_type,
                                                   self.erase_info.start_address,
                                                   self.erase_info.end_address,
                                                   self.erase_info.size_in_byte(),
                                                   self.setting.sync_response_timeout_in_second,
                                                   sense=True)
            if ret == ErrType.OK:
                self.logger.info("Erase ram done")
        else:
            # TBD
            pass

        return ret

    def set_spic_address_mode(self, mode):
        ret = ErrType.OK
        is_amebad = self.profile_info.is_amebad()
        otp_spic_addr_mode_addr = OtpSpicAddrModeAddrForAmebaD if is_amebad else OtpSpicAddrModeAddr
        otp_spic_addr_mode_mask = OtpSpicAddrModeMask if is_amebad else OtpSpicAddrModeMask
        otp_spic_addr_mode_pos = OtpSpicAddrModePosForAmebaD if is_amebad else OtpSpicAddrModePos

        if self.device_info.is_boot_from_nand():
            return ret

        ret, buf = self.floader_handler.otp_read_logical_map(0, self.profile_info.logical_efuse_len)
        if ret != ErrType.OK:
            self.logger.error(f"Fail to read eFuse: {ret}")
            return

        cfg = buf[otp_spic_addr_mode_addr]
        if cfg != 0xFF:
            if ((cfg & otp_spic_addr_mode_mask) >> otp_spic_addr_mode_pos) == mode:
                self.logger.info(f"No need to change supported flash size")
                return ErrType.OK

            cfg &= (~otp_spic_addr_mode_mask)
            cfg |= ((mode << otp_spic_addr_mode_pos) & otp_spic_addr_mode_mask)
        else:
            if mode == SpicAddrMode.FOUR_BYTE_MODE.value:
                cfg = OtpSpicAddrMode4Byte
            else:
                self.logger.info(f"No need to change default supported flash size")
                return ErrType.OK

        # buf[otp_spic_addr_mode_addr] = cfg
        buf_array = bytearray(buf)
        buf_array[otp_spic_addr_mode_addr] = cfg
        buf = bytes(buf_array)
        self.logger.info(f"Program eFuse to change supported flash size {'>' if (mode == SpicAddrMode.FOUR_BYTE_MODE.value) else '<='}16MB")

        ret = self.floader_handler.otp_write_logical_map(otp_spic_addr_mode_addr, 1, buf)
        if ret != ErrType.OK:
            self.logger.error(f"Fail to program eFuse[{otp_spic_addr_mode_addr}]: {ret}")
            return ret
        time.sleep(0.01)

        ret, buf = self.floader_handler.otp_read_logical_map(0, self.profile_info.logical_efuse_len)
        if ret != ErrType.OK:
            self.logger.error(f"Fail to read eFuse: {ret}")
            return ret

        cfg = buf[otp_spic_addr_mode_addr]
        if ((cfg & otp_spic_addr_mode_mask) >> otp_spic_addr_mode_pos) == mode:
            self.logger.info(f"Program eFuse done")
            return ErrType.OK
        else:
            self.logger.error(f"Fail to verify eFuse[{otp_spic_addr_mode_addr}]")
            self.logger.error(f"Fail to program eFuse")
            return ErrType.SYS_CHECKSUM

    def get_spic_address_mode(self):
        ret = ErrType.OK
        is_amebad = self.profile_info.is_amebad()
        otp_spic_addr_mode_addr = OtpSpicAddrModeAddrForAmebaD if is_amebad else OtpSpicAddrModeAddr
        otp_spic_addr_mode_mask = OtpSpicAddrModeMask if is_amebad else OtpSpicAddrModeMask
        otp_spic_addr_mode_4byte = OtpSpicAddrMode4ByteForAmebaD if is_amebad else OtpSpicAddrMode4Byte
        mode = SpicAddrMode.THREE_BYTE_MODE.value

        if self.device_info.is_boot_from_nand():
            return ret, mode

        ret, buf = self.floader_handler.otp_read_logical_map(0, self.profile_info.logical_efuse_len)
        if ret != ErrType.OK:
            self.logger.error(f"Fail to read eFuse: {ret}")
            return ret, mode

        cfg = buf[otp_spic_addr_mode_addr]
        if cfg != 0xFF:
            if (cfg & otp_spic_addr_mode_mask) == otp_spic_addr_mode_4byte:
                mode = SpicAddrMode.FOUR_BYTE_MODE.value
                self.logger.info(f"Current supported flash size >16MB")
            else:
                self.logger.info(f"Current supported flash size <=16MB")
        else:
            self.logger.info(f"Current supported flash size <=16MB as default")

        return ret, mode

    def check_supported_flash_size(self, memory_type):
        ret = ErrType.OK
        reset_system = False

        if memory_type == MemoryInfo.MEMORY_TYPE_NAND:
            return ret, reset_system

        self.logger.info(f"Check supported flash size...")
        ret = self.prepare(show_device_info=False)
        if ret != ErrType.OK:
            self.logger.error("Prepare for check supported flash size fail")
            return ret, reset_system

        ret, mode = self.get_spic_address_mode()
        if ret != ErrType.OK:
            return ret, reset_system

        flash_size = self.device_info.flash_capacity // 1024 // 1024  # MB
        if self.setting.auto_program_spic_addr_mode_4byte == 0:
            if (mode == SpicAddrMode.THREE_BYTE_MODE.value) and (flash_size > 16):
                self.logger.warning(f"Flash size: {flash_size}MB, OTP supported flash size <= 16MB")
                ret = ErrType.SYS_CANCEL
            if (mode == SpicAddrMode.FOUR_BYTE_MODE.value) and (flash_size <= 16):
                self.logger.warning(f"Flash size: {flash_size}MB, OTP supported flash size >16MB")
                ret = ErrType.SYS_CANCEL
        else:
            if mode == SpicAddrMode.THREE_BYTE_MODE.value and flash_size > 16:
                self.logger.warning(f"OTP supports flash size <=16MB, tool will program OTP to support flash size >16MB")
                ret = self.set_spic_address_mode(SpicAddrMode.FOUR_BYTE_MODE.value)
                reset_system = True
            elif mode == SpicAddrMode.FOUR_BYTE_MODE.value and flash_size <= 16:
                self.logger.warning(f"OTP supports flash size >16MB, tool will program OTP to support flash size <=16MB")
                ret = self.set_spic_address_mode(SpicAddrMode.THREE_BYTE_MODE.value)
                reset_system = True

            if reset_system:
                boot_delay = self.setting.usb_rom_boot_delay_in_second if self.profile_info.support_usb_download else self.setting.floader_boot_delay_in_second
                # reset floader with next option
                self.floader_handler.next_operation(NextOpType.REBURN, 0)
                self.logger.info(f"Reburn delay {boot_delay}s")
                time.sleep(boot_delay)

        return ret, reset_system
