#!/usr/bin/python

import sys
sys.path.append('/home/root/SeaCloudScripts')
import logging
import lib_system
from subprocess import CalledProcessError
import ctypes
import os
from time import sleep
import lib_uboot
import lib_logger
import lib_localUI

logger = logging.getLogger()

# Return values of the function get_list_rl78_to_update
NO_PENDING_UPDATES = 0
PENDING_UPDATES = 1

# RL78 firmware versions lower than 1.22 can loose master member file (#22) on fw update
# A mechanism to read this file before the update and write it again after the update has been implemented
MIN_VERSION_TO_SKIP_BACKUP_CONFIG = 1.22


def to_float(string):
    '''
    Converts a string to float
    @string: the string to convert
    Returns the value as float or -1.0 in case of error
    '''
    try:
        float_value = round(float(string),3)
    except ValueError:
        float_value = -1.0
    return float_value


def get_rl78_firmware_file_path_from_folder(folder, rl78_binfile_name):
    '''
    Gets the full path of the requested rl78 firmware binary file from a given folder
    '''

    file_to_search = rl78_binfile_name + ".bin"
    logger.info("file_to_search={0}".format(file_to_search))

    try:
        (result, output) = lib_system.call_shell(["/bin/sh","-c", "cd " + folder + " && ls -1 -r " + file_to_search], False)
        if result != 0:
            logger.info("binary file {0} not found in folder {1}".format(file_to_search,folder))
            return ""
        else:
            logger.info("binary file {0} found in folder {1}".format(file_to_search,folder))

    except CalledProcessError as e:
        logger.info("exception for binary file {0} search in folder {1}".format(file_to_search,folder))
        return ""

    return folder + "/" + output


def read_RL78_fw_version_from_file(firmware_file_name):
    '''
    Returns the version read as a float value
    '''

    (_, fw_version_from_file) = lib_system.call_shell(["/bin/sh","-c", "/usr/local/bin/RL78_update -i " + firmware_file_name + " 2>/dev/null | grep Version"])
    fw_version_from_file = fw_version_from_file.split("= ")[1]
    return to_float(fw_version_from_file)


def update_updatable_rl78s(firmware_file_name_mcb, firmware_file_name_seacloud, with_local_ui=False):
    '''
    Update all the connected RLs that need to be updated
    @with_local_ui: indicate if we manage the local UI during the update or not

    '''

    if firmware_file_name_mcb != "":
        # skip version check for mcb
        # -> force version to highest possible
        RL78_version_from_file = 99.9
    else:
        # get list of RLs to update with the firmware file
        if firmware_file_name_seacloud != "":
            RL78_version_from_file = read_RL78_fw_version_from_file(firmware_file_name_seacloud)
        else:
            logger.error("yyx844 and yyx815 file names are empty")
            return False
        if RL78_version_from_file == -1.0:
            return False

    (result, list_rl78s) = get_list_rl78_to_update(RL78_version_from_file)

    success = True

    if result == NO_PENDING_UPDATES:
        logger.info("All RL78s contains version embedded in OS ({}) or higher".format(RL78_version_from_file))

    elif result == PENDING_UPDATES:

        if with_local_ui:
            lib_localUI.init_local_UI(lib_localUI.UI_MODE_UPDATE)

        # update the RL78
        success = update_rl78s(list_rl78s, firmware_file_name_mcb, firmware_file_name_seacloud)

        if with_local_ui:
            lib_localUI.shut_down_local_UI()

    return success


def update_rl78s(list_rl78_to_update, firmware_file_name_mcb, firmware_file_name_seacloud):
    '''
    Update the list of RL78s with a given FW.
    @list_rl78_to_update:         list of RL indexes to be updated.
    @firmware_file_name_mcb:      full path of mcb RL78 binary YYX844.bin.
    @firmware_file_name_seacloud: full path of Seacloud RL78 binary YYX815.bin
    Returns True if everything went well, False in case of error
    '''

    if firmware_file_name_seacloud == "" and firmware_file_name_mcb != "":
        # in case of MCB update and no seacloud rl78 firmware available
        # limit rl78 to update list to index 0
        list_rl78_to_update=[ 0 ]

    success = True
    for rl78_index in list_rl78_to_update:
        if rl78_index == 0 and firmware_file_name_mcb != "":
            # update RL78-0 with MCB firmware
            RL78_version_read_from_file = read_RL78_fw_version_from_file(firmware_file_name_mcb)
            logger.info("update RL78-#0 with MCB/YYX844 firmware V{0}".format(RL78_version_read_from_file))
            firmware_file_name = firmware_file_name_mcb
        else:
            # update RL78-0/1...5 with Seacloud firmware
            RL78_version_read_from_file = read_RL78_fw_version_from_file(firmware_file_name_seacloud)
            logger.info("update RL78-#{0} with Seacloud/YYX815 firmware V{1}".format(rl78_index, RL78_version_read_from_file))
            firmware_file_name = firmware_file_name_seacloud

        # First try to update the RL78 (each try is made of 3 attempts)
        if not update_rl78_firmware(rl78_index,firmware_file_name):
            # Reset RLs
            logger.warning("First try to update RL78, failed, reset them and try again to update")
            reset_rl78s()

            # let time to RLs to start
            sleep(5)

            # Second try to update the RL78 (each try is made of 3 attempts)
            if not update_rl78_firmware(rl78_index, firmware_file_name):
                logger.error("Unable to update rl78 #{}".format(rl78_index))
                success = False

    return success


def read_write_file22(rl78_index, operation=None):
    '''
    Reads/writes file #22 from/to the I/O module associated to rl78_index
    IMPORTANT: No return value is used. On error, an exception will be generated, on success no exception is generated.
               The consumer of this function must handle the exception properly.
    @rl78_index: index of rl78 associated to the module
    @operation: "read" to read, "write" to write
    '''
    tmp_file_name = "/tmp/file22_rl78_{}".format(rl78_index)
    if operation == "read":
        op = "r"
    elif operation == "write":
        op = "w"
    else:
        return

    # On error, exception is raised
    # Read or write file
    lib_system.call_shell(["/bin/sh","-c", "/usr/local/bin/RL78_update -d " + str(rl78_index) + " -" + op +" 22 -f " + tmp_file_name])


def update_rl78_firmware(rl78_index, firmware_file):
    '''
    Updates the RL78 controller
    @rl78_index: index of the RL78 to update (0..6)
    @firmware_file: firmware file (.bin format)
    Warning: we try to backup/restore the file #22 for old firmware version, but if it fails we will update the firmware anyway. There is a risk to loose settings in that case, but it is better than an update failure.
    Returns True on successfuly update, otherwise False.
    '''
    success = False
    nb_retries = 3

    for retries in range(1,nb_retries + 1):
        try:
            # Check if we need to backup and restore the configuration (file #22, master member file)
            restore_configuration = False
            if (rl78_index != 0):
                read_fw_version = read_rl78_fw_version(rl78_index)
                if read_fw_version != -1.0 and read_fw_version < MIN_VERSION_TO_SKIP_BACKUP_CONFIG:
                    logger.info("rl78 #{}: OLD_VERSION (<{}) -> NEEDS backup of file#22: read actual file".format(rl78_index,MIN_VERSION_TO_SKIP_BACKUP_CONFIG))
                    read_write_file22(rl78_index,"read")
                    restore_configuration = True
                    break
        except CalledProcessError:
            logger.exception("Fail to backup file #22 of RL78 #{0}".format(rl78_index))

    for retries in range(1,nb_retries + 1):
        try:
            # Update the RL78
            logger.info("Update RL78 #{} with {}, try {}/{}".format(str(rl78_index), firmware_file, retries, nb_retries))
            # On error, exception is raised
            lib_system.call_shell(["/bin/sh","-c", "/usr/local/bin/RL78_update -d" + str(rl78_index) + " -u " + firmware_file + " 2>/dev/null"])
            sleep(5)
            success = True
            break
        except CalledProcessError:
            logger.exception("Fail to update firmware of RL78 #{0}".format(rl78_index))

    # Restore configuration (file #22, master member file)
    if restore_configuration:
        for retries in range(1,nb_retries + 1):
            try:
                logger.info("rl78 #{}: OLD_VERSION (<{}) -> Restore previously backuped file#22".format(rl78_index,MIN_VERSION_TO_SKIP_BACKUP_CONFIG))
                read_write_file22(rl78_index,"write")
                break
            except CalledProcessError:
                logger.exception("Fail to restore file #22 of RL78 #{0}".format(rl78_index))

    return success


def get_list_rl78_to_update(fw_version_for_update):
    '''
    Get the list of RL78 to update with a given firmware version
    @fw_version_for_update: the RL78 fw version with the one we want to update RL78s
    Returns a tuple (result, rl78s_to_update):
    result:
               NO_PENDING_UPDATES (all RL78 up to date)
               PENDING_UPDATES (some RL78 must be updated)
    rl78s_to_update: a list with the indexes (0..6) of the RL78 that need to be updated
    '''
    rl78s_to_update = [ ]
    result = NO_PENDING_UPDATES
    for rl78_index in range (0,6):
        is_in_FUM_mode = is_rl78_in_FUM_mode(rl78_index)

        if is_in_FUM_mode:
            logger.info("rl78 #{}: FUM_MODE, RL appli is down -> NEEDS update".format(rl78_index))
            result = PENDING_UPDATES
            rl78s_to_update.append(rl78_index)
        else:
            read_fw_version = read_rl78_fw_version(rl78_index)
            if read_fw_version == -1.0:
                logger.info("rl78 #{}: ABSENT, RL is not here, NO update".format(rl78_index))
            elif read_fw_version < fw_version_for_update:
                logger.info("rl78 #{}: OLD_VERSION -> NEEDS update from version {} to version {}".format(rl78_index,read_fw_version,fw_version_for_update))
                result = PENDING_UPDATES
                rl78s_to_update.append(rl78_index)
            else:
                logger.info("rl78 #{}: UP-TO-DATE -> NO update".format(rl78_index))

    return (result, rl78s_to_update)


def is_rl78_in_FUM_mode(rl78_index):
    '''
    Returns True if the RL whose index is given in parameter is in FUM mode, False otherwise
    '''
    try:
        (_, output) = lib_system.call_shell(["/bin/sh","-c", "/usr/local/bin/RL78_update -m -d" + str(rl78_index) + " 2>/dev/null"], False)

        return output.find("FUM mode") != -1
    except CalledProcessError as e:
        return False



def read_rl78_fw_version(rl78_index):
    '''
    Reads the firmware version from the RL78 controller
    @rl78_index: index of the RL78 to read (0..6)
    Returns a tuple (success, fw_version):
    success: True/False
    fw_version: firmware version as float value
    '''
    try:
        (_, fw_version) = lib_system.call_shell(["/bin/sh","-c", "/usr/local/bin/RL78_update -v -d" + str(rl78_index) + " 2>/dev/null | grep Version"])
        fw_version = fw_version.split("= ")[1]
        return to_float(fw_version)
    except CalledProcessError as e:
        return -1.0


def release_rl78s():
    '''
    Release the RL78 processors. Keep other process running to update LocalUI.
    '''
    try:
        # Disable HW watchdog because we will stop registered clients (BDEngineII and DevCommProcessor)
        # Ignore errors because the script is only added with package 43 (before hw watchdog is not enabled)
        lib_system.call_shell(["/bin/sh","-c", "/home/root/SeaCloudScripts/config_watchdog_hw.sh disable"], False)
        # Stop BDEngineII to avoid auto restart if a RL is not properly working
        lib_system.call_shell(["killall", "BDEngineII"], False)
        # Stop DevCommProcessor to release RL78
        lib_system.call_shell(["killall", "DevCommProcessor"], False)

        logger.info("RL78 processors released")

        # wait 7 second to let the RL with no communication for 6 seconds, so that we guarantee that
        # the outputs will be set on hold or will output the transfer value during the RL78 update
        sleep(7)

    except CalledProcessError as e:
        logger.warning("Unable to release RL78 processors, continue anyway")


def reset_rl78s():

    logger.info("Reset RLs")

    CARRIER_BOARD_SYS_FILE = "/sys/class/leds/Reset_CB/brightness"  # To drive the reset signal of the carrier board
    MAIN_BOARD_SYS_FILE = "/sys/class/leds/Reset_RL78/brightness"   # To drive the reset signal of the main board
    lib_system.call_shell(["/bin/sh","-c", "[ -f {} ] && echo 0 > {}".format(CARRIER_BOARD_SYS_FILE, CARRIER_BOARD_SYS_FILE)], False)
    lib_system.call_shell(["/bin/sh","-c", "[ -f {} ] && echo 0 > {}".format(MAIN_BOARD_SYS_FILE, MAIN_BOARD_SYS_FILE)], False)
    sleep (1)
    lib_system.call_shell(["/bin/sh","-c", "[ -f {} ] && echo 255 > {}".format(CARRIER_BOARD_SYS_FILE, CARRIER_BOARD_SYS_FILE)], False)
    lib_system.call_shell(["/bin/sh","-c", "[ -f {} ] && echo 255 > {}".format(MAIN_BOARD_SYS_FILE, MAIN_BOARD_SYS_FILE)], False)


