#!/usr/bin/python
#

# Copyright (C) 2006, 2007 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.


"""Tool to do manual changes to the config file.

"""

# functions in this module need to have a given name structure, so:
# pylint: disable-msg=C0103


import optparse
import cmd

try:
  import readline
  _wd = readline.get_completer_delims()
  _wd = _wd.replace("-", "")
  readline.set_completer_delims(_wd)
  del _wd
except ImportError:
  pass

from ganeti import errors
from ganeti import config
from ganeti import objects


class ConfigShell(cmd.Cmd):
  """Command tool for editing the config file.

  Note that although we don't do saves after remove, the current
  ConfigWriter code does that; so we can't prevent someone from
  actually breaking the config with this tool. It's the users'
  responsibility to know what they're doing.

  """
  prompt = "(/) "

  def __init__(self, cfg_file=None):
    """Constructor for the ConfigShell object.

    The optional cfg_file argument will be used to load a config file
    at startup.

    """
    cmd.Cmd.__init__(self)
    self.cfg = None
    self.parents = []
    self.path = []
    if cfg_file:
      self.do_load(cfg_file)
      self.postcmd(False, "")

  def emptyline(self):
    """Empty line handling.

    Note that the default will re-run the last command. We don't want
    that, and just ignore the empty line.

    """
    return False

  @staticmethod
  def _get_entries(obj):
    """Computes the list of subdirs and files in the given object.

    This, depending on the passed object entry, look at each logical
    child of the object and decides if it's a container or a simple
    object. Based on this, it computes the list of subdir and files.

    """
    dirs = []
    entries = []
    if isinstance(obj, objects.ConfigObject):
      for name in obj.__slots__:
        child = getattr(obj, name, None)
        if isinstance(child, (list, dict, tuple, objects.ConfigObject)):
          dirs.append(name)
        else:
          entries.append(name)
    elif isinstance(obj, (list, tuple)):
      for idx, child in enumerate(obj):
        if isinstance(child, (list, dict, tuple, objects.ConfigObject)):
          dirs.append(str(idx))
        else:
          entries.append(str(idx))
    elif isinstance(obj, dict):
      dirs = obj.keys()

    return dirs, entries

  def precmd(self, line):
    """Precmd hook to prevent commands in invalid states.

    This will prevent everything except load and quit when no
    configuration is loaded.

    """
    if line.startswith("load") or line == 'EOF' or line == "quit":
      return line
    if not self.parents or self.cfg is None:
      print "No config data loaded"
      return ""
    return line

  def postcmd(self, stop, line):
    """Postcmd hook to update the prompt.

    We show the current location in the prompt and this function is
    used to update it; this is only needed after cd and load, but we
    update it anyway.

    """
    if self.cfg is None:
      self.prompt = "(#no config) "
    else:
      self.prompt = "(/%s) " % ("/".join(self.path),)
    return stop

  def do_load(self, line):
    """Load function.

    Syntax: load [/path/to/config/file]

    This will load a new configuration, discarding any existing data
    (if any). If no argument has been passed, it will use the default
    config file location.

    """
    if line:
      arg = line
    else:
      arg = None
    try:
      self.cfg = config.ConfigWriter(cfg_file=arg, offline=True)
      self.cfg._OpenConfig()
      self.parents = [self.cfg._config_data]
      self.path = []
    except errors.ConfigurationError, err:
      print "Error: %s" % str(err)
    return False

  def do_ls(self, line):
    """List the current entry.

    This will show directories with a slash appended and files
    normally.

    """
    dirs, entries = self._get_entries(self.parents[-1])
    for i in dirs:
      print i + "/"
    for i in entries:
      print i
    return False

  def complete_cd(self, text, line, begidx, endidx):
    """Completion function for the cd command.

    """
    pointer = self.parents[-1]
    dirs, entries = self._get_entries(pointer)
    matches = [str(name) for name in dirs if name.startswith(text)]
    return matches

  def do_cd(self, line):
    """Changes the current path.

    Valid arguments: either .., /, "" (no argument) or a child of the current
    object.

    """
    if line == "..":
      if self.path:
        self.path.pop()
        self.parents.pop()
        return False
      else:
        print "Already at top level"
        return False
    elif len(line) == 0 or line == "/":
      self.parents = self.parents[0:1]
      self.path = []
      return False

    pointer = self.parents[-1]
    dirs, entries = self._get_entries(pointer)

    if line not in dirs:
      print "No such child"
      return False
    if isinstance(pointer, (dict, list, tuple)):
      if isinstance(pointer, (list, tuple)):
        line = int(line)
      new_obj = pointer[line]
    else:
      new_obj = getattr(pointer, line)
    self.parents.append(new_obj)
    self.path.append(str(line))
    return False

  def do_pwd(self, line):
    """Shows the current path.

    This duplicates the prompt functionality, but it's reasonable to
    have.

    """
    print "/" + "/".join(self.path)
    return False

  def complete_cat(self, text, line, begidx, endidx):
    """Completion for the cat command.

    """
    pointer = self.parents[-1]
    dirs, entries = self._get_entries(pointer)
    matches = [name for name in entries if name.startswith(text)]
    return matches

  def do_cat(self, line):
    """Shows the contents of the given file.

    This will display the contents of the given file, which must be a
    child of the current path (as shows by `ls`).

    """
    pointer = self.parents[-1]
    dirs, entries = self._get_entries(pointer)
    if line not in entries:
      print "No such entry"
      return False

    if isinstance(pointer, (dict, list, tuple)):
      if isinstance(pointer, (list, tuple)):
        line = int(line)
      val = pointer[line]
    else:
      val = getattr(pointer, line)
    print val
    return False

  def do_verify(self, line):
    """Verify the configuration.

    This verifies the contents of the configuration file (and not the
    in-memory data, as every modify operation automatically saves the
    file).

    """
    vdata = self.cfg.VerifyConfig()
    if vdata:
      print "Validation failed. Errors:"
      for text in vdata:
        print text
    return False

  def do_save(self, line):
    """Saves the configuration data.

    Note that is redundant (all modify operations automatically save
    the data), but it is good to use it as in the future that could
    change.

    """
    if self.cfg.VerifyConfig():
      print "Config data does not validate, refusing to save."
      return False
    self.cfg._WriteConfig()

  def do_rm(self, line):
    """Removes an instance or a node.

    This function works only on instances or nodes. You must be in
    either `/nodes` or `/instances` and give a valid argument.

    """
    pointer = self.parents[-1]
    data = self.cfg._config_data
    if pointer not in (data.instances, data.nodes):
      print "Can only delete instances and nodes"
      return False
    if pointer == data.instances:
      if line in data.instances:
        self.cfg.RemoveInstance(line)
      else:
        print "Invalid instance name"
    else:
      if line in data.nodes:
        self.cfg.RemoveNode(line)
      else:
        print "Invalid node name"

  def do_EOF(self, line):
    """Exit the application.

    """
    print
    return True

  def do_quit(self, line):
    """Exit the application.

    """
    print
    return True


class Error(Exception):
  """Generic exception"""
  pass


def ParseOptions():
  """Parses the command line options.

  In case of command line errors, it will show the usage and exit the
  program.

  Returns:
    (options, args), as returned by OptionParser.parse_args
  """

  parser = optparse.OptionParser()

  options, args = parser.parse_args()

  return options, args


def main():
  """Application entry point.

  This is just a wrapper over BootStrap, to handle our own exceptions.
  """
  options, args = ParseOptions()
  if args:
    cfg_file = args[0]
  else:
    cfg_file = None
  shell = ConfigShell(cfg_file=cfg_file)
  shell.cmdloop()


if __name__ == "__main__":
  main()
