import traceback
from sys import stderr

from attr import attrib, attrs
from twisted.internet.defer import inlineCallbacks
from twisted.internet.threads import blockingCallFromThread, deferToThread

from .errors import AlreadyInputNameplateError, KeyFormatError

try:
    import readline
except ImportError:
    readline = None

errf = None


# uncomment this to enable tab-completion debugging
# import os ; errf = open("err", "w") if os.path.exists("err") else None
def debug(*args, **kwargs):  # pragma: no cover
    if errf:
        print(*args, file=errf, **kwargs)
        errf.flush()


@attrs
class CodeInputter:
    _input_helper = attrib()
    _reactor = attrib()

    def __attrs_post_init__(self):
        self.used_completion = False
        self._matches = None
        # once we've claimed the nameplate, we can't go back
        self._committed_nameplate = None  # or string

    def bcft(self, f, *a, **kw):
        return blockingCallFromThread(self._reactor, f, *a, **kw)

    def completer(self, text, state):
        try:
            return self._wrapped_completer(text, state)
        except Exception as e:
            # completer exceptions are normally silently discarded, which
            # makes debugging challenging
            print(f"completer exception: {e}")
            traceback.print_exc()
            raise

    def _wrapped_completer(self, text, state):
        self.used_completion = True
        # if we get here, then readline must be active
        ct = readline.get_completion_type()
        if state == 0:
            debug("completer starting (%s) (state=0) (ct=%d)" % (text, ct))
            self._matches = self._commit_and_build_completions(text)
            debug(" matches:", " ".join([f"'{m}'" for m in self._matches]))
        else:
            debug(" s%d t'%s' ct=%d" % (state, text, ct))

        if state >= len(self._matches):
            debug("  returning None")
            return None
        debug(f"  returning '{self._matches[state]}'")
        return self._matches[state]

    def _commit_and_build_completions(self, text):
        ih = self._input_helper
        if "-" in text:
            got_nameplate = True
            nameplate, words = text.split("-", 1)
        else:
            got_nameplate = False
            nameplate = text  # partial

        # 'text' is one of these categories:
        #  "" or "12": complete on nameplates (all that match, maybe just one)

        #  "123-": if we haven't already committed to a nameplate, commit and
        #  wait for the wordlist. Then (either way) return the whole wordlist.

        #  "123-supp": if we haven't already committed to a nameplate, commit
        #  and wait for the wordlist. Then (either way) return all current
        #  matches.

        if self._committed_nameplate:
            if not got_nameplate or nameplate != self._committed_nameplate:
                # they deleted past the commitment point: we can't use
                # this. For now, bail, but in the future let's find a
                # gentler way to encourage them to not do that.
                raise AlreadyInputNameplateError(
                    "nameplate (%s-) already entered, cannot go back" %
                    self._committed_nameplate)
        if not got_nameplate:
            # we're completing on nameplates: "" or "12" or "123"
            self.bcft(ih.refresh_nameplates)  # results arrive later
            debug("  getting nameplates")
            completions = self.bcft(ih.get_nameplate_completions, nameplate)
        else:  # "123-" or "123-supp"
            # time to commit to this nameplate, if they haven't already
            if not self._committed_nameplate:
                debug(f"  choose_nameplate({nameplate})")
                self.bcft(ih.choose_nameplate, nameplate)
                self._committed_nameplate = nameplate

                # Now we want to wait for the wordlist to be available. If
                # the user just typed "12-supp TAB", we'll claim "12" but
                # will need a server roundtrip to discover that "supportive"
                # is the only match. If we don't block, we'd return an empty
                # wordlist to readline (which will beep and show no
                # completions). *Then* when the user hits TAB again a moment
                # later (after the wordlist has arrived, but the user hasn't
                # modified the input line since the previous empty response),
                # readline would show one match but not complete anything.

                # In general we want to avoid returning empty lists to
                # readline. If the user hits TAB when typing in the nameplate
                # (before the sender has established one, or before we're
                # heard about it from the server), it can't be helped. But
                # for the rest of the code, a simple wait-for-wordlist will
                # improve the user experience.
                self.bcft(ih.when_wordlist_is_available)  # blocks on CLAIM
            # and we're completing on words now
            debug(f"  getting words ({words})")
            completions = [
                nameplate + "-" + c
                for c in self.bcft(ih.get_word_completions, words)
            ]

        # rlcompleter wants full strings
        return sorted(completions)

    def finish(self, text):
        if "-" not in text:
            raise KeyFormatError("incomplete wormhole code")
        nameplate, words = text.split("-", 1)

        if self._committed_nameplate:
            if nameplate != self._committed_nameplate:
                # they deleted past the commitment point: we can't use
                # this. For now, bail, but in the future let's find a
                # gentler way to encourage them to not do that.
                raise AlreadyInputNameplateError(
                    "nameplate (%s-) already entered, cannot go back" %
                    self._committed_nameplate)
        else:
            debug(f"  choose_nameplate({nameplate})")
            self.bcft(self._input_helper.choose_nameplate, nameplate)
        debug(f"  choose_words({words})")
        self.bcft(self._input_helper.choose_words, words)


def _input_code_with_completion(prompt, input_helper, reactor):
    # reminder: this all occurs in a separate thread. All calls to input_helper
    # must go through blockingCallFromThread()
    c = CodeInputter(input_helper, reactor)
    if readline is not None:
        if readline.__doc__ and "libedit" in readline.__doc__:
            readline.parse_and_bind("bind ^I rl_complete")
        else:
            readline.parse_and_bind("tab: complete")
        readline.set_completer(c.completer)
        readline.set_completer_delims("")
        debug("==== readline-based completion is prepared")
    else:
        debug("==== unable to import readline, disabling completion")
    code = input(prompt)
    # Code is str(bytes) on py2, and str(unicode) on py3. We want unicode.
    if isinstance(code, bytes):
        code = code.decode("utf-8")
    c.finish(code)
    return c.used_completion


def warn_readline():
    # When our process receives a SIGINT, Twisted's SIGINT handler will
    # stop the reactor and wait for all threads to terminate before the
    # process exits. However, if we were waiting for
    # input_code_with_completion() when SIGINT happened, the readline
    # thread will be blocked waiting for something on stdin. Trick the
    # user into satisfying the blocking read so we can exit.
    print("\nCommand interrupted: please press Return to quit", file=stderr)

    # Other potential approaches to this problem:
    # * hard-terminate our process with os._exit(1), but make sure the
    #   tty gets reset to a normal mode ("cooked"?) first, so that the
    #   next shell command the user types is echoed correctly
    # * track down the thread (t.p.threadable.getThreadID from inside the
    #   thread), get a cffi binding to pthread_kill, deliver SIGINT to it
    # * allocate a pty pair (pty.openpty), replace sys.stdin with the
    #   slave, build a pty bridge that copies bytes (and other PTY
    #   things) from the real stdin to the master, then close the slave
    #   at shutdown, so readline sees EOF
    # * write tab-completion and basic editing (TTY raw mode,
    #   backspace-is-erase) without readline, probably with curses or
    #   twisted.conch.insults
    # * write a separate program to get codes (maybe just "wormhole
    #   --internal-get-code"), run it as a subprocess, let it inherit
    #   stdin/stdout, send it SIGINT when we receive SIGINT ourselves. It
    #   needs an RPC mechanism (over some extra file descriptors) to ask
    #   us to fetch the current nameplate_id list.
    #
    # Note that hard-terminating our process with os.kill(os.getpid(),
    # signal.SIGKILL), or SIGTERM, doesn't seem to work: the thread
    # doesn't see the signal, and we must still wait for stdin to make
    # readline finish.


@inlineCallbacks
def input_with_completion(prompt, input_helper, reactor):
    t = reactor.addSystemEventTrigger("before", "shutdown", warn_readline)
    # input_helper.refresh_nameplates()
    used_completion = yield deferToThread(_input_code_with_completion, prompt,
                                          input_helper, reactor)
    reactor.removeSystemEventTrigger(t)
    return used_completion
