#!/usr/local/bin/python2.2


__version__ = "0.6f"
"""
PyBook v%s - python Project Gutenberg e-book reader.
Copyright (C) 2001 Andrei Kulakov <ak@silmarill.org> GPL (see LICENSE)

Usage:
    pybook.py [argument]

Arguments:
    -h              this help message
    -v              version info
""" % __version__
    
import os, os.path, sys, re, cPickle, glob, random, time, commands, ftplib

# Settings:
viewer = "gvim"
prompt = "PyBook> "
# size of 'last read books' list
last_size = 20
# END of Settings.

progdir = os.path.expanduser("~/.pybook")
booklist = "/usr/local/share/pybook/booklist"
gutindex = os.path.join(progdir, "GUTINDEX.ALL")
datafile = os.path.join(progdir, "data")
local_books_path = os.path.join(progdir, "books")
vimrc = "/usr/local/share/pybook/vimrc"
ftp_site = "ibiblio.org"
ftp_path = "/pub/docs/books/gutenberg"
debugging = 0

class Book:
    def __init__(self):
        self.title = ""
        self.author = ""
        self.filename = ""
        self.year = ""

class Books:
    """Library of books."""
    def __init__(self):
        self.list = []
        self.last = []
        self.local = []

    def browse(self):
        """Browse library of books."""
        lines = 23
        n = 0
        while 1:
            for book in self.list[n:n+lines]:
                if book.author: display = "%s - %s" % (book.author, book.title)
                else: display = book.title
                msg = "%d) %s" % (n+1, display)
                print msg
                n = n+1
            while 1:
                answer = raw_input("browse>>> ")
                if not answer: break
                if answer in ("h","help"):
                    print "Hit q to stop browsing, number to read a book, Enter for next page"
                elif answer in ("q","quit"):
                    return
                else:
                    try: 
                        num = int(answer)
                        read_book(self.books.list[num-1])
                    except ValueError:
                        print "Unknown command:", answer

    def random(self):
        """Read random book."""
        self.confirm(random.choice(self.list))

    def read_last(self):
        """Read last read book."""
        if self.last:
            read_book(self.last[-1])
        else:
            print "No last book."

    def list_local(self):
        """List local books (on hard drive)."""
        if self.local:
            self.choose(self.local)
        else:
            print "There are no local books."

    def list_last(self):
        """List last read books."""
        if self.last:
            self.choose(self.last)
        else:
            print "No last read books."

    def display(self, lst):
        """Display a list of books."""
        n = 1
        for book in lst:
            if book.author: display = "%s - %s" % (book.author, book.title)
            else: display = book.title
            print "%d) %s" % (n, display)
            n = n+1

    def choose(self, lst):
        """Menu for choosing a book out of a list of books"""
        lb = 0 # choosing from local books list
        if lst == self.local: lb = 1
        self.display(lst)
        while 1:
            if lb: 
                print "\nd<number> deletes book from local cache"
            try: 
                line = raw_input("Enter book number to read: ")
            except EOFError: 
                return
            if not line: 
                return
            if line[0] == "d":
                if len(line) > 1:
                    try: 
                        num = int(line[1:]) - 1
                        book = self.local[num]
                        del self.local[num]
                        l = glob.glob(os.path.join(local_books_path, book.year[2:], book.filename[:-7] + "*"))
                        if l: os.remove(l[0])
                        print "Deleted: ", book.title
                        self.display(self.local)
                    except (IndexError, ValueError):
                        print "Invalid input, try again\n", sys.exc_info()
            else:
                try: 
                    read_book(lst[int(line)-1])
                    return
                except (IndexError, ValueError):
                    print "Invalid input, try again\n", sys.exc_info()

    def find(self):
        """Find a book."""
        line = raw_input("search for: ")
        res = self.search(line)
        self.handle_results(res)

    def author(self):
        """Find a book by author."""
        line = raw_input("search for author: ")
        res = self.search(line, "author")
        self.handle_results(res)

    def title(self):
        """Find a book by title."""
        line = raw_input("search for title: ")
        res = self.search(line, "title")
        self.handle_results(res)

    def handle_results(self, results):
        """Process found books."""
        if not results: 
            print "Not found"
        elif len(results) == 1: 
            self.confirm(results[0])
        else: 
            self.choose(results)

    def search(self, pattern, by=None):
        """Find book matching given pattern, by "author" or "title";
        return list of results."""
        results = []
        pattern = pattern.lower().split()
        if by:
            for book in books.list:
                if re.search(pattern[0], re.escape(eval("book."+by)).lower()):
                    results.append(book)
                    if len(pattern) > 1:
                        for word in pattern[1:]:
                            if not re.search(word, re.escape(eval("book."+by)).lower()):
                                results.remove(book)
                                break
        else:
            for book in books.list:
                t = re.escape(book.title).lower()
                a = re.escape(book.author).lower()
                if re.search(pattern[0], t) or re.search(pattern[0], a):
                    results.append(book)
                    if len(pattern) > 1:
                        for word in pattern[1:]:
                            if not (re.search(word, t) or re.search(word, a)):
                                results.remove(book)
                                break
        return results

    def confirm(self, book):
        """Confirm that user wants to read given book."""
        if book.author: 
            display = "%s - %s" % (book.author, book.title)
        else: 
            display = book.title
        print display
        answer = raw_input("Read it? [Y/n] ")
        if not answer or answer in "Yy":
            read_book(book)

def setup_vim():
    """Set vim up for reading e-books."""
    if not commands.getoutput("which " + viewer):
        print "Viewer '%s' wasn't found in path" % viewer
        print "You can get vim at http://www.vim.org or apt-get install vim in Debian"
        print "Vim is the recommended viewer for pybook because it can be easily set up to auto-bookmark files."
        print "If you wish to use a different viewer, edit pybook.py and change 'viewer' variable at the top of the file to your preferred viewer"
        sys.exit()

def debug(line):
    """Pring info if debugging is on."""
    if debugging:
        print line

def parse_gutindex(file):
    """Parse index of all gutenberg texts."""
    books = []
    months = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split()
    try:
        f = open(file)
    except IOError:
        print "GUTINDEX file not found: %s; Exiting.." % file
        sys.exit()
    while 1:
        l = f.readline()
        if l.startswith("Mon Year Title and Author"):
            f.readline()
            break
    lines = []
    while 1:
        l = f.readline()
        # that's the last entry
        if l.startswith("Dec 1971"):
            lines.append(l)
            break
        for month in months:
            if l.startswith(month + " 1") or l.startswith(month + " 2"):
                lines.append(l)
                continue
    ErrorParsingGutindex = "There was an error while parsing gutindex file."
    # Ugly: GUTINDEX format is inconsistent.
    for l in lines:
        author = ""
        title = ""
        year = l.split()[1]
        fields = l.split("[")
        try: 
            title_author = fields[0].split()[2:]
        except IndexError: 
            ln = l.split()
            ln = ln[3:]
            ln = " ".join(ln)
            fields = ln.split("[")
            try: title_author = fields[0].split()[2:]
            except IndexError: pass
        title_author = " ".join(title_author)
        if len(fields) == 2:
            filename = fields[1].split("]")[0]
        elif len(fields) == 3:
            filename = fields[2].split("]")[0]
        elif len(fields) == 4:
            filename = fields[3].split("]")[0]
        if re.search("by", title_author):
            ta = title_author.split("by")
            title = ta[0][:-1]
            author = ta[1]
        elif re.search("of", title_author):
            if len(title_author.split("of")) == 2:
                title, author = title_author.split("of")
            else:
                title = title_author
        elif re.search("\,", title_author):
            if len(title_author.split(",")) == 2:
                title, author = title_author.split(",")
            else:
                title = title_author
        else:
            title = title_author
        book = Book()
        book.title = title
        book.author = author
        book.year = year
        book.filename = filename.strip()
        books.append(book)
    return books

def help():
    print """
        PyBook help
    f, find, search         find books by author or title
    r, read                 read last book
    l, last                 list last read books
    q, quit                 quit

    t, title                find books by title
    a, author               find books by author
    b, browse               browse booklist
    L, local                list locally cached (downloaded) books
    R, random               read a random book
    
        Vim help
    Space                   page down
    Ctrl-B                  page up
    Ctrl-G                  print position in file
    /                       search
    ZZ                      exit
    :help                   help
    ,k                      look up word under cursor (move cursor using keys
                            h,j,k and l)"""

def fetch(book):
    """Download book from ftp site."""
    print "fetching " + book.title
    ftp = ftplib.FTP(ftp_site)
    ftp.login()
    ftp.cwd(ftp_path)
    ftp.cwd("etext" + book.year[2:])
    lst = []
    ftp.retrlines("LIST", lst.append)
    lst = [l.split()[-1] for l in lst]      # get filenames.
    for line in lst:
        # book.filename is of form namexxx, real filename is name[revision num].
        if line.startswith(book.filename[:-7]):
            fname = os.path.join(local_books_path, book.year[2:], line)
            if line[-4:] == ".txt":
                print "Getting %s..." % line,
                f = open(fname, "w")
                #def Write(line, f=f):
                #    f.write(line + "\n")
                #try: 
                #    ftp.retrlines("RETR " + line, Write)
                #except TypeError: 
                #    pass
                ftp.retrbinary("RETR " + line, f.write)
            elif line[-4:] == ".zip":
                print "Getting %s..." % line,
                f = open(fname, "wb")
                ftp.retrbinary("RETR " + line, f.write)
                os.system("unzip %s -d %s" % (fname, os.path.split(fname)[0]))
                os.remove(fname)
            else:
                print "Error - file is neither .txt nor .zip!"
                return
            f.close()
            books.local.append(book)
            print "done"
            return

def read_book(book):
    """Open given book in a viewer"""
    arg = ""
    # see if exists locally
    path = os.path.join(local_books_path, book.year[2:])
    if not os.path.exists(path):
        os.mkdir(path)
    glob_pat = os.path.join(path, book.filename[:-7].strip() + "*")
    lst = glob.glob(glob_pat)
    if not lst:
        fetch(book)
        # to scroll past gutenberg licence stuff.. and -R is read-only viewing
        if viewer.endswith("vim"): arg = "+/\*END\*"
        lst = glob.glob(glob_pat)
    if not lst:
        print "Error!"
        print "Book was not found.."
        return
    filename = lst[0]
    if books.last and book != books.last[-1]:
        try:
            print "Compressing last book..",
            os.system("gzip " + glob.glob(os.path.join(
                local_books_path, books.last[-1].year[2:], 
                books.last[-1].filename[:-7] + "*"))[0])
            print "done"
        except IndexError: 
            pass
    if filename[-2:] == "gz":
        print "Uncompressing new book..",
        os.system("gunzip " + filename)
        filename = filename[:-3]
        print "done"
    try: 
        if viewer.endswith("vim"): arg = " -N -u %s %s " % (vimrc, arg)
        os.system(viewer + arg + filename)
        print
    except IndexError: 
        print "index error", lst
        sys.exit()
    if not books.last:
        books.last.append(book)
    else:
        if book != books.last[-1]:
            books.last.append(book)
            if len(books.last) > last_size:
                del books.last[0]

def load_data(file):
    """Load from data file: books.list, books.local, books.last"""
    try:
        f = open(file)
        data = cPickle.load(f)
    except IOError:
        data = [[],[]]
    f = open(booklist)
    bl = cPickle.load(f)
    f.close()
    return bl, data[0], data[1]

def write_data(file):
    """Write data file: books.local, books.last"""
    f = open(file, "w")
    cPickle.dump((books.local, books.last), f, 1)
    f.close()

def process_time(secs):
    secs = int(secs)
    hours = secs / (60*60)
    secs = secs - hours*60*60
    minutes = secs / 60
    return "%01dh:%02dm" % (hours, minutes)

#------------------------------------------------------------------------------

def init():
    print "\tPyBook v" + __version__
    if not os.path.exists(progdir):
        os.mkdir(progdir)
    if not os.path.exists(local_books_path):
        os.mkdir(local_books_path)
    if not os.path.exists(booklist):
        print "Can't find booklist file - did you run setup.py as user (important!)?"
        print "I'll try parsing GUTINDEX instead.."
        data = (parse_gutindex(gutindex), [], [])
    if not os.path.exists(vimrc):
        print "Error: pybook vimrc not found at this location:", vimrc
    data = load_data(datafile)
    books.list, books.local, books.last = data

def quit():
    write_data(datafile)
    print process_time(time.time() - start)
    sys.exit()

def main():
    while 1:
        try: line = raw_input(prompt)
        except EOFError: quit()
        if not line: pass
        elif line in key_map.keys():
            key_map[line]()
        else:
            print "Unknown command."


books = Books()
key_map = {
    "h"         :   help,
    "help"      :   help,
    "f"         :   books.find,
    "find"      :   books.find,
    "search"    :   books.find,
    "a"         :   books.author,
    "author"    :   books.author,
    "t"         :   books.title,
    "title"     :   books.title,
    "b"         :   books.browse,
    "browse"    :   books.browse,
    "L"         :   books.list_local,
    "local"     :   books.list_local,
    "l"         :   books.list_last,
    "last"      :   books.list_last,
    "R"         :   books.random,
    "random"    :   books.random,
    "r"         :   books.read_last,
    "read"      :   books.read_last,
    "q"         :   quit,
    "quit"      :   quit,
}

if __name__ == "__main__":
    setup_vim()
    init()
    print "Number of books in booklist:", len(books.list)
    start = time.time()
    main()
