#! /usr/bin/env python3

# OS modules
import os
import sys

import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk

# inkscape extension files
import cairo
from lxml import etree
import inkex  # required
from inkex.utils import NSS
from inkex.utils import errormsg as show_errormsg
from inkex.styles import Style
from inkex.colors import Color

__version__ = '1.0.0'

saved_gradient_path = "../my-gradients.svg"

def create_new_file(gradient_data):
    root = etree.Element("svg", nsmap=NSS)
    def_tree = etree.SubElement(root, "defs")
    for i, item in enumerate(gradient_data):
        gradient = etree.SubElement(def_tree, item.tag, attrib=item.attrib)
        for j, gradient_stop in enumerate(item):
            etree.SubElement(gradient, gradient_stop.tag, attrib=gradient_stop.attrib, id="stop%d%d" % (i, j))
    with open(saved_gradient_path, "w") as f:
        f.write(etree.tostring(
            root, encoding="utf-8", xml_declaration=True, pretty_print=True).decode("utf-8"))

def save_to_file(data):
    """ Wrapper for saving gradients to file. """
    if len(data) == 0:
        return 1
    else:
        try:
            # read previous data then append it with current data
            if os.path.exists(saved_gradient_path):
                previous_data = load_gradients_from_file()
                data = previous_data + data
            create_new_file(data)
            return 0
        except Exception as e:
            import traceback
            show_errormsg(e)
            show_errormsg(traceback.print_exc())
            return -1

def load_gradients_from_file():
    """ Load gradients from saved gradient, returned as List """
    if os.path.exists(saved_gradient_path) and os.stat(saved_gradient_path).st_size != 0:
        root = etree.parse(saved_gradient_path)
        mygradients = root.xpath("//linearGradient", namespaces=NSS)
    else:
        mygradients = []
    return mygradients

def read_stop_gradient(gradient):
    stop_data = {
        "id": gradient.attrib.get("id"),
        "stops": []
    }
    for stop in gradient:
        offset = stop.attrib.get("offset")
        style = Style(Style.parse_str(stop.attrib['style']))
        color = Color.parse_str(style.get("stop-color"))[1]
        opacity = style.get("stop-opacity")
        stop_data.get("stops").append(
            tuple([float(offset)] + [x/256.0 for x in color] + [float(opacity)]))
    return stop_data

class MainWindow(Gtk.Builder):
    def __init__(self, gradientSaver):
        Gtk.Builder.__init__(self)
        self.gradientSaver = gradientSaver
        self.add_from_file("GUI.glade")
        self.window = self.get_object("window")
        self.app_version = self.get_object("extension_version")
        self.app_version.set_label(self.app_version.get_label().replace("$VERSION",__version__))
        self.information_dialog = self.get_object("information_dialog")
        # parsing components
        # save gradient components
        self.save_container = self.get_object("save_gradients_container")
        save_template = self.get_object("save_gradient1")
        self.save_container.remove(save_template)
        for idx, item in enumerate(self.gradientSaver.doc_selected_gradients):
            new_save_template = self.SaveGradientTemplate(item)
            new_save_template.set_name("gradient%d" % idx)
            self.save_container.add(new_save_template)
            self.save_container.show_all()
        # - end save gradient components
        # load gradient components
        self.load_container = self.get_object("load_gradients_container")
        load_template = self.get_object("load_gradient1")
        self.load_container.remove(load_template)
        # - end load gradient components
        # show the GUI
        self.connect_signals(self.Handler(self))
        self.window.show()

    class SaveGradientTemplate(Gtk.HBox):
        """
        Template for generating gradient name 
        and preview of selected object in the save page.
        """

        def __init__(self, gradient_data):
            Gtk.HBox.__init__(self)
            self.gradient_data = gradient_data
            self.set_spacing(20)
            preview = Gtk.DrawingArea()
            preview.set_size_request(150, 42)
            preview.set_app_paintable(True)
            preview.connect("draw", self.on_draw, gradient_data)
            self.pack_start(preview, False, True, 0)
            self.input_entry = Gtk.Entry()
            self.input_entry.set_placeholder_text("e.g Beautiful Color")
            self.input_entry.set_size_request(250, 42)
            self.input_entry.set_text(gradient_data.get("id"))
            self.input_entry.set_max_length(25)
            self.pack_start(self.input_entry, False, True, 1)

        def on_draw(self, wid, cr, data):
            """
            Calllback for draw signal for rendering gradient.
            params:
                - wid :GtkWidget
                - cr :Cairo
                - data :list -> gradient data
            """
            lg = cairo.LinearGradient(0.0, 20.0, 150.0, 20.0)
            for stop in data["stops"]:
                lg.add_color_stop_rgba(
                    stop[0], stop[1], stop[2], stop[3], stop[4])
            cr.rectangle(10.0, 0.0, 150.0, 42.0)
            cr.set_source(lg)
            cr.fill()

        def get_save_gradient_text(self):
            return self.input_entry.get_text()

        def get_compiled_gradient(self, new_id):
            # compiling gradient stops
            root = etree.Element("linearGradient", id=new_id)
            for idx, stop in enumerate(self.gradient_data["stops"]):
                stop_id = self.get_name() + str(idx)
                offset = stop[0]
                color = Color([stop[1], stop[2], stop[3]],"rgb")
                opacity = stop[4]
                tmp_stops = {
                    "id": stop_id,
                    "offset": str(offset),
                    "style": Style({
                                "stop-color": str(color), 
                                "stop-opacity": str(opacity)
                            }).to_str()
                }
                current_stop = etree.SubElement(root, "stop", attrib=tmp_stops)
            return root

    class LoadGradientTemplate(Gtk.FlowBoxChild):
        """
        Template for generating gradient name 
        and preview of saved gradient in the load page.
        """
        def __init__(self, gradient_data):
            Gtk.FlowBoxChild.__init__(self)
            self.gradient_data = gradient_data
            self.set_size_request(60,32)
            self.set_halign(Gtk.Align.START)
            self.set_valign(Gtk.Align.START)
            container = Gtk.HBox()
            container.set_spacing(5)
            container.set_baseline_position(Gtk.BaselinePosition.TOP)
            self.checkbox = Gtk.CheckButton()
            self.checkbox.draw_indicator = True
            container.pack_start(self.checkbox,False,True,0)
            preview = Gtk.DrawingArea()
            preview.set_size_request(100, 32)
            preview.set_app_paintable(True)
            preview.connect("draw", self.on_draw, gradient_data)
            container.pack_start(preview,False,True,0)
            self.text_gradient = Gtk.Label()
            self.text_gradient.set_text(gradient_data.get("id"))
            self.text_gradient.set_line_wrap(True)
            self.text_gradient.set_line_wrap_mode(1)
            self.text_gradient.set_max_width_chars(25)
            container.pack_start(self.text_gradient,False,True,0)
            self.add(container)
        
        def on_draw(self, wid, cr, data):
            """
            Calllback for draw signal for rendering gradient.
            params:
                - wid :GtkWidget
                - cr :Cairo
                - data :list -> gradient data
            """
            lg = cairo.LinearGradient(0.0, 20.0, 100.0, 20.0)
            for stop in data["stops"]:
                lg.add_color_stop_rgba(
                    stop[0], stop[1], stop[2], stop[3], stop[4])
            cr.rectangle(10.0, 0.0, 110.0, 32.0)
            cr.set_source(lg)
            cr.fill()

    class Handler:
        """ Signal Handler for GUI """

        def __init__(self, main_window):
            self.main_window = main_window

        def onDestroy(self, *args):
            Gtk.main_quit()
        
        def onSwitchPage(self, notebook, page, page_num):
            if page_num == 0: # save tab
                pass
            elif page_num == 1: # load/remove tab
                self.main_window.gradients_to_load = []
                for children in self.main_window.load_container.get_children():
                    self.main_window.load_container.remove(children)
                loaded_gradients = load_gradients_from_file()
                # TODO render with disabled checkbox if it already exists in current project doc
                for idx,gradient in enumerate(loaded_gradients):
                    # parse gradient stops
                    stop_data = read_stop_gradient(gradient)
                    gradient_info = self.main_window.LoadGradientTemplate(stop_data)
                    gradient_info.checkbox.connect("toggled",self.onLoadGradientToggled, gradient)
                    gradient_info.set_name("gradient%d" % idx)
                    self.main_window.load_container.add(gradient_info)
                self.main_window.load_container.show_all()
            else:
                pass

        def onSaveGradientClicked(self, button):
            text = ""
            gradient_to_save = []
            # get all gradient data in save_container
            for item in self.main_window.save_container.get_children():
                # get new gradient name
                new_name_gradient = item.get_save_gradient_text()
                # strip all special chars
                if not new_name_gradient.isalnum():
                    new_name_gradient = ''.join(e for e in new_name_gradient if e.isalnum())
                # get gradient data
                gradient_data = item.get_compiled_gradient(new_name_gradient)
                text += "{0}\n-----\n".format(etree.tostring(gradient_data))
                gradient_to_save.append(gradient_data)
            # save to file
            status = save_to_file(gradient_to_save)
            if status == 0:
                info = "%d gradients saved successfully!" % len(gradient_to_save)
                # reload current document info with saved gradients
                self.main_window.gradientSaver.reload_current_gradients(gradient_to_save)
            elif status == 1:
                info = "Nothing to save, there is no object with gradient selected. Exiting..."
            elif status == -1:
                info = "Internal Error (-1)! "
            # showing popup information
            self.main_window.get_object("information_text").set_text(info)
            self.main_window.information_dialog.set_title("Save Gradient Information")
            self.main_window.information_dialog.show_all()
        
        def onLoadGradientToggled(self, togglebutton, gradient):
            # if active, queue gradient, otherwise pop it
            if togglebutton.get_active():
                self.main_window.gradients_to_load.append(gradient)
            else:
                self.main_window.gradients_to_load.remove(gradient)

        def onLoadGradientClicked(self, button):
            if len(self.main_window.gradients_to_load) > 0:
                self.main_window.gradientSaver.insert_new_gradients_to_current_doc(self.main_window.gradients_to_load)
                teks = "Successfully loading these gradients:\n"
                teks += "".join(["- "+gradient.attrib["id"]+"\n" for gradient in self.main_window.gradients_to_load])
            else:
                teks = "No gradient(s) selected to load. Exiting..."
            self.main_window.get_object("information_text").set_text(teks)
            self.main_window.information_dialog.set_title("Load Gradient Information")
            self.main_window.information_dialog.show_all()

        def onRemoveGradientClicked(self, button):
            loaded_gradients = load_gradients_from_file()
            if len(self.main_window.gradients_to_load) > 0:
                gradient_to_remove = [gradient.attrib["id"] for gradient in self.main_window.gradients_to_load]
                new_gradient_after = [gradient for gradient in loaded_gradients if gradient.attrib["id"] not in gradient_to_remove]
                create_new_file(new_gradient_after)
                teks = "Successfully removing these gradients:\n"
                teks += "".join(["- "+gradient+"\n" for gradient in gradient_to_remove])
            else:
                teks = "No gradient(s) selected to load. Exiting..."
            self.main_window.get_object("information_text").set_text(teks)
            self.main_window.information_dialog.set_title("Remove Gradient Information")
            self.main_window.information_dialog.show_all()


class GradientSaver(inkex.Effect):
    def __init__(self):
        " define how the options are mapped from the inx file "
        inkex.Effect.__init__(self)  # initialize the super class
        try:
            self.tty = open("/dev/tty", 'w')
        except:
            self.tty = open(os.devnull, 'w')
        self.doc_selected_gradients = []
    
    def insert_new_gradients_to_current_doc(self, gradients):
        defs_node = self.svg.getElement("//svg:defs")
        for item in gradients:
            gradient = etree.SubElement(defs_node,item.tag,attrib=item.attrib)
            for stop in item:
                etree.SubElement(gradient,stop.tag,attrib=stop.attrib)

    def reload_current_gradients(self, new_data):
        " reload gradients information in current project with stored gradient "
        for idx,gradient in enumerate(self.doc_selected_gradients):
            # set old gradient id to new id
            real_node = self.svg.getElement("//*[@id='%s']" % gradient["id"])
            # remove inkscape collect first
            real_node.attrib.pop("{"+NSS["inkscape"]+"}collect", None)
            real_node.attrib["id"] = new_data[idx].attrib["id"]
            # set old xlink:href to new id
            node_href = self.svg.getElement("//*[@xlink:href='#%s']" % gradient["id"])
            node_href.attrib["{"+NSS["xlink"]+"}href"] = "#"+new_data[idx].attrib["id"]
            # last set up inkscape collect again
            real_node.attrib["{"+NSS["inkscape"]+"}collect"] = "always"

    def get_all_doc_gradients(self):
        """TODO
        retrieve all gradient sources of current project document
        """
        pass

    def get_selected_gradients_data(self):
        selected_objects = self.svg.selected
        gradient_list = []
        if len(selected_objects) > 0:
            for item in selected_objects.values():
                style = Style(Style.parse_str(item.get('style')))
                fill = stroke = "None"
                if style.get("fill"):
                    fill = style.get("fill")[5:-1] if "url" in style.get("fill") else "None"
                if style.get("stroke"):
                    stroke = style("stroke")[5:-1] if "url" in style.get("stroke") else "None"
                if fill == "None" and stroke == "None":
                    continue
                # read fill data
                if "radialGradient" in fill or "linearGradient" in fill:
                    real_fill = self.svg.getElementById(fill).attrib["{"+NSS["xlink"]+"}href"][1:]
                    real_fill_node = self.svg.getElementById(real_fill)
                    if real_fill_node not in gradient_list:
                        gradient_list.append(real_fill_node)
                # read stroke data
                if "radialGradient" in stroke or "linearGradient" in stroke:
                    real_stroke = self.svg.getElementById(stroke).attrib["{"+NSS["xlink"]+"}href"][1:]
                    real_stroke_node = self.svg.getElementById(real_stroke)
                    if real_stroke_node not in gradient_list:
                        gradient_list.append(real_stroke_node)
        data = []
        # read gradients data
        for gradient in gradient_list:
            # parse gradient stops
            stop_data = read_stop_gradient(gradient)
            data.append(stop_data)
        return data

    # called when the extension is running.
    def effect(self):
        self.doc_selected_gradients = self.get_selected_gradients_data()
        try:
            app = MainWindow(self)
            Gtk.main()
        except Exception as e:
            import traceback
            show_errormsg(e)
            show_errormsg(traceback.print_exc())


if __name__ == '__main__':
    GradientSaver().run()