# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:

# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser 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.
#
# qutebrowser 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 qutebrowser.  If not, see <http://www.gnu.org/licenses/>.

"""Misc. utility commands exposed to the user."""

import functools
import os
import signal
import traceback

try:
    import hunter
except ImportError:
    hunter = None

import sip
from PyQt5.QtCore import QUrl
# so it's available for :debug-pyeval
from PyQt5.QtWidgets import QApplication  # pylint: disable=unused-import

from qutebrowser.browser import qutescheme
from qutebrowser.utils import log, objreg, usertypes, message, debug, utils
from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import style
from qutebrowser.misc import consolewidget


@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True)
@cmdutils.argument('win_id', win_id=True)
def later(ms: int, command, win_id):
    """Execute a command after some time.

    Args:
        ms: How many milliseconds to wait.
        command: The command to run, with optional args.
    """
    if ms < 0:
        raise cmdexc.CommandError("I can't run something in the past!")
    commandrunner = runners.CommandRunner(win_id)
    app = objreg.get('app')
    timer = usertypes.Timer(name='later', parent=app)
    try:
        timer.setSingleShot(True)
        try:
            timer.setInterval(ms)
        except OverflowError:
            raise cmdexc.CommandError("Numeric argument is too large for "
                                      "internal int representation.")
        timer.timeout.connect(
            functools.partial(commandrunner.run_safely, command))
        timer.timeout.connect(timer.deleteLater)
        timer.start()
    except:
        timer.deleteLater()
        raise


@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True)
@cmdutils.argument('win_id', win_id=True)
def repeat(times: int, command, win_id):
    """Repeat a given command.

    Args:
        times: How many times to repeat.
        command: The command to run, with optional args.
    """
    if times < 0:
        raise cmdexc.CommandError("A negative count doesn't make sense.")
    commandrunner = runners.CommandRunner(win_id)
    for _ in range(times):
        commandrunner.run_safely(command)


@cmdutils.register(maxsplit=1, hide=True, no_cmd_split=True,
                   no_replace_variables=True)
@cmdutils.argument('win_id', win_id=True)
@cmdutils.argument('count', count=True)
def run_with_count(count_arg: int, command, win_id, count=1):
    """Run a command with the given count.

    If run_with_count itself is run with a count, it multiplies count_arg.

    Args:
        count_arg: The count to pass to the command.
        command: The command to run, with optional args.
        count: The count that run_with_count itself received.
    """
    runners.CommandRunner(win_id).run(command, count_arg * count)


@cmdutils.register(hide=True)
def message_error(text):
    """Show an error message in the statusbar.

    Args:
        text: The text to show.
    """
    message.error(text)


@cmdutils.register(hide=True)
@cmdutils.argument('count', count=True)
def message_info(text, count=1):
    """Show an info message in the statusbar.

    Args:
        text: The text to show.
        count: How many times to show the message
    """
    for _ in range(count):
        message.info(text)


@cmdutils.register(hide=True)
def message_warning(text):
    """Show a warning message in the statusbar.

    Args:
        text: The text to show.
    """
    message.warning(text)


@cmdutils.register(hide=True)
def clear_messages():
    """Clear all message notifications."""
    message.global_bridge.clear_messages.emit()


@cmdutils.register(debug=True)
@cmdutils.argument('typ', choices=['exception', 'segfault'])
def debug_crash(typ='exception'):
    """Crash for debugging purposes.

    Args:
        typ: either 'exception' or 'segfault'.
    """
    if typ == 'segfault':
        os.kill(os.getpid(), signal.SIGSEGV)
        raise Exception("Segfault failed (wat.)")
    else:
        raise Exception("Forced crash")


@cmdutils.register(debug=True)
def debug_all_objects():
    """Print a list of  all objects to the debug log."""
    s = debug.get_all_objects()
    log.misc.debug(s)


@cmdutils.register(debug=True)
def debug_cache_stats():
    """Print LRU cache stats."""
    config_info = objreg.get('config').get.cache_info()
    style_info = style.get_stylesheet.cache_info()
    log.misc.debug('config: {}'.format(config_info))
    log.misc.debug('style: {}'.format(style_info))


@cmdutils.register(debug=True)
def debug_console():
    """Show the debugging console."""
    try:
        con_widget = objreg.get('debug-console')
    except KeyError:
        log.misc.debug('initializing debug console')
        con_widget = consolewidget.ConsoleWidget()
        objreg.register('debug-console', con_widget)

    if con_widget.isVisible():
        log.misc.debug('hiding debug console')
        con_widget.hide()
    else:
        log.misc.debug('showing debug console')
        con_widget.show()


@cmdutils.register(debug=True, maxsplit=0, no_cmd_split=True)
def debug_trace(expr=""):
    """Trace executed code via hunter.

    Args:
        expr: What to trace, passed to hunter.
    """
    if hunter is None:
        raise cmdexc.CommandError("You need to install 'hunter' to use this "
                                  "command!")
    try:
        eval('hunter.trace({})'.format(expr))
    except Exception as e:
        raise cmdexc.CommandError("{}: {}".format(e.__class__.__name__, e))


@cmdutils.register(maxsplit=0, debug=True, no_cmd_split=True)
def debug_pyeval(s, quiet=False):
    """Evaluate a python string and display the results as a web page.

    Args:
        s: The string to evaluate.
        quiet: Don't show the output in a new tab.
    """
    try:
        r = eval(s)
        out = repr(r)
    except Exception:
        out = traceback.format_exc()

    qutescheme.pyeval_output = out
    if quiet:
        log.misc.debug("pyeval output: {}".format(out))
    else:
        tabbed_browser = objreg.get('tabbed-browser', scope='window',
                                    window='last-focused')
        tabbed_browser.openurl(QUrl('qute://pyeval'), newtab=True)


@cmdutils.register(debug=True)
def debug_set_fake_clipboard(s=None):
    """Put data into the fake clipboard and enable logging, used for tests.

    Args:
        s: The text to put into the fake clipboard, or unset to enable logging.
    """
    if s is None:
        utils.log_clipboard = True
    else:
        utils.fake_clipboard = s


@cmdutils.register(hide=True)
@cmdutils.argument('win_id', win_id=True)
@cmdutils.argument('count', count=True)
def repeat_command(win_id, count=None):
    """Repeat the last executed command.

    Args:
        count: Which count to pass the command.
    """
    mode_manager = objreg.get('mode-manager', scope='window', window=win_id)
    if mode_manager.mode not in runners.last_command:
        raise cmdexc.CommandError("You didn't do anything yet.")
    cmd = runners.last_command[mode_manager.mode]
    commandrunner = runners.CommandRunner(win_id)
    commandrunner.run(cmd[0], count if count is not None else cmd[1])


@cmdutils.register(debug=True, name='debug-log-capacity')
def log_capacity(capacity: int):
    """Change the number of log lines to be stored in RAM.

    Args:
       capacity: Number of lines for the log.
    """
    if capacity < 0:
        raise cmdexc.CommandError("Can't set a negative log capacity!")
    else:
        log.ram_handler.change_log_capacity(capacity)


@cmdutils.register(debug=True)
@cmdutils.argument('level', choices=sorted(
    (level.lower() for level in log.LOG_LEVELS),
    key=lambda e: log.LOG_LEVELS[e.upper()]))
def debug_log_level(level: str):
    """Change the log level for console logging.

    Args:
        level: The log level to set.
    """
    log.change_console_formatter(log.LOG_LEVELS[level.upper()])
    log.console_handler.setLevel(log.LOG_LEVELS[level.upper()])


@cmdutils.register(debug=True)
def debug_log_filter(filters: str):
    """Change the log filter for console logging.

    Args:
        filters: A comma separated list of logger names. Can also be "none" to
                 clear any existing filters.
    """
    if log.console_filter is None:
        raise cmdexc.CommandError("No log.console_filter. Not attached "
                                  "to a console?")

    if filters.strip().lower() == 'none':
        log.console_filter.names = None
        return

    if not set(filters.split(',')).issubset(log.LOGGER_NAMES):
        raise cmdexc.CommandError("filters: Invalid value {} - expected one "
                                  "of: {}".format(filters,
                                                  ', '.join(log.LOGGER_NAMES)))

    log.console_filter.names = filters.split(',')


@cmdutils.register()
@cmdutils.argument('current_win_id', win_id=True)
def window_only(current_win_id):
    """Close all windows except for the current one."""
    for win_id, window in objreg.window_registry.items():

        # We could be in the middle of destroying a window here
        if sip.isdeleted(window):
            continue

        if win_id != current_win_id:
            window.close()
