from functools import partial
from importlib import import_module
from logging import getLogger
from math import log2
from pprint import pformat
from socket import socket
from typing import List, Tuple

from PIL import Image, ImageDraw

from dcspy import LcdColor, LcdMono, SUPPORTED_CRAFTS, SEND_ADDR
from dcspy.aircraft import Aircraft
from dcspy.dcsbios import ProtocolParser
from dcspy.sdk import lcd_sdk

LOG = getLogger(__name__)


class LogitechKeyboard:
    def __init__(self, parser_hook: ProtocolParser, **kwargs) -> None:
        """
        General keyboard with LCD form Logitech.

        It can be easily extended for any of:
        - Mono LCD: G13, G15 (v1 and v2) and G510
        - RGB LCD: G19

        However, it defines a bunch of functionally to be used be child class:
        - DCS-BIOS callback for currently used aircraft in DCS
        - auto-detecting aircraft and load its handling class
        - send button request to DCS-BIOS

        Child class needs redefine:
        - buttons with supported buttons as tuple of int
        - pass lcd_type argument as LcdInfo to super constructor

        :param parser_hook: BSC-BIOS parser
        """
        getattr(import_module('dcspy.dcsbios'), 'StringBuffer')(parser_hook, 0x0000, 16, partial(self.detecting_plane))
        self.parser = parser_hook
        self.plane_name = ''
        self.plane_detected = False
        self.already_pressed = False
        self.buttons: Tuple[int, ...] = (0,)
        self._display: List[str] = []
        self.lcd = kwargs.get('lcd_type', LcdMono)
        lcd_sdk.logi_lcd_init('DCS World', self.lcd.type)
        self.plane = Aircraft(self.lcd)
        self.vert_space = 0

    @property
    def display(self) -> List[str]:
        """
        Get the latest text from LCD.

        :return: list of strings with data, row by row
        """
        return self._display

    @display.setter
    def display(self, message: List[str]) -> None:
        """
        Display message as image at LCD.

        For G13/G15/G510 takes first 4 or fewer elements of list and display as 4 rows.
        For G19 takes first 8 or fewer elements of list and display as 8 rows.
        :param message: List of strings to display, row by row.
        """
        self._display = message
        img = self._prepare_image()
        lcd_sdk.update_display(img)

    @staticmethod
    def text(message: List[str]) -> None:
        """
        Display message at LCD.

        For G13/G15/G510 takes first 4 or fewer elements of list and display as 4 rows.
        For G19 takes first 8 or fewer elements of list and display as 8 rows.
        :param message: List of strings to display, row by row.
        """
        lcd_sdk.update_text(message)

    def detecting_plane(self, value: str) -> None:
        """
        Try to detect airplane base on value received from DCS-BIOS.

        :param value: data from DCS-BIOS
        """
        value = value.replace('-', '').replace('_', '')
        if self.plane_name != value:
            self.plane_name = value
            if self.plane_name in SUPPORTED_CRAFTS:
                self.plane_name = value
                LOG.info(f'Detected Aircraft: {value}')
                self.display = ['Detected aircraft:', self.plane_name]
                self.plane_detected = True
            else:
                LOG.warning(f'Not supported aircraft: {value}')
                self.display = ['Detected aircraft:', self.plane_name, 'Not supported yet!']

    def load_new_plane(self) -> None:
        """
        Dynamic load of new detected aircraft.

        Setup callbacks for detected plane inside DCS-BIOS parser.
        """
        self.plane_detected = False
        self.plane = getattr(import_module('dcspy.aircraft'), self.plane_name)(self.lcd)
        LOG.debug(f'Dynamic load of: {self.plane_name} as {SUPPORTED_CRAFTS[self.plane_name]}')
        LOG.debug(f'{repr(self)}')
        for field_name, proto_data in self.plane.bios_data.items():
            buffer = getattr(import_module('dcspy.dcsbios'), proto_data['class'])
            buffer(parser=self.parser, callback=partial(self.plane.set_bios, field_name), **proto_data['args'])

    def check_buttons(self) -> int:
        """
        Check if button was pressed and return it`s number.

        For G13/G15/G510: 1-4
        For G19 9-15: LEFT = 9, RIGHT = 10, OK = 11, CANCEL = 12, UP = 13, DOWN = 14, MENU = 15
        :return: number of pressed button
        """
        for btn in self.buttons:
            if lcd_sdk.logi_lcd_is_button_pressed(btn):
                if not self.already_pressed:
                    self.already_pressed = True
                    return int(log2(btn)) + 1
                return 0
        self.already_pressed = False
        return 0

    def button_handle(self, sock: socket) -> None:
        """
        Button handler.

        * detect if button was pressed
        * fetch DCS-BIOS request from current plane
        * sent it action to DCS-BIOS via. network socket
        :param sock: network socket
        """
        button = self.check_buttons()
        if button:
            sock.sendto(bytes(self.plane.button_request(button), 'utf-8'), SEND_ADDR)

    def clear(self, true_clear=False):
        """
        Clear LCD.

        :param true_clear:
        """
        LOG.debug(f'Clear LCD type: {self.lcd.type}')
        lcd_sdk.clear_display(true_clear)

    def _prepare_image(self) -> Image.Image:
        """
        Prepare image for base of LCD type.

        For G13/G15/G510 takes first 4 or fewer elements of list and display as 4 rows.
        For G19 takes first 8 or fewer elements of list and display as 8 rows.
        :return: image instance ready display on LCD
        """
        img = Image.new(mode=self.lcd.mode, size=(self.lcd.width, self.lcd.height), color=self.lcd.background)
        draw = ImageDraw.Draw(img)
        for line_no, line in enumerate(self._display):
            draw.text(xy=(0, self.vert_space * line_no), text=line, fill=self.lcd.foreground, font=self.lcd.font_s)
        return img

    def __str__(self):
        return f'{self.__class__.__name__}: {self.lcd.width}x{self.lcd.height}'

    def __repr__(self):
        return f'{super().__repr__()} with: {pformat(self.__dict__)}'


class KeyboardMono(LogitechKeyboard):
    def __init__(self, parser_hook: ProtocolParser) -> None:
        """
        Logitech`s keyboard with mono LCD.

        Support for: G510, G13, G15 (v1 and v2)
        :param parser_hook: BSC-BIOS parser
        """
        super().__init__(parser_hook, lcd_type=LcdMono)
        self.buttons = (lcd_sdk.MONO_BUTTON_0, lcd_sdk.MONO_BUTTON_1, lcd_sdk.MONO_BUTTON_2, lcd_sdk.MONO_BUTTON_3)
        self.vert_space = 10


class KeyboardColor(LogitechKeyboard):
    def __init__(self, parser_hook: ProtocolParser) -> None:
        """
        Logitech`s keyboard with color LCD.

        Support for: G19
        :param parser_hook: BSC-BIOS parser
        """
        super().__init__(parser_hook, lcd_type=LcdColor)
        self.buttons = (lcd_sdk.COLOR_BUTTON_LEFT, lcd_sdk.COLOR_BUTTON_RIGHT, lcd_sdk.COLOR_BUTTON_OK,
                        lcd_sdk.COLOR_BUTTON_CANCEL, lcd_sdk.COLOR_BUTTON_UP, lcd_sdk.COLOR_BUTTON_DOWN,
                        lcd_sdk.COLOR_BUTTON_MENU)
        self.vert_space = 40
