from functools import wraps
from copy import deepcopy
import importlib

# These base classes document functions that
# different output devices are expected to have.

class OutputDevice(object):
    """Common class for all OutputDevices, no matter if they're graphical or character-based."""

    current_proxy = None

    def attach_new_proxy(self, proxy):
        self.detach_current_proxy()
        self.attach_proxy(proxy)

    def detach_current_proxy(self):
        self.current_proxy = None

    def attach_proxy(self, proxy):
        self.current_proxy = proxy
        proxy.on_attach()

    def init_proxy(self, proxy):
        base_classes = self.__base_classes__
        base_classes_items = sum([cls.__dict__.items() for cls in base_classes], [])
        #print(base_classes_items)
        public_attributes = [ (k, v) for (k, v) in base_classes_items if not k.startswith("_") ]
        hidden_attributes = ["current_proxy", "current_image"]
        hidden_methods = ["init_proxy", "proxify_method", "detach_current_proxy", "attach_proxy", "get_proxied_method", "proxify_method"]
        attribute_names = [ k for (k, v) in public_attributes if not callable(v) and k not in hidden_attributes]
        method_names = [ k for (k, v) in public_attributes if callable(v) and k not in hidden_methods]
        direct_methods = ["display_data_onto_image"]
        
        for attribute_name in attribute_names:
            #print("attribute: {}".format(attribute_name))
            # Setting attributes of the proxy object to the same values
            # as the current output device has them
            value = getattr(self, attribute_name)
            # Making sure that changing the proxy object's attributes
            # won't change the attributes of the original object
            copied_value = deepcopy(value)
            setattr(proxy, attribute_name, copied_value)
            #print("Setting to value {}".format(copied_value))
        for method_name in method_names:
            #print("method: {}".format(method_name))
            # Setting functions to proxy the current object's 
            self.proxify_method(proxy, method_name)
        for method_name in direct_methods:
            # Methods that are proxied directly
            setattr(proxy, method_name, getattr(self, method_name))
        # Proxies have no use for hidden methods
        # Unless 
        #for attribute_name in hidden_attributes+hidden_methods:
        #    if hasattr(proxy, attribute_name):
        #        delattr(proxy, attribute_name)

    def get_proxied_method(o, proxy, method_name, sideeffect):
        method = getattr(o, method_name)
        @wraps(method)
        def wrapper(*args, **kwargs):
            #print("Method: "+method_name)
            sideeffect(*args, **kwargs)
            #print("Current: "+o.current_proxy.context_alias)
            #print("Called from: "+proxy.context_alias)
            if o.current_proxy.context_alias == proxy.context_alias:
                #print("Calling the method directly!")
                #print(args)
                method(*args, **kwargs)
            else:
                pass #print("Not calling the method directly!")
            #print("")
        return wrapper

    def proxify_method(self, proxy, method_name):
        # If a proxy defines a side effect to happen when the function is called,
        # we call it with same arguments and kwargs as the function received
        # (and we make a small empty lambda if there's no side effect)
        sideeffect = getattr(proxy, "_"+method_name, lambda *a, **k: None)
        proxied_method = self.get_proxied_method(proxy, method_name, sideeffect)
        setattr(proxy, method_name, proxied_method)


class CharacterOutputDevice(OutputDevice):
    """Common class for all character-based OutputDevices."""
    rows = None  # number of columns
    cols = None  # number of rows
    type = ["char"]  # supported output type list
    
    def cursor(self):
        """
        Enables the cursor.
        """
        raise NotImplementedError

    def noCursor(self):
        """
        Disables the cursor.
        """
        raise NotImplementedError

    def setCursor(self, row, column):
        """
        Sets the cursor's position.
        """
        raise NotImplementedError

    def display_data(self, *data):
        """
        A function that is called to show text on the display.
        Each positional argument is one like of text.
        """
        raise NotImplementedError


class GraphicalOutputDevice(OutputDevice):
    """Common class for all graphical OutputDevices."""
    height = None  # height of display in pixels
    width = None  # width of display in pixels
    type = ["b&w-pixel"]  # supported output type list
    device_mode = "1"  # a "mode" parameter for PIL
    char_height = 8 # height of the default font
    char_width = 8 # width of the default font

    current_image = None #an attribute for storing the currently displayed image

    def display_data_onto_image(self, *args, **kwargs):
        """
        A function that draws text on a PIL.Image, emulating
        character displays (to be used with display_data).
        """
        raise NotImplementedError

    def display_image(self, image):
        """
        A function that shows a PIL.Image on the display.
        It also saves it in the current_image attribute.
        """
        raise NotImplementedError

    def clear(self):
        """
        Clears the display, so that there's nothing shown on it.
        """
        raise NotImplementedError

class OutputProxy(CharacterOutputDevice, GraphicalOutputDevice):

    current_image = None
    __cursor_enabled = False
    __cursor_position = (0, 0)

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

    def _display_image(self, image):
        self.current_image = image

    def _clear(self):
        print("Clearing screen proxy")
        self.current_image = None

    def _display_data(self, *data):
        cursor_position = self.__cursor_position if self.__cursor_enabled else None
        self.current_image = self.display_data_onto_image(*data, cursor_position=cursor_position)

    def _cursor(self):
        self.__cursor_enabled = True

    def _noCursor(self):
        self.__cursor_enabled = False

    def _setCursor(self, *position):
        self.__cursor_position = position

    def get_current_image(self):
        return self.current_image

    def on_attach(self):
        if self.current_image:
            self.display_image(self.current_image)

def init(output_config):
    # type: (list) -> None
    """ This function is called by main.py to read the output configuration, pick the corresponding drivers and initialize a Screen object. Returns the screen object created. """
    # Currently only the first screen in the config is initialized
    screen_config = output_config[0]
    driver_name = screen_config["driver"]
    driver_module = importlib.import_module("output.drivers." + driver_name)
    args = screen_config["args"] if "args" in screen_config else []
    kwargs = screen_config["kwargs"] if "kwargs" in screen_config else {}
    return driver_module.Screen(*args, **kwargs)

if __name__ == "__main__":
    o = type("OD", (GraphicalOutputDevice, CharacterOutputDevice), {})()
    op = type("OP", (o.__class__, ), {})()
    o.init_proxy(op)
