#!/usr/bin/env bash

set -e

# Locale can change behavior of utilities like 'sort' but we want the output
# to be stable on all machines.  Force the locale to 'C' for consistency.
export LC_ALL=C

function show_usage() {
    cat \
        << EOF
Usage: $(basename "$0") [options] <command-args>*

Generate meson.build files from a directory tree containing YAML files and
facilitate building the sdbus++ sources.

Options:
    --help              - Display this message
    --command <cmd>     - Command mode to execute (default 'meson').
    --directory <path>  - Root directory of the YAML source (default '.').
    --output <path>     - Root directory of the output (default '.').
    --tool <path>       - Path to the processing tool (default 'sdbus++').
    --version           - Display this tool's version string.

Commands:
    meson               - Generate a tree of meson.build files corresponding
                          to the source YAML files.
    cpp <intf>          - Generate the source files from a YAML interface.
    markdown <intf>     - Generate the markdown files from a YAML interface.
    registry <intf>     - Generate the Redfish registry from a YAML interface.
    version             - Display this tool's version string.

EOF
}

## The version is somewhat arbitrary but is used to create a warning message
## if a repository contains old copies of the generated meson.build files and
## needs an update.  We should increment the version number whenever the
## resulting meson.build would change.
tool_version="sdbus++-gen-meson version 10"
function show_version() {
    echo "${tool_version}"
}

# Set up defaults.
sdbuspp="sdbus++"
outputdir="."
cmd="meson"
rootdir="."

# Parse options.
options="$(getopt -o hc:d:o:t:v --long help,command:,directory:,output:,tool:,version -- "$@")"
eval set -- "${options}"

while true; do
    case "$1" in
        -h | --help)
            show_usage
            exit
            ;;

        -c | --command)
            shift
            cmd="$1"
            shift
            ;;

        -d | --directory)
            shift
            rootdir="$1"
            shift
            ;;

        -o | --output)
            shift
            outputdir="$1"
            shift
            ;;

        -t | --tool)
            shift
            sdbuspp="$1"
            shift
            ;;

        -v | --version)
            show_version
            exit
            ;;

        --)
            shift
            break
            ;;

        *)
            echo "Invalid argument $1"
            exit 1
            ;;
    esac
done

## Create an initially empty meson.build file.
## $1 - path to create meson.build at.
function meson_empty_file() {
    mkdir -p "$1"
    echo "# Generated file; do not modify." > "$1/meson.build"
}

## Create the root-level meson.build
##
## Inserts rules to run the available version of this tool to ensure the
## version has not changed.
function meson_create_root() {
    meson_empty_file "${outputdir}"

    cat >> "${outputdir}/meson.build" \
        << EOF
sdbuspp_gen_meson_ver = run_command(
    sdbuspp_gen_meson_prog,
    '--version',
    check: true,
).stdout().strip().split('\n')[0]

if sdbuspp_gen_meson_ver != '${tool_version}'
    warning('Generated meson files from wrong version of sdbus++-gen-meson.')
    warning(
        'Expected "${tool_version}", got:',
        sdbuspp_gen_meson_ver
    )
endif

inst_markdown_dir = get_option('datadir') / 'doc' / meson.project_name()
inst_registry_dir = get_option('datadir') / 'redfish-registry' / meson.project_name()

generated_sources = []
generated_markdown = []
generated_registry = []

foreach d : yaml_selected_subdirs
    subdir(d)
endforeach

generated_headers = []
foreach s : generated_sources
    foreach f : s.to_list()
        if f.full_path().endswith('.hpp')
            generated_headers += f
        endif
    endforeach
endforeach

EOF
}

## hash-tables to store:
##      meson_paths - list of subdirectory paths for which an empty meson.build
##                    has already been created.
##      interfaces - list of interface paths which a YAML has been found and
##                   which YAML types (interface, errors, etc.).
declare -A meson_paths
declare -A interfaces

## Ensure the meson.build files to a path have been created.
## $1 - The path requiring to be created.
function meson_create_path() {

    meson_path="${outputdir}"
    prev_meson_path=""

    # Split the path into segments.
    for part in $(echo "$1" | tr '/' '\n'); do
        prev_meson_path="${meson_path}"
        meson_path="${meson_path}/${part}"

        # Create the meson.build for this segment if it doesn't already exist.
        if [[ "" == "${meson_paths[${meson_path}]}" ]]; then
            meson_paths["${meson_path}"]="1"
            meson_empty_file "${meson_path}"

            # Add the 'subdir' link into the parent's meson.build.
            # We need to skip adding the links into the 'root' meson.build
            # because most repositories want to selectively add TLDs based
            # on config flags.  Let them figure out their own logic for that.
            if [[ ${outputdir} != "${prev_meson_path}" ]]; then
                echo "subdir('${part}')" >> "${prev_meson_path}/meson.build"
            fi
        fi
    done
}

## Add the 'sdbusplus_current_path' to the meson file.
##
## $1 - The path to insert into.
##
## This is done after the subdir processing because we need to leave
## the meson variable in this state for the install directives.
function meson_insert_current_path() {
    meson_path="${outputdir}/${1}"
    current_path="${1}"
    cat >> "${meson_path}/meson.build" \
        << EOF

sdbusplus_current_path = '${current_path}'

EOF
}

## Generate the meson target for the source files (.cpp/.hpp) from a YAML
## interface.
##
## $1 - The interface to generate a target for.
function meson_cpp_target() {
    mesondir="${outputdir}/$1"
    yamldir="$(realpath --relative-to="${mesondir}" "${rootdir}")"
    nl=$'\n'
    ind="        "

    # Determine the source and output files based on the YAMLs present.
    sources=""
    outputs=""
    install=""
    for s in ${interfaces[$1]}; do
        sources="${sources}${ind}'${yamldir}/$1.${s}',${nl}"

        case "${s}" in
            errors.yaml)
                outputs="${outputs}${ind}'error.cpp',${nl}"
                install="${install}${ind}false,${nl}"

                outputs="${outputs}${ind}'error.hpp',${nl}"
                install="${install}${ind}get_option('includedir') / sdbusplus_current_path,${nl}"
                ;;

            events.yaml)
                outputs="${outputs}${ind}'event.cpp',${nl}"
                install="${install}${ind}false,${nl}"

                outputs="${outputs}${ind}'event.hpp',${nl}"
                install="${install}${ind}get_option('includedir') / sdbusplus_current_path,${nl}"
                ;;

            interface.yaml)
                outputs="${outputs}${ind}'common.hpp',${nl}"
                install="${install}${ind}get_option('includedir') / sdbusplus_current_path,${nl}"

                outputs="${outputs}${ind}'server.hpp',${nl}"
                install="${install}${ind}get_option('includedir') / sdbusplus_current_path,${nl}"

                outputs="${outputs}${ind}'server.cpp',${nl}"
                install="${install}${ind}false,${nl}"

                outputs="${outputs}${ind}'aserver.hpp',${nl}"
                install="${install}${ind}get_option('includedir') / sdbusplus_current_path,${nl}"

                outputs="${outputs}${ind}'client.hpp',${nl}"
                install="${install}${ind}get_option('includedir') / sdbusplus_current_path,${nl}"
                ;;

            *)
                echo "Unknown interface type: ${s}"
                exit 1
                ;;
        esac
    done

    # Create the target to generate the 'outputs'.
    cat >> "${mesondir}/meson.build" \
        << EOF
generated_sources += custom_target(
    '$1__cpp'.underscorify(),
    input: [
${sources}    ],
    output: [
${outputs}    ],
    depend_files: sdbusplusplus_depfiles,
    command: [
        sdbuspp_gen_meson_prog, '--command', 'cpp',
        '--output', meson.current_build_dir(),
        '--tool', sdbusplusplus_prog,
        '--directory', meson.current_source_dir() / '${yamldir}',
        '$1',
    ],
    install: should_generate_cpp,
    install_dir: [
${install}    ],
    build_by_default: should_generate_cpp,
)

EOF
}

## Generate the meson target for the markdown files from a YAML interface.
## $1 - The interface to generate a target for.
function meson_md_target() {
    mesondir="${outputdir}/$(dirname "$1")"
    yamldir="$(realpath --relative-to="${mesondir}" "${rootdir}")"

    # Determine the source files based on the YAMLs present.
    sources=""
    for s in ${interfaces[$1]}; do
        sources="${sources}'${yamldir}/$1.${s}', "
    done

    # Create the target to generate the interface.md file.
    cat >> "${mesondir}/meson.build" \
        << EOF
generated_markdown += custom_target(
    '$1__markdown'.underscorify(),
    input: [ ${sources} ],
    output: [ '$(basename "$1").md' ],
    depend_files: sdbusplusplus_depfiles,
    command: [
        sdbuspp_gen_meson_prog, '--command', 'markdown',
        '--output', meson.current_build_dir(),
        '--tool', sdbusplusplus_prog,
        '--directory', meson.current_source_dir() / '${yamldir}',
        '$1',
    ],
    install: should_generate_markdown,
    install_dir: [inst_markdown_dir / sdbusplus_current_path],
    build_by_default: should_generate_markdown,
)

EOF
}

## Generate the meson target for the registry files from a YAML interface.
## $1 - The interface to generate a target for.
function meson_registry_target() {
    mesondir="${outputdir}/$(dirname "$1")"
    yamldir="$(realpath --relative-to="${mesondir}" "${rootdir}")"

    # Determine the source and output files based on the YAMLs present.
    sources=""
    outputs=""
    for s in ${interfaces[$1]}; do
        case "${s}" in
            errors.yaml)
                ;;

            events.yaml)
                sources="${sources}'${yamldir}/$1.${s}', "
                outputs="${outputs}'event.cpp', 'event.hpp', "
                ;;

            interface.yaml)
                ;;

            *)
                echo "Unknown interface type: ${s}"
                exit 1
                ;;
        esac
    done

    if [[ -z "${sources}" ]]; then
        return
    fi

    # Create the target to generate the interface.md file.
    cat >> "${mesondir}/meson.build" \
        << EOF
generated_registry += custom_target(
    '$1__registry'.underscorify(),
    input: [ ${sources} ],
    output: [ '$(basename "$1").json' ],
    depend_files: sdbusplusplus_depfiles,
    command: [
        sdbuspp_gen_meson_prog, '--command', 'registry',
        '--output', meson.current_build_dir(),
        '--tool', sdbusplusplus_prog,
        '--directory', meson.current_source_dir() / '${yamldir}',
        '$1',
    ],
    install: should_generate_registry,
    install_dir: [inst_registry_dir / sdbusplus_current_path],
    build_by_default: should_generate_registry,
)

EOF
}


## Handle command=meson by generating the tree of meson.build files.
function cmd_meson() {
    # Find and sort all the YAML files
    yamls="$(find "${rootdir}" -name '*.interface.yaml' -o -name '*.errors.yaml' -o -name '*.events.yaml')"
    yamls="$(echo "${yamls}" | sort)"

    # Assign the YAML files into the hash-table by interface name.
    for y in ${yamls}; do
        rel="$(realpath "--relative-to=${rootdir}" "${y}")"
        dir="$(dirname "${rel}")"
        ext="${rel#*.}"
        base="$(basename "${rel}" ".${ext}")"
        key="${dir}/${base}"

        interfaces["${key}"]="${interfaces[${key}]} ${ext}"
    done

    # Create the meson.build files.
    meson_create_root
    # shellcheck disable=SC2312
    sorted_ifaces="$(echo "${!interfaces[@]}" | tr " " "\n" | sort)"
    iface_parent_dirs="$(echo "${sorted_ifaces}" | xargs -n1 dirname)"
    # shellcheck disable=SC2312
    sorted_dirs="$(echo "${sorted_ifaces} ${iface_parent_dirs}" | tr " " "\n" | sort | uniq)"
    for i in ${sorted_ifaces}; do
        meson_create_path "${i}"
    done
    for i in ${sorted_dirs}; do
        meson_insert_current_path "${i}"
    done
    for i in ${sorted_ifaces}; do
        meson_cpp_target "${i}"
        meson_md_target "${i}"
        meson_registry_target "${i}"
    done
}

## Handle command=cpp by calling sdbus++ as appropriate.
## $1 - interface to generate.
##
## For an interface foo/bar, the outputdir is expected to be foo/bar.
function cmd_cpp() {

    if [[ "" == "$1" ]]; then
        show_usage
        exit 1
    fi

    if [[ ! -e "${rootdir}/$1.interface.yaml" ]] &&
    [[ ! -e "${rootdir}/$1.errors.yaml" ]] &&
    [[ ! -e "${rootdir}/$1.events.yaml" ]]; then
        echo "Missing YAML for $1."
        exit 1
    fi

    mkdir -p "${outputdir}"

    sdbusppcmd="${sdbuspp} -r ${rootdir}"
    intf="${1//\//.}"

    if [[ -e "${rootdir}/$1.interface.yaml" ]]; then
        ${sdbusppcmd} interface common-header "${intf}" > "${outputdir}/common.hpp"
        ${sdbusppcmd} interface server-header "${intf}" > "${outputdir}/server.hpp"
        ${sdbusppcmd} interface server-cpp "${intf}" > "${outputdir}/server.cpp"
        ${sdbusppcmd} interface client-header "${intf}" > "${outputdir}/client.hpp"
        ${sdbusppcmd} interface aserver-header "${intf}" > "${outputdir}/aserver.hpp"
    fi

    if [[ -e "${rootdir}/$1.errors.yaml" ]]; then
        ${sdbusppcmd} error exception-header "${intf}" > "${outputdir}/error.hpp"
        ${sdbusppcmd} error exception-cpp "${intf}" > "${outputdir}/error.cpp"
    fi

    if [[ -e "${rootdir}/$1.events.yaml" ]]; then
        ${sdbusppcmd} event exception-header "${intf}" > "${outputdir}/event.hpp"
        ${sdbusppcmd} event exception-cpp "${intf}" > "${outputdir}/event.cpp"
    fi
}

## Handle command=markdown by calling sdbus++ as appropriate.
## $1 - interface to generate.
##
## For an interface foo/bar, the outputdir is expected to be foo.
function cmd_markdown() {

    if [[ "" == "$1" ]]; then
        show_usage
        exit 1
    fi

    if [[ ! -e "${rootdir}/$1.interface.yaml" ]] &&
    [[ ! -e "${rootdir}/$1.errors.yaml" ]] &&
    [[ ! -e "${rootdir}/$1.events.yaml" ]]; then
        echo "Missing YAML for $1."
        exit 1
    fi

    mkdir -p "${outputdir}"

    sdbusppcmd="${sdbuspp} -r ${rootdir}"
    intf="${1//\//.}"
    base="$(basename "$1")"

    echo -n > "${outputdir}/${base}.md"
    if [[ -e "${rootdir}/$1.interface.yaml" ]]; then
        ${sdbusppcmd} interface markdown "${intf}" >> "${outputdir}/${base}.md"
    fi

    if [[ -e "${rootdir}/$1.errors.yaml" ]]; then
        ${sdbusppcmd} error markdown "${intf}" >> "${outputdir}/${base}.md"
    fi

    if [[ -e "${rootdir}/$1.events.yaml" ]]; then
        ${sdbusppcmd} event markdown "${intf}" >> "${outputdir}/${base}.md"
    fi
}

## Handle command=registry by calling sdbus++ as appropriate.
## $1 - interface to generate.
##
## For an interface foo/bar, the outputdir is expected to be foo.
function cmd_registry() {

    if [[ "" == "$1" ]]; then
        show_usage
        exit 1
    fi

    if [[ ! -e "${rootdir}/$1.events.yaml" ]]; then
        echo "Missing YAML for $1."
        exit 1
    fi

    mkdir -p "${outputdir}"

    sdbusppcmd="${sdbuspp} -r ${rootdir}"
    intf="${1//\//.}"
    base="$(basename "$1")"

    if [[ -e "${rootdir}/$1.events.yaml" ]]; then
        ${sdbusppcmd} event exception-registry "${intf}" > "${outputdir}/${base}.json"
    fi
}


## Handle command=version.
function cmd_version() {
    show_version
}

"cmd_${cmd}" "$*"
