"""
/******************************************************************************

 @file toad_image_tool.py

 @brief This tool generates the Turbo OAD (delta) image files

 Group: LPRF SW RND
 $Target Device: DEVICES $

 ******************************************************************************
 $License: BSD3 2019 $
 ******************************************************************************
 $Release Name: PACKAGE NAME $
 $Release Date: PACKAGE RELEASE DATE $
 *****************************************************************************/
"""

from __future__ import print_function
import argparse
import textwrap
import os
import sys
import struct
import ntpath
import tempfile
import json
from collections import namedtuple

import delta_util

# -----------------------------------------------------------------------------
#                             Conditional Module Setup
# -----------------------------------------------------------------------------
SDK_PRODUCT_CC13X2_CC26X2 = "cc13x2_cc26x2"
SDK_PRODUCT_CC13X0 = "cc13x0"
SDK_PRODUCT = SDK_PRODUCT_CC13X0

if SDK_PRODUCT == SDK_PRODUCT_CC13X2_CC26X2:
    # Get SDK path to import OAD image tool modules
    SDK_DIR = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])),
                                            "..", "..", ".."))
    OAD_TOOLS_DIR = os.path.join(SDK_DIR, "tools", "common", "oad")

    sys.path.append(OAD_TOOLS_DIR)
    import computeCRC32
    import signUtil
    import imgBinUtil as util
elif SDK_PRODUCT == SDK_PRODUCT_CC13X0:
    import crcmod
    import product_util_13x0 as product

    # CRC Poly used by OAD for CC13x0
    crc16 = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0x0000, xorOut=0x0000)

# -----------------------------------------------------------------------------
#                                   Constants
# -----------------------------------------------------------------------------

__version__ = "1.1"
__prog__ = "toad_image_tool"

# Python 2 only supported for 13x0 SDK
PYTHON_VERSION = sys.version_info.major

MEMORY_CFG_OFFCHIP = "offchip"
MEMORY_CFG_ONCHIP = "onchip"
WORD_LEN_IN_BYTES = 4

# Product Specific Constants

# 13x2/26x2
SEG_TYPE_DELTA = 5
IMG_PAYLOAD_LEN = 12
DELTA_IMG_MEMORY_CFG_OFFCHIP = 0x01
DELTA_IMG_MEMORY_CFG_ONCHIP = 0x02

PRODUCT_CC13X2_CC26X2_DELTA_SEG_LEN = 0x14
PRODUCT_CC13X2_CC26X2_FIXED_IMG_HEADER_FORMAT = "<8sIBBHBBBBIIIIIHH"
PRODUCT_CC13X2_CC26X2_DELTA_SEG_FORMAT = "<BHBIBBBBII"
PRODUCT_CC13X2_CC26X2_FIXED_IMG_HEADER_FIELDS = """imgID crc32 bimVer metaVer techType imgCpStat
                                                   crcStat imgType imgNo imgVld len prgEntry softVer
                                                   imgEndAddr hdrLen rfu2"""
PRODUCT_CC13X2_CC26X2_DELTA_SEG_FIELDS = """segType wirelessTech rfuSeg payloadLen isDeltaImg
                                            toadMetaVer toadVer memoryCfg oldImgCrc newImgLen"""

if SDK_PRODUCT == SDK_PRODUCT_CC13X2_CC26X2:
    FixedImgHdr = namedtuple("FixedImgHdr", PRODUCT_CC13X2_CC26X2_FIXED_IMG_HEADER_FIELDS)
    DeltaSegHdr = namedtuple("DeltaSegHdr", PRODUCT_CC13X2_CC26X2_DELTA_SEG_FIELDS)
elif SDK_PRODUCT == SDK_PRODUCT_CC13X0:
    FixedImgHdr = namedtuple("FixedImgHdr", product.FIXED_IMG_HEADER_FIELDS)
    DeltaSegHdr = namedtuple("DeltaSegHdr", product.DELTA_SEG_FIELDS)


# -----------------------------------------------------------------------------
#                                   Functions
# -----------------------------------------------------------------------------


def print_console_header():
    """Prints the program entry header"""

    print("************************************************")
    print("TI Turbo OAD Image Tool")
    print("Version: " + __version__)
    print("************************************************")


def print_args_info(old_img_path, new_img_path, delta_path, memory_cfg):
    """Prints out the main arguments of the program"""

    print("OAD Configuration: " + memory_cfg)
    print("Old Image: " + ntpath.basename(old_img_path))
    print("New Image: " + ntpath.basename(new_img_path))
    print("Output Delta Image: " + ntpath.basename(delta_path))
    print("************************************************")
    print("Runtime Output:")


def print_comp_savings(original_len, compressed_len):
    """Prints out the size savings of the delta image"""

    savings = (1.0 - float(compressed_len)/original_len) * 100.0

    print()
    print("New Image Length:   {:>9,} {:>5}".format(original_len, "bytes"))
    print("Delta Image Length: {:>9,} {:>5}".format(compressed_len, "bytes"))
    print("                   ================")
    print("Savings:  {:.4}%".format(savings))
    print()
    print("************************************************")


def print_delta_header(path):
    """
    This function prints out information contained in an delta image header

    :param path: Path to the image file
    :return:
    """

    try:
        with open(path, "rb") as file:
            image = bytearray(file.read())

    except (OSError, IOError) as e:
        # Skip verbose printing if error
        return

    if SDK_PRODUCT == SDK_PRODUCT_CC13X2_CC26X2:
        delta_seg_offset = util.getSegAddr(path, SEG_TYPE_DELTA)
        delta_header = DeltaSegHdr._make(struct.unpack(PRODUCT_CC13X2_CC26X2_DELTA_SEG_FORMAT,
                                                        image[delta_seg_offset:(delta_seg_offset + PRODUCT_CC13X2_CC26X2_DELTA_SEG_LEN)]))

        hdr = delta_header._asdict()
        # Checks the the wireless tech type
        tech_value = hdr["wirelessTech"]
        hdr["wirelessTech"] = ''
        if tech_value == 0:
            hdr["wirelessTech"] = "No Wireless Technology"
        if tech_value & 1 == 0:
            hdr["wirelessTech"] += "[BLE]"
        if tech_value & 2 == 0:
            hdr["wirelessTech"] += "[TIMAC Sub1G]"
        if tech_value & 4 == 0:
            hdr["wirelessTech"] += "[TIMAC 2.4G]"
        if tech_value & 8 == 0:
            hdr["wirelessTech"] += "[Zigbee]"
        if tech_value & 16 == 0:
            hdr["wirelessTech"] += "[RF4CE]"
        if tech_value & 32 == 0:
            hdr["wirelessTech"] += "[Thread]"
        if tech_value & 64 == 0:
            hdr["wirelessTech"] += "[EasyLink]"

        hdr["memoryCfg"] = "Off-Chip" if hdr["memoryCfg"] == DELTA_IMG_MEMORY_CFG_OFFCHIP else "On-Chip"

        print(textwrap.dedent("""
        ____________________________
                 Delta HDR
        ____________________________
        Field            |      Value
        Segment Type     |      Delta Segment
        Wireless Tech    |      {wirelessTech}
        Segment Length   |      0x{payloadLen:X}
        isDeltaImg       |      {isDeltaImg}
        toadMetaVer      |      {toadMetaVer}
        toadVer          |      {toadVer}
        memoryCfg        |      {memoryCfg}
        oldImgCrc        |      0x{oldImgCrc:X}
        newImgLen        |      0x{newImgLen:X}
            """.format(**hdr)))

    elif SDK_PRODUCT == SDK_PRODUCT_CC13X0:
        delta_seg_offset = product.find_header_offset(image) + product.DELTA_SEG_OFFSET
        delta_header = DeltaSegHdr._make(struct.unpack(product.DELTA_SEG_FORMAT,
                                                        image[delta_seg_offset:(delta_seg_offset + product.DELTA_SEG_LEN)]))

        hdr = delta_header._asdict()
        print(textwrap.dedent("""
        ____________________________
                 Delta HDR
        ____________________________
        Field            |      Value
        deltaImgID       |      {deltaImgID}
        toadMetaVer      |      {toadMetaVer}
        toadVer          |      {toadVer}
        oldImgCrc        |      0x{oldImgCrc:X}
        newImgLen        |      0x{newImgLen:X}
        oldImgLen*       |      0x{oldImgLen:X} (*Padding Removed)
            """.format(**hdr)))


def write_delta_image(old_header, new_header, new_img, delta_img):
    """
    Creates the OAD header segments for a Turbo OAD delta image

    :param old_header: byte array of the old image header
    :param new_header: byte array of the new image header
    :param new_img: byte array of the new image
    :param delta_img: byte array containing the delta of the current and new application images
    :return:
    """
    # OAD binaries are word aligned
    if SDK_PRODUCT == SDK_PRODUCT_CC13X0:
        # Delta segment does not already exist like in 13x2/26x2
        new_header += bytearray(product.DELTA_SEG_LEN)

        # Bytes before header when TIRTOS_IN_ROM are not delta compressed if enabled
        total_len = product.find_header_offset(new_img) + len(new_header) + len(delta_img)
    else:
        total_len = len(new_header) + len(delta_img)

    remaining_bytes = 0 if total_len % 4 == 0 else 4 - (total_len % 4)
    pad_buffer = bytearray([0xFF for x in range(remaining_bytes)])
    total_len += len(pad_buffer)

    # Set product specific variables for delta image generation
    if SDK_PRODUCT == SDK_PRODUCT_CC13X2_CC26X2:
        fixed_img_hdr_format = PRODUCT_CC13X2_CC26X2_FIXED_IMG_HEADER_FORMAT
        fixed_img_hdr_len = util.FIXED_HDR_LEN

        delta_seg_format = PRODUCT_CC13X2_CC26X2_DELTA_SEG_FORMAT
        delta_seg_len = PRODUCT_CC13X2_CC26X2_DELTA_SEG_LEN
    elif SDK_PRODUCT == SDK_PRODUCT_CC13X0:
        total_len = int(total_len / 4)
        fixed_img_hdr_format = product.FIXED_IMG_HEADER_FORMAT
        fixed_img_hdr_len = product.FIXED_IMG_HEADER_LEN

        delta_seg_format = product.DELTA_SEG_FORMAT
        delta_seg_len = product.DELTA_SEG_LEN

    if SDK_PRODUCT == SDK_PRODUCT_CC13X2_CC26X2:
        # A temporary file needs to be created since the OAD image tool operates
        # off of file paths for adding signature and CRC
        temp_new_img = tempfile.NamedTemporaryFile(delete=False)
        temp_new_img.write(new_img)
        temp_new_img.close()

    with open(vargs.output, "w+b") as outFile:
        new_img_fixed_hdr = FixedImgHdr._make(struct.unpack(fixed_img_hdr_format, new_header[:fixed_img_hdr_len]))

        # Calculate CRC fields for oldImgCrc validation on initiating OAD transfer
        # old_img_data_crc used for validating delta image is compatible with old image (excludes OAD header)
        # new_img_data_crc used for OAD header CRC calculation done on OAD completion
        if SDK_PRODUCT == SDK_PRODUCT_CC13X2_CC26X2:
            old_img_data_crc = computeCRC32.crc32_withOffset(vargs.old_img, len(old_header))
            old_img_data_crc = int(old_img_data_crc, 16)
            new_img_data_crc = computeCRC32.crc32_withOffset(vargs.new_img, len(new_header))
            new_img_data_crc = int(new_img_data_crc, 16)

            # Delta information segment
            delta_offset = util.getSegAddr(temp_new_img.name, SEG_TYPE_DELTA)
            delta_seg_header = DeltaSegHdr._make(struct.unpack(delta_seg_format,
                                                               new_header[delta_offset:(delta_offset + delta_seg_len)]))
            delta_seg_header = delta_seg_header._replace(isDeltaImg=True, oldImgCrc=new_img_data_crc,
                                                         newImgLen=new_img_fixed_hdr.len)

            # Update header with new signature and CRC after adding delta
            with open(temp_new_img.name, "w+b") as f:
                updated_new_img = bytearray(new_img)
                updated_new_img = updated_new_img.replace(updated_new_img[delta_offset:(delta_offset + delta_seg_len)],
                                                          struct.pack(delta_seg_format, *delta_seg_header), 1)
                f.write(updated_new_img)

            if util.isSecure(temp_new_img.name):
                if vargs.key_file:
                    signUtil.signImage(temp_new_img.name, vargs.key_file)
                else:
                    raise Exception("Key file not specified for secure image!")

            computeCRC32.computeCRC32(temp_new_img.name)

            # Update header for delta image
            with open(temp_new_img.name, "rb") as f:
                new_header = f.read(len(new_header))

            new_img_fixed_hdr = FixedImgHdr._make(struct.unpack(fixed_img_hdr_format, new_header[:fixed_img_hdr_len]))

            delta_seg_header = DeltaSegHdr._make(struct.unpack(delta_seg_format,
                                                               new_header[delta_offset:(delta_offset + delta_seg_len)]))
            delta_seg_header = delta_seg_header._replace(oldImgCrc=old_img_data_crc)

        elif SDK_PRODUCT == SDK_PRODUCT_CC13X0:
            delta_offset = product.DELTA_SEG_OFFSET
            old_img_data = open(vargs.old_img, "r+b").read()
            header_offset = product.find_header_offset(old_img_data)

            # NV pages (flash >= FLASH_NV_BASE) must be excluded from:
            #   - oldImgCrc: runtime NV writes (after network join) would
            #     otherwise change the running image CRC vs the binary CRC
            #   - newImgLen/oldImgLen: sensor CRC check and BIM copy length
            #     must not cover NV so NV is preserved across OAD updates
            #   - new image crc field: checkDL() verifies this CRC over the
            #     same app-only range written to external flash
            nv_offset_bin = product.FLASH_NV_BASE - product.FLASH_APP_BASE
            bim_hdr_offset = header_offset  # 0x4F0 for TIRTOS_IN_ROM, 0 otherwise

            if PYTHON_VERSION == 2:
                # Lengths in 4-byte words, app code only (NV excluded)
                old_img_len = nv_offset_bin / WORD_LEN_IN_BYTES
                new_img_len = nv_offset_bin / WORD_LEN_IN_BYTES

                # CRC over old app code only (start after OAD header, end before NV)
                old_img_data_crc = crc16(old_img_data[(header_offset + len(old_header))
                                                      :nv_offset_bin])

                # CRC over new app code only, skipping 4 CRC bytes at bim_hdr_offset.
                # The len field at (bim_hdr_offset + 6) will be overwritten to
                # new_img_len by OADStorage_imgBlockWrite before writing to external
                # flash, so the stored crc must be computed with that modified value.
                img_len_off = bim_hdr_offset + 6  # IMG_LEN_OFFSET = OAD_IMG_HDR_OSET(4) + offsetof(len)(2)
                new_img_for_crc = bytearray(new_img[:nv_offset_bin])
                new_img_for_crc[img_len_off]     = new_img_len & 0xFF
                new_img_for_crc[img_len_off + 1] = (new_img_len >> 8) & 0xFF
                new_img_crc_data = (bytes(new_img_for_crc[:bim_hdr_offset]) +
                                    bytes(new_img_for_crc[bim_hdr_offset + 4:nv_offset_bin]))
                new_img_crc = crc16(new_img_crc_data)

                delta_seg_header = DeltaSegHdr(deltaImgID=product.DELTA_SEG_DELTA_IMG_ID,
                                               toadMetaVer=product.DELTA_SEG_DELTA_META_VER,
                                               toadVer=product.DELTA_SEG_DELTA_TOAD_VER,
                                               oldImgCrc=old_img_data_crc,
                                               newImgLen=new_img_len,
                                               oldImgLen=old_img_len)
            else:
                # Lengths in 4-byte words, app code only (NV excluded)
                old_img_len = nv_offset_bin // WORD_LEN_IN_BYTES
                new_img_len = nv_offset_bin // WORD_LEN_IN_BYTES

                # CRC over old app code only (start after OAD header, end before NV)
                old_img_data_crc = crc16(old_img_data[(header_offset + len(old_header))
                                                      :nv_offset_bin])

                # CRC over new app code only, skipping 4 CRC bytes at bim_hdr_offset.
                # The len field at (bim_hdr_offset + 6) will be overwritten to
                # new_img_len by OADStorage_imgBlockWrite before writing to external
                # flash, so the stored crc must be computed with that modified value.
                img_len_off = bim_hdr_offset + 6  # IMG_LEN_OFFSET = OAD_IMG_HDR_OSET(4) + offsetof(len)(2)
                new_img_for_crc = bytearray(new_img[:nv_offset_bin])
                new_img_for_crc[img_len_off]     = new_img_len & 0xFF
                new_img_for_crc[img_len_off + 1] = (new_img_len >> 8) & 0xFF
                new_img_crc_data = (bytes(new_img_for_crc[:bim_hdr_offset]) +
                                    bytes(new_img_for_crc[bim_hdr_offset + 4:nv_offset_bin]))
                new_img_crc = crc16(new_img_crc_data)

                delta_seg_header = DeltaSegHdr(deltaImgID=bytearray(product.DELTA_SEG_DELTA_IMG_ID.encode()),
                                               toadMetaVer=product.DELTA_SEG_DELTA_META_VER,
                                               toadVer=product.DELTA_SEG_DELTA_TOAD_VER,
                                               oldImgCrc=old_img_data_crc,
                                               newImgLen=new_img_len,
                                               oldImgLen=old_img_len)

            # Update the crc field in the new image header with the app-only CRC
            # (computed with modified len field, matching what ext flash will contain)
            new_img_fixed_hdr = new_img_fixed_hdr._replace(crc=new_img_crc)

        # Update delta image with length of the delta image instead of the full image
        new_img_fixed_hdr = new_img_fixed_hdr._replace(len=total_len)

        delta_header = bytearray(new_header)
        delta_header = delta_header.replace(new_header[:fixed_img_hdr_len],
                                            struct.pack(fixed_img_hdr_format, *new_img_fixed_hdr), 1)
        delta_header = delta_header.replace(new_header[delta_offset:(delta_offset + delta_seg_len)],
                                            struct.pack(delta_seg_format, *delta_seg_header), 1)

        # Bytes before header when TIRTOS_IN_ROM are not delta compressed if enabled
        if SDK_PRODUCT == SDK_PRODUCT_CC13X0:
            if product.find_header_offset(new_img) != 0x0:
                outFile.write(new_img[:product.find_header_offset(new_img)])

        outFile.write(delta_header)
        outFile.write(delta_img)
        outFile.write(pad_buffer)

        if SDK_PRODUCT == SDK_PRODUCT_CC13X2_CC26X2:
            os.remove(temp_new_img.name)


def open_binary(path):
    """
    Reads an OAD image from the specified path

    :param path: Path to the image file
    :return: A tuple consisting of the OAD image header and the OAD image data
    """

    try:
        with open(path, "rb") as file:
            file.seek(0x00, os.SEEK_SET)
            image = bytearray(file.read())

            # Read header bytearray, dependent on product SDK
            if SDK_PRODUCT == SDK_PRODUCT_CC13X2_CC26X2:
                header_len = util.getOverheadSize(path) + IMG_PAYLOAD_LEN
                header_offset = 0x0
            elif SDK_PRODUCT == SDK_PRODUCT_CC13X0:
                header_len = product.FIXED_IMG_HEADER_LEN
                header_offset = product.find_header_offset(image)

            file.seek(header_offset, os.SEEK_SET)
            header = bytearray(file.read(header_len))

    except (OSError, IOError) as e:
        print("Fatal Error: -- {:s}. Exiting.".format(e.strerror))
        sys.exit(1)

    return (header, image)


def parse_json_args(args):
    """
    Parses a JSON file and adds additional arguments to the arguments dictionary

    :param args: Arguments dictionary
    :return:
    """

    try:
        with open(args.json, "rb") as file:
            if sys.version_info.minor < 6:
                json_content = file.read()
                json_args = json.loads(json_content.decode("utf-8"))
            else:
                json_args = json.load(file)
            args.__dict__.update(json_args)

    except (OSError, IOError) as e:
        print("Fatal Error: -- {:s}. Exiting.".format(e.strerror))
        sys.exit(1)


def parse_cmdline_args():
    """Parses command line arguments"""

    parser = argparse.ArgumentParser(
        prog=__prog__,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description=textwrap.dedent('''
          Generates a Turbo OAD delta image from two OAD images
        '''),
        epilog=textwrap.dedent('''
          Example:
            %(prog)s -oimg app_v1.bin -nimg app_v2.bin -o app_v2.dim -m offchip
        '''))

    parser.add_argument("-oimg", "--old_img",
                        help="Path to an OAD binary of the old image running on the device")
    parser.add_argument("-nimg", "--new_img", required=True,
                        help="Path to an OAD binary of the new image")
    parser.add_argument("-o", "--output", required=True,
                        help="Output path to write the delta image")
    parser.add_argument("-j", "--json", help="Path to json file containing program arguments")
    parser.add_argument("-k", "--key_file", nargs="?",
                        help="Provide the location of the file containing your private key")
    parser.add_argument("-m", "--memory_cfg", choices=[MEMORY_CFG_OFFCHIP, MEMORY_CFG_ONCHIP],
                        help="OAD memory configuration (offchip/onchip)")
    parser.add_argument("-v", "--version", action="version", version="%(prog)s " + __version__)
    parser.add_argument("-verbose", "--verbose", action="store_true")

    return parser.parse_args()


if __name__ == "__main__":
    vargs = parse_cmdline_args()

    # 13x0 does not support on-chip OAD, input required argument for the user if not specified
    if SDK_PRODUCT == SDK_PRODUCT_CC13X0 and (vargs.memory_cfg is None or vargs.memory_cfg == ""):
        vargs.memory_cfg = "offchip"

    # Load additional arguments if passed a JSON file
    if not(vargs.json is None or vargs.json == ""):
        parse_json_args(vargs)

        # exit early if turbo oad is not enabled
        if not vargs.enabled:
            # exit silently
            sys.exit(0)

    # Ensure required arguments are passed in either via cmd line or json file:
    if (vargs.old_img is None or vargs.old_img == "") or (vargs.memory_cfg is None or vargs.memory_cfg == ""):
        if not(vargs.json is None or vargs.json == ""):
            print("Info: Path to old image is not specified. Delta image not created.")
            sys.exit(0)
        else:
            if SDK_PRODUCT == SDK_PRODUCT_CC13X0:
                print("Error: --old_img must be specified")
            else:
                print("Error: --memory_cfg and --old_img must be specified")
            sys.exit(1)

    # Prevent creation of on-chip images
    if vargs.memory_cfg == MEMORY_CFG_ONCHIP:
        print("Error: On-chip memory configuration is not supported")
        sys.exit(1)

    # first, print a neat header
    print_console_header()

    # Open image binaries and store data for delta encoding
    old_img_header, old_img = open_binary(vargs.old_img)
    new_img_header, new_img = open_binary(vargs.new_img)

    # Display input file and output file name info
    print_args_info(vargs.old_img, vargs.new_img, vargs.output, vargs.memory_cfg)

    # OAD headers and NV pages are excluded from the delta image.
    # NV exclusion ensures the delta decoder only touches app code, so
    # runtime NV contents (written after network join) are preserved and
    # BIM copies only the app pages (newImgLen words) leaving NV intact.
    if SDK_PRODUCT == SDK_PRODUCT_CC13X0:
        nv_offset_bin = product.FLASH_NV_BASE - product.FLASH_APP_BASE
        delta_img = delta_util.create_delta(
            old_img[product.find_header_offset(old_img) + len(old_img_header):nv_offset_bin],
            new_img[product.find_header_offset(new_img) + len(new_img_header):nv_offset_bin])
    else:
        delta_img = delta_util.create_delta(old_img[len(old_img_header):],
                                            new_img[len(new_img_header):])

    write_delta_image(old_img_header, new_img_header, new_img, delta_img)

    if vargs.verbose:
        print_delta_header(vargs.output)

    # Print out statistics
    delta_img_size = os.path.getsize(vargs.output)
    print_comp_savings(len(new_img), delta_img_size)

    # Print out warning if it is not advantages to perform a delta update
    savings = (1.0 - float(delta_img_size) / len(new_img)) * 100.0
    if savings < 25.0:
        print("Warning: Recommended to use regular OAD since size savings " +
              "is less than 25% of the new image size")
