from locale import normalize
from termios import tcgetattr, tcsetattr, TCSADRAIN
from tty import setraw
from sys import stdin, stdout

# Keys
UP       = b'\x1B[A'
DOWN     = b'\x1B[B'
RIGHT    = b'\x1B[C'
LEFT     = b'\x1B[D'
SPACEBAR = b''
ENTER    = b'\r'
ETX      = b'\x03'

# Color & text styles
class Fore:
    normalize  =  "\x1B[0m" 
    black      =  "\x1B[1;30m"
    red        =  "\x1B[1;31m"
    green      =  "\x1B[1;32m"
    yellow     =  "\x1B[1;33m"
    blue       =  "\x1B[1;34m"
    magenta    =  "\x1B[1;35m"
    cyan       =  "\x1B[1;36m"
    white      =  "\x1B[1;37m"

    bright_black    =  "\x1B[0;90m"
    bright_red      =  "\x1B[0;91m"
    bright_green    =  "\x1B[0;92m"
    bright_yellow   =  "\x1B[0;93m"
    bright_blue     =  "\x1B[0;94m"
    bright_magenta  =  "\x1B[0;95m"
    bright_cyan     =  "\x1B[0;96m"
    bright_white    =  "\x1B[0;97m"

class Back:
    normalize  =  "\x1B[0m"
    black      =  "\x1B[40m"
    red        =  "\x1B[41m"
    green      =  "\x1B[42m"
    yellow     =  "\x1B[43m"
    blue       =  "\x1B[44m"
    magenta    =  "\x1B[45m"
    cyan       =  "\x1B[46m"
    white      =  "\x1B[47m"

    bright_black    = "\x1B[0;100m"
    bright_red      = "\x1B[0;101m"
    bright_green    = "\x1B[0;102m"
    bright_yellow   = "\x1B[0;103m"
    bright_blue     = "\x1B[0;104m"
    bright_magenta  = "\x1B[0;105m"
    bright_cyan     = "\x1B[0;106m"
    bright_white    = "\x1B[0;107m"

class Style:
    reset          = "\x1b[0m"
    bold           = "\x1B[1m"
    dim            = "\x1B[2m"
    italic         = "\x1B[3m"
    underline      = "\x1B[4m"
    blinking       = "\x1B[5m"
    inverse        = "\x1B[7m"
    invisible      = "\x1B[8m"
    strikethrough  = "\x1B[9m"

def getc():
    """Get character"""
    fd       = stdin.fileno()
    settings = tcgetattr(fd)
    try:
        setraw(stdin.fileno())
        ch = stdin.buffer.raw.read(3) 
    finally: tcsetattr(fd, TCSADRAIN, settings)

    if ch == ETX: raise KeyboardInterrupt
    return ch

def tcmove(x, y):
    """Move terminal cursor"""
    if    x > 0: stdout.write("\x1B[%sC" % (x))
    elif  x < 0: stdout.write("\x1B[%sD" % (x*-1))
        
    if    y > 0: stdout.write("\x1B[%sB" % (y))
    elif  y < 0: stdout.write("\x1b[%sA" % (y*-1))

    stdout.flush()

def tcshow():
    """Show terminal cursor"""
    stdout.write("\x1B[?25h")
    stdout.flush()

def tchide():
    """Hide terminal cursor"""
    stdout.write("\x1B[?25l")
    stdout.flush()

def tcsetcol(col):
    """Set the column the terminal cursor is in"""
    stdout.write(f"\x1B[{col}G")
    stdout.flush()

class Listbox(object):
    def __init__(
        self, 
        options                      : list, 
        title                        : str   = "",
        accept_keys                  : list  = [ENTER],
        clear_menu_on_exit           : bool  = False,
        clear_screen                 : bool  = False,
        cycle_cursor                 : bool  = False,
        menu_cursor                  : str   = "> ",
        menu_cursor_style            : tuple = (Fore.red, Style.bold),
        menu_highlight_style         : tuple = (Style.inverse),
        multi_select                 : bool  = False,
        multi_select_cursor          : str   = "* ",
        multi_select_cursor_style    : tuple = (Fore.blue, Style.bold),
        multi_select_highlight_style : tuple = (Style.inverse),
        multi_select_keys            : list  = [ENTER, SPACEBAR],
        preselected_entries          : list  = None,  # List of index's
        quit_keys                    : list  = [],
        raise_error_on_interrupt     : bool  = False,
        #search_case_sensitive        : bool  = DEFAULT_SEARCH_CASE_SENSITIVE,
        #search_highlight_style       : tuple = DEFAULT_SEARCH_HIGHLIGHT_STYLE,
        #search_key                   : list  = DEFAULT_SEARCH_KEYS,
        shortcut_highlight_style     : tuple = (Fore.blue, Style.bold),
        skip_empty_entries           : bool  = False
    ):
        self.options                      = options
        self.title                        = title
        self.accept_keys                  = accept_keys
        self.clear_menu_on_exit           = clear_menu_on_exit
        self.clear_screen                 = clear_screen
        self.cycle_cursor                 = cycle_cursor
        self.menu_cursor                  = menu_cursor
        self.menu_cursor_style            = menu_cursor_style
        self.menu_highlight_style         = menu_highlight_style
        self.multi_select                 = multi_select
        self.multi_select_cursor          = multi_select_cursor
        self.multi_select_cursor_style    = multi_select_cursor_style
        self.multi_select_highlight_style = multi_select_highlight_style
        self.multi_select_keys            = multi_select_keys
        self.preselected_entries          = preselected_entries
        self.quit_keys                    = quit_keys
        self.raise_error_on_interrupt     = raise_error_on_interrupt
        self.shortcut_highlight_style     = shortcut_highlight_style
        self.skip_empty_entries           = skip_empty_entries

    def refresh(self, p, advanced:bool=None):
        """Used internally to refresh theme"""
        if advanced == None :  # Refresh whole theme
            if p != 0: tcmove(0, int("-"+str((len(self.options)+1)-p)))

            # Reset theme
            [stdout.write(
                # Theme cursor
                " "*len(self.menu_cursor)

                +option+"\n"
            ) for option in self.options]
            stdout.flush()

            # Fix cursor position after write
            tcmove(0, int("-"+str(len(self.options))))

            # Refresh theme
            tcmove(0, p)  # From 0 go to pointer

            stdout.write(
                "".join([style for style in self.menu_cursor_style])+self.menu_cursor+Style.reset

                +"".join([style for style in self.menu_highlight_style])+self.options[p]+Style.reset
            )
            
            tcmove(int("-"+str(len(self.options[p-1]))), 0)
            stdout.flush()


        else:  # Refresh only whats needed
            tcsetcol(0)
            stdout.write(
                # Theme cursor
                " "*len(self.menu_cursor)

                +self.options[p]
            )
            stdout.flush()

            if advanced : tcmove(0, 1)
            else        : tcmove(0, -1)
            tcsetcol(0)
            stdout.flush()

            stdout.write(
                "".join([style for style in self.menu_cursor_style])+self.menu_cursor+Style.reset

                +"".join([style for style in self.menu_highlight_style])+self.options[p+1 if advanced else p-1]+Style.reset
            )
            stdout.flush()

    def show(self) -> str:
        """Prompt user with listbox"""
        p = 0  # Pointer that shows where we are on the options list

        # Hide cursor
        tchide()

        # Show initial listbox
        self.refresh(p)

        while True:
            try: key = getc()          # Get pressed character
            except KeyboardInterrupt:  # Makes sure to quit properly
                tcmove(0, (len(self.options)+1)-p)
                tcsetcol(0)
                tcshow()
                print()
                exit(0)
        
            if key == UP:
                # Check if move is possible
                if p-1 < 0 : continue
                else       : p-=1

                self.refresh(p+1, advanced=False)

            elif key == DOWN:
                # Check if move is possible
                if p+1 > len(self.options) : continue
                else                       : p+=1

                self.refresh(p-1, advanced=True)

            elif key == ENTER:
                tcmove(0, (len(self.options)+1)-p)
                tcsetcol(0)
                tcshow()
                choice = self.options[p]
                break
        return choice

listbox = Listbox(["option1", "option2", "option3"])
listbox.show()