import colorsys
import sys
import xml.etree.ElementTree as ET

# from io import BytesIO

import gi

try:
    gi.require_version("Gdk", "3.0")
    gi.require_version("GObject", "2.0")
    gi.require_version("Gtk", "3.0")
    gi.require_version("Pango", "1.0")
    from gi.repository import Gtk, Gdk, GObject, Pango

    gi.require_version("GdkPixbuf", "2.0")
    from gi.repository.GdkPixbuf import Pixbuf
except Exception:
    print("Failed to import required gi module version")
    sys.exit(1)

from pychess.System import conf
from pychess.System.Log import log
from pychess.System.prefix import addDataPrefix


def createCombo(combo, data=[], name=None, ellipsize_mode=None):
    if name is not None:
        combo.set_name(name)
    lst_store = Gtk.ListStore(Pixbuf, str)
    for row in data:
        lst_store.append(row)
    combo.clear()

    combo.set_model(lst_store)
    crp = Gtk.CellRendererPixbuf()
    crp.set_property("xalign", 0)
    crp.set_property("xpad", 2)
    combo.pack_start(crp, False)
    combo.add_attribute(crp, "pixbuf", 0)

    crt = Gtk.CellRendererText()
    crt.set_property("xalign", 0)
    crt.set_property("xpad", 4)
    combo.pack_start(crt, True)
    combo.add_attribute(crt, "text", 1)
    if ellipsize_mode is not None:
        crt.set_property("ellipsize", ellipsize_mode)


def updateCombo(combo, data):
    def get_active(combobox):
        model = combobox.get_model()
        active = combobox.get_active()
        if active < 0:
            return None
        return model[active][1]

    last_active = get_active(combo)
    lst_store = combo.get_model()
    lst_store.clear()
    new_active = 0
    for i, row in enumerate(data):
        lst_store.append(row)
        if last_active == row[1]:
            new_active = i
    combo.set_active(new_active)


def genColor(n, startpoint=0):
    assert n >= 1
    # This splits the 0 - 1 segment in the pizza way
    hue = (2 * n - 1) / (2.0 ** (n - 1).bit_length()) - 1
    hue = (hue + startpoint) % 1
    # We set saturation based on the amount of green, scaled to the interval
    # [0.6..0.8]. This ensures a consistent lightness over all colors.
    rgb = colorsys.hsv_to_rgb(hue, 1, 1)
    rgb = colorsys.hsv_to_rgb(hue, 1, (1 - rgb[1]) * 0.2 + 0.6)
    # This algorithm ought to balance colors more precisely, but it overrates
    # the lightness of yellow, and nearly makes it black
    # yiq = colorsys.rgb_to_yiq(*rgb)
    # rgb = colorsys.yiq_to_rgb(.125, yiq[1], yiq[2])
    return rgb


def keepDown(scrolledWindow):
    def changed(vadjust):
        if not hasattr(vadjust, "need_scroll") or vadjust.need_scroll:
            vadjust.set_value(vadjust.get_upper() - vadjust.get_page_size())
            vadjust.need_scroll = True

    scrolledWindow.get_vadjustment().connect("changed", changed)

    def value_changed(vadjust):
        vadjust.need_scroll = (
            abs(vadjust.get_value() + vadjust.get_page_size() - vadjust.get_upper())
            < vadjust.get_step_increment()
        )

    scrolledWindow.get_vadjustment().connect("value-changed", value_changed)


# wrap analysis text column. thanks to
# http://www.islascruz.org/html/index.php?blog/show/Wrap-text-in-a-TreeView-column.html
def appendAutowrapColumn(treeview, name, **kvargs):
    cell = Gtk.CellRendererText()
    # cell.props.wrap_mode = Pango.WrapMode.WORD
    # TODO:
    # changed to ellipsize instead until "never ending grow" bug gets fixed
    # see https://github.com/pychess/pychess/issues/1054
    cell.props.ellipsize = Pango.EllipsizeMode.END
    column = Gtk.TreeViewColumn(name, cell, **kvargs)
    treeview.append_column(column)

    def callback(treeview, allocation, column, cell):
        otherColumns = [c for c in treeview.get_columns() if c != column]
        newWidth = allocation.width - sum(c.get_width() for c in otherColumns)

        hsep = GObject.Value()
        hsep.init(GObject.TYPE_INT)
        hsep.set_int(0)
        treeview.style_get_property("horizontal-separator", hsep)
        newWidth -= hsep.get_int() * (len(otherColumns) + 1) * 2
        if cell.props.wrap_width == newWidth or newWidth <= 0:
            return
        cell.props.wrap_width = newWidth
        store = treeview.get_model()
        store_iter = store.get_iter_first()
        while store_iter and store.iter_is_valid(store_iter):
            store.row_changed(store.get_path(store_iter), store_iter)
            store_iter = store.iter_next(store_iter)
        treeview.set_size_request(0, -1)

    # treeview.connect_after("size-allocate", callback, column, cell)

    scroll = treeview.get_parent()
    if isinstance(scroll, Gtk.ScrolledWindow):
        scroll.set_policy(Gtk.PolicyType.NEVER, scroll.get_policy()[1])

    return cell


METHODS = (
    # Gtk.SpinButton should be listed prior to Gtk.Entry, as it is a
    # subclass, but requires different handling
    (Gtk.SpinButton, ("get_value", "set_value", "value-changed")),
    (Gtk.Entry, ("get_text", "set_text", "changed")),
    (Gtk.Expander, ("get_expanded", "set_expanded", "notify::expanded")),
    (Gtk.ComboBox, ("get_active", "set_active", "changed")),
    (Gtk.IconView, ("_get_active", "_set_active", "selection-changed")),
    (Gtk.ToggleButton, ("get_active", "set_active", "toggled")),
    (Gtk.CheckMenuItem, ("get_active", "set_active", "toggled")),
    (Gtk.Range, ("get_value", "set_value", "value-changed")),
    (Gtk.TreeSortable, ("get_value", "set_value", "sort-column-changed")),
    (Gtk.Paned, ("get_position", "set_position", "notify::position")),
)


def keep(widget, key, get_value_=None, set_value_=None):  # , first_value=None):
    if widget is None:
        raise AttributeError("key '%s' isn't in widgets" % key)

    for class_, methods_ in METHODS:
        # Use try-except just to make spinx happy...
        try:
            if isinstance(widget, class_):
                getter, setter, signal = methods_
                break
        except TypeError:
            getter, setter, signal = methods_
            break
    else:
        raise AttributeError("I don't have any knowledge of type: '%s'" % widget)

    if get_value_:

        def get_value():
            return get_value_(widget)

    else:
        get_value = getattr(widget, getter)

    if set_value_:

        def set_value(v):
            return set_value_(widget, v)

    else:
        set_value = getattr(widget, setter)

    def setFromConf():
        try:
            v = conf.get(key)
        except TypeError:
            log.warning(
                "uistuff.keep.setFromConf: Key '%s' from conf had the wrong type '%s', ignored"
                % (key, type(conf.get(key)))
            )
            # print("uistuff.keep TypeError %s %s" % (key, conf.get(key)))
        else:
            set_value(v)

    def callback(*args):
        if not conf.hasKey(key) or conf.get(key) != get_value():
            conf.set(key, get_value())

    widget.connect(signal, callback)
    conf.notify_add(key, lambda *args: setFromConf())

    if conf.hasKey(key):
        setFromConf()
    elif conf.get(key) is not None:
        conf.set(key, conf.get(key))


# loadDialogWidget() and saveDialogWidget() are similar to uistuff.keep() but are needed
# for saving widget values for Gtk.Dialog instances that are loaded with different
# sets of values/configurations and which also aren't instant save like in
# uistuff.keep(), but rather are saved later if and when the user clicks
# the dialog's OK button
def loadDialogWidget(
    widget,
    widget_name,
    config_number,
    get_value_=None,
    set_value_=None,
    first_value=None,
):
    key = widget_name + "-" + str(config_number)

    if widget is None:
        raise AttributeError("key '%s' isn't in widgets" % widget_name)

    for class_, methods_ in METHODS:
        if isinstance(widget, class_):
            getter, setter, signal = methods_
            break
    else:
        if set_value_ is None:
            raise AttributeError("I don't have any knowledge of type: '%s'" % widget)

    if get_value_:

        def get_value():
            return get_value_(widget)

    else:
        get_value = getattr(widget, getter)

    if set_value_:

        def set_value(v):
            return set_value_(widget, v)

    else:
        set_value = getattr(widget, setter)

    if conf.hasKey(key):
        try:
            v = conf.get(key)
        except TypeError:
            log.warning(
                "uistuff.loadDialogWidget: Key '%s' from conf had the wrong type '%s', ignored"
                % (key, type(conf.get(key)))
            )
            if first_value is not None:
                conf.set(key, first_value)
            else:
                conf.set(key, get_value())
        else:
            set_value(v)
    elif first_value is not None:
        conf.set(key, first_value)
        set_value(conf.get(key))
    else:
        log.warning(
            'Didn\'t load widget "%s": no conf value and no first_value arg'
            % widget_name
        )


def saveDialogWidget(widget, widget_name, config_number, get_value_=None):
    key = widget_name + "-" + str(config_number)

    if widget is None:
        raise AttributeError("key '%s' isn't in widgets" % widget_name)

    for class_, methods_ in METHODS:
        if isinstance(widget, class_):
            getter, setter, signal = methods_
            break
    else:
        if get_value_ is None:
            raise AttributeError("I don't have any knowledge of type: '%s'" % widget)

    if get_value_:

        def get_value():
            return get_value_(widget)

    else:
        get_value = getattr(widget, getter)

    if not conf.hasKey(key) or conf.get(key) != get_value():
        conf.set(key, get_value())


POSITION_NONE, POSITION_CENTER, POSITION_GOLDEN = range(3)


def keepWindowSize(key, window, defaultSize=None, defaultPosition=POSITION_NONE):
    """You should call keepWindowSize before show on your windows"""

    key = key + "window"

    def savePosition(window, *event):
        log.debug("keepWindowSize.savePosition: %s" % window.get_title())
        width = window.get_allocation().width
        height = window.get_allocation().height
        x_loc, y_loc = window.get_position()

        if width <= 0:
            log.error("Setting width = '%d' for %s to conf" % (width, key))
        if height <= 0:
            log.error("Setting height = '%d' for %s to conf" % (height, key))

        log.debug(
            "Saving window position width=%s height=%s x=%s y=%s"
            % (width, height, x_loc, y_loc)
        )
        conf.set(key + "_width", width)
        conf.set(key + "_height", height)
        conf.set(key + "_x", x_loc)
        conf.set(key + "_y", y_loc)

        return False

    window.connect("delete-event", savePosition, "delete-event")

    def loadPosition(window):
        # log.debug("keepWindowSize.loadPosition: %s" % window.title)
        # Just to make sphinx happy...
        try:
            width, height = window.get_size_request()
        except TypeError:
            pass

        if conf.hasKey(key + "_width") and conf.hasKey(key + "_height"):
            width = conf.get(key + "_width")
            height = conf.get(key + "_height")
            log.debug(f"Resizing window to width={width} height={height}")
            window.resize(width, height)

        elif defaultSize:
            width, height = defaultSize
            log.debug(f"Resizing window to width={width} height={height}")
            window.resize(width, height)

        elif key == "mainwindow":
            monitor_x, monitor_y, monitor_width, monitor_height = getMonitorBounds()
            width = int(monitor_width / 2)
            height = int(monitor_height / 4) * 3
            log.debug(f"Resizing window to width={width} height={height}")
            window.resize(width, height)

        elif key == "preferencesdialogwindow":
            monitor_x, monitor_y, monitor_width, monitor_height = getMonitorBounds()
            width = int(monitor_width / 2)
            height = int(monitor_height / 4) * 3
            window.resize(1, 1)
        else:
            monitor_x, monitor_y, monitor_width, monitor_height = getMonitorBounds()
            width = int(monitor_width / 2)
            height = int(monitor_height / 4) * 3

        if conf.hasKey(key + "_x") and conf.hasKey(key + "_y"):
            x = max(0, conf.get(key + "_x"))
            y = max(0, conf.get(key + "_y"))
            log.debug(f"Moving window to x={x} y={y}")
            window.move(x, y)

        elif defaultPosition in (POSITION_CENTER, POSITION_GOLDEN):
            monitor_x, monitor_y, monitor_width, monitor_height = getMonitorBounds()
            x_loc = int(monitor_width / 2 - width / 2) + monitor_x
            if defaultPosition == POSITION_CENTER:
                y_loc = int(monitor_height / 2 - height / 2) + monitor_y
            else:
                # Place the window on the upper golden ratio line
                y_loc = int(monitor_height / 2.618 - height / 2) + monitor_y
            log.debug(f"Moving window to x={x_loc} y={y_loc}")
            window.move(x_loc, y_loc)

    loadPosition(window)

    # In rare cases, gtk throws some gtk_size_allocation error, which is
    # probably a race condition. To avoid the window forgets its size in
    # these cases, we add this extra hook
    def callback(window):
        loadPosition(window)

    onceWhenReady(window, callback)


# Some properties can only be set, once the window is sufficiently initialized,
# This function lets you queue your request until that has happened.
def onceWhenReady(window, func, *args, **kwargs):
    def cb(window, alloc, func, *args, **kwargs):
        func(window, *args, **kwargs)
        window.disconnect(handler_id)

    handler_id = window.connect_after("size-allocate", cb, func, *args, **kwargs)


def getMonitorBounds():
    screen = Gdk.Screen.get_default()
    root_window = screen.get_root_window()
    # Just to make sphinx happy...
    try:
        ptr_window, mouse_x, mouse_y, mouse_mods = root_window.get_pointer()
        current_monitor_number = screen.get_monitor_at_point(mouse_x, mouse_y)
        monitor_geometry = screen.get_monitor_geometry(current_monitor_number)
        return (
            monitor_geometry.x,
            monitor_geometry.y,
            monitor_geometry.width,
            monitor_geometry.height,
        )
    except TypeError:
        return (0, 0, 0, 0)


def makeYellow(box):
    def on_box_expose_event(box, context):
        # box.style.paint_flat_box (box.window,
        #    Gtk.StateType.NORMAL, Gtk.ShadowType.NONE, None, box, "tooltip",
        #    box.allocation.x, box.allocation.y,
        #    box.allocation.width, box.allocation.height)
        pass

    def cb(box):
        tooltip = Gtk.Window(Gtk.WindowType.POPUP)
        tooltip.set_name("gtk-tooltip")
        tooltip.ensure_style()
        tooltipStyle = tooltip.get_style()
        box.set_style(tooltipStyle)
        box.connect("draw", on_box_expose_event)

    onceWhenReady(box, cb)


class GladeWidgets:
    """A simple class that wraps a the glade get_widget function
    into the python __getitem__ version"""

    def __init__(self, filename):
        # TODO: remove this when upstream fixes translations with Python3+Windows
        if sys.platform == "win32" and not conf.no_gettext:
            tree = ET.parse(addDataPrefix("glade/%s" % filename))
            for node in tree.iter():
                if "translatable" in node.attrib:
                    node.text = _(node.text)
                    del node.attrib["translatable"]
                if node.get("name") in ("pixbuf", "logo"):
                    node.text = addDataPrefix("glade/%s" % node.text)
            xml_text = ET.tostring(tree.getroot(), encoding="unicode", method="xml")
            self.builder = Gtk.Builder.new_from_string(xml_text, -1)
        else:
            self.builder = Gtk.Builder()
            if not conf.no_gettext:
                self.builder.set_translation_domain("pychess")
            self.builder.add_from_file(addDataPrefix("glade/%s" % filename))

    def __getitem__(self, key):
        return self.builder.get_object(key)

    def getGlade(self):
        return self.builder
