# Copyright 2023-2024 Arm Limited
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Parse and print header, TLV area and trailer information of a signed image.
"""
import os.path
import struct

import click
import yaml

from imgtool import image

HEADER_ITEMS = ("magic", "load_addr", "hdr_size", "protected_tlv_size",
                "img_size", "flags", "version")
TLV_TYPES = dict((value, key) for key, value in image.TLV_VALUES.items())
BOOT_MAGIC = bytes([
    0x77, 0xc2, 0x95, 0xf3,
    0x60, 0xd2, 0xef, 0x7f,
    0x35, 0x52, 0x50, 0x0f,
    0x2c, 0xb6, 0x79, 0x80, ])
BOOT_MAGIC_2 = bytes([
    0x2d, 0xe1, 0x5d, 0x29,
    0x41, 0x0b, 0x8d, 0x77,
    0x67, 0x9c, 0x11, 0x0f,
    0x1f, 0x8a, ])
BOOT_MAGIC_SIZE = len(BOOT_MAGIC)
_LINE_LENGTH = 60
STATUS = {
    '0x1': 'SET',
    '0x2': 'BAD',
    '0x3': 'UNSET',
    '0x4': 'ANY',
}


def parse_enc(key_field_len):
    if key_field_len is not None:
        return f"(len: {hex(key_field_len)}, if BOOT_SWAP_SAVE_ENCTLV is unset)"
    else:
        return "Image not encrypted"


def parse_size(size_hex):
    if size_hex == '0xffffffff':
        return "unknown"
    return size_hex + " octal: " + str(int(size_hex, 0))


def parse_status(status_hex):
    return (
        f"{STATUS[status_hex]} ({status_hex})"
        if status_hex in STATUS
        else f"INVALID ({status_hex})"
    )


def parse_boot_magic(trailer_magic):
    magic = ""
    for i in range(BOOT_MAGIC_SIZE):
        magic += f"{trailer_magic[i]:#04x} "
        if i == (BOOT_MAGIC_SIZE / 2 - 1):
            magic += ("\n" + "             ")
    return magic


def print_in_frame(header_text, content):
    sepc = " "
    header = "#### " + header_text + sepc
    post_header = "#" * (_LINE_LENGTH - len(header))
    print(header + post_header)

    print("|", sepc * (_LINE_LENGTH - 2), "|", sep="")
    offset = (_LINE_LENGTH - len(content)) // 2
    pre = "|" + (sepc * (offset - 1))
    post = sepc * (_LINE_LENGTH - len(pre) - len(content) - 1) + "|"
    print(pre, content, post, sep="")
    print("|", sepc * (_LINE_LENGTH - 2), "|", sep="")
    print("#" * _LINE_LENGTH)


def print_in_row(row_text):
    row_text = "#### " + row_text + " "
    fill = "#" * (_LINE_LENGTH - len(row_text))
    print(row_text + fill)


def print_tlv_records(tlv_list):
    indent = _LINE_LENGTH // 8
    for tlv in tlv_list:
        print(" " * indent, "-" * 45)
        tlv_type, tlv_length, tlv_data = tlv.keys()

        if tlv[tlv_type] in TLV_TYPES:
            print(" " * indent, f"{tlv_type}: {TLV_TYPES[tlv[tlv_type]]} ({hex(tlv[tlv_type])})")
        else:
            print(" " * indent, "{}: {} ({})".format(
                tlv_type, "UNKNOWN", hex(tlv[tlv_type])))
        print(" " * indent, f"{tlv_length}: ", hex(tlv[tlv_length]))
        print(" " * indent, f"{tlv_data}: ", end="")

        for j, data in enumerate(tlv[tlv_data]):
            print(f"{data:#04x}", end=" ")
            if ((j + 1) % 8 == 0) and ((j + 1) != len(tlv[tlv_data])):
                print("\n", end=" " * (indent + 7))
        print()


def dump_imginfo(imgfile, outfile=None, silent=False):
    """Parse a signed image binary and print/save the available information."""
    trailer_magic = None
    #   set to INVALID by default
    swap_size = 0x99
    swap_info = 0x99
    copy_done = 0x99
    image_ok = 0x99
    trailer = {}
    key_field_len = None

    try:
        with open(imgfile, "rb") as f:
            b = f.read()
    except FileNotFoundError:
        raise click.UsageError(f"Image file not found ({imgfile})") from None

    # Parsing the image header
    _header = struct.unpack('IIHHIIBBHI', b[:28])
    # Image version consists of the last 4 item ('BBHI')
    _version = _header[-4:]
    header = {}
    for i, key in enumerate(HEADER_ITEMS):
        if key == "version":
            header[key] = "{}.{}.{}+{}".format(*_version)
        else:
            header[key] = _header[i]

    # Parsing the TLV area
    tlv_area = {"tlv_hdr_prot": {},
                "tlvs_prot": [],
                "tlv_hdr": {},
                "tlvs": []}
    tlv_off = header["hdr_size"] + header["img_size"]
    protected_tlv_size = header["protected_tlv_size"]

    if protected_tlv_size != 0:
        _tlv_prot_head = struct.unpack(
            'HH',
            b[tlv_off:(tlv_off + image.TLV_INFO_SIZE)])
        tlv_area["tlv_hdr_prot"]["magic"] = _tlv_prot_head[0]
        tlv_area["tlv_hdr_prot"]["tlv_tot"] = _tlv_prot_head[1]
        tlv_end = tlv_off + tlv_area["tlv_hdr_prot"]["tlv_tot"]
        tlv_off += image.TLV_INFO_SIZE

        # Iterating through the protected TLV area
        while tlv_off < tlv_end:
            tlv_type, tlv_len = struct.unpack(
                'HH',
                b[tlv_off:(tlv_off + image.TLV_INFO_SIZE)])
            tlv_off += image.TLV_INFO_SIZE
            tlv_data = b[tlv_off:(tlv_off + tlv_len)]
            tlv_area["tlvs_prot"].append(
                {"type": tlv_type, "len": tlv_len, "data": tlv_data})
            tlv_off += tlv_len

    _tlv_head = struct.unpack('HH', b[tlv_off:(tlv_off + image.TLV_INFO_SIZE)])
    tlv_area["tlv_hdr"]["magic"] = _tlv_head[0]
    tlv_area["tlv_hdr"]["tlv_tot"] = _tlv_head[1]

    tlv_end = tlv_off + tlv_area["tlv_hdr"]["tlv_tot"]
    tlv_off += image.TLV_INFO_SIZE

    # Iterating through the TLV area
    while tlv_off < tlv_end:
        tlv_type, tlv_len = struct.unpack(
            'HH',
            b[tlv_off:(tlv_off + image.TLV_INFO_SIZE)])
        tlv_off += image.TLV_INFO_SIZE
        tlv_data = b[tlv_off:(tlv_off + tlv_len)]
        tlv_area["tlvs"].append(
            {"type": tlv_type, "len": tlv_len, "data": tlv_data})
        tlv_off += tlv_len

    _img_pad_size = len(b) - tlv_end

    if _img_pad_size:
        # Parsing the image trailer
        trailer_off = -BOOT_MAGIC_SIZE
        trailer_magic = b[trailer_off:]
        trailer["magic"] = trailer_magic
        max_align = None
        if trailer_magic == BOOT_MAGIC:
            # The maximum supported write alignment is the default 8 Bytes
            max_align = 8
        elif trailer_magic[-len(BOOT_MAGIC_2):] == BOOT_MAGIC_2:
            # The alignment value is encoded in the magic field
            max_align = int.from_bytes(trailer_magic[:2], "little")
        else:
            # Invalid magic: the rest of the image trailer cannot be processed.
            print("Warning: the trailer magic value is invalid!")

        if max_align is not None:
            if max_align > BOOT_MAGIC_SIZE:
                trailer_off -= max_align - BOOT_MAGIC_SIZE
            # Parsing rest of the trailer fields
            trailer_off -= max_align
            image_ok = b[trailer_off]
            trailer["image_ok"] = image_ok

            trailer_off -= max_align
            copy_done = b[trailer_off]
            trailer["copy_done"] = copy_done

            trailer_off -= max_align
            swap_info = b[trailer_off]
            trailer["swap_info"] = swap_info

            trailer_off -= max_align
            swap_size = int.from_bytes(b[trailer_off:(trailer_off + 4)],
                                       "little")
            trailer["swap_size"] = swap_size

            # Encryption key 0/1
            if ((header["flags"] & image.IMAGE_F["ENCRYPTED_AES128"]) or
                    (header["flags"] & image.IMAGE_F["ENCRYPTED_AES256"])):
                # The image is encrypted
                #    Estimated value of key_field_len is correct if
                #    BOOT_SWAP_SAVE_ENCTLV is unset
                key_field_len = image.align_up(16, max_align) * 2

    # Generating output yaml file
    if outfile is not None:
        imgdata = {"header": header,
                   "tlv_area": tlv_area,
                   "trailer": trailer}
        with open(outfile, "w") as outf:
            # sort_keys - from pyyaml 5.1
            yaml.dump(imgdata, outf, sort_keys=False)

    ###############################################################################

    if silent:
        return

    print("Printing content of signed image:", os.path.basename(imgfile), "\n")

    # Image header
    section_name = "Image header (offset: 0x0)"
    print_in_row(section_name)
    for key, value in header.items():
        if key == "flags":
            if not value:
                flag_string = hex(value)
            else:
                flag_string = ""
                for flag in image.IMAGE_F:
                    if value & image.IMAGE_F[flag]:
                        if flag_string:
                            flag_string += ("\n" + (" " * 20))
                        flag_string += f"{flag} ({hex(image.IMAGE_F[flag])})"
            value = flag_string

        if not isinstance(value, str):
            value = hex(value)
        print(key, ":", " " * (19 - len(key)), value, sep="")
    print("#" * _LINE_LENGTH)

    # Image payload
    _sectionoff = header["hdr_size"]
    frame_header_text = f"Payload (offset: {hex(_sectionoff)})"
    frame_content = "FW image (size: {} Bytes)".format(hex(header["img_size"]))
    print_in_frame(frame_header_text, frame_content)

    # TLV area
    _sectionoff += header["img_size"]
    if protected_tlv_size != 0:
        # Protected TLV area
        section_name = f"Protected TLV area (offset: {hex(_sectionoff)})"
        print_in_row(section_name)
        print("magic:    ", hex(tlv_area["tlv_hdr_prot"]["magic"]))
        print("area size:", hex(tlv_area["tlv_hdr_prot"]["tlv_tot"]))
        print_tlv_records(tlv_area["tlvs_prot"])
        print("#" * _LINE_LENGTH)

    _sectionoff += protected_tlv_size
    section_name = f"TLV area (offset: {hex(_sectionoff)})"
    print_in_row(section_name)
    print("magic:    ", hex(tlv_area["tlv_hdr"]["magic"]))
    print("area size:", hex(tlv_area["tlv_hdr"]["tlv_tot"]))
    print_tlv_records(tlv_area["tlvs"])
    print("#" * _LINE_LENGTH)

    if _img_pad_size:
        _sectionoff += tlv_area["tlv_hdr"]["tlv_tot"]
        _erased_val = b[_sectionoff]
        frame_header_text = f"Image padding (offset: {hex(_sectionoff)})"
        frame_content = f"padding ({hex(_erased_val)})"
        print_in_frame(frame_header_text, frame_content)

        # Image trailer
        section_name = "Image trailer (offset: unknown)"
        print_in_row(section_name)
        notice = "(Note: some fields may not be used, depending on the update strategy)\n"
        notice = '\n'.join(notice[i:i + _LINE_LENGTH] for i in range(0, len(notice), _LINE_LENGTH))
        print(notice)
        print("swap status: (len: unknown)")
        print("enc. keys:  ", parse_enc(key_field_len))
        print("swap size:  ", parse_size(hex(swap_size)))
        print("swap_info:  ", parse_status(hex(swap_info)))
        print("copy_done:  ", parse_status(hex(copy_done)))
        print("image_ok:   ", parse_status(hex(image_ok)))
        print("boot magic: ", parse_boot_magic(trailer_magic))
        print()

    footer = "End of Image "
    print_in_row(footer)
