#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
   @project: HsPyLib
   @package: hspylib.modules.cli
      @file: keyboard.py
   @created: Tue, 4 May 2021
    @author: <B>H</B>ugo <B>S</B>aporetti <B>J</B>unior"
      @site: https://github.com/yorevs/hspylib
   @license: MIT - Please refer to <https://opensource.org/licenses/MIT>

   Copyright 2023, HsPyLib team
"""
from time import sleep

from hspylib.core.enums.enumeration import Enumeration
from hspylib.core.exception.exceptions import KeyboardInputError
from typing import List, Optional

import getkey
import select
import string
import sys
import termios


# pylint: disable=multiple-statements
class Keyboard(Enumeration):
    """Provides keyboard interaction with the terminal."""

    # fmt: off

    # Control keys
    VK_NONE         = ''; VK_DISABLED = ''

    VK_ESC          = getkey.keys.ESC; VK_ENTER             = getkey.keys.ENTER
    VK_UP           = getkey.keys.UP; VK_DELETE             = getkey.keys.DELETE
    VK_DOWN         = getkey.keys.DOWN; VK_SPACE            = getkey.keys.SPACE
    VK_LEFT         = getkey.keys.LEFT; VK_HOME             = getkey.keys.HOME
    VK_RIGHT        = getkey.keys.RIGHT; VK_END             = getkey.keys.END
    VK_BACKSPACE    = getkey.keys.BACKSPACE; VK_PAGE_UP     = getkey.keys.PAGE_UP
    VK_INSERT       = getkey.keys.INSERT; VK_PAGE_DOWN      = getkey.keys.PAGE_DOWN
    VK_TAB          = '\t'; VK_SHIFT_TAB                    = '\x1b[Z'

    VK_CTRL_A       = '\x01';       VK_CTRL_B = '\x02';       VK_CTRL_C = VK_DISABLED; VK_CTRL_D = '\x04'
    VK_CTRL_E       = '\x05';       VK_CTRL_F = '\x06';       VK_CTRL_G = '\x07';      VK_CTRL_H = '\x08'
    VK_CTRL_I       = VK_DISABLED;  VK_CTRL_J = VK_DISABLED;  VK_CTRL_K = '\x0b';      VK_CTRL_L = '\x0c'
    VK_CTRL_M       = VK_DISABLED;  VK_CTRL_N = '\x0e';       VK_CTRL_O = VK_DISABLED; VK_CTRL_P = '\x10'
    VK_CTRL_Q       = VK_DISABLED;  VK_CTRL_R = '\x12';       VK_CTRL_S = VK_DISABLED; VK_CTRL_T = '\x14'
    VK_CTRL_U       = '\x15';       VK_CTRL_V = VK_DISABLED;  VK_CTRL_W = '\x17';      VK_CTRL_X = '\x18'
    VK_CTRL_Y       = '\x19';       VK_CTRL_Z = VK_DISABLED

    # Letters
    VK_a = 'a'; VK_i = 'i'; VK_q = 'q'; VK_y = 'y'
    VK_A = 'A'; VK_I = 'I'; VK_Q = 'Q'; VK_Y = 'Y'
    VK_b = 'b'; VK_j = 'j'; VK_r = 'r'; VK_z = 'z'
    VK_B = 'B'; VK_J = 'J'; VK_R = 'R'; VK_Z = 'Z'
    VK_c = 'c'; VK_k = 'k'; VK_s = 's'
    VK_C = 'C'; VK_K = 'K'; VK_S = 'S'
    VK_d = 'd'; VK_l = 'l'; VK_t = 't'
    VK_D = 'D'; VK_L = 'L'; VK_T = 'T'
    VK_e = 'e'; VK_m = 'm'; VK_u = 'u'
    VK_E = 'E'; VK_M = 'M'; VK_U = 'U'
    VK_f = 'f'; VK_n = 'n'; VK_v = 'v'
    VK_F = 'F'; VK_N = 'N'; VK_V = 'V'
    VK_g = 'g'; VK_o = 'o'; VK_w = 'w'
    VK_G = 'G'; VK_O = 'O'; VK_W = 'W'
    VK_h = 'h'; VK_p = 'p'; VK_x = 'x'
    VK_H = 'H'; VK_P = 'P'; VK_X = 'X'

    # Numbers
    VK_ZERO          = '0'; VK_FIVE  = '5'
    VK_ONE           = '1'; VK_SIX   = '6'
    VK_TWO           = '2'; VK_SEVEN = '7'
    VK_THREE         = '3'; VK_EIGHT = '8'
    VK_FOUR          = '4'; VK_NINE  = '9'

    # Punctuation
    VK_BACKTICK      = '`'; VK_OPEN_PARENTHESIS  = '('; VK_PIPE              = '|'
    VK_TILDE         = '~'; VK_CLOSE_PARENTHESIS = ')'; VK_BACK_SLASH        = '\\'
    VK_EXCL_MARK     = '!'; VK_MINUS             = '-'; VK_BACK_COLON        = ':'
    VK_AT            = '@'; VK_UNDERSCORE        = '_'; VK_BACK_SEMI_COLON   = ';'
    VK_SHARP         = '#'; VK_PLUS              = '+'; VK_QUOTE             = '\''
    VK_DOLLAR        = '$'; VK_EQUALS            = '='; VK_DOUBLE_QUOTE      = '"'
    VK_PERCENTAGE    = '%'; VK_LEFT_BRACKET      = '['; VK_LOWER             = '<'
    VK_CIRCUMFLEX    = '^'; VK_RIGHT_BRACKET     = ']'; VK_GREATER           = '>'
    VK_AMPERSAND     = '&'; VK_LEFT_BRACE        = '{'; VK_COMMA             = ','
    VK_ASTERISK      = '*'; VK_RIGHT_BRACE       = '}'; VK_PERIOD            = '.'
    VK_SLASH         = '/'; VK_QUESTION_MARK     = '?'

    # fmt: on

    @staticmethod
    def kbhit(wait: bool = False, timeout: float = 0) -> bool:
        """Return whether there was an ENTER keyboard press."""
        while dr_list := select.select([sys.stdin], [], [], timeout):
            if not wait:
                return dr_list[0] != []
            elif dr_list[0]:
                return True

    @classmethod
    def getch(cls, n: int = 1) -> List["Keyboard"]:
        """Read and return (a) character(s) from a keyboard press.
        Method will return after ENTER is pressed.
        """
        return list(map(cls.of_value, sys.stdin.read(n)))

    @classmethod
    def digits(cls) -> List["Keyboard"]:
        """Return all Keyboard digits."""
        return list(map(cls.of_value, filter(lambda v: str(v).isdigit(), cls.values())))

    @classmethod
    def letters(cls) -> List["Keyboard"]:
        """Return all Keyboard letters."""
        return list(map(cls.of_value, filter(lambda v: str(v).isalpha(), cls.values())))

    @classmethod
    def wait_keystroke(cls, blocking: bool = True, ignore_error_keys: bool = True) -> Optional["Keyboard"]:
        """Wait until a keypress is detected."""
        try:
            sleep(0.01)
            while not (keystroke := getkey.getkey(blocking)):
                sleep(0.01)
            return cls.of_value(keystroke)
        except (KeyboardInterrupt, AssertionError) as err:
            if not ignore_error_keys:
                raise KeyboardInputError(f"Invalid keystroke => {str(err)}") from err
        except termios.error as err:
            raise KeyboardInputError("keyboard:: Requires a terminal (TTY)") from err
        finally:
            sys.stdin.flush()

        return None

    def __str__(self) -> str:
        return self.name

    def __repr__(self) -> str:
        return str(self)

    @property
    def val(self) -> str:
        return str(self.value)

    def isdigit(self) -> bool:
        """Return True if the keystroke is a digit string, False otherwise."""
        return str(self.value).isdigit()

    def isalpha(self) -> bool:
        """Return True if the keystroke is alphabetic string, False otherwise."""
        return str(self.value).isalpha()

    def isalnum(self) -> bool:
        """Return True if the keystroke is alpha-numeric string, False otherwise."""
        return str(self.value).isalnum()

    def ispunct(self) -> bool:
        """Return True if the keystroke is a punctuation string, False otherwise."""
        return all(ch in string.punctuation for ch in str(self.value))
