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

import luma.core.interface.serial
import luma.oled.device
from luma.core.render import canvas
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.
        """
        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.
        """
        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:
