import os
from typing import Optional

import gi

gi.require_version('Gdk', '3.0')
from gi.repository import Gdk
from gi.repository import GLib
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk

from config import CONFIG
from src import commands as commands_
from src import setting as setting_
from src import update
from src import utils
from src import utils_setting as utils_setting_
from src.gui import messages as messages_
from src.gui import utils as gui_utils_
from src.gui import utils_grid as gui_utils_grid_


class SettingsManager:

  _BUTTON_SETTINGS_SPACING = 3

  _LOAD_SETTINGS_FROM_FILE_CUSTOM_WIDGETS_BORDER_WIDTH = 3

  _PREVIEWS_LOAD_SETTINGS_KEY = 'load_settings'

  def __init__(
        self,
        settings,
        dialog,
        previews_controller=None,
        display_message_func=None,
        log_viewer=None,
  ):
    self._settings = settings
    self._dialog = dialog
    self._previews_controller = previews_controller
    self._display_message_func = (
      display_message_func if display_message_func is not None else utils.empty_func)
    self._log_viewer = log_viewer

    self._init_gui()

  def _init_gui(self):
    self._label_button_settings = Gtk.Label(
      label=_('_Settings'),
      use_underline=True,
    )

    self._hbox_button_settings_components = Gtk.Box(
      orientation=Gtk.Orientation.HORIZONTAL,
      spacing=self._BUTTON_SETTINGS_SPACING,
    )
    self._hbox_button_settings_components.pack_start(self._label_button_settings, False, False, 0)
    self._hbox_button_settings_components.pack_start(
      Gtk.Image.new_from_icon_name('pan-down', Gtk.IconSize.BUTTON), False, False, 0)

    self._hbox_button_settings = Gtk.Box(
      orientation=Gtk.Orientation.HORIZONTAL,
    )
    self._hbox_button_settings.set_center_widget(self._hbox_button_settings_components)

    self._button_settings = Gtk.Button()
    self._button_settings.add(self._hbox_button_settings)

    self._preferences = Preferences(self._settings, self._button_settings, self._dialog)

    self._menu_item_save_settings = Gtk.MenuItem(label=_('Save Settings'))
    self._menu_item_load_settings_from_file = Gtk.MenuItem(label=_('Load Settings from File...'))
    self._menu_item_save_settings_to_file = Gtk.MenuItem(label=_('Save Settings to File...'))
    self._menu_item_reset_settings = Gtk.MenuItem(label=_('Reset Settings'))
    if self._log_viewer is not None:
      self._menu_item_view_logs = Gtk.MenuItem(label=_('View Logs'))
    self._menu_item_preferences = Gtk.MenuItem(label=_('Preferences...'))

    self._menu_settings = Gtk.Menu()
    self._menu_settings.append(self._menu_item_save_settings)
    self._menu_settings.append(self._menu_item_load_settings_from_file)
    self._menu_settings.append(self._menu_item_save_settings_to_file)
    self._menu_settings.append(self._menu_item_reset_settings)
    if self._log_viewer is not None:
      self._menu_settings.append(self._menu_item_view_logs)
    self._menu_settings.append(self._menu_item_preferences)
    self._menu_settings.show_all()

    self._button_settings.connect('clicked', self._on_button_settings_clicked)

    self._menu_item_save_settings.connect('activate', self._on_save_settings_activate)
    self._menu_item_load_settings_from_file.connect(
      'activate', self._on_load_settings_from_file_activate)
    self._menu_item_save_settings_to_file.connect(
      'activate', self._on_save_settings_to_file_activate)
    self._menu_item_reset_settings.connect('activate', self._on_reset_settings_activate)
    if self._log_viewer is not None:
      self._menu_item_view_logs.connect('activate', self._on_view_logs_activate)
    self._menu_item_preferences.connect('activate', self._on_preferences_activate)

    self._dialog.connect('key-press-event', self._on_dialog_key_press_event)

  @property
  def button(self):
    return self._button_settings

  def save_settings(self, filepath=None):
    if filepath is None:
      save_result = self._settings.save()
    else:
      source = setting_.sources.JsonFileSource(CONFIG.PROCEDURE_GROUP, filepath)
      save_result = self._settings.save({'persistent': source})

    if setting_.Persistor.FAIL in save_result.statuses_per_source.values():
      if filepath is None:
        main_message = _('Failed to save settings.')
      else:
        main_message = _('Failed to save settings to file "{}".'.format(filepath))

      display_load_save_settings_failure_message(
        main_message,
        details=utils_setting_.format_message_from_persistor_statuses(save_result, separator='\n\n'),
        parent=self._dialog)
      return False
    else:
      return True

  def reset_settings(self):
    self._settings.reset()

  def _on_button_settings_clicked(self, button):
    gui_utils_.menu_popup_below_widget(self._menu_settings, button)

  def _on_save_settings_activate(self, _menu_item):
    self._save_settings_to_default_location()

  def _on_load_settings_from_file_activate(self, _menu_item):
    self._display_setting_file_dialog(action='load')

  def _on_save_settings_to_file_activate(self, _menu_item):
    self._display_setting_file_dialog(action='save')

  def _on_reset_settings_activate(self, _menu_item):
    dialog = Gtk.MessageDialog(
      parent=self._dialog,
      message_type=Gtk.MessageType.WARNING,
      modal=True,
      destroy_with_parent=True,
      buttons=Gtk.ButtonsType.YES_NO,
      attached_to=self._dialog,
      transient_for=self._dialog,
    )

    dialog.set_markup(GLib.markup_escape_text(_('Are you sure you want to reset settings?')))
    dialog.set_focus(dialog.get_widget_for_response(Gtk.ResponseType.NO))

    dialog.connect('response', self._on_reset_prompt_response)

    dialog.show_all()

  def _on_reset_prompt_response(self, dialog, response_id):
    dialog.destroy()

    if response_id == Gtk.ResponseType.YES:
      commands_.clear(self._settings['main/actions'])
      commands_.clear(self._settings['main/conditions'])

      self.reset_settings()

      setting_.Persistor.clear()

      self.save_settings()

      self._display_message_func(_('Settings reset.'))

  def _on_view_logs_activate(self, _menu_item):
    self._log_viewer.widget.show_all()

  def _on_preferences_activate(self, _menu_item):
    self._preferences.show()

  def _on_dialog_key_press_event(self, dialog, event):
    if not dialog.get_mapped():
      return False

    # Ctrl + S
    if ((event.state & Gtk.accelerator_get_default_mod_mask()) == Gdk.ModifierType.CONTROL_MASK
        and Gdk.keyval_name(Gdk.keyval_to_lower(event.keyval)) == 's'):
      self._save_settings_to_default_location()
      return True

    return False

  def _save_settings_to_default_location(self):
    self._settings.apply_gui_values_to_settings()

    save_successful = self.save_settings()
    if save_successful:
      self._display_message_func(_('Settings successfully saved.'))

  def _load_settings_from_file(self, filepath):
    source = setting_.sources.JsonFileSource(CONFIG.PROCEDURE_GROUP, filepath)

    commands_.clear(self._settings['main/actions'], add_initial_commands=False)
    commands_.clear(self._settings['main/conditions'], add_initial_commands=False)

    settings_to_ignore_for_reset = []
    for setting in self._settings.walk(lambda s: 'ignore_reset' not in s.tags):
      if setting.get_path('root').startswith('gui/size/'):
        setting.tags.add('ignore_reset')
        settings_to_ignore_for_reset.append(setting)

    self.reset_settings()

    for setting in settings_to_ignore_for_reset:
      setting.tags.discard('ignore_reset')

    if self._previews_controller is not None:
      self._previews_controller.lock_previews(self._PREVIEWS_LOAD_SETTINGS_KEY)

    size_settings_to_ignore_for_load = []
    if not self._settings['gui/load_size_settings_from_file']:
      for setting in self._settings['gui'].walk(lambda s: 'ignore_load' not in s.tags):
        if setting.get_path('root').startswith('gui/size/'):
          setting.tags.add('ignore_load')
          size_settings_to_ignore_for_load.append(setting)

    status, message = update.load_and_update(
      self._settings,
      sources={'persistent': source},
      update_sources=False,
      procedure_group=CONFIG.PROCEDURE_GROUP,
    )

    for setting in size_settings_to_ignore_for_load:
      setting.tags.discard('ignore_load')

    if status == update.UpdateStatuses.UPDATE_WITH_REMOVED_COMMANDS:
      display_commands_no_longer_available_in_loaded_settings_message(
        message,
        parent=self._dialog,
      )
    elif status == update.UpdateStatuses.TERMINATE:
      display_load_save_settings_failure_message(
        _('Failed to load settings from file "{}".'
          ' Settings must be reset completely.').format(filepath),
        details=message,
        parent=self._dialog,
      )

      self.reset_settings()
      commands_.clear(self._settings['main/actions'])
      commands_.clear(self._settings['main/conditions'])

    if self._previews_controller is not None:
      self._previews_controller.unlock_previews(self._PREVIEWS_LOAD_SETTINGS_KEY)

    return status != update.UpdateStatuses.TERMINATE

  def _display_setting_file_dialog(self, action, add_file_extension_if_missing=True):
    if action == 'load':
      dialog_action = Gtk.FileChooserAction.OPEN
      title = _('Load Settings from File')
    elif action == 'save':
      dialog_action = Gtk.FileChooserAction.SAVE
      title = _('Save Settings to File')
    else:
      raise ValueError('invalid action; valid values: "load", "save"')

    file_dialog = Gtk.FileChooserNative(
      title=title,
      action=dialog_action,
      do_overwrite_confirmation=True,
      modal=True,
      transient_for=gui_utils_.get_toplevel_window(self._dialog),
    )

    json_file_ext = '.json'

    filter_json = Gtk.FileFilter()
    filter_json.set_name(_('JSON file'))
    filter_json.add_pattern(f'*{json_file_ext}')
    file_dialog.add_filter(filter_json)

    default_file_ext = json_file_ext
    default_file_format = json_file_ext[1:]

    if action == 'load':
      filter_any = Gtk.FileFilter()
      filter_any.set_name(_('Any file'))
      filter_any.add_pattern('*')
      file_dialog.add_filter(filter_any)

    file_dialog.connect(
      'response',
      self._on_setting_file_dialog_response,
      action,
      default_file_ext,
      default_file_format,
      add_file_extension_if_missing,
    )

    file_dialog.show()

  def _on_setting_file_dialog_response(
        self,
        file_dialog,
        response_id,
        action,
        default_file_ext,
        default_file_format,
        add_file_extension_if_missing,
  ):
    filepath, _file_format = self._get_setting_file_and_options(
          file_dialog,
          response_id,
          default_file_ext,
          default_file_format,
          add_file_extension_if_missing,
    )

    file_dialog.destroy()

    if action == 'load':
      self._load_settings_from_file_from_dialog(filepath)
    elif action == 'save':
      self._save_settings_to_file_from_dialog(filepath)

  @staticmethod
  def _get_setting_file_and_options(
        file_dialog,
        response_id,
        default_file_ext,
        default_file_format,
        add_file_extension_if_missing,
  ):
    if response_id == Gtk.ResponseType.ACCEPT:
      filepath = file_dialog.get_filename() if file_dialog.get_filename() is not None else ''

      file_ext = os.path.splitext(filepath)[1]
      if add_file_extension_if_missing and not file_ext:
        filepath += default_file_ext

      file_ext = os.path.splitext(filepath)[1]
      if file_ext:
        file_format = file_ext[1:]
      else:
        file_format = default_file_format
    else:
      filepath = None
      file_format = None

    return filepath, file_format

  def _load_settings_from_file_from_dialog(self, filepath):
    if filepath:
      load_successful = self._load_settings_from_file(filepath)
      # Also override default setting sources so that the loaded settings actually persist.
      self.save_settings()

      if load_successful:
        self._display_message_func(_('Settings successfully loaded.'))

  def _save_settings_to_file_from_dialog(self, filepath):
    if filepath:
      self._settings.apply_gui_values_to_settings()

      save_successful = self.save_settings(filepath)
      if save_successful:
        self._display_message_func(
          _('Settings successfully saved to "{}".').format(os.path.basename(filepath)))


def display_load_save_settings_failure_message(
      main_message: str,
      details: str,
      parent: Optional[Gtk.Window] = None,
):
  messages_.display_failure_message(
    main_message,
    failure_message='',
    details=details,
    parent=parent,
    report_description=_(
      'If you believe this is an error in the plug-in, you can help fix it'
      ' by sending a report with the file and the text in the details to one of the sites below'))


def display_commands_no_longer_available_in_loaded_settings_message(
      message: str,
      parent: Optional[Gtk.Window] = None,
):
  main_message, commands_no_longer_available_str = message.split('\n\n')

  messages_.display_alert_message(
    message_type=Gtk.MessageType.WARNING,
    message_markup=GLib.markup_escape_text(main_message),
    message_secondary_markup=GLib.markup_escape_text(commands_no_longer_available_str),
    parent=parent,
  )


class Preferences:

  _BORDER_WIDTH = 6
  _GRID_ROW_SPACING = 3
  _GRID_COLUMN_SPACING = 8

  _DELAY_PREFERENCE_SAVE_MILLISECONDS = 500

  def __init__(self, settings, popover_anchor_widget, parent):
    self._settings = settings
    self._popover_anchor_widget = popover_anchor_widget
    self._parent = parent

    self._init_gui()

  def show(self):
    self._popover.popup()

  def _init_gui(self):
    self._grid = Gtk.Grid(
      row_spacing=self._GRID_ROW_SPACING,
      column_spacing=self._GRID_COLUMN_SPACING,
      border_width=self._BORDER_WIDTH,
    )
    self._grid.show()

    settings = self._get_available_settings()

    label_width_chars = gui_utils_grid_.get_max_label_width_from_settings(settings)

    for row_index, setting in enumerate(settings):
      gui_utils_grid_.attach_label_to_grid(
        self._grid,
        setting,
        row_index,
        width_chars=label_width_chars,
        max_width_chars=label_width_chars,
        include_name_in_tooltip=False,
      )

      gui_utils_grid_.attach_widget_to_grid(
        self._grid, setting, row_index, include_name_in_tooltip=False)

    self._popover = Gtk.Popover()
    self._popover.add(self._grid)
    self._popover.set_constrain_to(Gtk.PopoverConstraint.NONE)
    self._popover.set_relative_to(self._popover_anchor_widget)

  def _get_available_settings(self):
    setting_paths = [
      'gui/auto_close',
      'main/continue_on_error',
      'gui/keep_inputs',
      'gui/show_quick_settings',
      'gui/use_minimum_number_of_decimal_places',
      'gui/minimum_decimal_places',
      'gui/load_size_settings_from_file',
    ]

    settings = []

    for setting_path in setting_paths:
      setting_path_components = setting_path.rsplit('/', 1)
      if len(setting_path_components) == 2:
        setting_group = self._settings[setting_path_components[0]]
        setting_name = setting_path_components[1]
      else:
        setting_group = self._settings
        setting_name = setting_path_components[0]

      if setting_name in setting_group:
        setting = self._settings[setting_path]

        setting.connect_event('value-changed', self._save_preference)

        setting.set_gui()

        settings.append(setting)

    return settings

  def _save_preference(self, setting):
    utils.timeout_add_strict(
      self._DELAY_PREFERENCE_SAVE_MILLISECONDS,
      setting.save)
