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

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

import click

from spsdk.apps.utils import spsdk_logger
from spsdk.apps.utils.common_cli_options import (
    CommandsTreeGroup,
    spsdk_apps_common_options,
    spsdk_plugin_option,
)
from spsdk.apps.utils.utils import (
    INT,
    SPSDKAppError,
    catch_spsdk_error,
    filepath_from_config,
    get_key,
    store_key,
)
from spsdk.crypto.signature_provider import get_signature_provider
from spsdk.exceptions import SPSDKError
from spsdk.image import TrustZone, get_mbi_class
from spsdk.image.ahab.ahab_container import AHABImage
from spsdk.image.ahab.signed_msg import SignedMessage
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.hab.config_parser import ImageConfig
from spsdk.image.hab.hab_container import HabContainer
from spsdk.image.keystore import KeyStore
from spsdk.image.mbimg import (
    MasterBootImage,
    mbi_generate_config_templates,
    mbi_get_supported_families,
)
from spsdk.image.xmcd.xmcd import XMCD
from spsdk.sbfile.sb2 import sly_bd_parser as bd_parser
from spsdk.sbfile.sb2.commands import CmdLoad
from spsdk.sbfile.sb2.images import BootImageV21, generate_SB21
from spsdk.sbfile.sb31.images import SB3_SCH_FILE, SecureBinary31
from spsdk.utils.crypto.cert_blocks import CertBlockV2, CertBlockV31
from spsdk.utils.crypto.iee import IeeNxp
from spsdk.utils.crypto.otfad import OtfadNxp
from spsdk.utils.images import BinaryImage, BinaryPattern
from spsdk.utils.misc import (
    align,
    align_block,
    get_abs_path,
    load_binary,
    load_configuration,
    load_text,
    value_to_int,
    write_file,
)
from spsdk.utils.plugins import load_plugin_from_source
from spsdk.utils.schema_validator import (
    CommentedConfig,
    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.
    """
    spsdk_logger.install(level=log_level)


@main.group(name="mbi", no_args_is_help=True)
def mbi_group() -> None:
    """Group of sub-commands related to Master Boot Images."""


@mbi_group.command(name="export", no_args_is_help=True)
@click.argument("config", type=click.Path(exists=True, readable=True))
@spsdk_plugin_option
def mbi_export_command(
    config: str,
    plugin: 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'.
    """
    mbi_export(config, plugin)


def mbi_export(config: str, plugin: Optional[str] = None) -> None:
    """Generate Master Boot Image from YAML/JSON configuration.

    :param config: Path to the YAML/JSON configuration
    :param plugin: Path to external python file containing a custom SignatureProvider implementation.
    """
    config_data = load_configuration(config)
    if plugin:
        load_plugin_from_source(plugin)
    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_group.command(name="parse", 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(
    "-b",
    "--binary",
    type=click.Path(exists=True, readable=True, resolve_path=True),
    required=True,
    help="Path to binary MBI image to parse.",
)
@click.option(
    "-k",
    "--dek",
    type=str,
    required=False,
    help=(
        "Data encryption key, if it's specified, the parse method tries decrypt all encrypted images. "
        "It could be specified as binary/HEX text file path or directly HEX string"
    ),
)
@click.argument("parsed-data", type=click.Path(resolve_path=True))
def mbi_parse_command(chip_family: str, binary: str, dek: str, parsed_data: str) -> None:
    """Parse MBI Image into YAML configuration and binary images."""
    mbi_parse(chip_family, binary, dek, parsed_data)


def mbi_parse(chip_family: str, binary: str, dek: 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)

    mbi = MasterBootImage.parse(family=chip_family, data=load_binary(binary), dek=dek)

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

    mbi.create_config(output_folder=parsed_data)

    click.echo(f"Success. (MBI: {binary} has been parsed and stored into {parsed_data} )")


@mbi_group.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_command(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.
    """
    mbi_get_templates(chip_family, overwrite, output)


def mbi_get_templates(chip_family: str, overwrite: bool, output: str) -> None:
    """Create template of MBI configurations in YAML format."""
    templates = mbi_generate_config_templates(chip_family)
    for file_name, template in templates.items():
        full_file_name = os.path.abspath(os.path.join(output, file_name + ".yaml"))
        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(name="sb21", no_args_is_help=True)
def sb21_group() -> None:
    """Group of sub-commands related to Secure Binary 2.1."""


@sb21_group.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(resolve_path=True), 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=str,
    help="Path to private key or signature provider configuration used 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.option(
    "--plugin",
    type=click.Path(exists=True, dir_okay=False),
    required=False,
    help="External python file containing a custom SignatureProvider implementation.",
)
@click.argument("external", type=click.Path(), nargs=-1)
def sb21_export_command(
    command: str,
    output: str,
    key: str,
    pkey: str,
    cert: List[str],
    root_key_cert: List[str],
    hash_of_hashes: str,
    plugin: 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
    """
    sb21_export(command, output, key, pkey, cert, root_key_cert, hash_of_hashes, plugin, external)


def sb21_export(
    command: str,
    output: str,
    key: str,
    pkey: str,
    cert: List[str],
    root_key_cert: List[str],
    hash_of_hashes: str,
    plugin: str,
    external: List[str],
) -> None:
    """Generate Secure Binary v2.1 Image from configuration."""
    if plugin:
        load_plugin_from_source(plugin)
    if output is None:
        raise SPSDKAppError("Error: no output file was specified")
    signature_provider = (
        get_signature_provider(local_file_key=pkey)
        if os.path.isfile(pkey)
        else get_signature_provider(sp_cfg=pkey)
    )

    try:
        sb2_data = generate_SB21(
            bd_file_path=command,
            key_file_path=key,
            signature_provider=signature_provider,
            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_group.command(name="parse")
@click.option(
    "-b",
    "--binary",
    type=click.Path(exists=True, readable=True, resolve_path=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_command(binary: str, key: str, parsed_data: str) -> None:
    """Parse Secure Binary v2.1 Image."""
    sb21_parse(binary, key, parsed_data)


def sb21_parse(binary: str, key: str, parsed_data: str) -> None:
    """Parse Secure Binary v2.1 Image."""
    # 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 = os.path.abspath(
                os.path.join(parsed_data, f"certificate_{cert_idx}_der.cer")
            )
            logger.debug(f"Dumping certificate {file_name}")
            write_file(certificate.dump(), 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 = os.path.abspath(
                    os.path.join(
                        parsed_data, f"section_{section_idx}_load_command_{command_idx}_data.bin"
                    )
                )
                logger.debug(f"Dumping load command data {file_name}")
                write_file(command.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_group.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(resolve_path=True),
    help="Output folder where the sbkek.txt and sbkek.bin will be stored",
)
def get_sbkek_command(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,
    """
    get_sbkek(master_key, output_folder)


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."""
    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(name="sb31")
def sb31_group() -> None:
    """Group of sub-commands related to Secure Binary 3.1."""


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

    CONFIG is path to the YAML/JSON configuration.
    RoTKTH/SB3KDK is printed out in verbose mode.

    The configuration template files could be generated by subcommand 'get-template'.
    """
    sb31_export(config)


def sb31_export(config: str) -> None:
    """Generate Secure Binary v3.1 Image from YAML/JSON configuration."""
    config_data = load_configuration(config)
    config_dir = os.path.dirname(config)
    check_config(config_data, SecureBinary31.get_validation_schemas_family())
    schemas = SecureBinary31.get_validation_schemas(
        config_data["family"], 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_group.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(resolve_path=True))
def sb31_get_template_command(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.
    """
    sb31_get_template(chip_family, overwrite, output)


def sb31_get_template(chip_family: str, overwrite: bool, output: str) -> None:
    """Create template of configuration in YAML format."""
    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(name="cert-block", no_args_is_help=True)
def cert_block_group() -> None:  # pylint: disable=unused-argument
    """Group of sub-commands related to certification block."""


@cert_block_group.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(resolve_path=True))
def cert_block_get_template_command(overwrite: bool, output: str) -> None:
    """Create template of configuration in YAML format.

    The template file name is specified as argument of this command.
    """
    cert_block_get_template(overwrite, output)


def cert_block_get_template(overwrite: bool, output: str) -> None:
    """Create template of configuration in YAML format."""
    if not os.path.isfile(output) or overwrite:
        click.echo(f"Creating {output} template file.")
        write_file(CertBlockV31.generate_config_template(), output)
    else:
        click.echo(f"Skip creating {output}, this file already exists.")


@cert_block_group.command(name="export", no_args_is_help=True)
@click.argument("config", type=click.Path(exists=True, readable=True))
def cert_block_export_command(config: str) -> None:
    """Generate Certificate Block v3.1 from YAML/JSON configuration.

    CONFIG is path to the YAML/JSON configuration.
    RoTKTH is printed out in verbose mode.

    The configuration template files could be generated by subcommand 'get-template'.
    """
    cert_block_export(config)


def cert_block_export(config: str) -> None:
    """Generate Certificate Block v3.1 from YAML/JSON configuration."""
    config_data = load_configuration(config)
    config_dir = os.path.dirname(config)
    schemas = CertBlockV31.get_validation_schemas()
    check_config(config_data, schemas, search_paths=[config_dir])
    cert_block = CertBlockV31.from_config(config_data, search_paths=[config_dir])
    cert_data = cert_block.export()

    cert_block_output_file_path = get_abs_path(config_data["containerOutputFile"], config_dir)
    write_file(cert_data, cert_block_output_file_path, mode="wb")

    click.echo(f"Success. (Certificate Block 3.1: {cert_block_output_file_path} created.)")


@cert_block_group.command(name="parse", no_args_is_help=True)
@click.option(
    "-b",
    "--binary",
    type=click.Path(exists=True, readable=True, resolve_path=True),
    required=True,
    help="Path to binary Certificate v3.1 image to parse.",
)
@click.argument("parsed-data", type=click.Path(resolve_path=True))
def cert_block_parse_command(binary: str, parsed_data: str) -> None:
    """Parse Certificate Block v3.1.

    PARSED-DATA is path where the parsed certification block will be stored.
    RoTKTH is printed out in verbose mode.
    """
    cert_block_parse(binary, parsed_data)


def cert_block_parse(binary: str, parsed_data: str) -> None:
    """Parse Certificate Block v3.1."""
    cert_block = CertBlockV31.parse(load_binary(binary))
    logger.info(cert_block.info())
    write_file(
        cert_block.create_config(parsed_data), os.path.join(parsed_data, "cert_block_config.yaml")
    )
    click.echo(f"Success. (Certificate Block 3.1: {binary} has been parsed into {parsed_data}.)")


@main.group(name="tz", no_args_is_help=True)
def tz_group() -> None:
    """Group of sub-commands related to Trust Zone."""


@tz_group.command(name="export", no_args_is_help=True)
@click.argument("config", type=click.Path(exists=True, readable=True))
def tz_export_command(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'.
    """
    tz_export(config)


def tz_export(config: str) -> None:
    """Generate TrustZone Image from YAML/JSON configuration."""
    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_group.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(resolve_path=True))
def tz_get_template_command(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.
    """
    tz_get_template(chip_family, revision, overwrite, output)


def tz_get_template(chip_family: str, revision: str, overwrite: bool, output: str) -> None:
    """Create template of configuration in YAML format."""
    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(name="ahab", no_args_is_help=True)
def ahab_group() -> None:
    """Group of sub-commands related to AHAB."""


@ahab_group.command(name="export", no_args_is_help=True)
@click.argument("config", type=click.Path(exists=True, readable=True))
def ahab_export_command(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'.
    """
    ahab_export(config)


def ahab_export(config: str) -> None:
    """Generate AHAB Image from YAML/JSON configuration."""
    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):
        if container.flag_srk_set == "nxp":
            logger.debug("Skipping generating hashes for NXP container")
            continue
        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 ({os.path.abspath(file_name)}*.*).")


@ahab_group.command(name="parse", no_args_is_help=True)
@click.option(
    "-f",
    "--chip-family",
    default="rt118x",
    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, resolve_path=True),
    required=True,
    help="Path to binary AHAB image to parse.",
)
@click.option(
    "-k",
    "--dek",
    type=str,
    required=False,
    help=(
        "Data encryption key, if it's specified, the parse method tries decrypt all encrypted images. "
        "It could be specified as binary/HEX text file path or directly HEX string"
    ),
)
@click.argument("parsed-data", type=click.Path(resolve_path=True))
def ahab_parse_command(
    chip_family: str, image_type: str, binary: str, dek: str, parsed_data: str
) -> None:
    """Parse AHAB Image into YAML configuration and binary images."""
    ahab_parse(chip_family, image_type, binary, dek, parsed_data)


def ahab_parse(chip_family: str, image_type: str, binary: str, dek: 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()}")
    if dek:
        for container in ahab_image.ahab_containers:
            if container.flag_srk_set != "nxp" and container.signature_block.blob:
                container.signature_block.blob.dek = get_key(
                    dek, container.signature_block.blob._size // 8
                )
                container.decrypt_data()

    config = ahab_image.create_config(parsed_data)
    write_file(
        CommentedConfig(
            main_title=(
                f"AHAB recreated configuration from :"
                f"{datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S')}."
            ),
            schemas=AHABImage.get_validation_schemas(),
            values=config,
            export_template=False,
        ).export_to_yaml(),
        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_group.command(name="get-template", no_args_is_help=True)
@click.option(
    "-f",
    "--chip-family",
    default="rt118x",
    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(resolve_path=True))
def ahab_get_template_command(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.
    """
    ahab_get_template(chip_family, overwrite, output)


def ahab_get_template(chip_family: str, overwrite: bool, output: str) -> None:
    """Create template of configuration in YAML format."""
    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 signed_msg() -> None:  # pylint: disable=unused-argument
    """Group of sub-commands related to Signed messages."""


@signed_msg.command(name="export", no_args_is_help=True)
@click.argument("config", type=click.Path(exists=True, readable=True))
def signed_msg_export(config: str) -> None:
    """Generate Signed message 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 = SignedMessage.get_validation_schemas()
    check_config(config_data, schemas, search_paths=[config_dir])
    smsg = SignedMessage.load_from_config(config_data, search_paths=[config_dir])

    signed_msg_data = smsg.export()

    signed_msg_output_file_path = get_abs_path(config_data["output"], config_dir)
    write_file(signed_msg_data, signed_msg_output_file_path, mode="wb")

    logger.info(f"Created Signed message Image:\n{smsg.image_info().info()}")
    logger.info(f"Created Signed message Image memory map:\n{smsg.image_info().draw()}")
    click.echo(f"Success. (Signed message: {signed_msg_output_file_path} created.)")


@signed_msg.command(name="parse", no_args_is_help=True)
@click.option(
    "-b",
    "--binary",
    type=click.Path(exists=True, readable=True, resolve_path=True),
    required=True,
    help="Path to binary Signed message image to parse.",
)
@click.argument("parsed-data", type=click.Path(resolve_path=True))
def signed_msg_parse(binary: str, parsed_data: str) -> None:
    """Parse Signed message Image into YAML configuration and binary images."""
    if not os.path.exists(parsed_data):
        os.makedirs(parsed_data, exist_ok=True)
    try:
        signed_message = SignedMessage.parse(load_binary(binary))
        signed_message.update_fields()
        signed_message.validate({})
    except SPSDKError as exc:
        click.echo(f"Signed message parsing failed: {binary} ,({str(exc)})")
        return

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

    srk_table = signed_message.signature_block.srk_table
    file_name = os.path.join(parsed_data, f"{signed_message.flag_srk_set}_srk_hash")
    if srk_table:
        srkh = srk_table.compute_srk_hash()
        write_file(srkh.hex().upper(), f"{file_name}.txt")
        click.echo(f"Generated SRK hash files ({file_name}*.*).")


@signed_msg.command(name="get-template", no_args_is_help=True)
@click.option(
    "-f",
    "--chip-family",
    default="rt118x",
    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(resolve_path=True))
def signed_msg_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(
            SignedMessage.generate_config_template(chip_family)[f"{chip_family}_signed_msg"], output
        )
    else:
        click.echo(f"Skip creating {output}, this file already exists.")


@main.group(name="otfad", no_args_is_help=True)
def otfad_group() -> None:
    """Group of sub-commands related to OTFAD."""


@otfad_group.command(name="export", no_args_is_help=True)
@click.option(
    "-a",
    "--alignment",
    default="512",
    type=INT(),
    help="Alignment of key blob data blocks. Default value is 512 B if not specified.",
    required=False,
)
@click.option(
    "-i",
    "--index",
    type=INT(),
    help=(
        "OTFAD peripheral index - This is needed to generate proper "
        "indexes of fuses in optional BLHOST script. If not specified, BLHOST fuse script won't be generated"
    ),
    required=False,
)
@click.argument("config", type=click.Path(exists=True, readable=True))
def otfad_export_command(alignment: int, config: str, index: Optional[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'.
    """
    otfad_export(alignment, config, index)


def otfad_export(alignment: int, config: str, index: Optional[int] = None) -> None:
    """Generate OTFAD Images from YAML/JSON configuration."""
    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, config_dir, search_paths=[config_dir])

    output_folder = get_abs_path(config_data["output_folder"], config_dir)
    otfad_table_name = filepath_from_config(
        config_data, "keyblob_name", "OTFAD_Table", config_dir, output_folder
    )
    binary_image = otfad.binary_image(data_alignment=alignment, otfad_table_name=otfad_table_name)
    logger.info(f" The OTFAD image structure:\n{binary_image.draw()}")
    otfad_all = filepath_from_config(
        config_data, "output_name", "otfad_whole_image", config_dir, output_folder
    )
    if otfad_all != "":
        write_file(binary_image.export(), otfad_all, mode="wb")
        logger.info(f"Created OTFAD Image:\n{otfad_all}")
    else:
        logger.info("Skipping export of OTFAD image")
    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):
        if image.name != "":
            write_file(image.export(), image.name, mode="wb")
            logger.info(f"Created OTFAD Image:\n{image.name}")
            memory_map += f"\n{image.name}:\n{image.info()}"
        else:
            logger.info(
                f"Skipping export of {image.info()}, value is blank in the configuration file"
            )
        if sb21_supported:
            bd_file_sources += f'\n    image{i} = "{image.name}";'
            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")

    if config_data.get("generate_readme", True):
        write_file(memory_map, readme_file)
        logger.info(f"Created OTFAD readme file:\n{readme_file}")
    else:
        logger.info("Skipping generation of OTFAD 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("Success. OTFAD files have been created")


@otfad_group.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(resolve_path=True),
    help="Optional result output folder (otfad_kek.bin/txt, optionally BLHOST scripts to load keys into Fuses)",
)
def otfad_get_kek_command(
    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.
    """
    otfad_get_kek(otp_master_key, otfad_key, chip_family, output_folder)


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."""
    omk = get_key(otp_master_key, KeyStore.OTP_MASTER_KEY_SIZE)
    ok = get_key(otfad_key, KeyStore.OTFAD_KEY_SIZE)  # pylint:disable=invalid-name

    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_group.command(name="get-template", no_args_is_help=True)
@click.option(
    "-f",
    "--chip-family",
    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(resolve_path=True))
def otfad_get_template_command(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.
    """
    otfad_get_template(chip_family, overwrite, output)


def otfad_get_template(chip_family: str, overwrite: bool, output: str) -> None:
    """Create template of configuration in YAML format."""
    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(name="iee", no_args_is_help=True)
def iee_group() -> None:  # pylint: disable=unused-argument
    """Group of sub-commands related to IEE."""


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

    CONFIG is path to the YAML/JSON configuration

    The configuration template files could be generated by subcommand 'get-template'.
    """
    iee_export(config)


def iee_export(config: str) -> None:
    """Generate IEE Images from YAML/JSON configuration."""
    config_data = load_configuration(config)
    config_dir = os.path.dirname(config)
    check_config(config_data, IeeNxp.get_validation_schemas_family(), search_paths=[config_dir])
    family = config_data["family"]
    schemas = IeeNxp.get_validation_schemas(family)
    check_config(config_data, schemas, search_paths=[config_dir])
    iee = IeeNxp.load_from_config(config_data, config_dir, search_paths=[config_dir])

    output_folder = get_abs_path(config_data["output_folder"], config_dir)
    iee_all = filepath_from_config(config_data, "output_name", "iee_whole_image", output_folder)
    keyblob_name = filepath_from_config(
        config_data, "keyblob_name", "iee_keyblob", config_dir, output_folder
    )

    binary_image = iee.binary_image(keyblob_name=keyblob_name, image_name=iee_all)
    logger.info(binary_image.draw())

    if iee_all == "":
        logger.info("Skipping export of IEE whole image")
    else:
        write_file(binary_image.export(), iee_all, mode="wb")
        click.echo(f"Created IEE Image containing keyblobs and encrypted data:\n{iee_all}")

    memory_map = (
        "Output folder contains:\n"
        "  -  Binary file that contains whole image data including "
        f"IEE key blobs data {iee_all}.\n"
        f"IEE memory map:\n{binary_image.draw(no_color=True)}"
    )

    for image in binary_image.sub_images:
        if image.name != "":
            write_file(image.export(), image.name, mode="wb")
            logger.info(f"Created Encrypted IEE data blob {image.description}:\n{image.name}")
            memory_map += f"\n{image.name}:\n{image.info()}"
        else:
            logger.info(
                f"Skipping export of {image.info()}, value is blank in the configuration file"
            )

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

    if config_data.get("generate_readme", True):
        write_file(memory_map, readme_file)
        logger.info(f"Created IEE readme file:\n{readme_file}")
    else:
        logger.info("Skipping generation of IEE readme file")

    if iee.database.get_device_value("has_kek_fuses", iee.family) and config_data.get(
        "generate_fuses_script"
    ):
        blhost_script = iee.get_blhost_script_otp_kek()
        blhost_script_filename = os.path.join(output_folder, f"iee_{iee.family}_blhost.bcf")
        write_file(blhost_script, blhost_script_filename)
        click.echo(f"Created IEE BLHOST load fuses script:\n{blhost_script_filename}")
    else:
        logger.info("Skipping generation of IEE BLHOST load fuses script")

    click.echo("Success. IEE files have been created")


@iee_group.command(name="get-template", no_args_is_help=True)
@click.option(
    "-f",
    "--chip-family",
    default="rt117x",
    type=click.Choice(IeeNxp.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(resolve_path=True))
def iee_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(IeeNxp.generate_config_template(chip_family)[f"{chip_family}_iee"], output)
    else:
        click.echo(f"Skip creating {output}, this file already exists.")


@main.group(name="bee", no_args_is_help=True)
def bee_group() -> None:
    """Group of sub-commands related to BEE."""


@bee_group.command(name="export", no_args_is_help=True)
@click.argument("config", type=click.Path(exists=True, readable=True))
def bee_export_command(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'.
    """
    bee_export(config)


def bee_export(config: str) -> None:
    """Generate BEE Images from YAML/JSON configuration."""
    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_group.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(resolve_path=True))
def bee_get_template_command(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.
    """
    bee_get_template(chip_family, overwrite, output)


# pylint: disable=unused-argument    # preparation for the future
def bee_get_template(chip_family: str, overwrite: bool, output: str) -> None:
    """Create template of configuration in YAML format."""
    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(name="bootable-image", no_args_is_help=True)
def bootable_image_group() -> None:
    """Group of bootable image utilities."""


@bootable_image_group.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(resolve_path=True))
def bootable_image_merge_command(config: str, output: str) -> None:
    """Merge boot image blocks into one bootable image.

    The configuration template files could be generated by subcommand 'get-templates'.
    """
    bootable_image_merge(config, output)


def bootable_image_merge(config: str, output: str) -> None:
    """Merge boot image blocks into one bootable image."""
    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_group.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", "flexspi_nand", "semc_nand"], 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(resolve_path=True))
def bootable_image_parse_command(
    chip_family: str, mem_type: str, binary: str, parsed_data_path: str
) -> None:
    """Parse Bootable Image into YAML configuration and binary images."""
    bootable_image_parse(chip_family, mem_type, binary, parsed_data_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_group.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(resolve_path=True))
def bootable_image_get_templates_command(
    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.
    """
    bootable_image_get_templates(chip_family, overwrite, output_folder)


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."""
    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}.yaml")
        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.")


@main.group(name="hab", no_args_is_help=True)
def hab_group() -> None:  # pylint: disable=unused-argument
    """Group of sub-commands related to HAB container."""


@hab_group.command(name="export", no_args_is_help=True)
@click.option(
    "-c",
    "--command",
    type=click.Path(exists=True),
    required=True,
    help="BD configuration file to produce HAB container",
)
@click.option("-o", "--output", type=click.Path(resolve_path=True), help="Output file path.")
@click.argument("external", type=click.Path(), nargs=-1)
def hab_export_command(
    command: str,
    output: str,
    external: List[str],
) -> None:
    """Generate HAB container from configuration.

    EXTERNAL is a space separated list of external binary files defined in BD file
    """
    image = hab_export(command, external)
    write_file(image, output, mode="wb")
    click.echo(f"Success. (HAB container: {output} created.)")


def hab_export(command: str, external: List[str]) -> bytes:
    """Generate HAB container from configuration."""
    try:
        parser = bd_parser.BDParser()

        bd_file_content = load_text(command)
        bd_data = parser.parse(text=bd_file_content, extern=external)
        if bd_data is None:
            raise SPSDKError("Invalid bd file, secure binary file generation terminated")
        bd_config = ImageConfig.parse(bd_data)
        hab_container = HabContainer.load(
            bd_config,
            search_paths=[os.path.dirname(command)],
            timestamp=bd_config.options.signature_timestamp,
        )
        data = hab_container.export()
        return data
    except SPSDKError as exc:
        raise SPSDKAppError(f"The HAB container generation failed: ({str(exc)}).") from exc


@hab_group.command(name="parse", no_args_is_help=True)
@click.option(
    "-b",
    "--binary",
    type=click.Path(exists=True, readable=True, resolve_path=True),
    required=True,
    help="Path to binary HAB image to parse.",
)
@click.argument("output_folder", type=click.Path(resolve_path=True))
def hab_parse_command(
    binary: str,
    output_folder: str,
) -> None:
    """Parse HAB contianer into individual segments."""
    file_bin = load_binary(binary)
    created_files = hab_parse(file_bin, output_folder)
    for file_path in created_files:
        click.echo(f"File has been created: {file_path}")
    click.echo(f"Success. (HAB container parsed into: {output_folder}.)")


def hab_parse(binary: bytes, output: str) -> List[str]:
    """Generate HAB container from configuration."""
    hab_container = HabContainer.parse(binary)
    created_files = []
    for sub_image in hab_container.hab_image.sub_images:
        sub_image_data = sub_image.export()
        sub_image_path = os.path.join(output, f"{sub_image.name.lower()}.bin")
        write_file(sub_image_data, sub_image_path, mode="wb")
        created_files.append(sub_image_path)
    return created_files


@bootable_image_group.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(resolve_path=True))
def fcb_export_command(config: str, output: str) -> None:
    """Export FCB Image from YAML/JSON configuration.

    The configuration template files could be generated by subcommand 'get-templates'.
    """
    fcb_export(config, output)


def fcb_export(config: str, output: str) -> None:
    """Export FCB Image from YAML/JSON configuration."""
    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.registers.image_info().info()}")
    logger.info(f"Created FCB Image memory map:\n{fcb_image.registers.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, resolve_path=True),
    required=True,
    help="Path to binary FCB image to parse.",
)
@click.argument("config-file", type=click.Path(resolve_path=True))
def fcb_parse_command(chip_family: str, mem_type: str, binary: str, config_file: str) -> None:
    """Parse FCB Image into YAML configuration."""
    fcb_parse(chip_family, mem_type, binary, config_file)


def fcb_parse(chip_family: str, mem_type: str, binary: str, config_file: str) -> None:
    """Parse FCB Image into YAML configuration."""
    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.registers.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(resolve_path=True))
def fcb_get_templates_command(chip_family: str, overwrite: bool, output_folder: str) -> None:
    """Create template of configurations in YAML format for all memory types.

    The template files folder name is specified as argument of this command.
    """
    fcb_get_templates(chip_family, overwrite, output_folder)


def fcb_get_templates(chip_family: str, overwrite: bool, output_folder: str) -> None:
    """Create template of configurations in YAML format for all memory types."""
    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}.yaml")
        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.")


@bootable_image_group.group(name="xmcd", no_args_is_help=True)
def xmcd() -> None:  # pylint: disable=unused-argument
    """XMCD (External Memory Configuration Data) utilities."""


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

    The configuration template files could be generated by subcommand 'get-templates'.
    """
    xmcd_export(config, output)


def xmcd_export(config: str, output: str) -> None:
    """Export XMCD Image from YAML/JSON configuration."""
    config_data = load_configuration(config)
    xmcd_image = XMCD.load_from_config(config_data)
    xmcd_data = xmcd_image.export()
    write_file(xmcd_data, output, mode="wb")

    logger.info(f"Created XMCD :\n{xmcd_image.registers.image_info().info()}")
    logger.info(f"Created XMCD memory map:\n{xmcd_image.registers.image_info().draw()}")
    click.echo(f"Success. (XMCD: {output} created.)")


@xmcd.command(name="parse", no_args_is_help=True)
@click.option(
    "-f",
    "--chip-family",
    default="rt117x",
    type=click.Choice(XMCD.get_supported_families(), case_sensitive=False),
    required=True,
    help="Select the chip family.",
)
@click.option(
    "-b",
    "--binary",
    type=click.Path(exists=True, readable=True, resolve_path=True),
    required=True,
    help="Path to binary XMCD image to parse.",
)
@click.argument("config-file", type=click.Path(resolve_path=True))
def xmcd_parse_command(chip_family: str, binary: str, config_file: str) -> None:
    """Parse XMCD Image into YAML configuration."""
    xmcd_parse(chip_family, binary, config_file)


def xmcd_parse(chip_family: str, binary: str, config_file: str) -> None:
    """Parse XMCD Image into YAML configuration."""
    xmcd_image = XMCD(family=chip_family)
    xmcd_image.parse(load_binary(binary))

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


@xmcd.command(name="get-templates", no_args_is_help=True)
@click.option(
    "-f",
    "--chip-family",
    default="rt117x",
    type=click.Choice(XMCD.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(resolve_path=True))
def xmcd_get_templates_command(chip_family: str, overwrite: bool, output_folder: str) -> None:
    """Create template of configurations in YAML format for all memory types.

    The template files folder name is specified as argument of this command.
    """
    xmcd_get_templates(chip_family, overwrite, output_folder)


def xmcd_get_templates(chip_family: str, overwrite: bool, output_folder: str) -> None:
    """Create template of configurations in YAML format for all memory types."""
    mem_types = XMCD.get_supported_memory_types(chip_family)
    for mem_type in mem_types:
        config_types = XMCD.get_supported_configuration_types(chip_family, mem_type)
        for config_type in config_types:
            output = os.path.join(
                output_folder, f"xmcd_{chip_family}_{mem_type}_{config_type}.yaml"
            )
            if not os.path.isfile(output) or overwrite:
                click.echo(f"Creating {output} template file.")
                write_file(
                    XMCD.generate_config_template(chip_family, mem_type, config_type), output
                )
            else:
                click.echo(f"Skip creating {output}, this file already exists.")


@main.group(name="utils", no_args_is_help=True)
def utils_group() -> None:
    """Group of utilities."""


@utils_group.group(name="binary-image", no_args_is_help=True)
def bin_image_group() -> None:
    """Binary Image utilities."""


@bin_image_group.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(resolve_path=True))
def binary_create_command(size: int, pattern: str, output: str) -> None:
    """Create binary file with pattern.

    The helper utility to create simple binary file with pattern.
    """
    binary_create(size, pattern, output)


def binary_create(size: int, pattern: str, output: str) -> None:
    """Create 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_group.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(resolve_path=True))
def binary_merge_command(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'.
    """
    binary_merge(config, output)


def binary_merge(config: str, output: str) -> None:
    """Merge binary images together by description in YAML/JSON configuration."""
    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])
    try:
        image.validate()
    except SPSDKError as exc:
        click.echo(f"Image Validation fail:\n{image.draw()}")
        raise SPSDKError("Image validation failed") from exc
    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_group.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(resolve_path=True))
def binary_extract_command(binary: str, address: str, size: str, output: str) -> None:
    """Extract chunk from binary file.

    \b
    OUTPUT - File name of stored chunk.
    """
    binary_extract(binary, address, size, output)


def binary_extract(binary: str, address: str, size: str, output: str) -> None:
    """Extract chunk from binary file."""
    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_group.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",
    "output_format",
    type=click.Choice(["BIN", "HEX", "S19"], case_sensitive=False),
    required=True,
    help="Output format.",
)
@click.argument("output", type=click.Path(resolve_path=True))
def binary_convert_command(input_file: str, output_format: str, output: str) -> None:
    """Convert input data file into selected format.

    \b
    OUTPUT - File name of converted file.
    """
    binary_convert(input_file, output_format, output)


def binary_convert(input_file: str, output_format: str, output: str) -> None:
    """Convert input data file into selected format."""
    image = BinaryImage.load_binary_image(input_file)
    logger.info(image.draw())
    image.save_binary_image(output, file_format=output_format)
    click.echo(f"Success. (Converted file: {output} created.)")


@bin_image_group.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(resolve_path=True))
def binary_get_template_command(overwrite: bool, output: str) -> None:
    """Create template of configuration in YAML format.

    The template file name is specified as argument of this command.
    """
    binary_get_template(overwrite, output)


def binary_get_template(overwrite: bool, output: str) -> None:
    """Create template of configuration in YAML format."""
    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.")


@bin_image_group.command(name="align", no_args_is_help=True)
@click.option(
    "-i",
    "--input",
    "input_file",
    type=click.Path(file_okay=True, dir_okay=False, resolve_path=True),
    required=True,
    help="Binary file name to be aligned.",
)
@click.option(
    "-o",
    "--output",
    "output_file",
    type=click.Path(resolve_path=True),
    help="Aligned file name. If not specified, input file will be updated.",
)
@click.option(
    "-a",
    "--alignment",
    default=1,
    type=INT(),
    help="Alignment size.",
)
@click.option(
    "-p",
    "--pattern",
    type=str,
    default="zeros",
    help="Pattern of used padding ('zeros', 'ones', 'rand', 'inc' or any number value).",
)
def binary_align_command(input_file: str, output_file: str, alignment: int, pattern: str) -> None:
    """Align binary file to provided alignment and padded with specified pattern."""
    binary_align(input_file, output_file, alignment, pattern)


def binary_align(
    input_file: str, output_file: Optional[str] = None, alignment: int = 1, pattern: str = "zeros"
) -> None:
    """Align binary file to provided alignment and padded with specified pattern.

    :param input_file: Input file name.
    :param output_file: Output file name. If not specified, input file will be updated.
    :param alignment: Size of alignment block.
    :param pattern: Padding pattern.

    :raises SPSDKError: Invalid input arguments.
    """
    if not input_file or not os.path.isfile(input_file):
        raise SPSDKError("Binary file alignment: Invalid input file")
    if alignment <= 0:
        raise SPSDKError("Binary file alignment: Alignment value must be 1 or greater.")
    if not output_file:
        output_file = input_file
    binary = align_block(data=load_binary(input_file), alignment=alignment, padding=pattern)
    write_file(binary, output_file, mode="wb")
    click.echo(
        f"The {input_file} file has been aligned to {len(binary)} ({hex(len(binary))}) bytes \
            and stored into {output_file}"
    )


@bin_image_group.command(name="pad", no_args_is_help=True)
@click.option(
    "-i",
    "--input",
    "input_file",
    type=click.Path(file_okay=True, dir_okay=False, resolve_path=True),
    required=True,
    help="Binary file name to be padded.",
)
@click.option(
    "-o",
    "--output",
    type=click.Path(resolve_path=True),
    help="Padded file name. If not specified, input file will be updated.",
)
@click.option(
    "-s",
    "--size",
    required=True,
    type=INT(),
    help="Result file size.",
)
@click.option(
    "-p",
    "--pattern",
    type=str,
    default="zeros",
    help="Pattern of used padding ('zeros', 'ones', 'rand', 'inc' or any number value).",
)
def binary_pad_command(input_file: str, output: str, size: int, pattern: str) -> None:
    """Pad binary file to provided final size with specified pattern."""
    binary_pad(input_file, output, size, pattern)


def binary_pad(
    input_file: str, output: Optional[str] = None, size: int = 1, pattern: str = "zeros"
) -> None:
    """Pad binary file to provided final size with specified pattern.

    :param input_file: Input file name.
    :param output: Output file name. If not specified, input file will be updated.
    :param size: Final size of file.
    :param pattern: Padding pattern.

    :raises SPSDKError: Invalid input arguments.
    """
    if not input_file or not os.path.isfile(input_file):
        raise SPSDKError("Binary file alignment: Invalid input file")
    if not output:
        output = input_file
    binary = load_binary(input_file)
    if len(binary) < size:
        binary = align_block(data=binary, alignment=size, padding=pattern)
    write_file(binary, output, mode="wb")

    click.echo(
        f"The {input_file} file has been padded to {len(binary)} ({hex(len(binary))}) byte size\
            and stored into {output}"
    )


@utils_group.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(resolve_path=True))
def convert_hex2bin_command(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.
    """
    convert_hex2bin(input_file, reverse, output)


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."""
    try:
        value = bytearray.fromhex(load_text(input_file))
    except (SPSDKError, ValueError) as e:
        raise SPSDKAppError(f"Failed loading hexadecimal value from: {input_file}") from e
    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,
    short_help="Convert binary file to hexadecimal text file.",
    help="Convert binary file into hexadecimal text file with optional reverse order of stored bytes.",
)
@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(resolve_path=True))
def convert_bin2hex_command(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.
    """
    convert_bin2hex(input_file, reverse, output)


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."""
    value = bytearray(load_binary(input_file))
    if reverse:
        value.reverse()
    write_file(value.hex(), output, mode="w")
    click.echo(f"Success. Converted file: {output}")


@convert.command(name="bin2carr", no_args_is_help=True)
@click.option(
    "-i",
    "--input-file",
    type=click.Path(exists=True, readable=True),
    required=True,
    help="Path to binary file to be converted to C array.",
)
@click.option(
    "-n",
    "--name",
    type=str,
    help="The output C array name, if not specified the name of input file will be used.",
)
@click.option(
    "-t",
    "--type",
    "output_type",
    type=click.Choice(["uint8_t", "uint16_t", "uint32_t", "uint64_t"]),
    default="uint8_t",
    help="The output C array type.",
)
@click.option(
    "-e",
    "--endian",
    type=click.Choice(["big", "little"]),
    default="big",
    help="The binary input file endian.",
)
@click.option(
    "-p",
    "--padding",
    type=str,
    help="The padding value in case of non aligned binary image.",
)
@click.option(
    "-c",
    "--count-per-line",
    type=click.IntRange(1, 1024),
    default=8,
    help="The array items count per line.",
)
@click.option(
    "--tab",
    type=click.IntRange(0, 32),
    default=4,
    help="The tabulator size for new line.",
)
@click.option(
    "-o",
    "--output-file",
    type=click.Path(resolve_path=True),
    help=(
        "Path to output text file with created C array, "
        "if not specified the output will be printed into standard output."
    ),
)
def convert_bin2carr(
    input_file: str,
    name: str,
    output_type: str,
    endian: str,
    padding: str,
    count_per_line: int,
    tab: int,
    output_file: str,
) -> None:
    """Convert binary file into C array string.

    Default output is byte representation, but it could be
    converted to 16/32 or 64 bit unsigned types with specified endianness.
    """
    raw_binary = load_binary(input_file)
    width = {"uint8_t": 1, "uint16_t": 2, "uint32_t": 4, "uint64_t": 8}[output_type]

    if len(raw_binary) != align(len(raw_binary), width) and padding is None:
        raise SPSDKAppError("Unaligned binary image, if still to be used, define '-p' padding.")

    binary = align_block(
        data=raw_binary, alignment=width, padding=BinaryPattern(padding) if padding else None
    )
    rest_bytes = len(binary)
    count = rest_bytes // width
    name = name or os.path.splitext(os.path.basename(input_file))[0]
    index = 0
    ret = f"const {output_type} {name}[{count}] = {{\n"
    while rest_bytes:
        line_cnt = min(count_per_line, rest_bytes // width)
        comment = f"// 0x{index:09_X}"
        ret += " " * tab
        for _ in range(line_cnt):
            data = bytearray(binary[index : index + width])
            if endian.lower() == "little":
                data.reverse()
            ret += "0x" + data.hex() + ", "
            index += width
            rest_bytes -= width
        ret += f" {comment}\n"
    ret += "};\n"

    if output_file:
        write_file(ret, output_file)
        click.echo(f"Success. Created C file: {output_file}")
    else:
        click.echo(ret)


@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()
