import enum
import os
from typing import Optional, Union, Tuple, List

from PIL import Image, ImageFont

from platypush.plugins import Plugin, action


class DeviceInterface(enum.Enum):
    I2C = 'i2c'
    SPI = 'spi'


class DeviceSlot(enum.IntEnum):
    BACK = 0
    FRONT = 1


class DeviceRotation(enum.IntEnum):
    ROTATE_0 = 0
    ROTATE_90 = 1
    ROTATE_180 = 2
    ROTATE_270 = 3


class LumaOledPlugin(Plugin):
    """
    Plugin to interact with small OLED-based RaspberryPi displays through the luma.oled driver.

    Requires:

        * **luma.oled** (``pip install git+https://github.com/rm-hull/luma.oled``)

    """

    def __init__(self,
                 interface: str,
                 device: str,
                 port: int = 0,
                 slot: int = DeviceSlot.BACK.value,
                 width: int = 128,
                 height: int = 64,
                 rotate: int = DeviceRotation.ROTATE_0.value,
                 gpio_DC: int = 24,
                 gpio_RST: int = 25,
                 bus_speed_hz: int = 8000000,
                 address: int = 0x3c,
                 cs_high: bool = False,
                 transfer_size: int = 4096,
                 spi_mode: Optional[int] = None,
                 font: Optional[str] = None,
                 font_size: int = 10,
                 **kwargs):
        """
        :param interface: Serial interface the display is connected to (``spi`` or ``i2c``).
        :param device: Display chipset type (supported: ssd1306 ssd1309, ssd1322, ssd1325, ssd1327, ssd1331, ssd1351, ssd1362, sh1106).
		:param port: Device port (usually 0 or 1).
        :param slot: Device slot (0 for back, 1 for front).
        :param width: Display width.
        :param height: Display height.
        :param rotate: Display rotation (0 for no rotation, 1 for 90 degrees, 2 for 180 degrees, 3 for 270 degrees).
        :param gpio_DC: [SPI only] GPIO PIN used for data (default: 24).
        :param gpio_RST: [SPI only] GPIO PIN used for RST (default: 25).
        :param bus_speed_hz: [SPI only] Bus speed in Hz (default: 8 MHz).
        :param address: [I2C only] Device address (default: 0x3c).
        :param cs_high: [SPI only] Set to True if the SPI chip select is high.
        :param transfer_size: [SPI only] Maximum amount of bytes to transfer in one go (default: 4096).
        :param spi_mode: [SPI only] SPI mode as two bit pattern of clock polarity and phase [CPOL|CPHA], 0-3 (default:None).
        :param font: Path to a default TTF font used to display the text.
        :param font_size: Font size - it only applies if ``font`` is set.
        """
        import luma.core.interface.serial
        import luma.oled.device
        from luma.core.render import canvas

        super().__init__(**kwargs)

        iface_name = interface
        interface = getattr(luma.core.interface.serial, DeviceInterface(interface).value)

        if iface_name == DeviceInterface.SPI.value:
            self.serial = interface(port=port, device=slot, cs_high=cs_high, gpio_DC=gpio_DC,
                                    gpio_RST=gpio_RST, bus_speed_hz=bus_speed_hz,
                                    transfer_size=transfer_size, spi_mode=spi_mode)
        else:
            self.serial = interface(port=port, address=address)

        device = getattr(luma.oled.device, device)
        self.device = device(self.serial, width=width, height=height, rotate=rotate)
        self.canvas = canvas(self.device)
        self.font = None
        self.font_size = font_size
        self.font = self._get_font(font, font_size)

    def _get_font(self, font: Optional[str] = None, font_size: Optional[int] = None):
        if font:
            return ImageFont.truetype(os.path.abspath(os.path.expanduser(font)), font_size or self.font_size)

        return self.font

    @action
    def clear(self):
        """
        clear the display canvas.
        """
        from luma.core.render import canvas
        self.device.clear()
        del self.canvas
        self.canvas = canvas(self.device)

    @action
    def text(self, text: str, pos: Union[Tuple[int], List[int]] = (0, 0),
             fill: str = 'white', font: Optional[str] = None, font_size: Optional[int] = None, clear: bool = False):
        """
        Draw text on the canvas.

        :param text: Text to be drawn.
        :param pos: Position of the text.
        :param fill: Text color (default: ``white``).
        :param font: ``font`` type override.
        :param font_size: ``font_size`` override.
        :param clear: Set to True if you want to clear the canvas before writing the text (default: False).
        """
        if clear:
            self.clear()

        font = self._get_font(font, font_size)

        with self.canvas as draw:
            draw.text(pos, text, fill=fill, font=font)

    @action
    def rectangle(self, xy: Optional[Union[Tuple[int], List[int]]] = None,
                  fill: Optional[str] = None, outline: Optional[str] = None,
                  width: int = 1, clear: bool = False):
        """
        Draw a rectangle on the canvas.

        :param xy: Two points defining the bounding box, either as [(x0, y0), (x1, y1)] or [x0, y0, x1, y1]. Default: bounding box of the device.
        :param fill: Fill color - can be ``black`` or ``white``.
        :param outline: Outline color - can be ``black`` or ``white``.
        :param width: Figure width in pixels (default: 1).
        :param clear: Set to True if you want to clear the canvas before writing the text (default: False).
        """
        if clear:
            self.clear()

        if not xy:
            xy = self.device.bounding_box

        with self.canvas as draw:
            draw.rectangle(xy, outline=outline, fill=fill, width=width)

    @action
    def arc(self, start: int, end: int, xy: Optional[Union[Tuple[int], List[int]]] = None,
            fill: Optional[str] = None, outline: Optional[str] = None,
            width: int = 1, clear: bool = False):
        """
        Draw an arc on the canvas.

        :param start: Starting angle, in degrees (measured from 3 o' clock and increasing clockwise).
        :param end: Ending angle, in degrees (measured from 3 o' clock and increasing clockwise).
        :param xy: Two points defining the bounding box, either as [(x0, y0), (x1, y1)] or [x0, y0, x1, y1]. Default: bounding box of the device.
        :param fill: Fill color - can be ``black`` or ``white``.
        :param outline: Outline color - can be ``black`` or ``white``.
        :param width: Figure width in pixels (default: 1).
        :param clear: Set to True if you want to clear the canvas before writing the text (default: False).
        """
        if clear:
            self.clear()

        if not xy:
            xy = self.device.bounding_box

        with self.canvas as draw:
            draw.arc(xy, start=start, end=end, outline=outline, fill=fill, width=width)

    @action
    def chord(self, start: int, end: int, xy: Optional[Union[Tuple[int], List[int]]] = None,
              fill: Optional[str] = None, outline: Optional[str] = None,
              width: int = 1, clear: bool = False):
        """
        Same as ``arc``, but it connects the end points with a straight line.

        :param start: Starting angle, in degrees (measured from 3 o' clock and increasing clockwise).
        :param end: Ending angle, in degrees (measured from 3 o' clock and increasing clockwise).
        :param xy: Two points defining the bounding box, either as [(x0, y0), (x1, y1)] or [x0, y0, x1, y1]. Default: bounding box of the device.
        :param fill: Fill color - can be ``black`` or ``white``.
        :param outline: Outline color - can be ``black`` or ``white``.
        :param width: Figure width in pixels (default: 1).
        :param clear: Set to True if you want to clear the canvas before writing the text (default: False).
        """
        if clear:
            self.clear()

        if not xy:
            xy = self.device.bounding_box

        with self.canvas as draw:
            draw.chord(xy, start=start, end=end, outline=outline, fill=fill, width=width)

    @action
    def pieslice(self, start: int, end: int, xy: Optional[Union[Tuple[int], List[int]]] = None,
                 fill: Optional[str] = None, outline: Optional[str] = None,
                 width: int = 1, clear: bool = False):
        """
        Same as ``arc``, but it also draws straight lines between the end points and the center of the bounding box.

        :param start: Starting angle, in degrees (measured from 3 o' clock and increasing clockwise).
        :param end: Ending angle, in degrees (measured from 3 o' clock and increasing clockwise).
        :param xy: Two points defining the bounding box, either as [(x0, y0), (x1, y1)] or [x0, y0, x1, y1]. Default: bounding box of the device.
        :param fill: Fill color - can be ``black`` or ``white``.
        :param outline: Outline color - can be ``black`` or ``white``.
        :param width: Figure width in pixels (default: 1).
        :param clear: Set to True if you want to clear the canvas before writing the text (default: False).
        """
        if clear:
            self.clear()

        if not xy:
            xy = self.device.bounding_box

        with self.canvas as draw:
            draw.pieslice(xy, start=start, end=end, outline=outline, fill=fill, width=width)

    @action
    def ellipse(self, xy: Optional[Union[Tuple[int], List[int]]] = None,
                fill: Optional[str] = None, outline: Optional[str] = None,
                width: int = 1, clear: bool = False):
        """
        Draw an ellipse on the canvas.

        :param xy: Two points defining the bounding box, either as [(x0, y0), (x1, y1)] or [x0, y0, x1, y1]. Default: bounding box of the device.
        :param fill: Fill color - can be ``black`` or ``white``.
        :param outline: Outline color - can be ``black`` or ``white``.
        :param width: Figure width in pixels (default: 1).
        :param clear: Set to True if you want to clear the canvas before writing the text (default: False).
        """
        if clear:
            self.clear()

        if not xy:
            xy = self.device.bounding_box

        with self.canvas as draw:
            draw.ellipse(xy, outline=outline, fill=fill, width=width)

    @action
    def line(self, xy: Optional[Union[Tuple[int], List[int]]] = None,
             fill: Optional[str] = None, outline: Optional[str] = None,
             width: int = 1, curve: bool = False, clear: bool = False):
        """
        Draw a line on the canvas.

        :param xy: Sequence of either 2-tuples like [(x, y), (x, y), ...] or numeric values like [x, y, x, y, ...].
        :param fill: Fill color - can be ``black`` or ``white``.
        :param outline: Outline color - can be ``black`` or ``white``.
        :param width: Figure width in pixels (default: 1).
        :param curve: Set to True for rounded edges (default: False).
        :param clear: Set to True if you want to clear the canvas before writing the text (default: False).
        """
        if clear:
            self.clear()

        if not xy:
            xy = self.device.bounding_box

        with self.canvas as draw:
            draw.line(xy, outline=outline, fill=fill, width=width, joint='curve' if curve else None)

    @action
    def point(self, xy: Optional[Union[Tuple[int], List[int]]] = None,
              fill: Optional[str] = None, clear: bool = False):
        """
        Draw one or more points on the canvas.

        :param xy: Sequence of either 2-tuples like [(x, y), (x, y), ...] or numeric values like [x, y, x, y, ...].
        :param fill: Fill color - can be ``black`` or ``white``.
        :param clear: Set to True if you want to clear the canvas before writing the text (default: False).
        """
        if clear:
            self.clear()

        if not xy:
            xy = self.device.bounding_box

        with self.canvas as draw:
            draw.point(xy, fill=fill)

    @action
    def polygon(self, xy: Optional[Union[Tuple[int], List[int]]] = None,
                fill: Optional[str] = None, outline: Optional[str] = None,
                clear: bool = False):
        """
        Draw a polygon on the canvas.

        :param xy: Sequence of either 2-tuples like [(x, y), (x, y), ...] or numeric values like [x, y, x, y, ...].
        :param fill: Fill color - can be ``black`` or ``white``.
        :param outline: Outline color - can be ``black`` or ``white``.
        :param clear: Set to True if you want to clear the canvas before writing the text (default: False).
        """
        if clear:
            self.clear()

        if not xy:
            xy = self.device.bounding_box

        with self.canvas as draw:
            draw.polygon(xy, outline=outline, fill=fill)

    @action
    def image(self, image: str):
        """
        Draws an image to the canvas (this will clear the existing canvas).

        :param image: Image path.
        """
        image = Image.open(os.path.abspath(os.path.expanduser(image)))
        self.clear()
        self.device.display(image)


# vim:sw=4:ts=4:et: