# -*- coding: utf-8 -*-
#
# Copyright (C) 2014-2019 khalim19
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
This module defines:
* GTK exception dialog
* GTK generic message dialog
* wrapper for `sys.excepthook` that displays the GTK exception dialog when an
  unhandled exception is raised

This module should not be used directly. Use `pggui` as the contents of this
module are included in `pggui`.
"""

# NOTE: In order to allow logging errors as early as possible (before plug-in
# initialization), the `future` library is not imported in case some modules in
# the library are not available in the installed Python distribution and would
# thus cause an `ImportError` to be raised.

from __future__ import absolute_import, division, print_function, unicode_literals

str = unicode

import __builtin__
import functools
import sys
import traceback

try:
  import webbrowser
except ImportError:
  _webbrowser_module_found = False
else:
  _webbrowser_module_found = True

import pygtk
pygtk.require("2.0")
import gtk
import gobject
import pango

__all__ = [
  "display_error_message",
  "display_message",
  "add_gui_excepthook",
  "set_gui_excepthook",
  "set_gui_excepthook_parent",
  "set_gui_excepthook_additional_callback",
]


def display_error_message(
      title=None,
      app_name=None,
      parent=None,
      message_type=gtk.MESSAGE_ERROR,
      flags=gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
      message_markup=None,
      message_secondary_markup=None,
      details=None,
      display_details_initially=True,
      report_uri_list=None,
      report_description=None,
      button_stock_id=gtk.STOCK_CLOSE,
      button_response_id=gtk.RESPONSE_CLOSE,
      focus_on_button=False):
  """
  Display a message to alert the user about an error or an exception that
  occurred in the application.
  
  Parameters:
  
  * `title` - Message title.
  
  * `app_name` - Name of the application to use in the default contents of
    `message_secondary_markup`.
  
  * `parent` - Parent widget.
  
  * `message_type` - GTK message type (`gtk.MESSAGE_ERROR`, etc.).
  
  * `flags` - GTK dialog flags.
  
  * `message_markup` - Primary message text to display as markup.
  
  * `message_secondary_markup` - Secondary message text to display as markup.
  
  * `details` - Text to display in a box with details. If `None`, do not display
    any box.
  
  * `display_details_initially` - If `True`, display the details by default,
    otherwise hide them in an expander.
  
  * `report_uri_list` - List of (name, URL) pairs where the user can report
    the error. If no report list is desired, pass `None` or an empty sequence.
  
  * `report_description` - Text accompanying `report_uri_list`. If `None`, use
    default text. To suppress displaying the report description, pass an empty
    string.
  
  * `button_stock_id` - Stock ID of the button to close the dialog with.
  
  * `button_response_id` - Response ID of the button to close the dialog with.
  
  * `focus_on_button` - If `True`, focus on the button to close the dialog with.
    If `False`, focus on the box with details if `details` is not `None`,
    otherwise let the message dialog determine the focus widget.
  """
  if not ("_" in __builtin__.__dict__ or "_" in globals()):
    # This is a placeholder function until `gettext` is properly initialized.
    def _(str_):
      return str_
  else:
    # This is necessary since defining a local variable/function, even inside a
    # condition, obscures a global variable/function of the same name.
    _ = __builtin__.__dict__.get("_", None) or globals()["_"]
  
  if app_name is None:
    app_name = _("Plug-in")
  
  if message_markup is None:
    message_markup = (
      '<span font_size="large"><b>{0}</b></span>'.format(
        _("Oops. Something went wrong.")))
  
  if message_secondary_markup is None:
    message_secondary_markup = _(
      "{0} encountered an unexpected error and has to close. Sorry about that!").format(
        gobject.markup_escape_text(app_name))
  
  if report_description is None:
    report_description = _(
      "You can help fix this error by sending a report with the text "
      "in the details above to one of the following sites")
  
  dialog = gtk.MessageDialog(parent, type=message_type, flags=flags)
  dialog.set_transient_for(parent)
  if title is not None:
    dialog.set_title(title)
  
  dialog.set_markup(message_markup)
  dialog.format_secondary_markup(message_secondary_markup)
  
  if details is not None:
    expander = _get_details_expander(details, _("Details"))
    if display_details_initially:
      expander.set_expanded(True)
  else:
    expander = None
  
  if report_uri_list:
    vbox_labels_report = _get_report_link_buttons(
      report_uri_list, report_description, _("(right-click to copy link)"))
    dialog.vbox.pack_end(vbox_labels_report, expand=False, fill=False)
  
  if expander is not None:
    dialog.vbox.pack_start(expander, expand=False, fill=False)
  
  dialog.add_button(button_stock_id, button_response_id)
  
  if focus_on_button:
    button = dialog.get_widget_for_response(button_response_id)
    if button is not None:
      dialog.set_focus(button)
  else:
    if (expander is not None
        and expander.get_child() is not None
        and display_details_initially):
      dialog.set_focus(expander.get_child())
  
  dialog.show_all()
  response_id = dialog.run()
  dialog.destroy()
  
  return response_id


def _get_details_expander(details_text, details_label):
  expander = gtk.Expander()
  expander.set_use_markup(True)
  expander.set_label("<b>" + details_label + "</b>")
  
  scrolled_window = gtk.ScrolledWindow()
  scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
  scrolled_window.set_size_request(400, 200)
  scrolled_window.set_shadow_type(gtk.SHADOW_IN)
  
  exception_text_view = gtk.TextView()
  exception_text_view.set_editable(False)
  exception_text_view.set_wrap_mode(gtk.WRAP_WORD)
  exception_text_view.set_cursor_visible(False)
  exception_text_view.set_pixels_above_lines(1)
  exception_text_view.set_pixels_below_lines(1)
  exception_text_view.set_pixels_inside_wrap(0)
  exception_text_view.set_left_margin(5)
  exception_text_view.set_right_margin(5)
  exception_text_view.get_buffer().set_text(details_text)
  
  scrolled_window.add(exception_text_view)
  expander.add(scrolled_window)
  
  return expander


def _get_report_link_buttons(
      report_uri_list, report_description, label_report_text_instructions):
  if not report_uri_list:
    return None
  
  vbox_link_buttons = gtk.VBox(homogeneous=False)
  
  if report_description:
    label_report_text = report_description
    if not _webbrowser_module_found:
      label_report_text += " " + label_report_text_instructions
    label_report_text += ":"
    
    label_report = gtk.Label(label_report_text)
    label_report.set_alignment(0, 0.5)
    label_report.set_padding(3, 3)
    label_report.set_line_wrap(True)
    label_report.set_line_wrap_mode(pango.WRAP_WORD)
    vbox_link_buttons.pack_start(label_report, expand=False, fill=False)
  
  report_linkbuttons = []
  for name, uri in report_uri_list:
    linkbutton = gtk.LinkButton(uri, label=name)
    linkbutton.set_alignment(0, 0.5)
    report_linkbuttons.append(linkbutton)
  
  for linkbutton in report_linkbuttons:
    vbox_link_buttons.pack_start(linkbutton, expand=False, fill=False)
  
  if _webbrowser_module_found:
    # Apparently, GTK doesn't know how to open URLs on Windows, hence the custom
    # solution.
    for linkbutton in report_linkbuttons:
      linkbutton.connect(
        "clicked", lambda linkbutton: webbrowser.open_new_tab(linkbutton.get_uri()))
  
  return vbox_link_buttons


def display_message(
      message,
      message_type,
      title=None,
      parent=None,
      buttons=gtk.BUTTONS_OK,
      message_in_text_view=False,
      button_response_id_to_focus=None):
  """
  Display a generic message.
  
  Parameters:
  
  * `message` - The message to display.
  
  * `message_type` - GTK message type (`gtk.MESSAGE_INFO`, etc.).
  
  * `title` - Message title.
  
  * `parent` - Parent GUI element.
  
  * `buttons` - Buttons to display in the dialog.
  
  * `message_in_text_view` - If `True`, display text the after the first newline
    character in a text view.
  
  * `button_response_id_to_focus` - Response ID of the button to set as the
    focus. If `None`, the dialog determines which widget gets the focus.
  """
  dialog = gtk.MessageDialog(
    parent=parent,
    type=message_type,
    flags=gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
    buttons=buttons)
  dialog.set_transient_for(parent)
  if title is not None:
    dialog.set_title(title)
  
  messages = message.split("\n", 1)
  if len(messages) > 1:
    dialog.set_markup(gobject.markup_escape_text(messages[0]))
    
    if message_in_text_view:
      text_view = gtk.TextView()
      text_view.set_editable(False)
      text_view.set_wrap_mode(gtk.WRAP_WORD)
      text_view.set_cursor_visible(False)
      text_view.set_pixels_above_lines(1)
      text_view.set_pixels_below_lines(1)
      text_view.set_pixels_inside_wrap(0)
      text_view.get_buffer().set_text(messages[1])
      
      scrolled_window = gtk.ScrolledWindow()
      scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
      scrolled_window.set_property("height-request", 100)
      scrolled_window.set_shadow_type(gtk.SHADOW_IN)
      
      scrolled_window.add(text_view)
      
      vbox = dialog.get_message_area()
      vbox.pack_end(scrolled_window, expand=True, fill=True)
    else:
      dialog.format_secondary_markup(gobject.markup_escape_text(messages[1]))
  else:
    dialog.set_markup(gobject.markup_escape_text(message))
  
  if button_response_id_to_focus is not None:
    button = dialog.get_widget_for_response(button_response_id_to_focus)
    if button is not None:
      dialog.set_focus(button)
  
  dialog.show_all()
  response_id = dialog.run()
  dialog.destroy()
  
  return response_id


#===============================================================================

_gui_excepthook_parent = None
_gui_excepthook_additional_callback = lambda *args, **kwargs: False


def add_gui_excepthook(title, app_name, report_uri_list=None, parent=None):
  """
  Return a decorator that modifies `sys.excepthook` to display an error dialog
  for unhandled exceptions and terminates the application. `sys.excepthook` is
  restored once the decorated function finishes its execution.
  
  The dialog will not be displayed for exceptions which are not subclasses of
  `Exception` (such as `SystemExit` or `KeyboardInterrupt`).
  
  Parameters:
  
  * `title` - Dialog title.
  
  * `report_uri_list` - List of (name, URL) tuples where the user can report
    the error. If no report list is desired, pass `None` or an empty sequence.
  
  * `parent` - Parent GUI element.
  """
  global _gui_excepthook_parent
  
  _gui_excepthook_parent = parent
  
  def gui_excepthook(func):
    
    @functools.wraps(func)
    def func_wrapper(self, *args, **kwargs):
      
      def _gui_excepthook(exc_type, exc_value, exc_traceback):
        _gui_excepthook_generic(
          exc_type,
          exc_value,
          exc_traceback,
          orig_sys_excepthook,
          title,
          app_name,
          _gui_excepthook_parent,
          report_uri_list)
      
      orig_sys_excepthook = sys.excepthook
      sys.excepthook = _gui_excepthook
      
      func(self, *args, **kwargs)
      
      sys.excepthook = orig_sys_excepthook
    
    return func_wrapper
  
  return gui_excepthook


def set_gui_excepthook(title, app_name, report_uri_list=None, parent=None):
  """
  Modify `sys.excepthook` to display an error dialog for unhandled exceptions.
  
  The dialog will not be displayed for exceptions which are not subclasses of
  `Exception` (such as `SystemExit` or `KeyboardInterrupt`).
  
  For information about parameters, see `add_gui_excepthook()`.
  """
  global _gui_excepthook_parent
  
  _gui_excepthook_parent = parent
  
  def gui_excepthook(exc_type, exc_value, exc_traceback):
    _gui_excepthook_generic(
      exc_type,
      exc_value,
      exc_traceback,
      orig_sys_excepthook,
      title,
      app_name,
      _gui_excepthook_parent,
      report_uri_list)
  
  orig_sys_excepthook = sys.excepthook
  sys.excepthook = gui_excepthook


def set_gui_excepthook_parent(parent):
  """
  Set the parent GUI element to attach the exception dialog to when using
  `add_gui_excepthook()`. This function allows to modify the parent dynamically
  even after decorating a function with `add_gui_excepthook()`.
  """
  global _gui_excepthook_parent
  
  _gui_excepthook_parent = parent


def set_gui_excepthook_additional_callback(callback):
  """
  Set a callback to be executed at the beginning of exception handling. If the
  callback returns `True`, terminate exception handling at this point. Returning
  `True` consequently prevents the error dialog from being displayed and the
  application from being terminated.
  
  The callback takes the same parameters as `sys.excepthook`.
  """
  global _gui_excepthook_additional_callback
  
  _gui_excepthook_additional_callback = callback


def _gui_excepthook_generic(
      exc_type,
      exc_value,
      exc_traceback,
      orig_sys_excepthook,
      title,
      app_name,
      parent,
      report_uri_list):
  callback_result = _gui_excepthook_additional_callback(
    exc_type, exc_value, exc_traceback)
  
  if callback_result:
    return
  
  orig_sys_excepthook(exc_type, exc_value, exc_traceback)
  
  if issubclass(exc_type, Exception):
    exception_message = "".join(
      traceback.format_exception(exc_type, exc_value, exc_traceback))
    
    display_error_message(
      title=title,
      app_name=app_name,
      parent=parent,
      details=exception_message,
      report_uri_list=report_uri_list)
    
    # Make sure to quit the application since unhandled exceptions can
    # mess up the application state.
    if gtk.main_level() > 0:
      gtk.main_quit()