#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
# Copyright 2022 NXP
#
# SPDX-License-Identifier: BSD-3-Clause

"""NXP MCU Image tool."""
import logging
import os
import sys
from binascii import unhexlify
from typing import List

import click

from spsdk.apps.utils.common_cli_options import CommandsTreeGroup, spsdk_apps_common_options
from spsdk.apps.utils.utils import INT, SPSDKAppError, catch_spsdk_error, get_key, store_key
from spsdk.exceptions import SPSDKError
from spsdk.image import SB3_SCH_FILE, TrustZone, get_mbi_class
from spsdk.image.ahab.ahab_container import AHABImage
from spsdk.image.bee import BeeNxp
from spsdk.image.bootable_image.bimg import BootableImage, get_bimg_class
from spsdk.image.fcb.fcb import FCB
from spsdk.image.keystore import KeyStore
from spsdk.image.mbimg import mbi_generate_config_templates, mbi_get_supported_families
from spsdk.sbfile.sb2.commands import CmdLoad
from spsdk.sbfile.sb2.images import BootImageV21, generate_SB21
from spsdk.sbfile.sb31.images import SecureBinary31
from spsdk.utils.crypto.cert_blocks import CertBlockV2
from spsdk.utils.crypto.otfad import OtfadNxp
from spsdk.utils.images import BinaryImage, BinaryPattern
from spsdk.utils.misc import (
    get_abs_path,
    load_binary,
    load_configuration,
    load_text,
    value_to_int,
    write_file,
)
from spsdk.utils.schema_validator import ConfigTemplate, ValidationSchemas, check_config

logger = logging.getLogger(__name__)


@click.group(name="nxpimage", no_args_is_help=True, cls=CommandsTreeGroup)
@spsdk_apps_common_options
def main(log_level: int) -> None:
    """NXP Image tool.

    Manage various kinds of images for NXP parts.
    It's successor of obsolete ELFTOSB tool.
    """
    logging.basicConfig(level=log_level or logging.WARNING)


@main.group(no_args_is_help=True)
def mbi() -> None:  # pylint: disable=unused-argument
    """Group of sub-commands related to Master Boot Images."""


@mbi.command(name="export", no_args_is_help=True)
@click.argument("config", type=click.Path(exists=True, readable=True))
def mbi_export(config: str) -> None:
    """Generate Master Boot Image from YAML/JSON configuration.

    CONFIG is path to the YAML/JSON configuration

    The configuration template files could be generated by subcommand 'get-templates'.
    """
    config_data = load_configuration(config)
    config_dir = os.path.dirname(config)
    mbi_cls = get_mbi_class(config_data)
    check_config(config_data, mbi_cls.get_validation_schemas(), search_paths=[config_dir])
    mbi_obj = mbi_cls()
    mbi_obj.load_from_config(config_data, search_paths=[config_dir])
    mbi_data = mbi_obj.export()

    mbi_output_file_path = get_abs_path(config_data["masterBootOutputFile"], config_dir)
    write_file(mbi_data, mbi_output_file_path, mode="wb")

    click.echo(f"Success. (Master Boot Image: {mbi_output_file_path} created.)")


@mbi.command(name="get-templates", no_args_is_help=True)
@click.option(
    "-f",
    "--chip-family",
    default="lpc55s3x",
    type=click.Choice(mbi_get_supported_families(), case_sensitive=False),
    required=True,
    help="Select the chip family.",
)
@click.option(
    "-o",
    "--overwrite",
    default=False,
    type=bool,
    is_flag=True,
    help="Allow overwriting existing template file.",
)
@click.argument("output", type=click.Path())
def mbi_get_templates(chip_family: str, overwrite: bool, output: str) -> None:
    """Create template of MBI configurations in YAML format.

    The path for template files is specified as argument of this command.
    """
    if not os.path.exists(output):
        os.makedirs(output)
    templates = mbi_generate_config_templates(chip_family)
    for file_name, template in templates.items():
        full_file_name = os.path.join(output, file_name + ".yml")
        if not os.path.isfile(full_file_name) or overwrite:
            click.echo(f"Creating {full_file_name} template file.")
            write_file(template, full_file_name)
        else:
            click.echo(f"Skip creating {full_file_name}, this file already exists.")


@main.group(no_args_is_help=True)
def sb21() -> None:  # pylint: disable=unused-argument
    """Group of sub-commands related to Secure Binary 2.1."""


@sb21.command(name="export", no_args_is_help=True)
@click.option(
    "-c",
    "--command",
    type=click.Path(exists=True),
    help="BD configuration file to produce secure binary v2.x",
)
@click.option("-o", "--output", type=click.Path(), help="Output file path.")
@click.option(
    "-k", "--key", type=click.Path(exists=True), help="Add a key file and enable encryption."
)
@click.option("-s", "--pkey", type=click.Path(exists=True), help="Path to private key for signing.")
@click.option(
    "-S",
    "--cert",
    type=click.Path(exists=True),
    multiple=True,
    help="Path to certificate files for signing. The first certificate will be \
the self signed root key certificate.",
)
@click.option(
    "-R",
    "--root-key-cert",
    type=click.Path(exists=True),
    multiple=True,
    help="Path to root key certificate file(s) for verifying other certificates. \
Only 4 root key certificates are allowed, others are ignored. \
One of the certificates must match the first certificate passed \
with -S/--cert arg.",
)
@click.option(
    "-h",
    "--hash-of-hashes",
    type=click.Path(),
    help="Path to output hash of hashes of root keys. If argument is not \
provided, then by default the tool creates hash.bin in the working directory.",
)
@click.argument("external", type=click.Path(), nargs=-1)
def sb21_export(
    command: str,
    output: str,
    key: str,
    pkey: str,
    cert: List[str],
    root_key_cert: List[str],
    hash_of_hashes: str,
    external: List[str],
) -> None:
    """Generate Secure Binary v2.1 Image from configuration.

    EXTERNAL is a space separated list of external binary files defined in BD file
    """
    if output is None:
        raise SPSDKAppError("Error: no output file was specified")
    try:
        sb2_data = generate_SB21(
            bd_file_path=command,
            key_file_path=key,
            private_key_file_path=pkey,
            signing_certificate_file_paths=cert,
            root_key_certificate_paths=root_key_cert,
            hoh_out_path=hash_of_hashes,
            external_files=external,
        )
        write_file(sb2_data, output, mode="wb")
    except SPSDKError as exc:
        raise SPSDKAppError(f"The SB2.1 file generation failed: ({str(exc)}).") from exc
    else:
        click.echo(f"Success. (Secure binary 2.1: {output} created.)")


@sb21.command(name="parse")
@click.option(
    "-b",
    "--binary",
    type=click.Path(exists=True, readable=True),
    required=True,
    help="Path to the SB2 container that would be parsed.",
)
@click.option(
    "-k",
    "--key",
    type=click.Path(exists=True, readable=True),
    required=True,
    help="Key file for SB2 decryption in plaintext",
)
@click.argument("parsed-data", type=click.Path())
def sb21_parse(binary: str, key: str, parsed_data: str) -> None:
    """Parse Secure Binary v2.1 Image."""
    if not os.path.exists(parsed_data):
        os.makedirs(parsed_data, exist_ok=True)

    # transform text-based KEK into bytes
    sb_kek = unhexlify(load_text(key))

    try:
        parsed_sb = BootImageV21.parse(data=load_binary(binary), kek=sb_kek)
    except SPSDKError as exc:
        logger.debug(f"SB21 parse: Attempt to parse image failed: {str(exc)}")

    if isinstance(parsed_sb.cert_block, CertBlockV2):
        for cert_idx, certificate in enumerate(parsed_sb.cert_block.certificates):
            file_name = f"certificate_{cert_idx}_der.cer"
            logger.debug(f"Dumping certificate {file_name}")
            write_file(certificate.dump(), os.path.join(parsed_data, file_name), mode="wb")

    for section_idx, boot_sections in enumerate(parsed_sb._boot_sections):
        for command_idx, command in enumerate(boot_sections._commands):
            if isinstance(command, CmdLoad):
                file_name = f"section_{section_idx}_load_command_{command_idx}_data.bin"
                logger.debug(f"Dumping load command data {file_name}")
                write_file(command.data, os.path.join(parsed_data, file_name), mode="wb")

    logger.debug(parsed_sb.info())
    write_file(
        parsed_sb.info(),
        os.path.join(parsed_data, "parsed_info.txt"),
    )
    click.echo(f"Success. (SB21: {binary} has been parsed and stored into {parsed_data}.)")
    click.echo(
        "Please note that the exported binary images from load command might contain padding"
    )


@sb21.command(name="get-sbkek", no_args_is_help=False)
@click.option(
    "-k",
    "--master-key",
    type=str,
    help="AES-256 master key as hexadecimal string or path to file containing key in plain text or in binary",
)
@click.option(
    "-o",
    "--output-folder",
    type=click.Path(),
    help="Output folder where the sbkek.txt and sbkek.bin will be stored",
)
def get_sbkek(master_key: str, output_folder: str) -> None:
    """Compute SBKEK (AES-256) value and optionally store it as plain text and as binary.

    SBKEK is AES-256 symmetric key used for encryption and decryption of SB.
    Plain text version is used for SB generation.
    Binary format is to be written to the keystore.
    The same format is also used for USERKEK.

    For OTP, the SBKEK is derived from OTP master key:
    SB2_KEK = AES256(OTP_MASTER_KEY,
    03000000_00000000_00000000_00000000_04000000_00000000_00000000_00000000)

    Master key is not needed when using PUF as key storage

    The computed SBKEK is shown as hexadecimal text on STDOUT,
    SBKEK is stored in plain text and in binary if the 'output-folder' is specified,
    """
    otp_master_key = get_key(master_key, KeyStore.OTP_MASTER_KEY_SIZE)
    sbkek = KeyStore.derive_sb_kek_key(otp_master_key)

    click.echo(f"SBKEK: {sbkek.hex()}")
    click.echo(f"(OTP) MASTER KEY: {otp_master_key.hex()}")

    if output_folder:
        store_key(os.path.join(output_folder, "sbkek"), sbkek, reverse=True)
        store_key(os.path.join(output_folder, "otp_master_key"), otp_master_key)
        click.echo(f"Keys have been stored to: {output_folder}")


@main.group()
def sb31() -> None:  # pylint: disable=unused-argument
    """Group of sub-commands related to Secure Binary 3.1."""


@sb31.command(name="export", no_args_is_help=True)
@click.argument("config", type=click.Path(exists=True, readable=True))
def sb31_export(config: str) -> None:
    """Generate Secure Binary v3.1 Image from YAML/JSON configuration.

    CONFIG is path to the YAML/JSON configuration

    The configuration template files could be generated by subcommand 'get-template'.
    """
    config_data = load_configuration(config)
    config_dir = os.path.dirname(config)
    schemas = SecureBinary31.get_validation_schemas(include_test_configuration=True)
    schemas.append(ValidationSchemas.get_schema_file(SB3_SCH_FILE)["sb3_output"])
    check_config(config_data, schemas, search_paths=[config_dir])
    sb3 = SecureBinary31.load_from_config(config_data, search_paths=[config_dir])
    sb3_data = sb3.export()

    sb3_output_file_path = get_abs_path(config_data["containerOutputFile"], config_dir)
    write_file(sb3_data, sb3_output_file_path, mode="wb")

    click.echo(f"Success. (Secure binary 3.1: {sb3_output_file_path} created.)")


@sb31.command(name="get-template", no_args_is_help=True)
@click.option(
    "-f",
    "--chip-family",
    default="lpc55s3x",
    type=click.Choice(SecureBinary31.get_supported_families(), case_sensitive=False),
    required=True,
    help="Select the chip family.",
)
@click.option(
    "-o",
    "--overwrite",
    default=False,
    type=bool,
    is_flag=True,
    help="Allow overwriting existing template file.",
)
@click.argument("output", type=click.Path())
def sb31_get_template(chip_family: str, overwrite: bool, output: str) -> None:
    """Create template of configuration in YAML format.

    The template file name is specified as argument of this command.
    """
    if not os.path.isfile(output) or overwrite:
        click.echo(f"Creating {output} template file.")
        write_file(
            SecureBinary31.generate_config_template(chip_family)[f"{chip_family}_sb31"], output
        )
    else:
        click.echo(f"Skip creating {output}, this file already exists.")


@main.group(no_args_is_help=True)
def tz() -> None:  # pylint: disable=unused-argument
    """Group of sub-commands related to Trust Zone."""


@tz.command(name="export", no_args_is_help=True)
@click.argument("config", type=click.Path(exists=True, readable=True))
def tz_export(config: str) -> None:
    """Generate TrustZone Image from YAML/JSON configuration.

    CONFIG is path to the YAML/JSON configuration

    The configuration template files could be generated by subcommand 'get-template'.
    """
    config_data = load_configuration(config)
    config_dir = os.path.dirname(config)
    check_config(config_data, TrustZone.get_validation_schemas_family())
    check_config(
        config_data,
        TrustZone.get_validation_schemas(config_data["family"]),
        search_paths=[config_dir],
    )
    trust_zone = TrustZone.from_config(config_data)
    tz_data = trust_zone.export()
    output_file = get_abs_path(config_data["tzpOutputFile"], config_dir)
    write_file(tz_data, output_file, mode="wb")
    click.echo(f"Success. (Trust Zone binary: {output_file} created.)")


@tz.command(name="get-template", no_args_is_help=True)
@click.option(
    "-f",
    "--chip-family",
    default="lpc55s3x",
    type=click.Choice(TrustZone.get_supported_families(), case_sensitive=False),
    required=True,
    help="Select the chip family.",
)
@click.option(
    "-r",
    "--revision",
    type=str,
    default="latest",
    help="Chip revision; if not specified, most recent one will be used",
)
@click.option(
    "-o",
    "--overwrite",
    default=False,
    type=bool,
    is_flag=True,
    help="Allow overwriting existing template file.",
)
@click.argument("output", type=click.Path())
def tz_get_template(chip_family: str, revision: str, overwrite: bool, output: str) -> None:
    """Create template of configuration in YAML format.

    The template file name is specified as argument of this command.
    """
    if not os.path.isfile(output) or overwrite:
        click.echo(f"Creating {output} template file.")
        write_file(
            TrustZone.generate_config_template(chip_family, revision)[f"{chip_family}_tz"], output
        )
    else:
        click.echo(f"Skip creating {output}, this file already exists.")


@main.group(no_args_is_help=True)
def ahab() -> None:  # pylint: disable=unused-argument
    """Group of sub-commands related to AHAB."""


@ahab.command(name="export", no_args_is_help=True)
@click.argument("config", type=click.Path(exists=True, readable=True))
def ahab_export(config: str) -> None:
    """Generate AHAB Image from YAML/JSON configuration.

    CONFIG is path to the YAML/JSON configuration

    The configuration template files could be generated by subcommand 'get-template'.
    """
    config_data = load_configuration(config)
    config_dir = os.path.dirname(config)
    schemas = AHABImage.get_validation_schemas()
    check_config(config_data, schemas, search_paths=[config_dir])
    ahab_cnt = AHABImage.load_from_config(config_data, search_paths=[config_dir])
    ahab_data = ahab_cnt.export()

    ahab_output_file_path = get_abs_path(config_data["output"], config_dir)
    write_file(ahab_data, ahab_output_file_path, mode="wb")

    logger.info(f"Created AHAB Image:\n{ahab_cnt.image_info().info()}")
    logger.info(f"Created AHAB Image memory map:\n{ahab_cnt.image_info().draw()}")
    click.echo(f"Success. (AHAB: {ahab_output_file_path} created.)")

    ahab_output_dir, ahab_output_file = os.path.split(ahab_output_file_path)
    ahab_output_file_no_ext, _ = os.path.splitext(ahab_output_file)
    for cnt_ix, container in enumerate(ahab_cnt.ahab_containers):
        srk_table = container.signature_block.srk_table
        file_name = f"{ahab_output_file_no_ext}_{container.flag_srk_set}{cnt_ix}_srk_hash"
        if srk_table:
            srkh = srk_table.compute_srk_hash()
            write_file(srkh.hex().upper(), get_abs_path(f"{file_name}.txt", ahab_output_dir))
            try:
                blhost_script = ahab_cnt.create_srk_hash_blhost_script(cnt_ix)
                write_file(blhost_script, get_abs_path(f"{file_name}_blhost.bcf", ahab_output_dir))
            except SPSDKError:
                pass
            click.echo(f"Generated SRK hash files ({file_name}*.*).")


@ahab.command(name="parse", no_args_is_help=True)
@click.option(
    "-f",
    "--chip-family",
    default="rt1180",
    type=click.Choice(AHABImage.get_supported_families(), case_sensitive=False),
    required=True,
    help="Select the chip family.",
)
@click.option(
    "-t",
    "--image-type",
    type=click.Choice(AHABImage.IMAGE_TYPES),
    help="AHAB image type., default is XIP.",
)
@click.option(
    "-b",
    "--binary",
    type=click.Path(exists=True, readable=True),
    required=True,
    help="Path to binary AHAB image to parse.",
)
@click.argument("parsed-data", type=click.Path())
def ahab_parse(chip_family: str, image_type: str, binary: str, parsed_data: str) -> None:
    """Parse AHAB Image into YAML configuration and binary images."""
    if not os.path.exists(parsed_data):
        os.makedirs(parsed_data, exist_ok=True)
    image_types = [image_type] if image_type else AHABImage.IMAGE_TYPES
    for img_type in image_types:
        try:
            ahab_image = AHABImage(family=chip_family, image_type=img_type)
            ahab_image.parse(load_binary(binary))
            ahab_image.update_fields()
            ahab_image.validate()
        except SPSDKError as exc:
            logger.debug(f"AHAB parse: Attempt to parse image as {img_type} failed: {str(exc)}")
            ahab_image = None
        else:
            assert ahab_image
            if (
                ahab_image.get_first_data_image_address() < ahab_image.data_images_start
                and img_type != "serial_downloader"
            ):
                ahab_image = None
            else:
                break

    if not ahab_image:
        click.echo(f"Failed. (AHAB: {binary} parsing failed.)")
        return

    logger.info(f"Identified AHAB image type: {img_type}")
    logger.info(f"Parsed AHAB image memory map: {ahab_image.image_info().draw()}")
    config = ahab_image.create_config(parsed_data)
    write_file(
        ConfigTemplate.convert_cm_to_yaml(config),
        os.path.join(parsed_data, "parsed_config.yaml"),
    )
    click.echo(f"Success. (AHAB: {binary} has been parsed and stored into {parsed_data}.)")

    for cnt_ix, container in enumerate(ahab_image.ahab_containers):
        srk_table = container.signature_block.srk_table
        file_name = os.path.join(parsed_data, f"{container.flag_srk_set}{cnt_ix}_srk_hash")
        if srk_table:
            srkh = srk_table.compute_srk_hash()
            write_file(srkh.hex().upper(), f"{file_name}.txt")
            try:
                blhost_script = ahab_image.create_srk_hash_blhost_script(cnt_ix)
                write_file(blhost_script, f"{file_name}_blhost.bcf")
            except SPSDKError:
                pass
            click.echo(f"Generated SRK hash files ({file_name}*.*).")


@ahab.command(name="get-template", no_args_is_help=True)
@click.option(
    "-f",
    "--chip-family",
    default="rt1180",
    type=click.Choice(AHABImage.get_supported_families(), case_sensitive=False),
    required=True,
    help="Select the chip family.",
)
@click.option(
    "-o",
    "--overwrite",
    default=False,
    type=bool,
    is_flag=True,
    help="Allow overwriting existing template file.",
)
@click.argument("output", type=click.Path())
def ahab_get_template(chip_family: str, overwrite: bool, output: str) -> None:
    """Create template of configuration in YAML format.

    The template file name is specified as argument of this command.
    """
    if not os.path.isfile(output) or overwrite:
        click.echo(f"Creating {output} template file.")
        write_file(AHABImage.generate_config_template(chip_family)[f"{chip_family}_ahab"], output)
    else:
        click.echo(f"Skip creating {output}, this file already exists.")


@main.group(no_args_is_help=True)
def otfad() -> None:  # pylint: disable=unused-argument
    """Group of sub-commands related to OTFAD."""


@otfad.command(name="export", no_args_is_help=True)
@click.option(
    "-a",
    "--alignment",
    default="512",
    type=INT(),
    help="Alignment of key blob data blocks to simplify align data to external memory blocks.",
)
@click.option(
    "-i",
    "--index",
    type=INT(),
    help=(
        "OTFAD peripheral index - This is needed to generate proper "
        "indexes of fuses in optional BLHOST script. For example for RT1180."
    ),
)
@click.argument("config", type=click.Path(exists=True, readable=True))
def otfad_export(alignment: int, config: str, index: int = None) -> None:
    """Generate OTFAD Images from YAML/JSON configuration.

    CONFIG is path to the YAML/JSON configuration

    The configuration template files could be generated by subcommand 'get-template'.
    """
    config_data = load_configuration(config)
    config_dir = os.path.dirname(config)
    check_config(config_data, OtfadNxp.get_validation_schemas_family(), search_paths=[config_dir])
    family = config_data["family"]
    schemas = OtfadNxp.get_validation_schemas(family)
    check_config(config_data, schemas, search_paths=[config_dir])
    otfad = OtfadNxp.load_from_config(config_data, search_paths=[config_dir])
    binary_image = otfad.binary_image(data_alignment=alignment)
    logger.info(f" The OTFAD image structure:\n{binary_image.draw()}")

    output_folder = get_abs_path(config_data["output_folder"], config_dir)

    otfad_all = os.path.join(output_folder, "otfad_whole_image.bin")
    write_file(binary_image.export(), otfad_all, mode="wb")
    logger.info(f"Created OTFAD Image:\n{otfad_all}")
    sb21_supported = otfad.database.get_device_value("sb_21_supported", otfad.family, default=False)
    memory_map = (
        "In folder is stored two kind of files:\n"
        "  -  Binary file that contains whole image data including "
        "OTFAD table and key blobs data 'otfad_whole_image.bin'.\n"
    )
    if sb21_supported:
        memory_map += "  -  Example of BD file to simplify creating the SB2.1 file from the OTFAD source files.\n"
        bd_file_sources = "sources {"
        bd_file_section0 = "section (0) {"

    memory_map += (
        "  -  Set of separated binary files, one with OTFAD table, and one for each used key blob.\n"
        f"\nOTFAD memory map:\n{binary_image.draw(no_color=True)}"
    )

    for i, image in enumerate(binary_image.sub_images):
        image_file = os.path.join(output_folder, f"{image.name}.bin")
        write_file(image.export(), image_file, mode="wb")
        logger.info(f"Created OTFAD Image:\n{image_file}")
        memory_map += f"\n{image_file}:\n{image.info()}"
        if sb21_supported:
            bd_file_sources += f'\n    image{i} = "{image_file}";'
            bd_file_section0 += f"\n    // Load Image: {image.name}"
            bd_file_section0 += f"\n    erase {hex(image.absolute_address)}..{hex(image.absolute_address+len(image))};"
            bd_file_section0 += f"\n    load image{i} > {hex(image.absolute_address)}"

    readme_file = os.path.join(output_folder, "readme.txt")

    write_file(memory_map, readme_file)
    logger.info(f"Created OTFAD readme file:\n{readme_file}")

    if sb21_supported:
        bd_file_name = os.path.join(output_folder, "sb21_otfad_example.bd")
        bd_file_sources += "\n}\n"
        bd_file_section0 += "\n}\n"
        bd_file = (
            "options {\n"
            "    flags = 0x8; // for sb2.1 use only 0x8 encrypted + signed\n"
            "    buildNumber = 0x1;\n"
            '    productVersion = "1.00.00";\n'
            '    componentVersion = "1.00.00";\n'
            '    secureBinaryVersion = "2.1";\n'
            "}\n"
        )
        bd_file += bd_file_sources
        bd_file += bd_file_section0

        write_file(bd_file, bd_file_name)
        logger.info(f"Created OTFAD BD file example:\n{bd_file_name}")

    if otfad.database.get_device_value("has_kek_fuses", otfad.family, default=False) and index:
        blhost_script = None
        blhost_script = otfad.get_blhost_script_otp_kek(index)
        if blhost_script:
            blhost_script_filename = os.path.join(
                output_folder, f"otfad{index}_{otfad.family}_blhost.bcf"
            )
            write_file(blhost_script, blhost_script_filename)
            click.echo(f"Created OTFAD BLHOST load fuses script:\n{blhost_script_filename}")

    click.echo(f"Success. OTFAD files has been created and stored into: {output_folder}")


@otfad.command(name="get-kek", no_args_is_help=False)
@click.option(
    "-m",
    "--otp-master-key",
    type=str,
    help="OTP MASTER KEY in hexadecimal format or file name to binary or text file with key.",
)
@click.option(
    "-k",
    "--otfad-key",
    type=str,
    help="OTFAD KEY (OTFAD KEK seed) in hexadecimal format or file name to binary or text file with key.",
)
@click.option(
    "-f",
    "--chip-family",
    type=click.Choice(OtfadNxp.get_supported_families()),
    help=(
        "Optional family, if specified, the tool generates the BLHOST scripts to load key fuses."
        " To use this feature, the '-o' options has to be also defined!"
    ),
)
@click.option(
    "-o",
    "--output-folder",
    type=click.Path(),
    help="Optional result output folder (otfad_kek.bin/txt, optionally BLHOST scripts to load keys into Fuses)",
)
def otfad_get_kek(
    otp_master_key: str, otfad_key: str, chip_family: str, output_folder: str
) -> None:
    """Compute OTFAD KEK value and optionally store it into folder in various formats.

    The computed OTFAD KEK is shown in hexadecimal text, if the 'output-folder' is specified,
    it is stored in folder also in binary format.
    """
    omk = get_key(otp_master_key, KeyStore.OTP_MASTER_KEY_SIZE)
    ok = get_key(otfad_key, KeyStore.OTFAD_KEY_SIZE)

    otfad_kek = KeyStore.derive_otfad_kek_key(omk, ok)

    click.echo(f"OTP MASTER KEY: {omk.hex()}")
    click.echo(f"OTFAD KEY:      {ok.hex()}")
    click.echo(f"OTFAD KEK:      {otfad_kek.hex()}")

    blhost_script = None
    if chip_family and chip_family in OtfadNxp.get_supported_families():
        blhost_script = OtfadNxp.get_blhost_script_otp_keys(
            chip_family, otp_master_key=omk, otfad_key_seed=ok
        )
        if not output_folder:
            click.echo(f"OTFAD BLHOST load fuses script:\n{blhost_script}")

    if output_folder:
        store_key(os.path.join(output_folder, "otp_master_key"), omk)
        store_key(os.path.join(output_folder, "otfad_key"), ok)
        store_key(os.path.join(output_folder, "otfad_kek"), otfad_kek)
        if blhost_script:
            write_file(blhost_script, os.path.join(output_folder, "otfad_otp_fuses.bcf"))
        click.echo(f"Result files has been stored into: {output_folder}")


@otfad.command(name="get-template", no_args_is_help=True)
@click.option(
    "-f",
    "--chip-family",
    default="rt1180",
    type=click.Choice(OtfadNxp.get_supported_families(), case_sensitive=False),
    required=True,
    help="Select the chip family.",
)
@click.option(
    "-o",
    "--overwrite",
    default=False,
    type=bool,
    is_flag=True,
    help="Allow overwriting existing template file.",
)
@click.argument("output", type=click.Path())
def otfad_get_template(chip_family: str, overwrite: bool, output: str) -> None:
    """Create template of configuration in YAML format.

    The template file name is specified as argument of this command.
    """
    if not os.path.isfile(output) or overwrite:
        click.echo(f"Creating {output} template file.")
        write_file(OtfadNxp.generate_config_template(chip_family)[f"{chip_family}_otfad"], output)
    else:
        click.echo(f"Skip creating {output}, this file already exists.")


@main.group(no_args_is_help=True)
def bee() -> None:  # pylint: disable=unused-argument
    """Group of sub-commands related to BEE."""


@bee.command(name="export", no_args_is_help=True)
@click.argument("config", type=click.Path(exists=True, readable=True))
def bee_export(config: str) -> None:
    """Generate BEE Images from YAML/JSON configuration.

    CONFIG is path to the YAML/JSON configuration

    The configuration template files could be generated by subcommand 'get-template'.
    """
    config_data = load_configuration(config)
    config_dir = os.path.dirname(config)
    schemas = BeeNxp.get_validation_schemas()
    check_config(config_data, schemas, search_paths=[config_dir])
    bee = BeeNxp.load_from_config(config_data, search_paths=[config_dir])

    output_folder = get_abs_path(config_data["output_folder"], config_dir)

    output_name = config_data.get("output_name")
    if not output_name:
        output_name = "encrypted.bin"

    bee_image = os.path.join(output_folder, output_name)
    write_file(bee.export_image(), bee_image, mode="wb")
    logger.info(f"Created BEE Image:\n{bee_image}")

    header_name = config_data.get("header_name")
    if not header_name:
        header_name = "bee_ehdr.bin"

    for idx, header in enumerate(bee.export_headers()):
        header_output = os.path.join(output_folder, f"{idx}_" + header_name)
        write_file(header, header_output, mode="wb")
        logger.info(f"Created BEE Header:\n{header_output}")

    click.echo(f"Success. BEE files have been created and stored into: {output_folder}")


@bee.command(name="get-template", no_args_is_help=True)
@click.option(
    "-f",
    "--chip-family",
    default="rt1010",
    type=click.Choice(BeeNxp.get_supported_families(), case_sensitive=False),
    required=True,
    help="Select the chip family.",
)
@click.option(
    "-o",
    "--overwrite",
    default=False,
    type=bool,
    is_flag=True,
    help="Allow overwriting existing template file.",
)
@click.argument("output", type=click.Path())
def bee_get_template(chip_family: str, overwrite: bool, output: str) -> None:
    """Create template of configuration in YAML format.

    The template file name is specified as argument of this command.
    """
    if not os.path.isfile(output) or overwrite:
        click.echo(f"Creating {output} template file.")
        write_file(BeeNxp.generate_config_template(), output)
    else:
        click.echo(f"Skip creating {output}, this file already exists.")


@main.group(no_args_is_help=True)
def bootable_image() -> None:  # pylint: disable=unused-argument
    """Group of bootable image utilities."""


@bootable_image.command(name="merge", no_args_is_help=True)
@click.option(
    "-c",
    "--config",
    type=click.Path(exists=True, readable=True),
    required=True,
    help="Bootable Image YAML/JSON Configuration.",
)
@click.argument("output", type=click.Path())
def binary_image_merge(config: str, output: str) -> None:
    """Merge boot image blocks into one bootable image.

    The configuration template files could be generated by subcommand 'get-templates'.
    """
    config_data = load_configuration(config)
    config_dir = os.path.dirname(config)
    bimg_image = BootableImage.load_from_config(config_data, search_paths=[config_dir])
    bimg_image_info = bimg_image.image_info()

    write_file(bimg_image_info.export(), output, mode="wb")

    logger.info(f"Created Bootable Image:\n{bimg_image_info.info()}")
    logger.info(f"Created Bootable Image memory map:\n{bimg_image_info.draw()}")
    click.echo(f"Success. (Bootable Image: {output} created.)")


@bootable_image.command(name="parse", no_args_is_help=True)
@click.option(
    "-f",
    "--chip-family",
    default="rt5xx",
    type=click.Choice(BootableImage.get_supported_families(), case_sensitive=False),
    required=True,
    help="Select the chip family.",
)
@click.option(
    "-m",
    "--mem-type",
    default="flexspi_nor",
    type=click.Choice(["flexspi_nor"], case_sensitive=False),
    required=True,
    help="Select the chip used memory type.",
)
@click.option(
    "-b",
    "--binary",
    type=click.Path(exists=True, readable=True),
    required=True,
    help="Path to binary Bootable image to parse.",
)
@click.argument("parsed-data-path", type=click.Path())
def bootable_image_parse(
    chip_family: str, mem_type: str, binary: str, parsed_data_path: str
) -> None:
    """Parse Bootable Image into YAML configuration and binary images."""
    bimg_image = get_bimg_class(chip_family)(family=chip_family, mem_type=mem_type)
    bimg_image.parse(load_binary(binary))
    bimg_image_info = bimg_image.image_info()
    logger.info(f"Parsed Bootable image memory map: {bimg_image_info.draw()}")
    bimg_image.store_config(parsed_data_path)
    click.echo(
        f"Success. (Bootable Image: {binary} has been parsed and stored into {parsed_data_path} .)"
    )


@bootable_image.command(name="get-templates", no_args_is_help=True)
@click.option(
    "-f",
    "--chip-family",
    default="rt595",
    type=click.Choice(BootableImage.get_supported_families(), case_sensitive=False),
    required=True,
    help="Select the chip family.",
)
@click.option(
    "-o",
    "--overwrite",
    default=False,
    type=bool,
    is_flag=True,
    help="Allow overwriting existing template file.",
)
@click.argument("output_folder", type=click.Path())
def bootable_image_get_templates(chip_family: str, overwrite: bool, output_folder: str) -> None:
    """Create template of configurations in YAML format from all memory types.

    The template files folder name is specified as argument of this command.
    """
    if not os.path.exists(output_folder):
        os.makedirs(output_folder, exist_ok=True)
    mem_types = BootableImage.get_supported_memory_types(chip_family)
    for mem_type in mem_types:
        output = os.path.join(output_folder, f"bootimg_{chip_family}_{mem_type}.yml")
        if not os.path.isfile(output) or overwrite:
            click.echo(f"Creating {output} template file.")
            write_file(BootableImage.generate_config_template(chip_family, mem_type), output)
        else:
            click.echo(f"Skip creating {output}, this file already exists.")


@bootable_image.group(name="fcb", no_args_is_help=True)
def fcb() -> None:  # pylint: disable=unused-argument
    """FCB (Flash Configuration Block) utilities."""


@fcb.command(name="export", no_args_is_help=True)
@click.option(
    "-c",
    "--config",
    type=click.Path(exists=True, readable=True),
    required=True,
    help="FCB YAML/JSON Configuration.",
)
@click.argument("output", type=click.Path())
def fcb_export(config: str, output: str) -> None:
    """Export FCB Image from YAML/JSON configuration.

    The configuration template files could be generated by subcommand 'get-templates'.
    """
    config_data = load_configuration(config)
    check_config(config_data, FCB.get_validation_schemas_family())
    chip_family = config_data["family"]
    mem_type = config_data["type"]
    revision = config_data.get("revision", "latest")
    schemas = FCB.get_validation_schemas(chip_family, mem_type, revision)
    check_config(config_data, schemas, search_paths=[os.path.dirname(config)])
    fcb_image = FCB.load_from_config(config_data)
    fcb_data = fcb_image.export()
    write_file(fcb_data, output, mode="wb")

    logger.info(f"Created FCB Image:\n{fcb_image.regs.image_info().info()}")
    logger.info(f"Created FCB Image memory map:\n{fcb_image.regs.image_info().draw()}")
    click.echo(f"Success. (FCB: {output} created.)")


@fcb.command(name="parse", no_args_is_help=True)
@click.option(
    "-f",
    "--chip-family",
    default="rt5xx",
    type=click.Choice(FCB.get_supported_families(), case_sensitive=False),
    required=True,
    help="Select the chip family.",
)
@click.option(
    "-m",
    "--mem-type",
    default="flexspi_nor",
    type=click.Choice(["flexspi_nor"], case_sensitive=False),
    required=True,
    help="Select the chip used memory type.",
)
@click.option(
    "-b",
    "--binary",
    type=click.Path(exists=True, readable=True),
    required=True,
    help="Path to binary FCB image to parse.",
)
@click.argument("config-file", type=click.Path())
def fcb_parse(chip_family: str, mem_type: str, binary: str, config_file: str) -> None:
    """Parse FCB Image into YAML configuration and binary images."""
    if not os.path.exists(os.path.dirname(config_file)):
        os.makedirs(config_file, exist_ok=True)

    fcb_image = FCB(family=chip_family, mem_type=mem_type)
    fcb_image.parse(load_binary(binary))

    logger.info(f"Parsed FCB image memory map: {fcb_image.regs.image_info().draw()}")
    config = fcb_image.create_config()
    write_file(config, config_file)
    click.echo(f"Success. (FCB: {binary} has been parsed and stored into {config_file} .)")


@fcb.command(name="get-templates", no_args_is_help=True)
@click.option(
    "-f",
    "--chip-family",
    default="rt595",
    type=click.Choice(FCB.get_supported_families(), case_sensitive=False),
    required=True,
    help="Select the chip family.",
)
@click.option(
    "-o",
    "--overwrite",
    default=False,
    type=bool,
    is_flag=True,
    help="Allow overwriting existing template file.",
)
@click.argument("output_folder", type=click.Path())
def fcb_get_templates(chip_family: str, overwrite: bool, output_folder: str) -> None:
    """Create template of configurations in YAML format fro all memory types.

    The template files folder name is specified as argument of this command.
    """
    if not os.path.exists(output_folder):
        os.makedirs(output_folder, exist_ok=True)
    mem_types = FCB.get_supported_memory_types(chip_family)
    for mem_type in mem_types:
        output = os.path.join(output_folder, f"fcb_{chip_family}_{mem_type}.yml")
        if not os.path.isfile(output) or overwrite:
            click.echo(f"Creating {output} template file.")
            write_file(FCB.generate_config_template(chip_family, mem_type), output)
        else:
            click.echo(f"Skip creating {output}, this file already exists.")


@main.group(name="utils", no_args_is_help=True)
def utils() -> None:  # pylint: disable=unused-argument
    """Group of utilities."""


@utils.group(name="binary-image", no_args_is_help=True)
def bin_image() -> None:  # pylint: disable=unused-argument
    """Binary Image utilities."""


@bin_image.command(name="create", no_args_is_help=True)
@click.option(
    "-s",
    "--size",
    type=INT(),
    required=True,
    help="Size of file to be created.",
)
@click.option(
    "-p",
    "--pattern",
    type=str,
    default="zeros",
    help="Pattern of created file ('zeros', 'ones', 'rand', 'inc' or any number value).",
)
@click.argument("output", type=click.Path())
def binary_create(size: int, pattern: str, output: str) -> None:
    """Create binary file with pattern.

    The helper utility to create simple binary file with pattern.
    """
    image = BinaryImage(name="", size=size, pattern=BinaryPattern(pattern))
    image.validate()
    data = image.export()

    write_file(data, output, mode="wb")

    logger.info(f"Created file:\n{image.info()}")
    logger.info(f"Created file:\n{image.draw()}")
    click.echo(f"Success. (Created binary file: {output} )")


@bin_image.command(name="merge", no_args_is_help=True)
@click.option(
    "-c",
    "--config",
    type=click.Path(exists=True, readable=True),
    required=True,
    help="Path to configuration file of merge operation.",
)
@click.argument("output", type=click.Path())
def binary_merge(config: str, output: str) -> None:
    """Merge binary images together by description in YAML/JSON configuration.

    The configuration template files could be generated by subcommand 'utils bin-image get-template'.
    """
    cfg = load_configuration(config)
    config_dir = os.path.dirname(config)
    check_config(cfg, BinaryImage.get_validation_schemas(), search_paths=[config_dir])
    image = BinaryImage.load_from_config(cfg, search_paths=[config_dir])
    image.validate()
    data = image.export()

    write_file(data, output, mode="wb")

    logger.info(f"Merged Image:\n{image.info()}")
    logger.info(f"Merged Image:\n{image.draw()}")
    click.echo(f"Success. (Merged image: {output} created.)")


@bin_image.command(name="extract", no_args_is_help=True)
@click.option(
    "-b",
    "--binary",
    type=click.Path(exists=True, readable=True),
    required=True,
    help="Path to binary file to be used to extract chunk from.",
)
@click.option(
    "-a",
    "--address",
    type=str,
    required=True,
    help="Address of extracted chunk.",
)
@click.option(
    "-s",
    "--size",
    type=str,
    required=True,
    help="Size of extracted chunk. For '0' it extract rest of the file from given address.",
)
@click.argument("output", type=click.Path())
def binary_extract(binary: str, address: str, size: str, output: str) -> None:
    """Extract chunk from binary file.

    \b
    OUTPUT - File name of stored chunk.
    """
    bin_data = load_binary(binary)
    start = value_to_int(address)
    size_int = value_to_int(size)
    if not size_int:
        size_int = len(bin_data) - start
    end = start + size_int

    if end > len(bin_data):
        click.echo(f"The required binary chunk is out of [{binary}] file space.")
        return
    write_file(bin_data[start:end], output, mode="wb")
    click.echo(f"Success. (Extracted chunk: {output} created.)")


@bin_image.command(name="convert", no_args_is_help=True)
@click.option(
    "-i",
    "--input-file",
    type=click.Path(exists=True, readable=True),
    required=True,
    help="Path to BIN/HEX/S19/ELF file to be converted.",
)
@click.option(
    "-f",
    "--format",
    type=click.Choice(["BIN", "HEX", "S19"], case_sensitive=False),
    required=True,
    help="Output format.",
)
@click.argument("output", type=click.Path())
def binary_convert(input_file: str, format: str, output: str) -> None:
    """Convert input data file into selected format.

    \b
    OUTPUT - File name of converted file.
    """
    image = BinaryImage.load_binary_image(input_file)
    logger.info(image.draw())
    image.save_binary_image(output, file_format=format)
    click.echo(f"Success. (Converted file: {output} created.)")


@bin_image.command(name="get-template", no_args_is_help=True)
@click.option(
    "-o",
    "--overwrite",
    default=False,
    type=bool,
    is_flag=True,
    help="Allow overwriting existing template file.",
)
@click.argument("output", type=click.Path())
def binary_get_template(overwrite: bool, output: str) -> None:
    """Create template of configuration in YAML format.

    The template file name is specified as argument of this command.
    """
    if not os.path.isfile(output) or overwrite:
        click.echo(f"Creating {output} template file.")
        write_file(BinaryImage.generate_config_template(), output)
    else:
        click.echo(f"Skip creating {output}, this file already exists.")


@utils.group(name="convert", no_args_is_help=True)
def convert() -> None:  # pylint: disable=unused-argument
    """Conversion utilities."""


@convert.command(name="hex2bin", no_args_is_help=True)
@click.option(
    "-i",
    "--input-file",
    type=click.Path(exists=True, readable=True),
    required=True,
    help="Path to text file with hexadecimal string to be converted to binary.",
)
@click.option(
    "-r",
    "--reverse",
    type=bool,
    is_flag=True,
    default=False,
    help="The resulting binary bytes will be stored in reverse order (for example SBKEK in elftosb requires that).",
)
@click.argument("output", type=click.Path())
def convert_hex2bin(input_file: str, reverse: bool, output: str) -> None:
    """Convert file with hexadecimal string into binary file with optional reverse order of stored bytes.

    \b
    OUTPUT - File name of converted file.
    """
    try:
        value = bytearray.fromhex(load_text(input_file))
    except (SPSDKError, ValueError):
        raise SPSDKAppError(f"Failed loading hexadecimal value from: {input_file}")
    else:
        if reverse:
            value.reverse()
        write_file(value, output, mode="wb")
        click.echo(f"Success. Converted file: {output}")


@convert.command(name="bin2hex", no_args_is_help=True)
@click.option(
    "-i",
    "--input-file",
    type=click.Path(exists=True, readable=True),
    required=True,
    help="Path to binary file with to be converted to hexadecimal text file.",
)
@click.option(
    "-r",
    "--reverse",
    type=bool,
    is_flag=True,
    default=False,
    help="The result binary bytes will be stored in reverse order (for example SBKEK in elftosb this required).",
)
@click.argument("output", type=click.Path())
def convert_bin2hex(input_file: str, reverse: bool, output: str) -> None:
    """Convert binary file into hexadecimal text file with optional reverse order of stored bytes.

    \b
    OUTPUT - File name of converted file.
    """
    value = bytearray(load_binary(input_file))
    if reverse:
        value.reverse()
    write_file(value.hex(), output, mode="w")
    click.echo(f"Success. Converted file: {output}")


@catch_spsdk_error
def safe_main() -> None:
    """Call the main function."""
    sys.exit(main())  # pylint: disable=no-value-for-parameter


if __name__ == "__main__":
    safe_main()
