#!/usr/bin/env python

r"""
This module provides functions which are useful to plug-in call point programs.
"""

import sys
import os
import re
import collections

import gen_print as gp
import gen_misc as gm
import gen_cmd as gc

PLUG_VAR_PREFIX = os.environ.get("PLUG_VAR_PREFIX", "AUTOBOOT")


def get_plug_in_package_name(case=None):
    r"""
    Return the plug-in package name (e.g. "OS_Console", "DB_Logging").

    Description of argument(s):
    case                            Indicates whether the value returned
                                    should be converted to upper or lower
                                    case.  Valid values are "upper", "lower"
                                    or None.
    """

    plug_in_package_name = os.path.basename(gp.pgm_dir_path[:-1])
    if case == "upper":
        return plug_in_package_name.upper()
    elif case == "lower":
        return plug_in_package_name.lower()
    else:
        return plug_in_package_name


def return_plug_vars(general=True,
                     custom=True):
    r"""
    Return an OrderedDict which is sorted by key and which contains all of the
    plug-in environment variables.

    Example excerpt of resulting dictionary:

    plug_var_dict:
      [AUTOBOOT_BASE_TOOL_DIR_PATH]:  /tmp/
      [AUTOBOOT_BB_LEVEL]:            <blank>
      [AUTOBOOT_BOOT_FAIL]:           0
      ...

    This function also does the following:
    - Set a default value for environment variable
      AUTOBOOT_OPENBMC_NICKNAME/AUTOIPL_FSP1_NICKNAME if it is not already set.
    - Register PASSWORD variables to prevent their values from being printed.

    Note: The programmer may set a default for any given environment variable
    by declaring a global variable of the same name and setting its value.
    For example, let's say the calling program has this global declaration:

    PERF_EXERCISERS_TOTAL_TIMEOUT = '180'

    If environment variable PERF_EXERCISERS_TOTAL_TIMEOUT is blank or not set,
    this function will set it to 180.

    Description of argument(s):
    general                         Return general plug-in parms (e.g. those
                                    beginning with "AUTOBOOT" or "AUTOGUI").
    custom                          Return custom plug-in parms (i.e. those
                                    beginning with the upper case name of the
                                    plug-in package, for example
                                    "OBMC_SAMPLE_PARM1").
    """

    regex_list = []
    if not (general or custom):
        return collections.OrderedDict()
    plug_in_package_name = get_plug_in_package_name(case="upper")
    if general:
        regex_list = [PLUG_VAR_PREFIX, "AUTOGUI"]
    if custom:
        regex_list.append(plug_in_package_name)

    regex = "^(" + "|".join(regex_list) + ")_"

    # Set a default for nickname.
    if os.environ.get("AUTOBOOT_OPENBMC_NICKNAME", "") == "":
        os.environ['AUTOBOOT_OPENBMC_NICKNAME'] = \
            os.environ.get("AUTOBOOT_OPENBMC_HOST", "")

    if os.environ.get("AUTOIPL_FSP1_NICKNAME", "") == "":
        os.environ['AUTOIPL_FSP1_NICKNAME'] = \
            os.environ.get("AUTOIPL_FSP1_NAME", "").split(".")[0]

    # For all variables specified in the parm_def file, we want them to
    # default to "" rather than being unset.
    # Process the parm_def file if it exists.
    parm_def_file_path = gp.pgm_dir_path + "parm_def"
    if os.path.exists(parm_def_file_path):
        parm_defs = gm.my_parm_file(parm_def_file_path)
    else:
        parm_defs = collections.OrderedDict()
    # Example parm_defs:
    # parm_defs:
    #   parm_defs[rest_fail]:           boolean
    #   parm_defs[command]:             string
    #   parm_defs[esel_stop_file_path]: string

    # Create a list of plug-in environment variables by pre-pending <all caps
    # plug-in package name>_<all caps var name>
    plug_in_parm_names = [plug_in_package_name + "_" + x for x in
                          map(str.upper, parm_defs.keys())]
    # Example plug_in_parm_names:
    # plug_in_parm_names:
    #  plug_in_parm_names[0]: STOP_REST_FAIL
    #  plug_in_parm_names[1]: STOP_COMMAND
    #  plug_in_parm_names[2]: STOP_ESEL_STOP_FILE_PATH

    # Initialize unset plug-in vars.
    for var_name in plug_in_parm_names:
        # If there is a global variable with the same name as the environment
        # variable, use its value as a default.
        default_value = gm.get_mod_global(var_name, "")
        os.environ[var_name] = os.environ.get(var_name, default_value)
        if os.environ[var_name] == "":
            os.environ[var_name] = default_value

    plug_var_dict = \
        collections.OrderedDict(sorted({k: v for (k, v) in
                                        os.environ.items()
                                        if re.match(regex, k)}.items()))
    # Register password values to prevent printing them out.  Any plug var
    # whose name ends in PASSWORD will be registered.
    password_vals = {k: v for (k, v) in plug_var_dict.items()
                     if re.match(r".*_PASSWORD$", k)}.values()
    map(gp.register_passwords, password_vals)

    return plug_var_dict


def sprint_plug_vars(headers=1, **kwargs):
    r"""
    Sprint the plug-in environment variables (i.e. those that begin with the
    global PLUG_VAR_PREFIX value or those that begin with <plug-in
    package_name>_ in upper case letters.).

    Example excerpt of output:
    AUTOBOOT_BASE_TOOL_DIR_PATH=/tmp/
    AUTOBOOT_BB_LEVEL=
    AUTOBOOT_BOOT_FAIL=0
    AUTOBOOT_BOOT_FAIL_THRESHOLD=1000000

    Description of argument(s):
    headers                         Print a header and a footer.
    kwargs                          These are passed directly to
                                    return_plug_vars.  See return_plug_vars
                                    doc string for details.
    """
    plug_var_dict = return_plug_vars(**kwargs)
    buffer = ""
    if headers:
        buffer += "\n" + gp.sprint_dashes()
    for key, value in plug_var_dict.items():
        buffer += gp.sprint_varx(key, value)
    if headers:
        buffer += gp.sprint_dashes() + "\n"

    return buffer


def print_plug_in_header():
    r"""
    Print plug-in header.

    When debug is set, print all plug_prefix variables (e.g.
    AUTOBOOT_OPENBMC_HOST, etc.) and all plug-in environment variables (e.g.
    OBMC_SAMPLE_PARM1) with surrounding dashed lines.  When debug is not set,
    print only the plug-in environment variables (e.g. OBMC_SAMPLE_PARM1) with
    no surrounding dashed lines.

    NOTE: plug-in environment variables means any variable defined in the
    <plug-in dir>/parm_def file plus any environment variables whose names
    begin with the upper-case plug-in package name.
    """

    dprint_plug_vars()
    if not debug:
        qprint_plug_vars(headers=0, general=False, custom=True)


def get_plug_vars(mod_name="__main__"):
    r"""
    Get all plug-in variables and put them in corresponding global variables.

    This would include all environment variables beginning with either the
    global PLUG_VAR_PREFIX value or with the upper case version of the plug-in
    package name + underscore (e.g. OP_SAMPLE_VAR1 for plug-in OP_Sample).

    The global variables to be set will be both with and without the global
    PLUG_VAR_PREFIX value prefix.  For example, if the environment variable in
    question is AUTOBOOT_OPENBMC_HOST, this function will set global variable
    AUTOBOOT_OPENBMC_HOST and global variable OPENBMC_HOST.

    Description of argument(s):
    mod_name                        The name of the module whose global
                                    plug-in variables should be retrieved.
    """

    module = sys.modules[mod_name]
    plug_var_dict = return_plug_vars()

    # Get all PLUG_VAR_PREFIX environment variables and put them into globals.
    for key, value in plug_var_dict.items():
        setattr(module, key, value)
        setattr(module, re.sub("^" + PLUG_VAR_PREFIX + "_", "", key), value)


def get_plug_default(var_name,
                     default=None):
    r"""
    Derive and return a default value for the given parm variable.

    Dependencies:
    Global variable PLUG_VAR_PREFIX must be set.

    This function will assign a default by checking the following environment
    variables in the order shown.  The first one that has a value will be used.
    - <upper case package_name>_<var_name>
    - <PLUG_VAR_PREFIX>_OVERRIDE_<var_name>
    - <PLUG_VAR_PREFIX>_<var_name>

    If none of these are found, this function will return the value passed by
    the caller in the "default" parm.

    Example:

    Let's say your plug-in is named "OS_Console" and you call this function as
    follows:

    get_plug_default("quiet", 0)

    The first of these environment variables that is found to be set will be
    used to provide the default value.
    - OS_CONSOLE_QUIET
    - AUTOBOOT_OVERRIDE_QUIET
    - AUTOBOOT_QUIET

    If none of those has a value, 0 (as specified by the caller in this
    example) is returned.

    Let's say the master driver program is named obmc_boot.  obmc_boot program
    is responsible for calling plug-ins.  Let's further suppose that the user
    wishes to run the master program with --debug=0 but wishes to have all
    plug-ins run with --debug=1.  This could be accomplished with the
    following call:
    export AUTOBOOT_OVERRIDE_DEBUG=1 ; obmc_boot --debug=0
    --plug_in_dir_paths=<list of plug ins>

    As another example, let's suppose that the user wishes to have just the
    OS_Console plug-in run with debug and everything else to default to
    debug=0.  This could be accomplished as follows:
    export OS_CONSOLE_DEBUG=1 ; obmc_boot --debug=0 --plug_in_dir_paths=<list
    of plug ins>

    And as one more example, let's say the user wishes to have obmc_boot and
    OS_Console run without debug but have all other plug-ins run with debug:
    export AUTOBOOT_OVERRIDE_DEBUG=1 ; export OS_CONSOLE_DEBUG=0 ; obmc_boot
    --debug=0 --plug_in_dir_paths=<list of plug ins>

    Description of argument(s):
    var_name                        The name of the variable for which a
                                    default value is to be calculated.
    default                         The default value if one cannot be
                                    determined.
    """

    var_name = var_name.upper()
    plug_in_package_name = get_plug_in_package_name(case="upper")

    package_var_name = plug_in_package_name + "_" + var_name
    default_value = os.environ.get(package_var_name, None)
    if default_value is not None:
        # A package-name version of the variable was found so return its value.
        return(default_value)

    plug_var_name = PLUG_VAR_PREFIX + "_OVERRIDE_" + var_name
    default_value = os.environ.get(plug_var_name, None)
    if default_value is not None:
        # A PLUG_VAR_PREFIX version of the variable was found so return its
        # value.
        return default_value

    plug_var_name = PLUG_VAR_PREFIX + "_" + var_name
    default_value = os.environ.get(plug_var_name, None)
    if default_value is not None:
        # A PLUG_VAR_PREFIX version of the variable was found so return its
        # value.
        return default_value

    return default


def srequired_plug_in(req_plug_in_names,
                      plug_in_dir_paths=None):
    r"""
    Return an empty string if the required plug-ins are found in
    plug_in_dir_paths.  Otherwise, return an error string.

    Example call:
    error_message = srequired_plug_in(req_plug_in_names, plug_in_dir_paths)

    Description of argument(s):
    req_plug_in_names               A list of plug_in names that the caller
                                    requires (e.g. ['OS_Console']).
    plug_in_dir_paths               A string which is a colon-delimited list
                                    of plug-ins specified by the user (e.g.
                                    DB_Logging:FFDC:OS_Console:Perf).  Path
                                    values (e.g. "/home/robot/dir1") will be
                                    stripped from this list to do the
                                    analysis.  Default value is the
                                    <PLUG_VAR_PREFIX>_PLUG_IN_DIR_PATHS
                                    environment variable.
    """

    # Calculate default value for plug_in_dir_paths.
    if plug_in_dir_paths is None:
        plug_in_dir_paths = os.environ.get(PLUG_VAR_PREFIX
                                           + "_PLUG_IN_DIR_PATHS", "")

    error_message = ""

    # Convert plug_in_dir_paths to a list of base names.
    plug_in_dir_paths = \
        list(filter(None, map(os.path.basename, plug_in_dir_paths.split(":"))))

    # Check for each of the user's required plug-ins.
    for plug_in_name in req_plug_in_names:
        if plug_in_name not in plug_in_dir_paths:
            error_message = "The \"" + get_plug_in_package_name() +\
                "\" plug-in cannot run unless the user also selects the \"" +\
                plug_in_name + "\" plug in:\n" +\
                gp.sprint_var(plug_in_dir_paths)

    return error_message


def required_plug_in(req_plug_in_names,
                     plug_in_dir_paths=None):
    r"""
    Return True if each of the plug-ins in req_plug_in_names can be found in
    plug_in_dir_paths  Otherwise, return False and print an error message to
    stderr.

    Example call:
    if not required_plug_in(['OS_Console'], AUTOBOOT_PLUG_IN_DIR_PATHS):
        return False

    Description of argument(s):
    (See Description of arguments for srequired_plug_in (above)).
    """

    error_message = srequired_plug_in(req_plug_in_names, plug_in_dir_paths)
    if not error_message == "":
        gp.print_error_report(error_message)
        return False

    return True


def compose_plug_in_save_dir_path(plug_in_package_name=None):
    r"""
    Create and return a directory path name that is suitable for saving
    plug-in data.

    The name will be comprised of things such as plug_in package name, pid,
    etc. in order to guarantee that it is unique for a given test run.

    Description of argument(s):
    plug_in_package_name            The plug-in package name.  This defaults
                                    to the name of the caller's plug-in
                                    package.  However, the caller can specify
                                    another value in order to retrieve data
                                    saved by another plug-in package.
    """

    plug_in_package_name = gm.dft(plug_in_package_name,
                                  get_plug_in_package_name())

    BASE_TOOL_DIR_PATH = \
        gm.add_trailing_slash(os.environ.get(PLUG_VAR_PREFIX
                                             + "_BASE_TOOL_DIR_PATH",
                                             "/tmp/"))
    NICKNAME = os.environ.get("AUTOBOOT_OPENBMC_NICKNAME", "")
    if NICKNAME == "":
        NICKNAME = os.environ["AUTOIPL_FSP1_NICKNAME"]
    MASTER_PID = os.environ[PLUG_VAR_PREFIX + "_MASTER_PID"]
    gp.qprint_vars(BASE_TOOL_DIR_PATH, NICKNAME, plug_in_package_name,
                   MASTER_PID)
    return BASE_TOOL_DIR_PATH + gm.username() + "/" + NICKNAME + "/" +\
        plug_in_package_name + "/" + str(MASTER_PID) + "/"


def create_plug_in_save_dir(plug_in_package_name=None):
    r"""
    Create a directory suitable for saving plug-in processing data and return
    its path name.

    See compose_plug_in_save_dir_path for details.

    Description of argument(s):
    plug_in_package_name            See compose_plug_in_save_dir_path for
                                    details.
    """

    plug_in_save_dir_path = compose_plug_in_save_dir_path(plug_in_package_name)
    if os.path.isdir(plug_in_save_dir_path):
        return plug_in_save_dir_path
    gc.shell_cmd("mkdir -p " + plug_in_save_dir_path)
    return plug_in_save_dir_path


def delete_plug_in_save_dir(plug_in_package_name=None):
    r"""
    Delete the plug_in save directory.  See compose_plug_in_save_dir_path for
    details.

    Description of argument(s):
    plug_in_package_name            See compose_plug_in_save_dir_path for
                                    details.
    """

    gc.shell_cmd("rm -rf "
                 + compose_plug_in_save_dir_path(plug_in_package_name))


def save_plug_in_value(value, plug_in_package_name=None):
    r"""
    Save a value in a plug-in save file.  The value may be retrieved later via
    a call to the restore_plug_in_value function.

    This function will figure out the variable name of the value passed and
    use that name in creating the plug-in save file.

    Example call:

    my_var1 = 5
    save_plug_in_value(my_var1)

    In this example, the value "5" would be saved to the "my_var1" file in the
    plug-in save directory.

    Description of argument(s):
    value                           The value to be saved.
    plug_in_package_name            See compose_plug_in_save_dir_path for
                                    details.
    """

    # Get the name of the variable used as argument one to this function.
    var_name = gp.get_arg_name(0, 1, stack_frame_ix=2)
    plug_in_save_dir_path = create_plug_in_save_dir(plug_in_package_name)
    save_file_path = plug_in_save_dir_path + var_name
    gp.qprint_timen("Saving \"" + var_name + "\" value.")
    gc.shell_cmd("echo '" + str(value) + "' > " + save_file_path)


def restore_plug_in_value(default="", plug_in_package_name=None):
    r"""
    Return a value from a plug-in save file.

    The name of the value to be restored will be determined by this function
    based on the lvalue being assigned.  Consider the following example:

    my_var1 = restore_plug_in_value(2)

    In this example, this function would look for the "my_var1" file in the
    plug-in save directory, read its value and return it.  If no such file
    exists, the default value of 2 would be returned.

    Description of argument(s):
    default                         The default value to be returned if there
                                    is no plug-in save file for the value in
                                    question.
    plug_in_package_name            See compose_plug_in_save_dir_path for
                                    details.
    """

    # Get the lvalue from the caller's invocation of this function.
    lvalue = gp.get_arg_name(0, -1, stack_frame_ix=2)
    plug_in_save_dir_path = create_plug_in_save_dir(plug_in_package_name)
    save_file_path = plug_in_save_dir_path + lvalue
    if os.path.isfile(save_file_path):
        gp.qprint_timen("Restoring " + lvalue + " value from "
                        + save_file_path + ".")
        value = gm.file_to_list(save_file_path, newlines=0, comments=0,
                                trim=1)[0]
        if type(default) is bool:
            # Convert from string to bool.
            value = (value == 'True')
        if type(default) is int:
            # Convert from string to int.
            value = int(value)
        gp.qprint_varx(lvalue, value)
        return value
    else:
        gp.qprint_timen("Save file " + save_file_path
                        + " does not exist so returning default value.")
        gp.qprint_var(default)
        return default


def exit_not_master():
    r"""
    Exit the program with return code zero if this program was NOT called by
    the master program.

    There are cases where plug-ins are called by a multi-layered stack:

    master_wrapper
        obmc_boot_test.py
            Example_plug_in/cp_setup

    In a scenario like this, Example_plug_in/cp_setup may be called once
    directly by master_wrapper (the master) and and then called again directly
    by obmc_boot_test.py (the child).  Some plug-in programs may wish to avoid
    doing any processing on the second such call.  This function will achieve
    that purpose.

    This function will print a standard message to stdout prior to exiting.
    """

    AUTOBOOT_MASTER_PID = gm.get_mod_global("AUTOBOOT_MASTER_PID")
    AUTOBOOT_PROGRAM_PID = gm.get_mod_global("AUTOBOOT_PROGRAM_PID")

    if AUTOBOOT_MASTER_PID != AUTOBOOT_PROGRAM_PID:
        message = get_plug_in_package_name() + "/" + gp.pgm_name + " is not" \
            + " being called by the master program in the stack so no action" \
            + " will be taken."
        gp.qprint_timen(message)
        gp.qprint_vars(AUTOBOOT_MASTER_PID, AUTOBOOT_PROGRAM_PID)
        exit(0)


# Create print wrapper functions for all sprint functions defined above.
# func_names contains a list of all print functions which should be created
# from their sprint counterparts.
func_names = ['print_plug_vars']

# stderr_func_names is a list of functions whose output should go to stderr
# rather than stdout.
stderr_func_names = []

replace_dict = dict(gp.replace_dict)
replace_dict['mod_qualifier'] = 'gp.'
func_defs = gp.create_print_wrapper_funcs(func_names, stderr_func_names,
                                          replace_dict)
gp.gp_debug_print(func_defs)
exec(func_defs)