#!/usr/bin/wish

# This file provides many valuable validation procedures such as valid_value, valid_integer, etc.

my_source [list print.tcl call_stack.tcl]


proc valid_value { var_name { invalid_values {}} { valid_values {}} } {

  # If the value of the variable named in var_name is not valid, print an error message and exit the program
  # with a non-zero return code.

  # Description of arguments:
  # var_name                        The name of the variable whose value is to be validated.
  # invalid_values                  A list of invalid values.  If the variable value is equal to any value in
  #                                 the invalid_values list, it is deemed to be invalid.  Note that if you
  #                                 specify anything for invalid_values (below), the valid_values list is not
  #                                 even processed.  In other words, specify either invalid_values or
  #                                 valid_values but not both.  If no value is specified for either
  #                                 invalid_values or valid_values, invalid_values will default to a list
  #                                 with one blank entry.  This is useful if you simply want to ensure that
  #                                 your variable is non blank.
  # valid_values                    A list of invalid values.  The variable value must be equal to one of the
  #                                 values in this list to be considered valid.

  # Call get_stack_var_level to relieve the caller of the need for declaring the variable as global.
  set stack_level [get_stack_var_level $var_name]
  # Access the variable value.
  upvar $stack_level $var_name var_value

  set len_invalid_values [llength $invalid_values]
  set len_valid_values [llength $valid_values]

  if { $len_valid_values > 0 &&  $len_invalid_values > 0 } {
    append error_message "Programmer error - You must provide either an"
    append error_message " invalid_values list or a valid_values"
    append error_message " list but NOT both.\n"
    append error_message [sprint_list invalid_values "" "" 1]
    append error_message [sprint_list valid_values "" "" 1]
    print_error_report $error_message
    exit 1
  }

  set caller [get_stack_proc_name -2]
  if { $caller == "valid_list" } {
    set exit_on_fail 0
  } else {
    set exit_on_fail 1
  }
  if { $len_valid_values > 0 } {
    # Processing the valid_values list.
    if { [lsearch -exact $valid_values "${var_value}"] != -1 } { return }
    append error_message "The following variable has an invalid value:\n"
    append error_message [sprint_varx $var_name $var_value "" "" 1]
    append error_message "\nIt must be one of the following values:\n"
    append error_message [sprint_list valid_values "" "" 1]
    if { $exit_on_fail } {
      print_error_report $error_message
      exit 1
    } else {
      error [sprint_error_report $error_message]
    }
  }

  if { $len_invalid_values == 0 } {
    # Assign default value.
    set invalid_values [list ""]
  }

  # Assertion: We have an invalid_values list.  Processing it now.
  if { [lsearch -exact $invalid_values "${var_value}"] == -1 } { return }

  if { [lsearch -exact $valid_values "${var_value}"] != -1 } { return }
  append error_message "The following variable has an invalid value:\n"
  append error_message [sprint_varx $var_name $var_value "" "" 1]
  append error_message "\nIt must NOT be any of the following values:\n"
  append error_message [sprint_list invalid_values "" "" 1]
  if { $exit_on_fail } {
    print_error_report $error_message
    exit 1
  } else {
    error [sprint_error_report $error_message]
  }

}


proc valid_list { var_name args } {

  # If the value of the list variable named in var_name is not valid, print an error message and exit the
  # program with a non-zero return code.

  # Description of arguments:
  # var_name                        The name of the variable whose value is to be validated.  This variable
  #                                 should be a list.  For each list alement, a call to valid_value will be
  #                                 done.
  # args                            args will be passed directly to valid_value.  Please see valid_value for
  #                                 details.

  # Example call:

  # set valid_procs [list "one" "two" "three"]
  # set proc_names [list "zero" "one" "two" "three" "four"]
  # valid_list proc_names {} ${valid_procs}

  # In this example, this procedure will fail with the following message:

  ##(CDT) 2018/03/27 12:26:49.904870 - **ERROR** The following list has one or more invalid values (marked
  # #with "*"):
  #
  # proc_names:
  #   proc_names[0]:                                  zero*
  #   proc_names[1]:                                  one
  #   proc_names[2]:                                  two
  #   proc_names[3]:                                  three
  #   proc_names[4]:                                  four*
  #
  # It must be one of the following values:
  #
  # valid_values:
  #   valid_values[0]:                                one
  #   valid_values[1]:                                two
  #   valid_values[2]:                                three

  # Call get_stack_var_level to relieve the caller of the need for declaring the variable as global.
  set stack_level [get_stack_var_level $var_name]
  # Access the variable value.
  upvar $stack_level $var_name var_value

  set ix 0
  # Create a list of index values which point to invalid list elements.
  set invalid_ix_list [list]
  foreach list_entry $var_value {
    incr ix
    if { [catch {valid_value list_entry {*}$args} result] } {
      lappend invalid_ix_list ${ix}
    }
  }

  # No errors found so return.
  if { [llength $invalid_ix_list] == 0 } { return }

  # We want to do a print_list on the caller's list but we want to put an asterisk by each invalid entry
  # (see example in prolog).

  # Make the caller's variable name, contained in $var_name, directly accessible to this procedure.
  upvar $stack_level $var_name $var_name
  # print_list the caller's list to a string.
  set printed_var [sprint_list $var_name "" "" 1]
  # Now convert the caller's printed var string to a list for easy manipulation.
  set printed_var_list [split $printed_var "\n"]

  # Loop through the erroneous index list and mark corresponding entries in printed_var_list with asterisks.
  foreach ix $invalid_ix_list {
    set new_value "[lindex $printed_var_list $ix]*"
    set printed_var_list [lreplace $printed_var_list ${ix} ${ix} $new_value]
  }

  # Convert the printed var list back to a string.
  set printed_var [join $printed_var_list "\n"]
  append error_message "The following list has one or more invalid values"
  append error_message " (marked with \"*\"):\n\n"
  append error_message $printed_var
  # Determine whether the caller passed invalid_values or valid_values in order to create appropriate error
  # message.
  if { [lindex $args 0] != "" } {
    append error_message "\nIt must NOT be any of the following values:\n\n"
    set invalid_values [lindex $args 0]
    append error_message [sprint_list invalid_values "" "" 1]
  } else {
    append error_message "\nIt must be one of the following values:\n\n"
    set valid_values [lindex $args 1]
    append error_message [sprint_list valid_values "" "" 1]
  }
  print_error_report $error_message
  exit 1

}


proc valid_integer { var_name } {

  # If the value of the variable named in var_name is not a valid integer, print an error message and exit
  # the program with a non-zero return code.

  # Description of arguments:
  # var_name                        The name of the variable whose value is to be validated.

  # Call get_stack_var_level to relieve the caller of the need for declaring the variable as global.
  set stack_level [get_stack_var_level $var_name]
  # Access the variable value.
  upvar $stack_level $var_name var_value

  if { [catch {format "0x%08x" "$var_value"} result] } {
    append error_message "Invalid integer value:\n"
    append error_message [sprint_varx $var_name $var_value]
    print_error_report $error_message
    exit 1
  }

}


proc valid_dir_path { var_name { add_slash 1 } } {

  # If the value of the variable named in var_name is not a valid directory path, print an error message and
  # exit the program with a non-zero return code.

  # Description of arguments:
  # var_name                        The name of the variable whose value is to be validated.
  # add_slash                       If set to 1, this procedure will add a trailing slash to the directory
  #                                 path value.

  # Call get_stack_var_level to relieve the caller of the need for declaring the variable as global.
  set stack_level [get_stack_var_level $var_name]
  # Access the variable value.
  upvar $stack_level $var_name var_value

  expand_shell_string var_value

  if { ![file isdirectory $var_value] } {
    append error_message "The following directory does not exist:\n"
    append error_message [sprint_varx $var_name $var_value "" "" 1]
    print_error_report $error_message
    exit 1
  }

  if { $add_slash } { add_trailing_string var_value / }

}


proc valid_file_path { var_name } {

  # If the value of the variable named in var_name is not a valid file path, print an error message and exit
  # the program with a non-zero return code.

  # Description of arguments:
  # var_name                        The name of the variable whose value is to be validated.

  # Call get_stack_var_level to relieve the caller of the need for declaring the variable as global.
  set stack_level [get_stack_var_level $var_name]
  # Access the variable value.
  upvar $stack_level $var_name var_value

  expand_shell_string var_value

  if { ![file isfile $var_value] } {
    append error_message "The following file does not exist:\n"
    append error_message [sprint_varx $var_name $var_value "" "" 1]
    print_error_report $error_message
    exit 1
  }

}


proc get_password { {password_var_name password} } {

  # Prompt user for password and return result.

  # On error, print to stderr and terminate the program with non-zero return code.

  set prompt\
    [string trimright [sprint_varx "Please enter $password_var_name" ""] "\n"]
  puts -nonewline $prompt
  flush stdout
  stty -echo
  gets stdin password1
  stty echo
  puts ""

  set prompt [string\
    trimright [sprint_varx "Please re-enter $password_var_name" ""] "\n"]
  puts -nonewline $prompt
  flush stdout
  stty -echo
  gets stdin password2
  stty echo
  puts ""

  if { $password1 != $password2 } {
    print_error_report "Passwords do not match.\n"
    gen_exit_proc 1
  }

  if { $password1 == "" } {
    print_error_report "Need a non-blank value for $password_var_name.\n"
    gen_exit_proc 1
  }

  return $password1

}


proc valid_password { var_name { prompt_user 1 } } {

  # If the value of the variable named in var_name is not a valid password, print an error message and exit
  # the program with a non-zero return code.

  # Description of arguments:
  # var_name                        The name of the variable whose value is to be validated.
  # prompt_user                     If the variable has a blank value, prompt the user for a value.

  # Call get_stack_var_level to relieve the caller of the need for declaring the variable as global.
  set stack_level [get_stack_var_level $var_name]
  # Access the variable value.
  upvar $stack_level $var_name var_value

  if { $var_value == "" && $prompt_user } {
    global $var_name
    set $var_name [get_password $var_name]
  }

  if { $var_value == "" } {
    print_error_report "Need a non-blank value for $var_name.\n"
    gen_exit_proc 1
  }

}


proc process_pw_file_path {pw_file_path_var_name} {

  # Process a password file path parameter by setting or validating the corresponding password variable.

  # For example, let's say you have an os_pw_file_path parm defined.  This procedure will set the global
  # os_password variable.

  # If there is no os_password program parm defined, then the pw_file_path must exist and will be validated
  # by this procedure.  If there is an os_password program parm defined, then either the os_pw_file_path must
  # be valid or the os_password must be valid.  Again, this procedure will verify all of this.

  # When a valid pw_file_path exists, this program will read the password from it and set the global
  # password variable with the value.
  # Finally, this procedure will call valid_password which will prompt user if password has not been
  # obtained by this point.

  # Description of argument(s):
  # pw_file_path_var_name           The name of a global variable that contains a file path which in turn
  #                                 contains a password value.  The variable name must end in "pw_file_path"
  #                                 (e.g. "os_pw_file_path").

  # Verify that $pw_file_path_var_name ends with "pw_file_path".
  if { ! [regexp -expanded "pw_file_path$" $pw_file_path_var_name] } {
    append message "Programming error - Proc [get_stack_proc_name] its"
    append message " pw_file_path_var_name parameter to contain a value that"
    append message "ends in \"pw_file_path\" instead of the current value:\n"
    append message [sprint_var pw_file_path_var_name]
    print_error $message
    gen_exit_proc 1
  }

  global $pw_file_path_var_name
  expand_shell_string $pw_file_path_var_name

  # Get the prefix portion of pw_file_path_var_name which is obtained by stripping "pw_file_path" from the
  # end.
  regsub -expanded {pw_file_path$} $pw_file_path_var_name {} var_prefix

  # Create password_var_name.
  set password_var_name ${var_prefix}password
  global $password_var_name

  global longoptions pos_parms
  regsub -all ":" "${longoptions} ${pos_parms}" {} parm_names
  if { [lsearch -exact parm_names $password_var_name] == -1 } {
    # If no corresponding password program parm has been defined, then the pw_file_path must be valid.
    valid_file_path $pw_file_path_var_name
  }

  if { [file isfile [set $pw_file_path_var_name]] } {
    # Read the entire password file into a list, filtering comments out.
    set file_descriptor [open [set $pw_file_path_var_name] r]
    set file_data [list_filter_comments [split [read $file_descriptor] "\n"]]
    close $file_descriptor

    # Assign the password value to the global password variable.
    set $password_var_name [lindex $file_data 0]
    # Register the password to prevent printing it.
    register_passwords [set $password_var_name]
  }

  # Validate the password, which includes prompting the user if need be.
  valid_password $password_var_name

}