#!/usr/bin/env python
# Copyright (C) 2012-2013  Peter Hatina <phatina@redhat.com>
#
# 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, see <http://www.gnu.org/licenses/>.

import os
import sys
import code
import pywbem
import readline
import rlcompleter
import __builtin__
from lmi.lmi_client_base import LmiReturnValue
from lmi.lmi_client_base import LmiBaseClient
from lmi.lmi_client_shell import LmiConnection
from lmi.lmi_client_shell import _LmiNamespace
from lmi.lmi_client_shell import _LmiNamespaceRoot
from lmi.lmi_client_shell import _LmiClass
from lmi.lmi_client_shell import _LmiInstance
from lmi.lmi_client_shell import _LmiInstanceName

def get_item_or_default(dic, item, default):
    return dic[item] if item in dic else default

def _lmi_displayhook(o):
    if o is None:
        return
    __builtin__._ = None
    if isinstance(o, LmiReturnValue) and o.use_display_hook:
        o.use_display_hook = False
        if o.rval == 0 or (o.rval == 4096 and o.instance_classname == "LMI_PowerManagementService"):
            result_str = "ok"
        else:
            result_str = "fail"
        sys.stdout.write("%s: %s (%s)" % (o.hostname, result_str, str(o.rval)))
        if o.errorstr:
            sys.stdout.write(": %s" % o.errorstr)
        sys.stdout.write("\n")
    else:
        print o
    __builtin__._ = o

sys.displayhook = _lmi_displayhook

class _LmiShellOptions(object):
    def __init__(self, argv):
        self._argv = argv
        self._script_name = ""
        self._script_argv = []
        self._config_file = ""
        self._good = True
        self._inspect = False
        self._help = False
        for a in self._argv[1:]:
            if not self._script_name and a in ("-h", "--help"):
                self._help = True
                return
            elif not self._script_name and a in ("-i", "--inspect"):
                self._inspect = True
            elif not self._script_name and not a.startswith("-"):
                self._script_name = a
            elif self._script_name:
                self._script_argv.append(a)
            else:
                self._good = False
                break
    @property
    def config_file(self):
        return self._config_file

    @property
    def good(self):
        return self._good

    @property
    def help(self):
        return self._help

    @property
    def interactive(self):
        return not self._script_name

    @property
    def inspect(self):
        return self._inspect

    @property
    def script_name(self):
        return self._script_name

    @property
    def script_args(self):
        return [self._script_name] + self._script_argv

    def print_usage(self):
        sys.stdout.write("Usage: %s [options] [script] [script-options]\n" % os.path.basename(self._argv[0]))
        sys.stdout.write("\nOptions:\n")
        sys.stdout.write("  -h, --help            print this message\n")
        sys.stdout.write("  -i, --interact        inspect interactively after running a script\n")

class _LmiCompleter(rlcompleter.Completer):
    def __init__(self, namespace = None):
        rlcompleter.Completer.__init__(self, namespace)
        self._last_complete = []

    def __complete_object_methods(self, obj, text):
        result = filter(lambda m: not m.startswith("_") \
            and callable(getattr(obj, m)) \
            and m.lower().startswith(text.lower()), dir(obj))
        return result

    def __complete_object_properties(self, obj, text):
        result = filter(lambda m: not m.startswith("_") \
            and not callable(getattr(obj, m)) \
            and m.lower().startswith(text.lower()), dir(obj))
        return result

    def _callable_postfix(self, val, word):
        if hasattr(val, '__call__') \
            and not isinstance(val, (_LmiNamespace, _LmiClass, _LmiInstance, _LmiInstanceName, LmiReturnValue)):
            word = word + "("
        return word

    def complete(self, text, state):
        if not text:
            return ("\t", None)[state]
        if state > 0:
            return self._last_complete[state]
        self._last_complete = []
        members = text.split(".")
        if members[0] in self.namespace:
            cmd = ".".join(members[0:-1])
            to_complete = members[-1]
            expr = eval(cmd, self.namespace)
            methods = self.__complete_object_methods(expr, to_complete)
            properties = self.__complete_object_properties(expr, to_complete)
            if isinstance(expr, (LmiConnection, _LmiNamespaceRoot)):
                for n in expr.namespaces:
                    if n.lower().startswith(to_complete.lower()):
                        self._last_complete.append(cmd + "." + n)
                methods = [x for x in methods if x not in expr.namespaces]
            elif isinstance(expr, _LmiNamespace):
                for c in expr.classes():
                    if c.lower().startswith(to_complete.lower()):
                        self._last_complete.append(cmd + "." + c)
            elif isinstance(expr, _LmiInstance):
                for m in expr.methods():
                    if m.lower().startswith(to_complete.lower()):
                        self._last_complete.append(cmd + "." + m + "(")
                for p in expr.properties():
                    if p.lower().startswith(to_complete.lower()):
                        self._last_complete.append(cmd + "." + p)
            elif isinstance(expr, LmiReturnValue):
                for p in expr.properties():
                    if p.lower().startswith(to_complete.lower()):
                        self._last_complete.append(cmd + "." + p)
            self._last_complete.extend(cmd + "." + m + "(" for m in methods)
            self._last_complete.extend(cmd + "." + p for p in properties)
            return self._last_complete[state]
        return rlcompleter.Completer.complete(self, text, state)

class LmiInteractiveShell(code.InteractiveConsole):
    DEFAULT_CONFIG_FILE = "~/.lmishellrc"
    DEFAULT_HISTORY_FILE = "~/.lmi_shell_history"
    DEFAULT_HISTORY_LENGTH = -1

    def __init__(self, prompt, more_prompt, locals = None, **kwargs):
        sys.ps1 = prompt
        sys.ps2 = more_prompt
        if locals is None:
            locals = {}
        locals["LmiConnection"] = LmiConnection
        locals["connect"] = _connect_interactive
        locals["clear_history"] = self.clear_history
        locals["use_display_sugar"] = _use_display_sugar
        locals["use_exceptions"] = _use_exceptions
        code.InteractiveConsole.__init__(self, locals)
        self._completer = _LmiCompleter(self.locals)
        self.load_config(get_item_or_default(kwargs, "config", LmiInteractiveShell.DEFAULT_CONFIG_FILE))
        readline.set_completer(self._completer.complete)
        readline.parse_and_bind('tab: complete')

    def interact(self):
        self.load_history()
        try:
            sys.ps1
        except AttributeError:
            sys.ps1 = ">>> "
        try:
            sys.ps2
        except AttributeError:
            sys.ps2 = "... "
        more = 0
        while 1:
            try:
                if more:
                    prompt = sys.ps2
                else:
                    prompt = sys.ps1
                try:
                    line = self.raw_input(prompt)
                    # Can be None if sys.stdin was redefined
                    encoding = getattr(sys.stdin, "encoding", None)
                    if encoding and not isinstance(line, unicode):
                        line = line.decode(encoding)
                except EOFError:
                    self.write("\n")
                    break
                else:
                    more = self.push(line)
            except KeyboardInterrupt:
                self.write("\n")
                self.resetbuffer()
                more = 0
        self.save_history()

    def load_config(self, config_file):
        try:
            conf = {}
            execfile(os.path.expanduser(config_file), conf)
            self._history_file = os.path.expanduser(get_item_or_default(conf, "history_file",
                LmiInteractiveShell.DEFAULT_HISTORY_FILE))
            self._history_length = get_item_or_default(conf, "history_length",
                LmiInteractiveShell.DEFAULT_HISTORY_LENGTH)
        except (SyntaxError, IOError), e:
            if isinstance(e, SyntaxError):
                sys.stderr.write("Error: %s\n" % e)
            self._history_file = os.path.expanduser(LmiInteractiveShell.DEFAULT_HISTORY_FILE)
            self._history_length = LmiInteractiveShell.DEFAULT_HISTORY_LENGTH

    def load_history(self):
        if self._history_length == 0 or not os.path.exists(self._history_file):
            return
        readline.read_history_file(self._history_file)
        if self._history_length > 0 and readline.get_current_history_length() > self._history_length:
            readline.set_history_length(length)
            readline.write_history_file(self._history_file)
            readline.read_history_file(self._history_file)

    def save_history(self):
        if self._history_length == 0:
            return
        elif self._history_length > 0:
            readline.set_history_length(self._history_length)
        readline.write_history_file(self._history_file)

    def clear_history(self):
        readline.clear_history()

def _lmi_interact(locals = None):
    console = LmiInteractiveShell("> ", "... ", locals)
    console.interact()

def _connect_interactive(hostname, username = "", password = ""):
    return _connect(hostname, username, password, True)

def _connect_noninteractive(hostname, username = "", password = ""):
    return _connect(hostname, username, password, False)

def _connect(hostname, username = "", password = "", interactive = False):
    if not username:
        while True:
            try:
                username = raw_input("username: ")
                if username:
                    break
            except EOFError, e:
                sys.stdout.write("\n")
                continue
            except KeyboardInterrupt, e:
                sys.stdout.write("\n")
                return None
        readline.remove_history_item(readline.get_current_history_length() - 1)
    if not password:
        try:
            os.system("stty -echo")
            password = raw_input("password: ")
            os.system("stty echo")
        except EOFError, e:
            password = ""
        except KeyboardInterrupt, e:
            os.system("stty echo")
            sys.stdout.write("\n")
            return None
        sys.stdout.write("\n")
        readline.remove_history_item(readline.get_current_history_length() - 1)
    # Try to get some non-existing class as a login check
    connection = LmiConnection(hostname, username, password, interactive)
    use_exceptions = LmiBaseClient._get_use_exceptions()
    try:
        LmiBaseClient._set_use_exceptions(True)
        connection.root.cimv2.NonExistingClass
    except pywbem.cim_operations.CIMError, e:
        if e.args[0] == pywbem.cim_constants.CIM_ERR_NOT_FOUND:
            return connection
        if use_exceptions:
            raise
        return None
    except pywbem.cim_http.AuthError, e:
        return None
    finally:
        LmiBaseClient._set_use_exceptions(use_exceptions)
    return connection

def _use_display_sugar(use = True):
    sys.displayhook = _lmi_displayhook if use else sys.__displayhook__

def _use_exceptions(use = True):
    LmiBaseClient._set_use_exceptions(use)

if __name__ == "__main__":
    options = _LmiShellOptions(sys.argv)
    if not options.good:
        sys.stderr.write("Wrong tool usage!\n\n")
        options.print_usage()
        sys.exit(1)

    if options.help:
        options.print_usage()
        sys.exit(0)

    if options.interactive:
        _lmi_interact()
    else:
        locals = {
            "LmiConnection" : LmiConnection,
            "connect" : _connect_noninteractive,
            "use_exceptions" : _use_exceptions
        }
        sys.argv = options.script_args
        try:
            execfile(options.script_name, locals)
        except SystemExit, e:
            if not options.inspect:
                sys.exit(e.code)
        if options.inspect:
            _lmi_interact(locals)
    sys.exit(0)
