# SPDX-License-Identifier: GPL-2.0+
# Copyright (c) 2022-2023 Texas Instruments Incorporated - https://www.ti.com/
# Written by Neha Malcom Francis <n-francis@ti.com>
#
# Entry-type module for generating schema validated TI board
# configuration binary
#

import os
import struct
import yaml

from collections import OrderedDict
from jsonschema import validate
from shutil import copyfileobj

from binman.entry import Entry
from binman.etype.section import Entry_section
from dtoc import fdt_util
from u_boot_pylib import tools

BOARDCFG = 0xB
BOARDCFG_SEC = 0xD
BOARDCFG_PM = 0xE
BOARDCFG_RM = 0xC
BOARDCFG_NUM_ELEMS = 4

class Entry_ti_board_config(Entry_section):
    """An entry containing a TI schema validated board config binary

    This etype supports generation of two kinds of board configuration
    binaries: singular board config binary as well as combined board config
    binary.

    Properties / Entry arguments:
        - config-file: File containing board configuration data in YAML
        - schema-file: File containing board configuration YAML schema against
          which the config file is validated

    Output files:
        - board config binary: File containing board configuration binary

    These above parameters are used only when the generated binary is
    intended to be a single board configuration binary. Example::

        my-ti-board-config {
            ti-board-config {
                config = "board-config.yaml";
                schema = "schema.yaml";
            };
        };

    To generate a combined board configuration binary, we pack the
    needed individual binaries into a ti-board-config binary. In this case,
    the available supported subnode names are board-cfg, pm-cfg, sec-cfg and
    rm-cfg. The final binary is prepended with a header containing details about
    the included board config binaries. Example::

        my-combined-ti-board-config {
            ti-board-config {
                board-cfg {
                    config = "board-cfg.yaml";
                    schema = "schema.yaml";
                };
                sec-cfg {
                    config = "sec-cfg.yaml";
                    schema = "schema.yaml";
                };
            }
        }
    """
    def __init__(self, section, etype, node):
        super().__init__(section, etype, node)
        self._config = None
        self._schema = None
        self._entries = OrderedDict()
        self._num_elems = BOARDCFG_NUM_ELEMS
        self._fmt = '<HHHBB'
        self._index = 0
        self._binary_offset = 0
        self._sw_rev = 1
        self._devgrp = 0

    def ReadNode(self):
        super().ReadNode()
        self._config = fdt_util.GetString(self._node, 'config')
        self._schema = fdt_util.GetString(self._node, 'schema')
        # Depending on whether config file is present in node, we determine
        # whether it is a combined board config binary or not
        if self._config is None:
            self.ReadEntries()
        else:
            self._config_file = tools.get_input_filename(self._config)
            self._schema_file = tools.get_input_filename(self._schema)

    def ReadEntries(self):
        """Read the subnodes to find out what should go in this image
        """
        for node in self._node.subnodes:
            if 'type' not in node.props:
                entry = Entry.Create(self, node, 'ti-board-config')
                entry.ReadNode()
                cfg_data = entry.BuildSectionData(True)
                entry._cfg_data = cfg_data
                self._entries[entry.name] = entry
        self._num_elems = len(self._node.subnodes)

    def _convert_to_byte_chunk(self, val, data_type):
        """Convert value into byte array

        Args:
            val: value to convert into byte array
            data_type: data type used in schema, supported data types are u8,
                u16 and u32

        Returns:
            array of bytes representing value
        """
        size = 0
        if (data_type == '#/definitions/u8'):
            size = 1
        elif (data_type == '#/definitions/u16'):
            size = 2
        else:
            size = 4
        if type(val) == int:
            br = val.to_bytes(size, byteorder='little')
        return br

    def _compile_yaml(self, schema_yaml, file_yaml):
        """Convert YAML file into byte array based on YAML schema

        Args:
            schema_yaml: file containing YAML schema
            file_yaml: file containing config to compile

        Returns:
            array of bytes repesenting YAML file against YAML schema
        """
        br = bytearray()
        for key, node in file_yaml.items():
            node_schema = schema_yaml['properties'][key]
            node_type = node_schema.get('type')
            if not 'type' in node_schema:
                br += self._convert_to_byte_chunk(node,
                                                node_schema.get('$ref'))
            elif node_type == 'object':
                br += self._compile_yaml(node_schema, node)
            elif node_type == 'array':
                for item in node:
                    if not isinstance(item, dict):
                        br += self._convert_to_byte_chunk(
                            item, schema_yaml['properties'][key]['items']['$ref'])
                    else:
                        br += self._compile_yaml(node_schema.get('items'), item)
        return br

    def _generate_binaries(self):
        """Generate config binary artifacts from the loaded YAML configuration file

        Returns:
            byte array containing config binary artifacts
            or None if generation fails
        """
        cfg_binary = bytearray()
        for key, node in self.file_yaml.items():
            node_schema = self.schema_yaml['properties'][key]
            br = self._compile_yaml(node_schema, node)
            cfg_binary += br
        return cfg_binary

    def _add_boardcfg(self, bcfgtype, bcfgdata):
        """Add board config to combined board config binary

        Args:
            bcfgtype (int): board config type
            bcfgdata (byte array): board config data
        """
        size = len(bcfgdata)
        desc = struct.pack(self._fmt, bcfgtype,
                            self._binary_offset, size, self._devgrp, 0)
        with open(self.descfile, 'ab+') as desc_fh:
            desc_fh.write(desc)
        with open(self.bcfgfile, 'ab+') as bcfg_fh:
            bcfg_fh.write(bcfgdata)
        self._binary_offset += size
        self._index += 1

    def _finalize(self):
        """Generate final combined board config binary

        Returns:
            byte array containing combined board config data
            or None if unable to generate
        """
        with open(self.descfile, 'rb') as desc_fh:
            with open(self.bcfgfile, 'rb') as bcfg_fh:
                with open(self.fh_file, 'ab+') as fh:
                    copyfileobj(desc_fh, fh)
                    copyfileobj(bcfg_fh, fh)
        data = tools.read_file(self.fh_file)
        return data

    def BuildSectionData(self, required):
        if self._config is None:
            self._binary_offset = 0
            uniq = self.GetUniqueName()
            self.fh_file = tools.get_output_filename('fh.%s' % uniq)
            self.descfile = tools.get_output_filename('desc.%s' % uniq)
            self.bcfgfile = tools.get_output_filename('bcfg.%s' % uniq)

            # when binman runs again make sure we start clean
            if os.path.exists(self.fh_file):
                os.remove(self.fh_file)
            if os.path.exists(self.descfile):
                os.remove(self.descfile)
            if os.path.exists(self.bcfgfile):
                os.remove(self.bcfgfile)

            with open(self.fh_file, 'wb') as f:
                t_bytes = f.write(struct.pack(
                    '<BB', self._num_elems, self._sw_rev))
            self._binary_offset += t_bytes
            self._binary_offset += self._num_elems * struct.calcsize(self._fmt)

            if 'board-cfg' in self._entries:
                self._add_boardcfg(BOARDCFG, self._entries['board-cfg']._cfg_data)

            if 'sec-cfg' in self._entries:
                self._add_boardcfg(BOARDCFG_SEC, self._entries['sec-cfg']._cfg_data)

            if 'pm-cfg' in self._entries:
                self._add_boardcfg(BOARDCFG_PM, self._entries['pm-cfg']._cfg_data)

            if 'rm-cfg' in self._entries:
                self._add_boardcfg(BOARDCFG_RM, self._entries['rm-cfg']._cfg_data)

            data = self._finalize()
            return data

        else:
            with open(self._config_file, 'r') as f:
                self.file_yaml = yaml.safe_load(f)
            with open(self._schema_file, 'r') as sch:
                self.schema_yaml = yaml.safe_load(sch)

            try:
                validate(self.file_yaml, self.schema_yaml)
            except Exception as e:
                self.Raise(f"Schema validation error: {e}")

            data = self._generate_binaries()
            return data

    def SetImagePos(self, image_pos):
        Entry.SetImagePos(self, image_pos)

    def CheckEntries(self):
        Entry.CheckEntries(self)
