#!/usr/bin/env python3

# Copyright (c) 2025 Eclipse Foundation
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

import argparse
import collections.abc
import os
import shlex
import sys

import yaml

_DEFAULT_CONFIG_FILE = 'cve2_configs.yaml'


class ConfigException(Exception):
    pass


def _verify_config(name, config_dict):
    """Checks a config_dict matches expectations.

    A config_dict is the dictionary mapping parameters to values for a
    particular config, it must obey the following rules:
        - It's a mapping object e.g. OrderedDict
        - Its values can only be strings, integers or booleans

    Args:
        name: The name of the config being checked (used to form useful error
        messages)
        config_dict: The config_dict to check, must be a mapping object

    Returns:
        Nothing, an exception is thrown if an issue is found

    Raises:
        ConfigException: An issue was found with config_dict
    """

    if not isinstance(config_dict, collections.abc.Mapping):
        raise ConfigException('Config ' + name +
                              ' must have dictionary giving parameters')

    for k, v in config_dict.items():
        if isinstance(v, int):
            continue
        if isinstance(v, str):
            continue
        if isinstance(v, bool):
            continue

        raise ConfigException('Parameter ' + k + ' for config ' + name +
                              ' must be string, int or bool got ' +
                              str(type(v)))


def _verify_config_parameters(config_dicts):
    """Verifies all parameters across config_dicts match expectations.

    Each configuration must obey the following fules:
        - Each config has the same set of parameters specified

    Args:
        config_dicts: A dictionary of configurations, maps from configuration
        name to a configuration (itself a dictionary)

    Returns:
        Nothing, an exception is thrown if an issue is found

    Raises:
        ConfigException: An issue was found with config_dicts
    """

    parameters = set()

    first = True

    for name, config_dict in config_dicts.items():
        parameters_this_config = set()

        for parameter, value in config_dict.items():
            if first:
                parameters.add(parameter)

            parameters_this_config.add(parameter)

        if first:
            first = False
        else:
            parameter_difference = parameters ^ parameters_this_config
            if parameter_difference:
                raise ConfigException('Config ' + name +
                                      ' has differing parameters ' +
                                      ','.join(parameter_difference))


def get_config_dicts(config_file):
    """Extracts a dictionary of configuration dictionaries from a file object

    Given a file object parses YAML from it to obtain a dictionary of
    configurations

    Args:
        config_file: A file object for a file containing the YAML configuration
        file

    Returns:
        A dictionary of configurations, maps from a configuration name to a
        configuration (itself a dictionary mapping parameters to values)

    Raises:
        ConfigException: An issue was found with the configuration file
    """

    try:
        config_yaml = yaml.load(config_file, Loader=yaml.SafeLoader)
    except yaml.YAMLError as e:
        raise ConfigException('Could not decode yaml:\n' + str(e))

    for k, v in config_yaml.items():
        _verify_config(k, v)

    _verify_config_parameters(config_yaml)

    return config_yaml


class FusesocOpts:
    def setup_args(self, arg_subparser):
        output_argparser = arg_subparser.add_parser(
            'fusesoc_opts', help=('Outputs options for fusesoc'))
        output_argparser.set_defaults(output_fn=self.output)

    def output(self, config_dict, args):
        fusesoc_cmd = []
        for parameter, value in config_dict.items():
            if isinstance(value, bool):
                # For fusesoc boolean parameters are set to true if given on the
                # command line otherwise false. It doesn't support an explicit
                # --param=True style
                if value:
                    fusesoc_cmd.append(shlex.quote('--' + parameter))
            else:
                fusesoc_cmd.append(
                    shlex.quote('--' + parameter + '=' + str(value)))

        return ' '.join(fusesoc_cmd)


class SimOpts:
    def __init__(self, cmd_name, description, param_set_fn, define_set_fn,
                 hierarchy_sep):
        self.cmd_name = cmd_name
        self.description = description
        self.param_set_fn = param_set_fn
        self.define_set_fn = define_set_fn
        self.hierarchy_sep = hierarchy_sep

    def setup_args(self, arg_subparser):
        output_argparser = arg_subparser.add_parser(
            self.cmd_name,
            help=('Outputs options for {0}'.format(self.description)))

        output_argparser.add_argument(
            '--ins_hier_path',
            help=('Hierarchical path to the instance to set '
                  'configuration parameters on'),
            default='')
        output_argparser.add_argument(
            '--string_define_prefix',
            help=('Prefix to add to defines that are used to '
                  'pass string parameters'),
            default='')
        output_argparser.set_defaults(output_fn=self.output)

    def output(self, config_dict, args):
        if (args.ins_hier_path != ''):
            ins_hier_path = args.ins_hier_path + self.hierarchy_sep
        else:
            ins_hier_path = ''

        sim_opts = []

        for parameter, value in config_dict.items():
            if isinstance(value, str):
                parameter_define = args.string_define_prefix + parameter
                define_set_str = self.define_set_fn(parameter_define, value)

                if define_set_str:
                    sim_opts.append(shlex.quote(define_set_str))
            else:
                if isinstance(value, bool):
                    val_str = '1' if value else '0'
                else:
                    val_str = str(value)

                param_set_str = self.param_set_fn(ins_hier_path + parameter,
                                                  val_str)

                if param_set_str:
                    sim_opts.append(shlex.quote(param_set_str))

        return ' '.join(sim_opts)


def get_config_file_location():
    """Returns the location of the config file

    Default is _DEFAULT_CONFIG_FILE and the CVE2_CONFIG_FILE environment
    variable overrides the default"""

    return os.environ.get('CVE2_CONFIG_FILE', _DEFAULT_CONFIG_FILE)


def parse_config(config_name, config_filename):
    """Parses the selected config file and returns selected config information.

    Arguments:

        config_name: Name of the chosen CVE2 core config

        config_filename: Name of the configuration filename to be parsed

    Returns: the chosen CVE2 config as a YAML object.

    Raises a ConfigException if there are any error while parsing the YAML.

    Raises a FileNotFoundError if there are errors opening the chosen config file.
    """
    try:
        config_file = open(config_filename)
        config_dicts = get_config_dicts(config_file)

        if config_name not in config_dicts:
            print('ERROR: configuration {!r} not found in {!r}.'.format(
                  config_name, config_filename), file=sys.stderr)
            sys.exit(1)
        return config_dicts[config_name]
    except ConfigException as ce:
        print('ERROR: failure to process configuration from {!r} {!r}.'.format(
              config_filename, ce), file=sys.stderr)
        sys.exit(1)
    except FileNotFoundError:
        print('ERROR: could not find configuration file {!r}.'.format(
              config_filename), file=sys.stderr)
        sys.exit(1)


def main():
    outputters = [
        FusesocOpts(),
        SimOpts('vcs_opts', 'VCS compile',
                lambda p, v: '-pvalue+' + p + '=' + v,
                lambda d, v: '+define+' + d + '=' + v, '.'),
        SimOpts('riviera_sim_opts', 'Riviera simulate',
                lambda p, v: '-g/' + p + '=' + v,
                lambda d, v: None, '/'),
        SimOpts('riviera_compile_opts', 'Riviera compile',
                lambda p, v: None,
                lambda d, v: '+define+' + d + '=' + v, '/'),
        SimOpts('questa_sim_opts', 'Questa simulate',
                lambda p, v: '-g/' + p + '=' + v,
                lambda d, v: None, '/'),
        SimOpts('questa_compile_opts', 'Questa compile',
                lambda p, v: None,
                lambda d, v: '+define+' + d + '=' + v, '/'),
        SimOpts('xlm_opts', 'Xcelium compile',
                lambda p, v: '-defparam ' + p + '=' + v,
                lambda d, v: '-define ' + d + '=' + v, '.'),
        SimOpts('dsim_compile_opts', 'DSim compile',
                lambda p, v: '+define+' + p + '=' + v,
                lambda d, v: None, '/'),
    ]

    argparser = argparse.ArgumentParser(description=(
        'Outputs CVE2 configuration parameters for a named config in a number '
        'of formats.  If not specified on the command line the config will be '
        'read from {0}. This default can be overridden by setting the '
        'CVE2_CONFIG_FILE environment variable. Some output types support '
        'arguments to see help for them pass a config_name and an output type '
        'followed by --help').format(get_config_file_location()))

    argparser.add_argument('config_name',
                           help=('The name of the CVE2 '
                                 'configuration to output'))

    argparser.add_argument('--config_filename',
                           help='Config file to read',
                           default=get_config_file_location())

    arg_subparser = argparser.add_subparsers(
        help='Format to output the configuration parameters in',
        dest='output_fn',
        metavar='output_type')

    for outputter in outputters:
        outputter.setup_args(arg_subparser)

    args = argparser.parse_args()

    if args.output_fn is None:
        print('ERROR: No output format specified.')
        sys.exit(1)

    parsed_cve2_config = parse_config(args.config_name, args.config_filename)
    print(args.output_fn(parsed_cve2_config, args))

if __name__ == "__main__":
    main()
