#!/usr/bin/env python3

# pylint: disable=superfluous-parens,wrong-import-order
# superfluous-parens: parentheses are good for clarity and portability.
# wrong-import-order: we don't want the GTK+ version to depend on curses, or vice-versa.

"""Replay a tty record with forward and back motions, using curses or GTK."""

import os
import sys
import signal


def usage(retval):
    """Output a usage message."""
    if retval == 0:
        write = sys.stdout.write
    else:
        write = sys.stderr.write

    write('Usage: %s\n' % (sys.argv[0], ))
    write('\t--filename replay_filename\n')
    write('\t--help\n')
    write('\t--turn-off-bell\n')
    write('\t--add-carriage-returns\n')
    write('\t--guess-carriage-returns\n')
    write('\t--gtk\n')
    write('\t--curses\n')
    write('\n--gtk says to use a GTK+ GUI.  --curses is a tty thing.\n')
    write('\n--curses implies --add-carriage-returns\n')
    write('\nPlease note that --turn-off-bell can break some xterm escape sequences,\n')
    write('making some lines seem not to display properly.  The "set xterm title"\n')
    write('function is known to have this problem.  Others may as well.\n')

    sys.exit(retval)


REPLAY_FILENAME = './typescript'
BELL = True
ADD_CR = 0
USE_GTK = False
USE_CURSES = False

while sys.argv[1:]:
    if sys.argv[1] == '--filename':
        REPLAY_FILENAME = sys.argv[2]
        del sys.argv[1]
    elif sys.argv[1] == '--help':
        usage(0)
    elif sys.argv[1] == '--gtk':
        USE_GTK = True
    elif sys.argv[1] == '--curses':
        USE_CURSES = True
    elif sys.argv[1] == '--turn-off-bell':
        BELL = False
    elif sys.argv[1] == '--add-carriage-returns':
        ADD_CR = 1
    elif sys.argv[1] == '--guess-carriage-returns':
        ADD_CR = 2
    else:
        sys.stderr.write('%s: Illegal option: %s\n' % (sys.argv[0], sys.argv[1]))
        usage(1)
    del sys.argv[1]

if USE_GTK + USE_CURSES != 1:
    sys.stderr.write('%s: You must use exactly one of --gtk or --curses\n' % sys.argv[0])
    usage(1)

if USE_GTK:
    # We don't want to require Gtk unless we're actually going to use it
    import gi
    gi.require_version('Gtk', '3.0')
    from gi.repository import Gtk  # pylint: disable=wrong-import-position
elif USE_CURSES:
    # We don't want to require curses unless we're actually going to use it
    import curses  # pylint: disable=wrong-import-position
    ADD_CR = 1
else:
    sys.stderr.write('%s: Internal error: No display method selected\n' % sys.argv[0])


def ctrl(character):
    """For example, if passed b'a', return control-a."""
    ordinal = ord(character)
    if character >= b'a' and character <= b'z':
        return chr(ordinal - ord(b'a') + 1)
    elif character >= b'A' and character <= b'Z':
        return chr(ordinal - ord(b'A') + 1)
    else:
        return character


class LineClass(object):
    # pylint: disable=too-few-public-methods
    """Hold a line and its length."""

    def __init__(self, text):
        """Initialize."""
        self.text = text
        self.length = len(text)

    def __str__(self):
        """Return a string version of this object."""
        return 'length %d, ===> %s <===' % (self.length, self.text)


def bytes_readlines(fileno):
    """Read file descriptor fileno as a list of bytes lines."""
    list_ = []
    while True:
        block = os.read(fileno, 2 ** 10)
        if not block:
            break
        list_.append(block)
    whole_file_content = b''.join(list_)

    lines_sans_eols = whole_file_content.split(b'\n')

    if lines_sans_eols[-1] == b'':
        del lines_sans_eols[-1]

    lines = [line + b'\n' for line in lines_sans_eols]

    return lines


class CursorClass(object):
    """Enable moving around within the file."""

    # pylint: disable=too-few-public-methods,too-many-instance-attributes
    def __init__(self, replay_filename, bell, add_cr, replay_lines=300):
        """Initialize."""
        # IOError: [Errno 2] No such file or directory: 'input_fil'
        try:
            file_ = open(replay_filename, 'r')
        except IOError:
            sys.stderr.write('%s: IOError opening %s\n' % (sys.argv[0], replay_filename))
            sys.exit(1)
        self.bell = bell
        lines = [b'-=> START OF FILE <=-\n'] + bytes_readlines(file_.fileno()) + [b'-=> END OF FILE <=-\n']
        self.line = [LineClass(line) for line in lines]
        file_.close()
        self.num_lines = len(self.line)
        print("Got %d lines of input" % self.num_lines)
        # time.sleep(10)
        # there will always be at least two lines.  If this is an empty file,
        # they'll be numbered 0 and 1
        self.lineno = 0
        self.chrno = self.line[self.lineno].length
        if add_cr == 2:
            self.add_cr = 1
            for i in range(self.num_lines):
                if '\r' in self.line[i].text:
                    self.add_cr = 0
                    break
        else:
            self.add_cr = add_cr
        if USE_GTK:
            self.init_gtk()
        else:
            self.init_curses()
        self.show_status()
        self.clear()
        self.replay_lines = replay_lines
        self.replay()

    def init_gtk(self):
        """Set up GTK."""
        self.window = Gtk.Window(type=Gtk.WindowType.TOPLEVEL)
        self.window.set_title(sys.argv[0])
        self.window.connect("delete_event", self.delete_event)
        self.window.show()
        self.vbox = Gtk.VBox()
        self.vbox.show()
        self.window.add(self.vbox)
        self.quit_button = Gtk.Button(label='Quit')
        self.quit_button.show()
        self.quit_button.connect('clicked', self.delete_event)
        self.vbox.pack_start(self.quit_button, True, True, 0)
        self.table = Gtk.Table(n_rows=2, n_columns=2, homogeneous=False)
        self.table.show()
        self.vbox.pack_start(self.table, True, True, 0)
        self.back_chr_button = Gtk.Button(label='Back character')
        self.back_line_button = Gtk.Button(label='Back line')
        self.forward_chr_button = Gtk.Button(label='Forward character')
        self.forward_line_button = Gtk.Button(label='Forward line')
        self.back_chr_button.show()
        self.back_line_button.show()
        self.forward_chr_button.show()
        self.forward_line_button.show()
        self.table.attach(self.back_chr_button, 0, 1, 0, 1)
        self.table.attach(self.back_line_button, 1, 2, 0, 1)
        self.table.attach(self.forward_chr_button, 0, 1, 1, 2)
        self.table.attach(self.forward_line_button, 1, 2, 1, 2)
        self.back_chr_button.connect("clicked", self.back_chr)
        self.back_line_button.connect("clicked", self.back_line)
        self.forward_chr_button.connect("clicked", self.forward_chr)
        self.forward_line_button.connect("clicked", self.forward_line)
        self.beginning_button = Gtk.Button(label='Beginning')
        self.end_button = Gtk.Button(label='End')
        self.beginning_button.show()
        self.end_button.show()
        self.end_button.connect("clicked", self.end)
        self.beginning_button.connect("clicked", self.beginning)
        self.vbox.pack_start(self.end_button, True, True, 0)
        self.vbox.pack_start(self.beginning_button, True, True, 0)
        self.line_chr_label = Gtk.Label(label='foo!')
        self.line_chr_label.show()
        self.vbox.pack_start(self.line_chr_label, True, True, 0)

    def init_curses(self):
        """Set up curses."""
        self.screen = curses.initscr()
        curses.noecho()
        curses.cbreak()
        self.screen.keypad(1)
        self.help()

    def show_status(self):
        """Show where we are in the file."""
        if USE_GTK:
            self.line_chr_label.set_text('Line %d, character %d' % (self.lineno, self.chrno))

    @staticmethod
    def delete_event(*ignored_arguments):
        """Exit the program."""
        _ = ignored_arguments
        Gtk.main_quit()
        sys.exit(0)

    def clear(self):
        """Clear the screen."""
        if USE_CURSES:
            self.screen.clear()
            self.screen.refresh()
        else:
            # I'm sure there's a better way...
            os.system('tput clear')

    def putch(self, char):
        """Put a character."""
        if isinstance(char, int):
            bytes_char = bytes([char])
        else:
            bytes_char = char
        # bytes_char = bytes(text_char, 'UTF-8')
        if bytes_char == ctrl(b'g'):
            # beeps can be too annoying :)
            if self.bell:
                os.write(1, bytes_char)
        elif bytes_char == b'\n':
            if self.add_cr:
                os.write(1, b'\r')
            os.write(1, bytes_char)
        else:
            os.write(1, bytes_char)

    def puts(self, string):
        """Write a string."""
        for character in string:
            self.putch(character)

    def replay(self):
        """Re-output lines and characters."""
        self.clear()
        first_lineno = self.lineno - self.replay_lines
        if first_lineno < 0:
            first_lineno = 0
        last_lineno = self.lineno
        if last_lineno >= self.num_lines:
            last_lineno = self.num_lines - 1
        # put whole lines from a ways back.  If we go back far enough, there'll either
        # be a clear screen or enough lines to scroll away ancient history
        for temp_lineno in range(first_lineno, last_lineno):
            self.puts(self.line[temp_lineno].text)
        # put the fractional current line
        self.puts(self.line[self.lineno].text[:self.chrno+1])

    def beep(self):
        """Output a bell character."""
        self.puts(ctrl('g'))

    def forward_line(self, *dummy):
        """Move forward 1 line."""
        self.lineno += 1
        if self.lineno >= self.num_lines:
            self.lineno = self.num_lines - 1
        self.chrno = len(self.line[self.lineno].text)
        self.replay()
        self.show_status()

    def back_line(self, *dummy):
        """Move back a line."""
        self.lineno -= 1
        if self.lineno < 0:
            self.lineno = 0
        self.chrno = self.line[self.lineno].length
        self.replay()
        self.show_status()

    def forward_chr(self, *dummy):
        """Move forward 1 character."""
        old_chrno = self.chrno
        old_lineno = self.lineno
        self.chrno += 1
        if self.chrno >= self.line[self.lineno].length:
            self.lineno += 1
            self.chrno = 0
            if self.lineno >= self.num_lines:
                self.lineno = old_lineno
                self.chrno = old_chrno
        self.replay()
        self.show_status()

    def back_chr(self, *dummy):
        """Move back a character."""
        old_chrno = self.chrno
        old_lineno = self.lineno
        self.chrno -= 1
        if self.chrno < 0:
            self.lineno -= 1
            self.chrno = self.line[self.lineno].length - 1
            if self.lineno < 1:
                self.lineno = old_lineno
                self.chrno = old_chrno
        self.replay()
        self.show_status()

    def cleanup(self, *ignored_arguments):
        """Clean up and exit the program."""
        _ = ignored_arguments
        if USE_GTK:
            Gtk.main_quit()
        else:
            curses.nocbreak()
            self.screen.keypad(0)
            curses.echo()
            curses.endwin()
        sys.exit(0)

    def beginning(self, *ignored_arguments):
        """Move to the beginning of the file."""
        _ = ignored_arguments
        self.lineno = 0
        self.chrno = 0
        self.replay()
        self.show_status()

    def end(self, *ignored_arguments):
        """Move to the end of the file."""
        _ = ignored_arguments
        self.lineno = self.num_lines - 1
        self.chrno = self.line[self.lineno].length - 1
        self.replay()
        self.show_status()

    if USE_CURSES:
        def getch(self):
            """Use curses to get a single character."""
            return self.screen.getch()

        def help(self):
            """For curses mode, output a help message."""
            self.puts(b"Use ? for help\n")
            self.puts(b'\n')
            self.puts(b"Use j, control-n or down arrow to go down a line\n")
            self.puts(b"Use k, control-p or up arrow to go up a line\n")
            self.puts(b'\n')
            self.puts(b"Use l, control-f or right arrow to go forward one character\n")
            self.puts(b"Use h, control-f, left arrow or backspace to go back one character\n")
            self.puts(b'\n')
            self.puts(b"Use q or control-C (your interrupt character) to exit the program\n")
            self.puts(b'\n')
            self.puts(b"Hit any key to continue\n")
            _ = sys.stdin.read(1)


CURSOR = CursorClass(REPLAY_FILENAME, BELL, ADD_CR)

signal.signal(signal.SIGINT, CURSOR.cleanup)

if USE_GTK:
    Gtk.main()
else:
    while True:
        CHARACTER = CURSOR.getch()
        if CHARACTER in [ord(b'h'), ord(ctrl(b'b')), curses.KEY_LEFT, curses.KEY_BACKSPACE]:
            CURSOR.back_chr()
        elif CHARACTER in [ord(b'l'), ord(ctrl(b'f')), curses.KEY_RIGHT]:
            CURSOR.forward_chr()
        elif CHARACTER in [ord(b'k'), ord(ctrl(b'p')), curses.KEY_UP]:
            CURSOR.back_line()
        elif CHARACTER in [ord(b'j'), ord(ctrl(b'n')), ord(b'\n'), curses.KEY_DOWN]:
            CURSOR.forward_line()
        elif CHARACTER in [ord(b'0'), curses.KEY_HOME]:
            CURSOR.beginning()
        elif CHARACTER in [ord(b'9'), curses.KEY_END]:
            CURSOR.end()
        elif CHARACTER == ord(b'?'):
            CURSOR.clear()
            CURSOR.help()
            CURSOR.replay()
        elif CHARACTER == ord(b'q'):
            CURSOR.cleanup()
            sys.exit(0)
#        else:
#            CURSOR.beep()