#!/usr/bin/env python3
import gi

gi.require_version('Gtk', '3.0')

# noinspection PyPep8
from gi.repository import Gtk, Gio
# noinspection PyPep8
from automathemely.autoth_tools.utils import get_resource, get_local, read_dict, write_dic
# noinspection PyPep8
from automathemely.autoth_tools import extratools, envspecific
# noinspection PyPep8
import json

# noinspection PyPep8
import logging
logger = logging.getLogger(__name__)


def split_id_delimiter(obj_id):
    obj_id = obj_id.lstrip('*')
    if '~' in obj_id:
        return obj_id.split('~')
    else:
        return obj_id, None


# Handle unexpected or invalid values in user_settings
def try_or_default_type(val, try_type):
    try:
        return try_type(val)
    except ValueError:
        if try_type == str:
            return ''
        elif try_type == int:
            return 0
        elif try_type == float:
            return 0.0
        elif try_type == bool:
            return False
        else:
            return None


def isfloat(value):
    try:
        float(value)
        return True
    except ValueError:
        return False


# noinspection PyUnusedLocal
def get_object_data(obj, *args):
    if isinstance(obj, Gtk.ComboBoxText):
        return obj.get_active_id() if obj.get_active_id() != 'none' else ''
    elif isinstance(obj, Gtk.Switch):
        return obj.get_active()
    elif isinstance(obj, Gtk.SpinButton):
        return obj.get_value_as_int()
    elif isinstance(obj, Gtk.Entry):
        text = obj.get_text()
        if obj.get_name() == 'float_only' and isfloat(text):
            return float(text)
        else:
            return text


def scan_comboboxtext_descendants(obj, match):
    try:
        children = obj.get_children()
    except AttributeError:
        return
    else:
        if children:
            comboboxtexts = []
            for child in children:
                scan = scan_comboboxtext_descendants(child, match)
                if scan:
                    comboboxtexts.extend(scan)
            return comboboxtexts
        elif isinstance(obj, Gtk.ComboBoxText) and match in Gtk.Buildable.get_name(obj):
            return [obj]


def display_row_separators(row, before):
    # This was added in GTK 3.14, but since it really doesn't make much of a difference and is purely aesthetic, and
    # prevents it from running on GTK 3.10 lets just set this when it is possible but not enforce it
    # noinspection PyBroadException
    try:
        row.set_activatable(False)
        row.set_selectable(False)
    except:
        pass

    if before:
        row.set_header(Gtk.Separator())


def get_last_visible_row(max_number_of_rows, listbox_id, builder):
    for i in range(max_number_of_rows, 1, -1):
        entry = builder.get_object('{}.{}'.format(listbox_id, str(i)))
        row = entry.get_parent().get_parent()

        if row.get_visible():
            return i

    return 1


########
# This is a kind of weird system, but bear with me...
#
# GTk.Buildable.get_name() gets what is displayed as WIDGET ID in Glade
# obj.get_name() gets what is displayed as WIDGET NAME in Glade
#
# In data containing widgets (ID starts with an *):

# The widget ID is used for widget OUTPUT, i.e. value or PATH it is related to in user_settings, if there is a
# delimiter (~) it will also indicate which part of the GUI it affects (its SUBORDINATE), like in enabler Switches
#
# The widget NAME is used for special attributes dependant on the type of
# widget, such as if it is float only in Entries or what it will be populated with in ComboBoxTexts
########

# noinspection PyUnusedLocal
class App(Gtk.Application):

    def __init__(self, us_se):
        super().__init__(application_id="com.github.c2n14.automathemely",
                         flags=Gio.ApplicationFlags.FLAGS_NONE)

        self.main_window = None
        self.us_se = us_se

    #       BASIC Gtk.Application FUNCTIONS
    # noinspection PyAttributeOutsideInit
    def do_startup(self):
        Gtk.Application.do_startup(self)
        self.builder = Gtk.Builder()
        self.builder.add_from_file(get_resource('manager_gui.glade'))
        self.builder.set_application(self)
        self.builder.connect_signals(self)

        # This does not get initialized until needed
        self.extras = dict()

        self.system_themes = envspecific.get_installed_themes(self.us_se['desktop_environment'])

        self.listen_changes = False
        self.saved_settings = False
        self.entries_error = list()
        self.changed = list()

        #   Override the quit menu
        action = Gio.SimpleAction.new("quit", None)
        action.connect("activate", self.on_confirm_exit)
        self.add_action(action)

    # noinspection PyAttributeOutsideInit
    def do_activate(self):
        #   Called on primary instance activation
        #   Kinda like do_startup but after the window is displayed
        if not self.main_window:
            self.main_window = self.builder.get_object('main_window')
            self.main_window.set_application(self)
            self.main_window.set_title('AutomaThemely Settings')
            self.main_window.set_icon_from_file(get_resource('automathemely.svg'))

            sub_w_list = ['confirm_dialog', 'error_dialog']
            self.sub_windows = dict()
            for w in sub_w_list:
                self.sub_windows[w] = self.builder.get_object(w)
                self.sub_windows[w].set_transient_for(self.main_window)

            self.setup_all()
            self.listen_changes = True

        self.main_window.present()

    def do_shutdown(self):
        Gtk.Application.do_shutdown(self)

        #   Dump file
        if self.changed and self.saved_settings:
            with open(get_local('user_settings.json'), 'w') as file:
                json.dump(self.us_se, file, indent=4)
            exit_message = 'Successfully saved settings'
        else:
            exit_message = 'No changes were made'

        logger.info(exit_message)

    #      MISC
    # Called on primary activation
    def setup_all(self):
        for obj in self.builder.get_objects():
            try:
                obj_id = Gtk.Buildable.get_name(obj)
            except TypeError:
                continue

            # Filter out non-relevant objects
            if '___object_' not in obj_id:

                #   These don't contain data
                if isinstance(obj, Gtk.LinkButton):
                    obj.set_label("HINT: You can get most of this info at https://ipinfo.io/json")

                elif isinstance(obj, Gtk.ListBox):
                    obj.set_header_func(display_row_separators)

                #   These DO contain data
                elif obj_id.startswith('*'):
                    obj_path, obj_sub = split_id_delimiter(obj_id)
                    obj_attr = obj.get_name()
                    keys = obj_path.split('.')
                    val = read_dict(self.us_se, keys)

                    # Has to go before entry because a SpinButton is also an entry...
                    if isinstance(obj, Gtk.SpinButton):
                        obj.configure(Gtk.Adjustment(value=try_or_default_type(val, int), lower=-999, upper=999,
                                                     step_increment=1, page_increment=5, page_size=0), 1, 0)

                    elif isinstance(obj, Gtk.Entry):
                        obj.set_text(try_or_default_type(val, str))

                    elif isinstance(obj, Gtk.Switch):
                        if try_or_default_type(val, bool):
                            obj.set_active(True)
                        else:
                            obj.set_active(False)
                            if obj_sub:
                                self.on_container_toggle(obj)

                    # Most of this method has been moved to populate_themes_cboxts
                    elif isinstance(obj, Gtk.ComboBoxText):
                        if obj_attr == 'desk_envs':
                            # Special try or default
                            if not try_or_default_type(val, str):
                                val = 'custom'
                            obj.set_active_id(try_or_default_type(val, str))

        # Hardcoded setup scripts listboxes after their entries have already been setup
        scripts_listboxes = ['scripts_sunrise_listbox', 'scripts_sunset_listbox']
        for listbox in scripts_listboxes:
            listbox_type = listbox.split('_')[1]

            for i in range(5, 1, -1):
                entry = self.builder.get_object('*extras.scripts.{}.{}'.format(listbox_type, str(i)))
                entry_text = entry.get_text()

                if entry_text:
                    break
                else:
                    listbox_row = entry.get_parent().get_parent()
                    listbox_row.set_visible(False)

        # Artificially call function when pages are switched to enable or disable addrow_button according to row limit
        self.on_change_scripts_page()

    def populate_themes_cboxt(self, cboxt):
        # If ComboBoxText has an active id it has already been populated
        if cboxt.get_active_id():
            return

        cboxt_id = Gtk.Buildable.get_name(cboxt)
        cboxt_path = split_id_delimiter(cboxt_id)[0]
        cboxt_attr = cboxt.get_name()

        val = read_dict(self.us_se, cboxt_path.split('.'))
        themes = []

        if cboxt_id.startswith('*themes'):
            theme_type = cboxt_attr.split('-')[2]

            if not self.system_themes or theme_type not in self.system_themes:
                tmp = cboxt.get_children()
                cboxt.set_sensitive(False)
                return
            else:
                themes = self.system_themes[theme_type]

        elif cboxt_id.startswith('*extras'):
            cboxt_split = cboxt_attr.split('-')
            theme_type = cboxt_split[1]
            extra_type = cboxt_split[2]

            if theme_type not in self.extras[extra_type]:
                cboxt.set_sensitive(False)
            else:
                themes = self.extras[extra_type][theme_type]

        for theme in themes:
            t_id = theme[0]
            if len(theme) > 1:
                t_name = theme[1]
            else:
                t_name = t_id[0].upper() + t_id[1:]
            cboxt.append(t_id, t_name)

        active_id = try_or_default_type(val, str)
        if active_id:
            cboxt.set_active_id(active_id)
        else:
            # So it doesn't continue populating repeated options on non-set CBoxTs
            cboxt.set_active_id('none')

    #       HANDLERS
    # noinspection PyAttributeOutsideInit
    def on_update_deskenv(self, cboxt, *args):
        cboxt_val = cboxt.get_active_id()

        self.system_themes = envspecific.get_installed_themes(cboxt_val)

        revealer = self.builder.get_object('deskenvs_revealer')
        notebooks = self.builder.get_object('deskenvs_box').get_children()

        if cboxt_val == 'custom':
            revealer.set_reveal_child(False)
        else:
            # Populate CBoxTs before displaying
            env_notebook = self.builder.get_object(cboxt_val)
            env_cboxts = scan_comboboxtext_descendants(env_notebook, cboxt_val)

            for env_box in env_cboxts:
                self.populate_themes_cboxt(env_box)

            revealer.set_reveal_child(True)

            for obj in notebooks:
                if Gtk.Buildable.get_name(obj) == cboxt_val:
                    obj.set_visible(True)
                else:
                    obj.set_visible(False)

    def on_container_toggle(self, switch, *args):
        switch_id = Gtk.Buildable.get_name(switch)
        container = split_id_delimiter(switch_id)[1]
        switch_attr = switch.get_name()

        container_obj = self.builder.get_object(container)

        if switch_attr == 'inverse':
            disable = True
        else:
            disable = False

        if switch.get_active():
            container_obj.set_sensitive(not disable)
        else:
            container_obj.set_sensitive(disable)

    # To reduce start time (specially because some take a long time to setup, looking at you Atom ლ(ಠ益ಠლ)),
    # extras' CBoxTs are populated only if they are enabled to begin with or if they are enabled while it is running
    def on_enable_extra(self, switch, *args):
        if switch.get_active():
            switch_id = Gtk.Buildable.get_name(switch)
            switch_path, container = split_id_delimiter(switch_id)
            extra_type = switch_path.split('.')[1]

            # This is the culprit
            self.extras[extra_type] = extratools.get_installed_extra_themes(extra_type)

            if self.extras[extra_type]:
                extras_cboxts = scan_comboboxtext_descendants(self.builder.get_object(container), extra_type)
                for extra_cboxt in extras_cboxts:
                    self.populate_themes_cboxt(extra_cboxt)
            else:
                switch.set_active(False)
                switch.set_sensitive(False)
                return

    # These three functions related to scripts are kinda not great, and will probably change a lot in future revisions
    # because they were mostly an afterthought
    def on_remove_scripts_row(self, button, *args):
        button_id = Gtk.Buildable.get_name(button)
        sub_entry = split_id_delimiter(button_id)[1]
        listbox_type = sub_entry.split('.')[2]
        row_number = int(sub_entry.split('.')[-1])

        max_number_of_rows = 5
        last_row = get_last_visible_row(max_number_of_rows, '*extras.scripts.{}'.format(listbox_type), self.builder)

        if row_number == last_row:
            entry = self.builder.get_object('*extras.scripts.{}.{}'.format(listbox_type, str(row_number)))
            entry.set_text('')

            if row_number > 1:
                row = entry.get_parent().get_parent()
                row.set_visible(False)

        else:
            for i in range(row_number, last_row):
                origin_entry = self.builder.get_object('*extras.scripts.{}.{}'.format(listbox_type, str(i + 1)))
                origin_row = origin_entry.get_parent().get_parent()
                destination_entry = self.builder.get_object('*extras.scripts.{}.{}'.format(listbox_type, str(i)))

                destination_entry.set_text(origin_entry.get_text())

                if i + 1 == last_row:
                    origin_entry.set_text('')
                    origin_row.set_visible(False)

        add_button = self.builder.get_object('rowadd_button')
        add_button.set_sensitive(True)

    def on_add_scripts_row(self, button, *args):
        active_listbox_page = self.builder.get_object('scripts_notebook').get_current_page()

        if active_listbox_page == 0:
            listbox_type = 'sunrise'
        else:
            listbox_type = 'sunset'

        max_number_of_rows = 5
        last_row = get_last_visible_row(max_number_of_rows, '*extras.scripts.{}'.format(listbox_type), self.builder)

        next_row = self.builder.get_object('*extras.scripts.{}.{}'.format(listbox_type, str(last_row + 1))) \
            .get_parent().get_parent()

        next_row.set_visible(True)

        if last_row + 1 == max_number_of_rows:
            button.set_sensitive(False)

    def on_change_scripts_page(self, notebook=None, *args):
        if notebook:
            active_listbox_page = notebook.get_current_page()

            # Get the opposite tab, because signal is emitted before switching
            if active_listbox_page == 0:
                listbox_type = 'sunset'
            else:
                listbox_type = 'sunrise'

        # If called from setup the statement above does not hold
        else:
            listbox_type = 'sunrise'

        max_number_of_rows = 5
        last_row = get_last_visible_row(max_number_of_rows, '*extras.scripts.{}'.format(listbox_type), self.builder)

        add_button = self.builder.get_object('rowadd_button')
        if last_row == max_number_of_rows:
            add_button.set_sensitive(False)
        else:
            add_button.set_sensitive(True)

    def on_any_change(self, emitter, *args):
        if self.listen_changes:
            emitter_path = split_id_delimiter(Gtk.Buildable.get_name(emitter))[0]
            if get_object_data(emitter) != read_dict(self.us_se, emitter_path.split('.')):
                if not (emitter in self.changed):
                    self.changed.append(emitter)
            else:
                if emitter in self.changed:
                    self.changed.remove(emitter)

    def on_float_entry_change(self, emitter, *args):
        text = emitter.get_text()
        if not text.strip() or not isfloat(text):
            emitter.set_icon_from_stock(0, 'gtk-dialog-error')
            emitter.set_icon_tooltip_text(0, 'Input should not be empty and contain only valid decimal (float) numbers')
            if emitter not in self.entries_error:
                self.entries_error.append(emitter)
        elif emitter.get_icon_stock(0) == 'gtk-dialog-error':
            emitter.set_icon_from_stock(0, None)
            if emitter in self.entries_error:
                self.entries_error.remove(emitter)

    def on_confirm_exit(self, *args):
        if self.changed:
            response = self.sub_windows['confirm_dialog'].run()
            #   This should be destroy() but for some reason it won't work again once it's called
            self.sub_windows['confirm_dialog'].hide()
            if response == Gtk.ResponseType.YES:
                self.quit()
            elif response == Gtk.ResponseType.NO:
                self.on_save_settings()
                return True
        else:
            self.quit()

    # noinspection PyAttributeOutsideInit
    def on_save_settings(self, *args):
        if self.entries_error:
            self.sub_windows['error_dialog'].run()
            #   This should also be destroy()
            self.sub_windows['error_dialog'].hide()
        else:
            for change_obj in self.changed:
                change_path = split_id_delimiter(Gtk.Buildable.get_name(change_obj))[0]
                write_dic(self.us_se, change_path.split('.'), get_object_data(change_obj))
            self.saved_settings = True
            self.quit()


def main(user_settings):
    app = App(user_settings)
    app.run()