#!/opt/thinlinc/libexec/python3
# -*-Python-*-
import os
import sys
from thinlinc import hiveconf
import locale
import warnings

from thinlinc.optionparse import OptionParser, SUPPRESS_HELP
from thinlinc.optcomplete import FileCompleter, CompletionResult, NoneCompleter
from thinlinc.version import TL_VERSION

DEFAULT_TYPE="string"
DEFAULT_ROOT_HIVE="/etc/root.hconf"

def safe_string(s):
    """Makes a string safe to print on sys.stdout"""
    enc = sys.stdout.encoding
    if enc is None:
        enc = locale.getpreferredencoding(False)
    return s.encode(enc, errors="replace").decode(enc)

def get_type(s):
    """Parse parameter string. Returns (type, parameter)"""
    words = s.split(":", 1)
    if not words[1:]:
        # No colon in string
        return (DEFAULT_TYPE, s)
    
    (beforecolon, aftercolon) = words
    if beforecolon in ("string", "bool", "integer", "float", "binary",
                       "string_list", "bool_list", "integer_list",
                       "float_list", "binary_list"):
        return (beforecolon, aftercolon)
    else:
        return (DEFAULT_TYPE, s)


def get_value(s):
    """Parse parameter string. Returns (parameterpath, value)"""
    words = s.split("=", 1)
    if not words[1:]:
        # No equal sign in string
        return (s, None)
    else:
        return tuple(words)


def handle_param(hive, param):
    """Print parameter. Returns zero on success"""
    (paramtype, param) = get_type(param)
    (parampath, input_value) = get_value(param)

    current_value = None
    new_param = False
    method = getattr(hive, "get_%s" % paramtype)
    try:
        current_value = method(parampath)
    except hiveconf.NotAParameterError:
        display_error =  "%s: Not a parameter" % parampath
        new_param = True

    if current_value == None:
        display_error = "%s: No such parameter" % parampath
        new_param = True

    if not input_value:
        # Display value
        if new_param:
            # Can't display the value of a parameter that doesn't exist
            print(display_error, file=sys.stderr)
            return 1
        else:
            if isinstance(current_value, str):
                current_value = safe_string(current_value)
            print(current_value)
    else:
        # Set value
        method = getattr(hive, "set_%s" % paramtype)
        if not method(parampath, input_value):
            print("Failed to set parameter", parampath, file=sys.stderr)
            return 1
        if new_param:
            print("Created new parameter: ", parampath)
    return 0


def eval_print(hive, varname, param, export):
    (paramtype, parampath) = get_type(param)
    method = getattr(hive, "get_%s" % paramtype)
    value = method(parampath)
    if export:
        print("export", end=' ')
    if value:
        value = str(value)
        # ' is the only character that needs special handling
        value = value.replace("'", "'\\''")
        print("%s='%s'" % (varname, value))
    else:
        print("%s=''" % varname)
    if value == None:
        return 1
    else:
        return 0

def _walk(hive, folderpath="/"):
    params = hive.get_parameters(folderpath)
    folders = hive.get_folders(folderpath)

    yield (folderpath, params, folders)

    for subname in folders:
        if subname == "/":
            continue
        subfolder = os.path.join(folderpath, subname)
        yield from _walk(hive, subfolder)

def imp_walk(hive, ih):
    for (folderpath, params, folders) in _walk(ih):
        for paramname in params:
            parampath = os.path.join(folderpath, paramname)
            value = ih.get_string(parampath)
            #print "setting", parampath, "to", value
            hive.set_string(parampath, value)
            if parampath in purge_params:
                purge_params.remove(parampath)

# A list of parameters to delete
purge_params = []

def purge_walk(hive, ph):
    for (folderpath, params, folders) in _walk(ph):
        for paramname in params:
            parampath = os.path.join(folderpath, paramname)
            if hive.lookup(parampath) != None:
                # Add to delete-list
                purge_params.append(parampath)


def print_walk(hive, root, recursive=True):
    folder = hive.lookup(root)
    rootdepth = len(folder.sectionname.split("/"))

    # Iterate through the levels of the hive,
    # one sub-folder deeper for each iteration
    for (folderpath, params, folders) in _walk(hive, root):
        folder = hive.lookup(folderpath)
        section = folder.sectionname
        depth = len(folder.sectionname.split("/"))
        indent = (depth - rootdepth) * 4
        if len(folder.sources) > 0:
            source = folder.sources[0]
        else:
            source = None

        # Unless it's the first iteration - print sub-folder name.
        # It should be printed above it's contents in recursive mode.
        if folderpath != root:
            foldername = os.path.basename(folderpath)
            foldername = safe_string(foldername)
            sys.stdout.write(" " * (indent-4))
            sys.stdout.write(foldername + "/\n")

        # Print Parameters and values
        for param in params:
            parampath = os.path.join(folderpath, param)
            value = hive.get_string(parampath)
            param = safe_string(param)
            value = safe_string(value)
            sys.stdout.write(" " * indent)
            sys.stdout.write("%s = %s\n" % (param, value))

        if not recursive:
            # Print sub-folder names without the folder contents
            for folder in folders:
                foldername = safe_string(folder)
                if foldername == "/":
                    continue

                sys.stdout.write(" " * indent)
                sys.stdout.write(foldername + "/\n")

        if not recursive:
            break

def _try_open_root_hive_silently():
    # This function should only be used by tab completers that want to open the
    # root hive in a best-effort manner. All exceptions and syntax warnings are
    # ignored.
    hive = None
    try:
        # FIXME: We should not hard-code the ThinLinc path for the
        # root hive. Instead, we should attempt to guess it from the
        # commands the user has typed so far. This is not possible
        # currently due to bug 8347.
        with warnings.catch_warnings():
            warnings.simplefilter('ignore')
            origin = os.path.abspath(os.path.dirname(__file__))
            hive = hiveconf.open_hive("%s/../etc/thinlinc.hconf" % origin)
    except Exception:
        pass
    finally:
        warnings.simplefilter('default')

    return hive

def all_entries_completer(pwd, line, point, prefix, suffix):
    # Try to open default root hive for now.
    hive = _try_open_root_hive_silently()

    if not hive:
        return CompletionResult(completions=[], nospace=True)

    # Special case; only complete "/" if no prefix is given
    if not prefix:
        return CompletionResult(completions=["/"], nospace=True)

    # Set the path to the appropriate level according to the prefix
    path = ""
    if prefix.count("/") >= 1:
        # Strip stuff after last "/" in prefix
        path = prefix[:prefix.rfind("/")+1]
    else:
        path = "/" # root folders

    # Get names of folders on the prefix's level
    folder_names = hive.get_folders(path)

    if not folder_names:
        # No folders match the prefix
        return CompletionResult(completions=[], nospace=True)

    completions = []
    for folder_name in folder_names:
        # Use the full path in the completion
        compl = os.path.join(path, folder_name)

        # All valid arguments for "-a" are folders that start with "/"
        if not compl.startswith("/"):
            compl = "/" + compl

        # Only keep completions starting with prefix
        if not compl.startswith(prefix):
            continue

        # All valid arguments for "-a" are folders that end with "/"
        if not compl.endswith("/"):
            compl = compl + "/"

        # Don't need to complete whats already in the prefix
        if compl == prefix:
            continue

        completions.append(compl)

    return CompletionResult(completions=completions, nospace=True)

def param_completer(pwd, line, point, prefix, suffix):
    # Try to open default root hive for now.
    hive = _try_open_root_hive_silently()

    if not hive:
        return CompletionResult(completions=[], nospace=True)

    # Special case; only complete "/" if no prefix is given
    if not prefix:
        return CompletionResult(completions=["/"], nospace=True)

    # Set the path to the appropriate level according to the prefix
    path = ""
    if prefix.count("/") >= 1:
        # Strip stuff after last "/" in prefix
        path = prefix[:prefix.rfind("/")+1]
    else:
        path = "/" # root folders

    # Get names of folders and params on the prefix's level
    folder_names = hive.get_folders(path)
    folder_names = [ f + "/" for f in folder_names if not f.endswith("/") ]
    param_names = hive.get_parameters(path)

    if not folder_names and not param_names:
        # No folders or params on this level
        return CompletionResult(completions=[], nospace=True)

    completions = []
    for obj in folder_names + param_names:
        # Use the full path in the completion
        compl = os.path.join(path, obj)

        # Only keep completions starting with prefix
        if not compl.startswith(prefix):
            continue

        # Don't need to complete whats already in the prefix
        if compl == prefix:
            continue

        completions.append(compl)

    return CompletionResult(completions=completions, nospace=True)

def setup_optparser():

    parser = OptionParser(version=TL_VERSION,
                          usage="%prog [options] [type:]parameter[=value] ...",
                          epilog="""type is one of: string, bool, integer, float, binary, string_list,
                                 bool_list, integer_list, float_list, binary_list. Default is string""")

    file_completer = FileCompleter()
    parser.add_option("-a", "--all-entries",
                       action="append",
                       metavar="folder",
                       dest="walk_folders",
                       default=[],
                       help="print all parameters and values in a folder",
                      completer=all_entries_completer)

    parser.add_option("-i", "--import",
                       action="append",
                       metavar="<file>",
                       dest="imp_files",
                       default=[],
                       completer=file_completer,
                       help="import all parameters in specified file")

    parser.add_option("-p", "--purge",
                       action="append",
                       metavar="<file>",
                       dest="purge_files",
                       default=[],
                       completer=file_completer,
                       help="remove parameters in specified file which exists elsewhere")

    parser.add_option("-R", "--recursive",
                       action="store_true",
                       dest="recursive",
                       default=False,
                       help="when using -a, ascend folders recursively")

    parser.add_option("-r", "--root",
                       metavar="<file>",
                       dest="roothive",
                       default="/etc/root.hconf",
                       completer=file_completer,
                       help=SUPPRESS_HELP)

    parser.add_option("-e", "--eval",
                       action="append",
                       metavar="VAR=parameter",
                       dest="e_params",
                       default=[],
                       completer=NoneCompleter(),
                       help="""print parameter value in format suitable for
                          assignment to shell variable, via evaluation""")

    parser.add_option("-E",
                       action="append",
                       metavar="folder",
                       dest="E_params",
                       default=[],
                       completer=all_entries_completer,
                       help="as -e, but print all parameters in specified folder")

    parser.add_option("-x", "--export",
                       action="store_true",
                       dest="eval_export",
                       default=False,
                       help="when using -e,-E, export variables")

    parser.set_argument_completer(param_completer)

    return parser



def main():
    try:
        locale.setlocale(locale.LC_ALL, "")
    except locale.Error as e:
        print("\nWarning: Can't set the locale (%s); make sure $LC_* and $LANG are correct\n" %
              e, file=sys.stderr)

    parser = setup_optparser()

    (options, args) = parser.parse_args()

    e_params = []
    for e in options.e_params:
        words = e.split("=", 1)
        if not words[1:]:
            parser.error("invalid argument to -e")
        e_params.append(words)

    errors = 0

    # Try to open root hive
    try:
        hive = hiveconf.open_hive(options.roothive)
    except hiveconf.SyntaxError as e:
        print(e, file=sys.stderr)
        return

    # Retrieve parameters to purge from specified files
    for purge_file in options.purge_files:
        ph = hiveconf.open_hive(purge_file)
        reduced_hive = hiveconf.open_hive(options.roothive, blacklist=[purge_file])
        purge_walk(reduced_hive, ph)

    # Import specified files
    for imp_file in options.imp_files:
        ih = hiveconf.open_hive(imp_file)
        imp_walk(hive, ih)

    # Actually purge. Note that if you specify all files where the
    # parameter is defined as purge files, then all instances of the
    # parameter will be deleted.
    for purge_file in options.purge_files:
        ph = hiveconf.open_hive(purge_file)
        for pp in purge_params:
            ph.delete(pp)

    # Handle -e parameters
    for (varname, param) in e_params:
        errors += eval_print(hive, varname, param, options.eval_export)

    # Handle -E
    for foldername in options.E_params:
        folder = hive.lookup(foldername)
        params = hive.get_parameters(foldername)
        for param in params:
            errors += eval_print(folder, param, param, options.eval_export)

    # Get/set listed parameters
    for param in args:
        errors += handle_param(hive, param)

    # Walk
    for foldername in options.walk_folders:
        folder = hive.lookup(foldername)
        if not folder:
            print("%s: Folder not found" % foldername, file=sys.stderr)
            continue

        if not isinstance(folder, hiveconf.Folder):
            print("%s: not a folder" % foldername, file=sys.stderr)
            continue

        print_walk(hive, foldername, options.recursive)

    sys.exit(errors)

if __name__ == "__main__":
    main()

