# Stdio Mask
# By Al Sweigart al@inventwithpython.com

__version__ = '0.0.5'  # type: str

import sys


"""Notes about making this code backwards-compatible with Python 2:
sys.stdout.write() can only write unicode strings, not Python 2 str strings.
I create STR_TYPE to use for isinstance() checks. Also, the u prefix for
unicode strings causes syntax errors on Python 3.1 and 3.2, so instead I
pass those strings to STR_TYPE, which is set to unicode() on Python 2,
which effectively does the same thing as the u prefix.
"""
STR_TYPE = str # type: type
RUNNING_PYTHON_2 = sys.version_info[0] == 2  # type: bool
if RUNNING_PYTHON_2:
    STR_TYPE = unicode # Ignore the pyflakes warning on this line.


try:
    from typing import List
except ImportError:
    pass # There is no typing module on Python 2, but that's fine because we use the comment-style of type hints.

if sys.platform == 'win32':
    # For some reason, mypy reports that msvcrt doesn't have getch, ignore this warning:
    from msvcrt import getch # type: ignore

    def getpass(prompt='Password: ', mask='*'):
        # type: (str, str) -> str

        if RUNNING_PYTHON_2:
            # On Python 2, convert `prompt` and `mask` from str to unicode because sys.stdout.write requires unicode.
            if isinstance(prompt, str):
                # Mypy in Python 3 mode (the default mode) will complain about the following line:
                prompt = prompt.decode('utf-8') # type: ignore
            if isinstance(mask, str):
                # Mypy in Python 3 mode (the default mode) will complain about the following line:
                mask = mask.decode('utf-8') # type: ignore

        if not isinstance(prompt, STR_TYPE):
            raise TypeError('prompt argument must be a str, not %s' % (type(prompt).__name__))
        if not isinstance(mask, STR_TYPE):
            raise TypeError('mask argument must be a zero- or one-character str, not %s' % (type(prompt).__name__))
        if len(mask) > 1:
            raise ValueError('mask argument must be a zero- or one-character str')

        if mask == '' or sys.stdin is not sys.__stdin__:
            # Fall back on getpass if a mask is not needed.
            import getpass as gp
            return gp.getpass(prompt)

        enteredPassword = [] # type: List[str]
        sys.stdout.write(prompt)
        sys.stdout.flush()
        while True:
            key = ord(getch())
            if key == 13: # Enter key pressed.
                if RUNNING_PYTHON_2:
                    sys.stdout.write(STR_TYPE('\n'))
                else:
                    sys.stdout.write('\n')
                return ''.join(enteredPassword)
            elif key in (8, 127): # Backspace/Del key erases previous output.
                if len(enteredPassword) > 0:
                    # Erases previous character.
                    if RUNNING_PYTHON_2:
                        sys.stdout.write(STR_TYPE('\b \b')) # \b doesn't erase the character, it just moves the cursor back.
                    else:
                        sys.stdout.write('\b \b') # \b doesn't erase the character, it just moves the cursor back.
                    sys.stdout.flush()
                    enteredPassword = enteredPassword[:-1]
            elif 0 <= key <= 31:
                # Do nothing for unprintable characters.
                # TODO: Handle Esc, F1-F12, arrow keys, home, end, insert, del, pgup, pgdn
                pass
            else:
                # Key is part of the password; display the mask character.
                char = chr(key)
                sys.stdout.write(mask)
                sys.stdout.flush()
                enteredPassword.append(char)

else: # macOS and Linux
    import tty, termios
    def getch():
        # type: () -> str
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(sys.stdin.fileno())
            ch = sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return ch

    def getpass(prompt='Password: ', mask='*'):
        # type: (str, str) -> str

        if RUNNING_PYTHON_2:
            # On Python 2, convert `prompt` and `mask` from str to unicode because sys.stdout.write requires unicode.
            if isinstance(prompt, str):
                # Mypy in Python 3 mode (the default mode) will complain about the following line:
                prompt = prompt.decode('utf-8') # type: ignore
            if isinstance(mask, str):
                # Mypy in Python 3 mode (the default mode) will complain about the following line:
                mask = mask.decode('utf-8') # type: ignore

        if not isinstance(prompt, STR_TYPE):
            raise TypeError('prompt argument must be a str, not %s' % (type(prompt).__name__))
        if not isinstance(mask, STR_TYPE):
            raise TypeError('mask argument must be a zero- or one-character str, not %s' % (type(prompt).__name__))
        if len(mask) > 1:
            raise ValueError('mask argument must be a zero- or one-character str')

        if mask == '' or sys.stdin is not sys.__stdin__:
            # Fall back on getpass if a mask is not needed.
            import getpass as gp
            return gp.getpass(prompt)

        enteredPassword = [] # List[str]
        sys.stdout.write(prompt)
        sys.stdout.flush()
        while True:
            key = ord(getch())
            if key == 13: # Enter key pressed.
                if RUNNING_PYTHON_2:
                    sys.stdout.write(STR_TYPE('\n'))
                else:
                    sys.stdout.write('\n')
                return ''.join(enteredPassword)
            elif key in (8, 127): # Backspace/Del key erases previous output.
                if len(enteredPassword) > 0:
                    # Erases previous character.
                    if RUNNING_PYTHON_2:
                        sys.stdout.write(STR_TYPE('\b \b')) # \b doesn't erase the character, it just moves the cursor back.
                    else:
                        sys.stdout.write('\b \b') # \b doesn't erase the character, it just moves the cursor back.
                    sys.stdout.flush()
                    enteredPassword = enteredPassword[:-1]
            elif 0 <= key <= 31:
                # Do nothing for unprintable characters.
                # TODO: Handle Esc, F1-F12, arrow keys, home, end, insert, del, pgup, pgdn
                pass
            else:
                # Key is part of the password; display the mask character.
                char = chr(key)
                sys.stdout.write(mask)
                sys.stdout.flush()
                enteredPassword.append(char)
