# Source code for letsencrypt.display.util

    """Let's Encrypt display."""
    import os
    import textwrap

    import dialog
    import zope.interface

    from letsencrypt import interfaces
    from letsencrypt import errors

    WIDTH = 72
    HEIGHT = 20

    # Display exit codes
    OK = "ok"
    """Display exit code indicating user acceptance."""

    CANCEL = "cancel"
    """Display exit code for a user canceling the display."""

    HELP = "help"
    """Display exit code when for when the user requests more help."""

    def _wrap_lines(msg):
        """Format lines nicely to 80 chars.

        :param str msg: Original message

        :returns: Formatted message respecting newlines in message
        :rtype: str

        """
        lines = msg.splitlines()
        fixed_l = []
        for line in lines:
            fixed_l.append(textwrap.fill(line, 80))
        return os.linesep.join(fixed_l)

    @zope.interface.implementer(interfaces.IDisplay)
    class NcursesDisplay(object):
        """Ncurses-based display."""

        def __init__(self, width=WIDTH, height=HEIGHT):
            super(NcursesDisplay, self).__init__()
            self.dialog = dialog.Dialog()
            self.width = width
            self.height = height

        def notification(self, message, height=10, pause=False):
            # pylint: disable=unused-argument
            """Display a notification to the user and wait for user acceptance.

            .. todo:: It probably makes sense to use one of the transient message
                types for pause. It isn't straightforward how best to approach
                the matter though given the context of our messages.
                http://pythondialog.sourceforge.net/doc/widgets.html#displaying-transient-messages

            :param str message: Message to display
            :param int height: Height of the dialog box
            :param bool pause: Not applicable to NcursesDisplay

            """
            self.dialog.msgbox(message, height, width=self.width)

        def menu(self, message, choices, ok_label="OK", cancel_label="Cancel",
                 help_label="", **unused_kwargs):
            """Display a menu.

            :param str message: title of menu

            :param choices: menu lines, len must be > 0
            :type choices: list of tuples (`tag`, `item`) tags must be unique or
                list of items (tags will be enumerated)

            :param str ok_label: label of the OK button
            :param str help_label: label of the help button
            :param dict unused_kwargs: absorbs default / cli_args

            :returns: tuple of the form (`code`, `index`) where
                `code` - int display exit code
                `int` - index of the selected item
            :rtype: tuple

            """
            menu_options = {
                "choices": choices,
                "ok_label": ok_label,
                "cancel_label": cancel_label,
                "help_button": bool(help_label),
                "help_label": help_label,
                "width": self.width,
                "height": self.height,
                "menu_height": self.height - 6,
            }

            # Can accept either tuples or just the actual choices
            if choices and isinstance(choices[0], tuple):
                # pylint: disable=star-args
                code, selection = self.dialog.menu(message, **menu_options)

                # Return the selection index
                for i, choice in enumerate(choices):
                    if choice[0] == selection:
                        return code, i

                return code, -1

            else:
                # "choices" is not formatted the way the dialog.menu expects...
                menu_options["choices"] = [
                    (str(i), choice) for i, choice in enumerate(choices, 1)
                ]
                # pylint: disable=star-args
                code, index = self.dialog.menu(message, **menu_options)

                if code == CANCEL:
                    return code, -1

                return code, int(index) - 1

        def input(self, message, **unused_kwargs):
            """Display an input box to the user.

            :param str message: Message to display that asks for input.
            :param dict _kwargs: absorbs default / cli_args

            :returns: tuple of the form (`code`, `string`) where
                `code` - int display exit code
                `string` - input entered by the user

            """
            sections = message.split("\n")
            # each section takes at least one line, plus extras if it's longer than self.width
            wordlines = [1 + (len(section)/self.width) for section in sections]
            height = 6 + sum(wordlines) + len(sections)
            return self.dialog.inputbox(message, width=self.width, height=height)

        def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs):
            """Display a Yes/No dialog box.

            Yes and No label must begin with different letters.

            :param str message: message to display to user
            :param str yes_label: label on the "yes" button
            :param str no_label: label on the "no" button
            :param dict _kwargs: absorbs default / cli_args

            :returns: if yes_label was selected
            :rtype: bool

            """
            return self.dialog.DIALOG_OK == self.dialog.yesno(
                message, self.height, self.width,
                yes_label=yes_label, no_label=no_label)

        def checklist(self, message, tags, default_status=True, **unused_kwargs):
            """Displays a checklist.

            :param message: Message to display before choices
            :param list tags: where each is of type :class:`str` len(tags) > 0
            :param bool default_status: If True, items are in a selected state by
                default.
            :param dict _kwargs: absorbs default / cli_args

            :returns: tuple of the form (`code`, `list_tags`) where
                `code` - int display exit code
                `list_tags` - list of str tags selected by the user

            """
            choices = [(tag, "", default_status) for tag in tags]
            return self.dialog.checklist(
                message, width=self.width, height=self.height, choices=choices)

    @zope.interface.implementer(interfaces.IDisplay)
    class FileDisplay(object):
        """File-based display."""

        def __init__(self, outfile):
            super(FileDisplay, self).__init__()
            self.outfile = outfile

        def notification(self, message, height=10, pause=True):
            # pylint: disable=unused-argument
            """Displays a notification and waits for user acceptance.

            :param str message: Message to display
            :param int height: No effect for FileDisplay
            :param bool pause: Whether or not the program should pause for the
                user's confirmation

            """
            side_frame = "-" * 79
            message = _wrap_lines(message)
            self.outfile.write(
                "{line}{frame}{line}{msg}{line}{frame}{line}".format(
                    line=os.linesep, frame=side_frame, msg=message))
            if pause:
                raw_input("Press Enter to Continue")

        def menu(self, message, choices, ok_label="", cancel_label="",
                 help_label="", **unused_kwargs):
            # pylint: disable=unused-argument
            """Display a menu.

            .. todo:: This doesn't enable the help label/button (I wasn't sold on
               any interface I came up with for this). It would be a nice feature

            :param str message: title of menu
            :param choices: Menu lines, len must be > 0
            :type choices: list of tuples (tag, item) or
                list of descriptions (tags will be enumerated)
            :param dict _kwargs: absorbs default / cli_args

            :returns: tuple of (`code`, `index`) where
                `code` - str display exit code
                `index` - int index of the user's selection

            :rtype: tuple

            """
            self._print_menu(message, choices)

            code, selection = self._get_valid_int_ans(len(choices))

            return code, selection - 1

        def input(self, message, **unused_kwargs):
            # pylint: disable=no-self-use
            """Accept input from the user.

            :param str message: message to display to the user
            :param dict _kwargs: absorbs default / cli_args

            :returns: tuple of (`code`, `input`) where
                `code` - str display exit code
                `input` - str of the user's input
            :rtype: tuple

            """
            ans = raw_input(
                textwrap.fill("%s (Enter 'c' to cancel): " % message, 80))

            if ans == "c" or ans == "C":
                return CANCEL, "-1"
            else:
                return OK, ans

        def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs):
            """Query the user with a yes/no question.

            Yes and No label must begin with different letters, and must contain at
            least one letter each.

            :param str message: question for the user
            :param str yes_label: Label of the "Yes" parameter
            :param str no_label: Label of the "No" parameter
            :param dict _kwargs: absorbs default / cli_args

            :returns: True for "Yes", False for "No"
            :rtype: bool

            """
            side_frame = ("-" * 79) + os.linesep

            message = _wrap_lines(message)

            self.outfile.write("{0}{frame}{msg}{0}{frame}".format(
                os.linesep, frame=side_frame, msg=message))

            while True:
                ans = raw_input("{yes}/{no}: ".format(
                    yes=_parens_around_char(yes_label),
                    no=_parens_around_char(no_label)))

                # Couldn't get pylint indentation right with elif
                # elif doesn't matter in this situation
                if (ans.startswith(yes_label[0].lower()) or
                        ans.startswith(yes_label[0].upper())):
                    return True
                if (ans.startswith(no_label[0].lower()) or
                        ans.startswith(no_label[0].upper())):
                    return False

        def checklist(self, message, tags, default_status=True, **unused_kwargs):
            # pylint: disable=unused-argument
            """Display a checklist.

            :param str message: Message to display to user
            :param list tags: `str` tags to select, len(tags) > 0
            :param bool default_status: Not used for FileDisplay
            :param dict _kwargs: absorbs default / cli_args

            :returns: tuple of (`code`, `tags`) where
                `code` - str display exit code
                `tags` - list of selected tags
            :rtype: tuple

            """
            while True:
                self._print_menu(message, tags)

                code, ans = self.input("Select the appropriate numbers separated "
                                       "by commas and/or spaces")

                if code == OK:
                    indices = separate_list_input(ans)
                    selected_tags = self._scrub_checklist_input(indices, tags)
                    if selected_tags:
                        return code, selected_tags
                    else:
                        self.outfile.write(
                            "** Error - Invalid selection **%s" % os.linesep)
                else:
                    return code, []

        def _scrub_checklist_input(self, indices, tags):
            # pylint: disable=no-self-use
            """Validate input and transform indices to appropriate tags.

            :param list indices: input
            :param list tags: Original tags of the checklist

            :returns: valid tags the user selected
            :rtype: :class:`list` of :class:`str`

            """
            # They should all be of type int
            try:
                indices = [int(index) for index in indices]
            except ValueError:
                return []

            # Remove duplicates
            indices = list(set(indices))

            # Check all input is within range
            for index in indices:
                if index < 1 or index > len(tags):
                    return []
            # Transform indices to appropriate tags
            return [tags[index - 1] for index in indices]

        def _get_valid_int_ans(self, max_):
            """Get a numerical selection.

            :param int max: The maximum entry (len of choices), must be positive

            :returns: tuple of the form (`code`, `selection`) where
                `code` - str display exit code ('ok' or cancel')
                `selection` - int user's selection
            :rtype: tuple

            """
            selection = -1
            if max_ > 1:
                input_msg = ("Select the appropriate number "
                             "[1-{max_}] then [enter] (press 'c' to "
                             "cancel): ".format(max_=max_))
            else:
                input_msg = ("Press 1 [enter] to confirm the selection "
                             "(press 'c' to cancel): ")
            while selection < 1:
                ans = raw_input(input_msg)
                if ans.startswith("c") or ans.startswith("C"):
                    return CANCEL, -1
                try:
                    selection = int(ans)
                    if selection < 1 or selection > max_:
                        selection = -1
                        raise ValueError

                except ValueError:
                    self.outfile.write(
                        "{0}** Invalid input **{0}".format(os.linesep))

            return OK, selection

    @zope.interface.implementer(interfaces.IDisplay)
    class NoninteractiveDisplay(object):
        """An iDisplay implementation that never asks for interactive user input"""

        def __init__(self, outfile):
            super(NoninteractiveDisplay, self).__init__()
            self.outfile = outfile

        def _interaction_fail(self, message, cli_flag, extra=""):
            "Error out in case of an attempt to interact in noninteractive mode"
            msg = "Missing command line flag or config entry for this setting:\n"
            msg += message
            if extra:
                msg += "\n" + extra
            if cli_flag:
                msg += "\n\n(You can set this with the {0} flag)".format(cli_flag)
            raise errors.MissingCommandlineFlag(msg)

        def notification(self, message, height=10, pause=False):
            # pylint: disable=unused-argument
            """Displays a notification without waiting for user acceptance.

            :param str message: Message to display to stdout
            :param int height: No effect for NoninteractiveDisplay
            :param bool pause: The NoninteractiveDisplay waits for no keyboard

            """
            side_frame = "-" * 79
            message = _wrap_lines(message)
            self.outfile.write(
                "{line}{frame}{line}{msg}{line}{frame}{line}".format(
                    line=os.linesep, frame=side_frame, msg=message))

        def menu(self, message, choices, ok_label=None, cancel_label=None,
                 help_label=None, default=None, cli_flag=None):
            # pylint: disable=unused-argument,too-many-arguments
            """Avoid displaying a menu.

            :param str message: title of menu
            :param choices: Menu lines, len must be > 0
            :type choices: list of tuples (tag, item) or
                list of descriptions (tags will be enumerated)
            :param int default: the default choice
            :param dict kwargs: absorbs various irrelevant labelling arguments

            :returns: tuple of (`code`, `index`) where
                `code` - str display exit code
                `index` - int index of the user's selection
            :rtype: tuple
            :raises errors.MissingCommandlineFlag: if there was no default

            """
            if default is None:
                self._interaction_fail(message, cli_flag, "Choices: " + repr(choices))

            return OK, default

        def input(self, message, default=None, cli_flag=None):
            """Accept input from the user.

            :param str message: message to display to the user

            :returns: tuple of (`code`, `input`) where
                `code` - str display exit code
                `input` - str of the user's input
            :rtype: tuple
            :raises errors.MissingCommandlineFlag: if there was no default

            """
            if default is None:
                self._interaction_fail(message, cli_flag)
            else:
                return OK, default

        def yesno(self, message, yes_label=None, no_label=None, default=None, cli_flag=None):
            # pylint: disable=unused-argument
            """Decide Yes or No, without asking anybody

            :param str message: question for the user
            :param dict kwargs: absorbs yes_label, no_label

            :raises errors.MissingCommandlineFlag: if there was no default
            :returns: True for "Yes", False for "No"
            :rtype: bool

            """
            if default is None:
                self._interaction_fail(message, cli_flag)
            else:
                return default

        def checklist(self, message, tags, default=None, cli_flag=None, **kwargs):
            # pylint: disable=unused-argument
            """Display a checklist.

            :param str message: Message to display to user
            :param list tags: `str` tags to select, len(tags) > 0
            :param dict kwargs: absorbs default_status arg

            :returns: tuple of (`code`, `tags`) where
                `code` - str display exit code
                `tags` - list of selected tags
            :rtype: tuple

            """
            if default is None:
                self._interaction_fail(message, cli_flag, "? ".join(tags))
            else:
                return OK, default

    def separate_list_input(input_):
        """Separate a comma or space separated list.

        :param str input_: input from the user

        :returns: strings
        :rtype: list

        """
        no_commas = input_.replace(",", " ")
        # Each string is naturally unicode, this causes problems with M2Crypto SANs
        # TODO: check if above is still true when M2Crypto is gone ^
        return [str(string) for string in no_commas.split()]

    def _parens_around_char(label):
        """Place parens around first character of label.

        :param str label: Must contain at least one character

        """
        return "({first}){rest}".format(first=label[0], rest=label[1:])