# Copyright 2021 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Outputs the contents of blobs as a hard-coded arrays in a C++ library."""

import argparse
import itertools
import json
from pathlib import Path
from string import Template
import textwrap
from typing import (Any, Generator, Iterable, NamedTuple, Optional, Sequence,
                    Tuple)

COMMENT = f"""\
// This file was generated by {Path(__file__).name}.
//
// DO NOT EDIT!
//
// This file contains declarations for byte arrays created from files during the
// build. The byte arrays are constant initialized and are safe to access at any
// time, including before main().
//
// See https://pigweed.dev/pw_build/#pw-cc-blob-library for details.
"""

HEADER_PREFIX = COMMENT + textwrap.dedent("""\
    #pragma once

    #include <array>
    #include <cstddef>

    """)

SOURCE_PREFIX_TEMPLATE = Template(COMMENT + textwrap.dedent("""\

        #include "$header_path"

        #include <array>
        #include <cstddef>

        #include "pw_preprocessor/compiler.h"

        """))

NAMESPACE_OPEN_TEMPLATE = Template('namespace ${namespace} {\n')

NAMESPACE_CLOSE_TEMPLATE = Template('\n}  // namespace ${namespace}\n')

BLOB_DECLARATION_TEMPLATE = Template(
    '\nextern const std::array<std::byte, ${size_bytes}> ${symbol_name};\n')

LINKER_SECTION_TEMPLATE = Template(
    'PW_PLACE_IN_SECTION("${linker_section}")\n')

BLOB_DEFINITION_MULTI_LINE = Template(
    '\n${section_attr}'
    '${alignas}constexpr std::array<std::byte, ${size_bytes}> ${symbol_name}'
    ' = {\n${bytes_lines}\n};\n')

BYTES_PER_LINE = 4


class Blob(NamedTuple):
    symbol_name: str
    file_path: Path
    linker_section: Optional[str]
    alignas: Optional[str] = None

    @staticmethod
    def from_dict(blob_dict: dict) -> 'Blob':
        return Blob(blob_dict['symbol_name'], Path(blob_dict['file_path']),
                    blob_dict.get('linker_section'), blob_dict.get('alignas'))


def parse_args() -> dict:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--blob-file',
                        type=Path,
                        required=True,
                        help=('Path to json file containing the list of blobs '
                              'to generate.'))
    parser.add_argument('--header-include',
                        required=True,
                        help='Path to use in #includes for the header')
    parser.add_argument('--out-source',
                        type=Path,
                        required=True,
                        help='Path at which to write .cpp file')
    parser.add_argument('--out-header',
                        type=Path,
                        required=True,
                        help='Path at which to write .h file')
    parser.add_argument('--namespace',
                        type=str,
                        required=False,
                        help=('Optional namespace that blobs will be scoped'
                              'within.'))

    return vars(parser.parse_args())


def split_into_chunks(
        data: Iterable[Any],
        chunk_size: int) -> Generator[Tuple[Any, ...], None, None]:
    """Splits an iterable into chunks of a given size."""
    data_iterator = iter(data)
    chunk = tuple(itertools.islice(data_iterator, chunk_size))
    while chunk:
        yield chunk
        chunk = tuple(itertools.islice(data_iterator, chunk_size))


def header_from_blobs(blobs: Iterable[Blob],
                      namespace: Optional[str] = None) -> str:
    """Generate the contents of a C++ header file from blobs."""
    lines = [HEADER_PREFIX]
    if namespace:
        lines.append(NAMESPACE_OPEN_TEMPLATE.substitute(namespace=namespace))
    for blob in blobs:
        data = blob.file_path.read_bytes()
        lines.append(
            BLOB_DECLARATION_TEMPLATE.substitute(symbol_name=blob.symbol_name,
                                                 size_bytes=len(data)))
    if namespace:
        lines.append(NAMESPACE_CLOSE_TEMPLATE.substitute(namespace=namespace))

    return ''.join(lines)


def array_def_from_blob_data(blob: Blob, blob_data: bytes) -> str:
    """Generates an array definition for the given blob data."""
    if blob.linker_section:
        section_attr = LINKER_SECTION_TEMPLATE.substitute(
            linker_section=blob.linker_section)
    else:
        section_attr = ''

    byte_strs = ['std::byte{{0x{:02X}}}'.format(b) for b in blob_data]

    lines = []
    for byte_strs_for_line in split_into_chunks(byte_strs, BYTES_PER_LINE):
        bytes_segment = ', '.join(byte_strs_for_line)
        lines.append(f'    {bytes_segment},')

    return BLOB_DEFINITION_MULTI_LINE.substitute(
        section_attr=section_attr,
        alignas=f'alignas({blob.alignas}) ' if blob.alignas else '',
        symbol_name=blob.symbol_name,
        size_bytes=len(blob_data),
        bytes_lines='\n'.join(lines))


def source_from_blobs(blobs: Iterable[Blob],
                      header_path: str,
                      namespace: Optional[str] = None) -> str:
    """Generate the contents of a C++ source file from blobs."""
    lines = [SOURCE_PREFIX_TEMPLATE.substitute(header_path=header_path)]
    if namespace:
        lines.append(NAMESPACE_OPEN_TEMPLATE.substitute(namespace=namespace))
    for blob in blobs:
        data = blob.file_path.read_bytes()
        lines.append(array_def_from_blob_data(blob, data))
    if namespace:
        lines.append(NAMESPACE_CLOSE_TEMPLATE.substitute(namespace=namespace))

    return ''.join(lines)


def load_blobs(blob_file: Path) -> Sequence[Blob]:
    with blob_file.open() as blob_fp:
        return [Blob.from_dict(blob) for blob in json.load(blob_fp)]


def main(blob_file: Path,
         header_include: str,
         out_source: Path,
         out_header: Path,
         namespace: Optional[str] = None) -> None:
    blobs = load_blobs(blob_file)
    out_header.write_text(header_from_blobs(blobs, namespace))
    out_source.write_text(source_from_blobs(blobs, header_include, namespace))


if __name__ == '__main__':
    main(**parse_args())

@@ -6436,16 +6436,74 @@
 ob_file)
+%0A%0A    out_header.parent.mkdir(parents=True, exist_ok=True)
 %0A    out
@@ -6553,24 +6553,82 @@
  namespace))
+%0A%0A    out_source.parent.mkdir(parents=True, exist_ok=True)
 %0A    out_sou

Create parent directories for output files