"""Simple textpad editing widget with Emacs-like keybindings, using curses pad
objects. Adapted from the standard library module curses.textpad"""

import curses
import curses.ascii as ascii

def rectangle(win, uly, ulx, lry, lrx):
    """Draw a rectangle with corners at the provided upper-left
    and lower-right coordinates.
    """
    win.vline(uly+1, ulx, curses.ACS_VLINE, lry - uly - 1)
    win.hline(uly, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
    win.hline(lry, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
    win.vline(uly+1, lrx, curses.ACS_VLINE, lry - uly - 1)
    win.addch(uly, ulx, curses.ACS_ULCORNER)
    win.addch(uly, lrx, curses.ACS_URCORNER)
    win.addch(lry, lrx, curses.ACS_LRCORNER)
    win.addch(lry, ulx, curses.ACS_LLCORNER)

class TextboxPad:
    """Editing widget using the interior of a pad object.
     Supports the following Emacs-like key bindings:

    Ctrl-A      Go to left edge of window.
    Ctrl-B      Cursor left, wrapping to previous line if appropriate.
    Ctrl-D      Delete character under cursor.
    Ctrl-E      Go to right edge (stripspaces off) or end of line (stripspaces on).
    Ctrl-F      Cursor right, wrapping to next line when appropriate.
    Ctrl-G      Terminate, returning the window contents.
    Ctrl-H      Delete character backward.
    Ctrl-J      Terminate if the window is 1 line, otherwise insert newline.
    Ctrl-K      If line is blank, delete it, otherwise clear to end of line.
    Ctrl-L      Refresh screen.
    Ctrl-N      Cursor down; move down one line.
    Ctrl-O      Insert a blank line at cursor location.
    Ctrl-P      Cursor up; move up one line.

    Move operations do nothing if the cursor is at an edge where the movement
    is not possible.  The following synonyms are supported where possible:

    KEY_LEFT = Ctrl-B, KEY_RIGHT = Ctrl-F, KEY_UP = Ctrl-P, KEY_DOWN = Ctrl-N
    KEY_BACKSPACE = Ctrl-h
    """
    def __init__(self, pad, sminrow, smincol, smaxrow, smaxcol, history=None, getCompletions=None):
        self.pad = pad
        self.sminrow, self.smincol, self.smaxrow, self.smaxcol = (
                sminrow, smincol, smaxrow, smaxcol)
        self.sheight = smaxrow - sminrow
        self.swidth = smaxcol - smincol
        (self.maxy, self.maxx) = pad.getmaxyx()
        self.maxy = self.maxy - 1
        self.maxx = self.maxx - 1

        self.history, self.getCompletions = (history, getCompletions)
        self.historyPoint = 0
        self.currentCompletion = 0
        self.lastcmd = ''

        self.stripspaces = 1
        self.lastcmd = None
        pad.keypad(1)

    def _end_of_line(self, y):
        "Go to the location of the first blank on the given line."
        last = self.maxx
        while 1:
            if ascii.ascii(self.pad.inch(y, last)) != ascii.SP:
                last = min(self.maxx, last+1)
                break
            elif last == 0:
                break
            last = last - 1
        return last

    def do_command(self, ch):
        "Process a single editing command."
        (y, x) = self.pad.getyx()
        if ascii.isprint(ch):
            if y < self.maxy or x < self.maxx:
                # The try-catch ignores the error we trigger from some curses
                # versions by trying to write into the lowest-rightmost spot
                # in the window.
                try:
                    self.pad.insch(ch)
                    self.pad.move(y, x+1)
                except curses.error:
                    pass
        elif ch == ascii.SOH:                           # ^a
            self.pad.move(y, 0)
        elif ch in (ascii.STX,curses.KEY_LEFT, ascii.BS,curses.KEY_BACKSPACE):
            if x > 0:
                self.pad.move(y, x-1)
            elif y == 0:
                pass
            elif self.stripspaces:
                self.pad.move(y-1, self._end_of_line(y-1))
            else:
                self.pad.move(y-1, self.maxx)
            if ch in (ascii.BS, curses.KEY_BACKSPACE):
                self.pad.delch()
        elif ch == ascii.EOT:                           # ^d
            self.pad.delch()
        elif ch == ascii.ENQ:                           # ^e
            if self.stripspaces:
                self.pad.move(y, self._end_of_line(y))
            else:
                self.pad.move(y, self.maxx)
        elif ch in (ascii.ACK, curses.KEY_RIGHT):       # ^f
            if x < self.maxx:
                self.pad.move(y, x+1)
            elif y == self.maxy:
                pass
            else:
                self.pad.move(y+1, 0)
        elif ch == ascii.BEL:                           # ^g
            return 0
        elif (ch == ascii.TAB and                       # ^i
                self.getCompletions is not None):
            # completion, modelled after vim's: successive tabs cycle through
            # the possible completions.
            if self.lastcmd == ascii.TAB and self.completions:
                self.currentCompletion = ((self.currentCompletion + 1) %
                        len(self.completions))
            else:
                self.completions = self.getCompletions(
                        self.getCurrentContents())
                self.currentCompletion = 0
            if self.completions:
                self.pad.deleteln()
                self.pad.move(y,0)
                self.pad.addstr(self.completions[self.currentCompletion])
        elif ch == ascii.NL:                            # ^j
            if self.maxy == 0:
                return 0
            elif y < self.maxy:
                self.pad.move(y+1, 0)
        elif ch == ascii.VT:                            # ^k
            if x == 0 and self._end_of_line(y) == 0:
                self.pad.deleteln()
            else:
                # first undo the effect of self._end_of_line
                self.pad.move(y, x)
                self.pad.clrtoeol()
        elif ch == ascii.NAK:                           # ^u
            self.pad.move(y,0)
            for i in range(x):
                self.pad.delch()
        elif ch == ascii.FF:                            # ^l
            self.refresh()
        elif ch in (ascii.SO, curses.KEY_DOWN):         # ^n
            if self.history and self.maxy == 0:
                if self.historyPoint > 0:
                    self.historyPoint -= 1
                    self.pad.deleteln()
                    if self.historyPoint == 0:
                        line = self.postHistory
                    else:
                        line = self.history[-self.historyPoint]
                    self.pad.move(y,0)
                    self.pad.addstr(line)
            elif y < self.maxy:
                self.pad.move(y+1, x)
                if x > self._end_of_line(y+1):
                    self.pad.move(y+1, self._end_of_line(y+1))
        elif ch == ascii.SI:                            # ^o
            self.pad.insertln()
        elif ch in (ascii.DLE, curses.KEY_UP):          # ^p
            if self.history and self.maxy == 0:
                if self.historyPoint < len(self.history):
                    if self.historyPoint == 0:
                        self.pad.move(y,0)
                        self.postHistory = self.getCurrentContents()
                    self.historyPoint += 1
                    self.pad.deleteln()
                    self.pad.move(y,0)
                    self.pad.addstr(self.history[-self.historyPoint])
            elif y > 0:
                self.pad.move(y-1, x)
                if x > self._end_of_line(y-1):
                    self.pad.move(y-1, self._end_of_line(y-1))
        self.lastcmd = ch
        return 1

    def getCurrentContents(self):
        (y, x) = self.pad.getyx()
        result = ''
        for xt in range(self._end_of_line(y)):
            result += \
                    chr(ascii.ascii(self.pad.inch(y, xt)))
        self.pad.move(y, x)
        return result

    def gather(self):
        "Collect and return the contents of the window."
        result = ""
        for y in range(self.maxy+1):
            self.pad.move(y, 0)
            stop = self._end_of_line(y)
            if stop == 0 and self.stripspaces:
                continue
            for x in range(self.maxx+1):
                if self.stripspaces and x > stop:
                    break
                result = result + chr(ascii.ascii(self.pad.inch(y, x)))
            if self.maxy > 0:
                result = result + "\n"
        return result

    def refresh(self):
        (y, x) = self.pad.getyx()
        self.pad.refresh(
                max(0, y - self.sheight),
                max(0, x - self.swidth),
                self.sminrow, self.smincol, self.smaxrow, self.smaxcol)

    def edit(self, validate=None):
        "Edit in the widget window and collect the results."
        self.refresh()
        while 1:
            ch = self.pad.getch()
            if validate:
                ch = validate(ch)
            if not ch:
                continue
            if not self.do_command(ch):
                break
            self.refresh()
        return self.gather()

if __name__ == '__main__':
    def test_editbox(stdscr):
        ncols, nlines = 9, 4
        uly, ulx = 15, 20
        stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.")
        pad = curses.newpad(nlines*3, ncols*2)
        rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols)
        stdscr.refresh()
        return TextboxPad(pad, uly, ulx, uly+nlines-1, ulx+ncols-1).edit()

    str = curses.wrapper(test_editbox)
    print('Contents of text box:', repr(str))
