#!/usr/bin/env python
##
## This file is part of OpenSIPS CLI
## (see https://github.com/OpenSIPS/opensips-cli).
##
## 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 3 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 cmd
import sys
import os
import shlex
import readline
import atexit
import importlib
from opensipscli import comm
from opensipscli import defaults
from opensipscli.config import cfg
from opensipscli.logger import logger
from opensipscli.modules import *

class OpenSIPSCLIShell(cmd.Cmd, object):
    """
    OpenSIPS-Cli shell
    """
    modules = {}
    excluded_errs = {}
    registered_atexit = False

    def __init__(self, options):
        """
        contructor for OpenSIPS-Cli
        """

        self.debug = options.debug
        self.execute = options.execute
        self.command = options.command
        self.modules_dir_inserted = None

        if self.debug:
            logger.setLevel("DEBUG")

        if not options.config:
            cfg_file = None
            for f in defaults.CFG_PATHS:
                if os.path.isfile(f) and os.access(f, os.R_OK):
                    # found a valid config file
                    cfg_file = f
                    break
        else:
            cfg_file = options.config
        if not cfg_file:
            logger.debug("no config file found in any of {}".
                    format(", ".join(defaults.CFG_PATHS)))
        else:
            logger.debug("using config file {}".format(cfg_file))

        # __init__ of the configuration file
        cfg.parse(cfg_file)
        if not cfg.has_instance(options.instance):
            logger.warning("Unknown instance '{}'! Using default instance '{}'!".
                    format(options.instance, defaults.DEFAULT_SECTION))
            instance = defaults.DEFAULT_SECTION
        else:
            instance = options.instance
        cfg.set_instance(instance)
        cfg.set_custom_options(options.extra_options)

        if not self.execute:
            # __init__ of cmd.Cmd module
            cmd.Cmd.__init__(self)

        # Opening the current working instance
        self.update_instance(cfg.current_instance)

    def update_logger(self):
        """
        alter logging level
        """

        # first of all, let's handle logging
        if self.debug:
            level = "DEBUG"
        else:
            level = cfg.get("log_level")
        logger.setLevel(level)

    def clear_instance(self):
        """
        update history
        """
        # make sure we dump everything before swapping files
        self.history_write()

    def update_instance(self, instance):
        """
        constructor of an OpenSIPS-Cli instance
        """

        # first of all, let's handle logging
        self.current_instance = instance
        self.update_logger()

        # Update the intro and prompt
        self.intro = cfg.get('prompt_intro')
        self.prompt = '(%s): ' % cfg.get('prompt_name')

        # initialize communcation handler
        self.handler = comm.initialize()

        # remove all loaded modules
        self.modules = {}

        if not self.execute:
            print(self.intro)
            # add the built-in modules and commands list
            for mod in ['set', 'clear', 'help', 'history', 'exit', 'quit']:
                self.modules[mod] = (self, None)

        if not cfg.exists('skip_modules'):
            skip_modules = []
        else:
            skip_modules = cfg.get('skip_modules')

        available_modules = { key[20:]: sys.modules[key] for key in
                sys.modules.keys() if
                key.startswith("opensipscli.modules.") and
                key[20:] not in skip_modules }
        for name, module in available_modules.items():
            m = importlib.import_module("opensipscli.modules.{}".format(name))
            if not hasattr(m, "Module"):
                logger.debug("Skipping module '{}' - does not extend Module".
                        format(name))
                continue
            if not hasattr(m, name):
                logger.debug("Skipping module '{}' - module implementation not found".
                        format(name))
                continue
            mod = getattr(module, name)
            if not hasattr(mod, '__exclude__') or not hasattr(mod, '__get_methods__'):
                logger.debug("Skipping module '{}' - module does not implement Module".
                        format(name))
                continue
            excl_mod = mod.__exclude__(mod)
            if excl_mod[0] is True:
                if excl_mod[1]:
                    self.excluded_errs[name] = excl_mod[1]
                logger.debug("Skipping module '{}' - excluded on purpose".format(name))
                continue
            logger.debug("Loaded module '{}'".format(name))
            imod = mod()
            self.modules[name] = (imod, mod.__get_methods__(imod))

    def history_write(self):
        """
        save history file
        """
        history_file = cfg.get('history_file')
        logger.debug("saving history in {}".format(history_file))
        os.makedirs(os.path.expanduser(os.path.dirname(history_file)), exist_ok=True)
        try:
            readline.write_history_file(os.path.expanduser(history_file))
        except PermissionError:
            logger.warning("failed to write CLI history to {} " +
                            "(no permission)".format(
                history_file))

    def preloop(self):
        """
        preload a history file
        """
        history_file = cfg.get('history_file')
        logger.debug("using history file {}".format(history_file))
        try:
            readline.read_history_file(os.path.expanduser(history_file))
        except PermissionError:
            logger.warning("failed to read CLI history from {} " +
                            "(no permission)".format(
                history_file))
        except FileNotFoundError:
            pass

        readline.set_history_length(int(cfg.get('history_file_size')))
        if not self.registered_atexit:
            atexit.register(self.history_write)

    def postcmd(self, stop, line):
        """
        post command after switching instance
        """
        if self.current_instance != cfg.current_instance:
            self.clear_instance()
            self.update_instance(cfg.current_instance)
            # make sure we update all the history information
            self.preloop()

        return stop

    def print_topics(self, header, cmds, cmdlen, maxcol):
        """
        print topics, omit misc commands
        """
        if header is not None:
            if cmds:
                self.stdout.write('%s\n' % str(header))
                if self.ruler:
                    self.stdout.write('%s\n' % str(self.ruler*len(header)))
                self.columnize(cmds, maxcol-1)
                self.stdout.write('\n')

    def cmdloop(self, intro=None):
        """
        command loop, catching SIGINT
        """
        if self.execute:
            if len(self.command) < 1:
                logger.error("no modules to run specified!")
                return -1
            if len(self.command) < 2:
                logger.debug("no method to in '{}' run specified!".
                        format(self.command[0]))
                command = None
                params = None
            else:
                command = self.command[1]
                params = self.command[2:]

            logger.debug("running in non-interactive mode '{}'".format(self.command))

            try:
                ret = self.run_command(self.command[0], command, params)
            except KeyboardInterrupt:
                print('^C')
                return -1

            # assume that by default it exists with success
            if ret is None:
                ret = 0
            return ret
        while True:
            try:
                super(OpenSIPSCLIShell, self).cmdloop(intro='')
                break
            except KeyboardInterrupt:
                print('^C')
        # any other commands exits with negative value
        return -1

    def emptyline(self):
        if cfg.getBool('prompt_emptyline_repeat_cmd'):
            super().emptyline()

    def complete_modules(self, text):
        """
        complete modules selection based on given text
        """
        l = [a for a in self.modules.keys() if a.startswith(text)]
        if len(l) == 1:
            l[0] = l[0] + " "
        return l

    def complete_functions(self, module, text, line, begidx, endidx):
        """
        complete function selection based on given text
        """

        # builtin commands
        params = line.split()
        if module[1] is not None and \
                (len(params) < 2 or (len(params) == 2 and line[-1] != ' ')):
            l = [a for a in module[1] if a.startswith(text)]
            if len(l) == 1:
                l[0] += " "
        else:
            try:
                compfunc = getattr(module[0], '__complete__')
                p = params[1] if len(params) > 1 else None
                l = compfunc(p, text, line, begidx, endidx)
                if not l:
                    return None
            except AttributeError:
                return ['']
            # looking for a different command
        return l

    # Overwritten function for our customized auto-complete
    def complete(self, text, state):
        """
        auto-complete selection based on given text and state parameters
        """
        if state == 0:
            origline = readline.get_line_buffer()
            line = origline.lstrip()
            stripped = len(origline) - len(line)
            begidx = readline.get_begidx() - stripped
            endidx = readline.get_endidx() - stripped
            if begidx > 0:
                mod, args, foo = self.parseline(line)
                if mod == '':
                    return self.complete_modules(text)[state]
                elif not mod in self.modules:
                    logger.error("BUG: mod '{}' not found!".format(mod))
                else:
                    module = self.modules[mod]
                    self.completion_matches = \
                        self.complete_functions(module, text, line, begidx, endidx)
            else:
                self.completion_matches = self.complete_modules(text)
        try:
            return self.completion_matches[state]
        except IndexError:
            return ['']

    # Execute commands from Modules
    def run_command(self, module, cmd, params):
        """
        run a module command with given parameters
        """
        try:
            mod = self.modules[module]
        except (AttributeError, KeyError):
            if module in self.excluded_errs:
                for err_msg in self.excluded_errs[module]:
                    logger.error(err_msg)
                return -1
            else:
                logger.error("no module '{}' loaded".format(module))
                return -1
        # if the module does not return any methods (returned None)
        # we simply call the module's name method
        if not mod[1]:
            if params is not None:
                params.insert(0, cmd)
            cmd = mod[0].__module__
            if cmd.startswith("opensipscli.modules."):
                cmd = cmd[20:]
        elif not cmd and '' not in mod[1]:
            logger.error("module '{}' expects the following commands: {}".
                   format(module, ", ".join(mod[1])))
            return -1
        elif cmd and not cmd in mod[1]:
            logger.error("no command '{}' in module '{}'".
                    format(cmd, module))
            return -1
        logger.debug("running command '{}' '{}'".format(cmd, params))
        return mod[0].__invoke__(cmd, params)

    def default(self, line):
        try:
            aux = shlex.split(line)
        except ValueError:
            """ if the line ends in a backspace, just clean it"""
            line = line[:-1]
            aux = shlex.split(line)

        module = str(aux[0])
        if len(aux) == 1:
            cmd = None
            params = None
        else:
            cmd = str(aux[1])
            params = aux[2:]
        self.run_command(module, cmd, params)

    def do_history(self, line):
        """
        print entries in history file
        """
        if not line:
            try:
                with open(os.path.expanduser(cfg.get('history_file'))) as hf:
                    for num, line in enumerate(hf, 1):
                        print(num, line, end='')
            except FileNotFoundError:
                pass

    def do_set(self, line):
        """
        handle dynamic settings (key-value pairs)
        """
        parsed = line.split('=', 1)
        if len(parsed) < 2:
            logger.error("setting value format is 'key=value'!")
            return
        key = parsed[0]
        value = parsed[1]
        cfg.set(key, value)

    # Used to get info for a certain command
    def do_help(self, line):
        # TODO: Add help for commands
        print("Usage:: help cmd - returns information about \"cmd\"")

    # Clear the terminal screen
    def do_clear(self, line):
        os.system('clear')

    # Commands used to exit the shell
    def do_EOF(self, line):  # It catches Ctrl+D
        print('^D')
        return True

    def do_quit(self, line):
        return True

    def do_exit(self, line):
        return True