#!/usr/bin/env python3

"""This routine parses plain-text parameter files that list runtime
parameters for use in our codes.  The general format of a parameter
is:

max_step                            integer            1
small_dt                            real               1.d-10
xlo_boundary_type                   character          ""
octant                              logical            .false.

This specifies the runtime parameter name, datatype, and default
value.

An optional 4th column can be used to indicate the priority -- if,
when parsing the collection of parameter files, a duplicate of an
existing parameter is encountered, the value from the one with
the highest priority (largest integer) is retained.

The script outputs the C++ header and .cpp files needed to read and
manage the runtime parameters via the AMReX ParmParse functionality

"""

import argparse
import os
import sys

import runtime_parameters

HEADER = """
! DO NOT EDIT THIS FILE!!!
!
! This file is automatically generated by write_probin.py at
! compile-time.
!
! To add a runtime parameter, do so by editing the appropriate _parameters
! file.

"""

CXX_HEADER = """
#ifndef EXTERNAL_PARAMETERS_H
#define EXTERNAL_PARAMETERS_H
#include <AMReX_BLFort.H>
#include <AMReX_REAL.H>

"""

CXX_FOOTER = """
#endif
"""


def get_next_line(fin):
    """return the next, non-blank line, with comments stripped"""
    line = fin.readline()

    pos = line.find("#")

    while (pos == 0 or line.strip() == "") and line:
        line = fin.readline()
        pos = line.find("#")

    return line[:pos]


def parse_param_file(params_list, param_file, use_namespace=False):
    """read all the parameters in a given parameter file and add valid
    parameters to the params list.
    """

    namespace = None

    try:
        f = open(param_file, "r")
    except IOError:
        sys.exit(f"write_probin.py: ERROR: file {param_file} does not exist")
    else:
        print(f"write_probin.py: working on parameter file {param_file}...")

    line = get_next_line(f)

    err = 0

    while line and not err:

        if line[0] == "@":
            # this defines a namespace
            cmd, value = line.split(":")
            if cmd == "@namespace":
                namespace = value
            else:
                sys.exit("invalid command")

            line = get_next_line(f)
            continue

        fields = line.split()

        if len(fields) < 3:
            print("write_probin.py: ERROR: missing one or more fields in parameter definition.")
            err = 1
            continue

        name = fields[0]
        dtype = fields[1]
        default = fields[2]

        if use_namespace:
            skip_namespace_in_declare = False
        else:
            skip_namespace_in_declare = True

        current_param = runtime_parameters.Param(name, dtype, default,
                                                 namespace=namespace, namespace_suffix="_rp",
                                                 skip_namespace_in_declare=skip_namespace_in_declare)

        try:
            current_param.priority = int(fields[3])
        except:
            pass

        skip = 0

        # check to see if this parameter is defined in the current list
        # if so, keep the one with the highest priority
        p_names = [p.name for p in params_list]
        try:
            idx = p_names.index(current_param.name)
        except ValueError:
            pass
        else:
            if params_list[idx].namespace == current_param.namespace:
                if params_list[idx] < current_param:
                    params_list.pop(idx)
                else:
                    skip = 1

        if not err == 1 and not skip == 1:
            params_list.append(current_param)

        line = get_next_line(f)

    return err


def abort(outfile):
    """ abort exits when there is an error.  A dummy stub file is written
    out, which will cause a compilation failure """

    fout = open(outfile, "w")
    fout.write("There was an error parsing the parameter files")
    fout.close()
    sys.exit(1)


def write_probin(param_files,
                 out_file, use_namespace, cxx_prefix):

    """ write_probin will read through the list of parameter files and
    output the new out_file """

    params = []

    print(" ")
    print("write_probin.py: creating extern parameter files")

    # read the parameters defined in the parameter files

    for f in param_files:
        err = parse_param_file(params, f, use_namespace=use_namespace)
        if err:
            abort(out_file)

    # find all of the unique namespaces
    namespaces = {q.namespace for q in params}

    # now the main C++ header with the global data

    cxx_base = os.path.basename(cxx_prefix)

    ofile = f"{cxx_prefix}_parameters.H"
    with open(ofile, "w") as fout:
        fout.write(CXX_HEADER)

        fout.write(f"  void init_{cxx_base}_parameters();\n\n")

        for nm in sorted(namespaces):
            params_in_nm = [q for q in params if q.namespace == nm]

            if use_namespace:
                fout.write(f"  namespace {nm}_rp {{\n")

            for p in params_in_nm:
                fout.write(f"    {p.get_declare_string(with_extern=True)}")

            if use_namespace:
                fout.write("  }\n")

        fout.write(CXX_FOOTER)

    # now the C++ job_info tests

    ofile = f"{cxx_prefix}_job_info_tests.H"
    with open(ofile, "w") as fout:
        for p in params:
            fout.write(p.get_job_info_test())

    # finally the C++ initialization routines

    ofile = f"{cxx_prefix}_parameters.cpp"
    with open(ofile, "w") as fout:
        fout.write(f"#include <{cxx_base}_parameters.H>\n")
        fout.write("#include <AMReX_ParmParse.H>\n\n")
        fout.write("#include <AMReX_REAL.H>\n\n")

        # find all of the unique namespaces
        namespaces = {q.namespace for q in params}

        for nm in sorted(namespaces):
            params_in_nm = [q for q in params if q.namespace == nm]

            if use_namespace:
                fout.write(f"  namespace {nm}_rp {{\n")

            for p in params_in_nm:
                fout.write(f"    {p.get_declare_string()}")

            if use_namespace:
                fout.write("  }\n")

        fout.write("\n")
        fout.write(f"  void init_{cxx_base}_parameters() {{\n")

        # we need access to _rt
        fout.write("      using namespace amrex;\n\n")

        # now write the parmparse code to get the value from the C++
        # inputs.  this will overwrite

        fout.write("    // get the value from the inputs file\n")

        namespaces = {q.namespace for q in params}

        for nm in sorted(namespaces):
            params_nm = [q for q in params if q.namespace == nm]

            # open namespace
            fout.write("    {\n")
            fout.write(f"      amrex::ParmParse pp(\"{nm}\");\n")
            for p in params_nm:
                fout.write(f"      {p.get_default_string()}")
                fout.write(f"      {p.get_query_string('C++')}\n")
            fout.write("    }\n")

        fout.write("  }\n")

def main():

    parser = argparse.ArgumentParser()
    parser.add_argument('-o', type=str, default="", help='out_file')
    parser.add_argument('--pa', type=str, help='parameter files')
    parser.add_argument('--use_namespaces', action="store_true",
                        help="put parameters in namespaces")
    parser.add_argument('--cxx_prefix', type=str, default="extern",
                        help="a name to use in the C++ file names")

    args = parser.parse_args()

    param_files = args.pa.split()

    write_probin(param_files, args.o, args.use_namespaces, args.cxx_prefix)

if __name__ == "__main__":
    main()
