#!/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.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_file,
    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"
    )


@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.argument("config", type=click.Path(exists=True, readable=True))
def otfad_export(alignment: int, config: str) -> 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)
    schemas = OtfadNxp.get_validation_schemas()
    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}")

    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"
        "  -  Example of BD file to simplify creating the SB2.1 file from the OTFAD source files."
        " 'otfad_whole_image.bin'.\n"
        "  -  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)}"
    )

    bd_file_sources = "sources {"
    bd_file_section0 = "section (0) {"

    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()}"
        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)}"

    bd_file_sources += "\n}\n"
    bd_file_section0 += "\n}\n"
    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}")

    bd_file_name = os.path.join(output_folder, "sb21_otfad_example.bd")

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

    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 it 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, omk, 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 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()
