#!/usr/bin/python

# Copyright (C) Mark Eichin.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the 3-clause BSD terms are met.
# (License subject to change.)

"""Hiveminder Braindump entry tool.

Takes one item on the command line, or "-" to take a list as input.
"""

__revision__ = "$Id: hive-braindump.py,v 1.13 2008/02/21 06:59:50 eichin Exp $"
__version__ = "%s/%s" % (__revision__.split()[3], __revision__.split()[2])


import os
import urllib
import optparse
import sys
import subprocess
import time
import string

try:
    import cElementTree as etree
except ImportError:
    try:
        import elementtree.ElementTree as etree
    except ImportError:
        import xml.etree.ElementTree as etree

# In the interests of keeping hive-braindump.py self-contained, for the moment
#  I'm including encode_locator and decode_locator inline, as lifted from todo.py.
#  Given that the protocol now passes them as decimal numbers, though, it may
#  be possible to drop them again...

# Roughly this translates numbers into base 32...
# would be base 36 except that 0->O, 1->I, S->F, B->P
# to do this with existing encoders, we need to pad rather than replace

rep_digits = string.digits + string.uppercase
compressed_digits = rep_digits.translate(string.maketrans("",""), "01SB") + "____"
map_digits = rep_digits.translate(string.maketrans("01SB", "OIFP"))
loc_to_rep = string.maketrans(compressed_digits, rep_digits)
rep_to_loc = string.maketrans(rep_digits, compressed_digits)
rep_to_map = string.maketrans(rep_digits, map_digits)

def encode_locator(number):
    """Number::RecordLocator encoder"""
    # this *should* be as simple as:
    # return something(locator, 32).translate(rep_to_loc)
    # but while was proposed for 2.5 it doesn't seem to have happened...
    res = []
    while number:
        number, digit = divmod(number, 32)
        res.append(digit)
    return "".join(compressed_digits[digit] for digit in reversed(res))
        

def decode_locator(locator):
    """Number::RecordLocator decoder"""
    # normalize to upper case
    # then translate to the digit subset
    # then compact into 32-contiguous values for int
    return int(locator.upper().translate(rep_to_map).translate(loc_to_rep), 32)

# tests, from Number-RecordLocator-0.001/t/00.load.t
assert encode_locator(1) == "3", "We skip one and zero so should end up with 3 when encoding 1"
assert encode_locator(12354) == 'F44'
assert encode_locator(123456) == '5RL2'
assert decode_locator('5RL2') == 123456
assert decode_locator(encode_locator(123456)) == 123456
assert decode_locator('1234') == decode_locator('I234')
assert decode_locator('10SB') == decode_locator('IOFP')

def my_sid(known_sid=[]):
    """fetch my sid from ~/.hiveminder"""

    if not known_sid:
        hive_file = os.path.expanduser("~/.hiveminder")
        if file(hive_file).readline().startswith("---"):
            for line in file(hive_file):
                if line.startswith("sid:"):
                    known_sid.append(line.strip().split(":", 1)[-1].strip())
                    break
        elif file(hive_file).readline().startswith("{"):
            for line in "".join(file(hive_file).readlines()).split(","):
                line = line.strip()
                if line.startswith("sid:"):
                    known_sid.append(line.strip().split(":", 1)[-1].strip())
                    break
        else:
            raise Exception("no sid found!")
        if not known_sid: # still!
            raise Exception("no sid parser worked!")
    return known_sid[0]

def braindump(items):
    """do a braindump of these items"""

    base_url = "http://hiveminder.com/__jifty/webservices/xml"
    
    uo = urllib.FancyURLopener()
    uo.addheader("Cookie", "JIFTY_SID_HIVEMINDER=%s" % my_sid())
    
    # "J:A:F-text-quickcreate": "do this now %s [deleteme]" % time.ctime(),

    u = uo.open(base_url, data=urllib.urlencode({
        "J:A-quickcreate": "BTDT::Action::ParseTasksMagically",
        "J:A:F-text-quickcreate": "\r\n".join(items)
        }))
    xmlresponse = u.read()
    try:
        response = etree.fromstring(xmlresponse)
    except Exception, xerr:     # xml.parsers.expat.ExpatError
        print "got", xerr, "parsing", xmlresponse
        raise
    success = response.find("result/success").text
    if success and int(success):
        message = response.find("result/message").text
        ids = map(lambda ids: "#"+encode_locator(int(ids.text)), response.findall("result/content/ids"))
        if ids:
            return message + ": " + ", ".join(ids)
        else:
            return message
    else:
        raise Exception("braindump failed, %s" % response.find("result/message").text)

# TODO: figure out a general abstraction for this kind of prompt/queue
def we_have_net():
    """Do we (probably) have net?"""
    # *also* need reachability...
    return "default" in subprocess.Popen(["netstat", "-r"],
                                         stdout=subprocess.PIPE).communicate()[0]

# DiskQueue (or PersistQueue) should be a common package
# (I implemented one in pagefeeder too)
queue_dir = os.path.expanduser("~/.hiveminder_outbound")
if not os.path.isdir(queue_dir):
    os.mkdir(queue_dir)
def enqueue(item):
    """stuff an item on the queue"""
    while True:
        qname = os.path.join(queue_dir, "h%s" % time.time())
        if not os.path.exists(qname):
            break
        # This limits us to a maximum queuing rate of 100/sec. Boo Hoo.

    qfile = file(qname, "w")
    for line in item:
        print >> qfile, line
    qfile.close()

def queue_empty():
    return 0 == len(list(qitem for qitem in os.listdir(queue_dir) if qitem.startswith("h")))

def dequeue():
    """return one item (and cleanup handle) from the queue"""
    first_file = sorted((os.path.join(queue_dir, qitem)
                         for qitem in os.listdir(queue_dir)
                         if qitem.startswith("h")),
                        key=lambda p: float(os.path.basename(p).replace("h","")))[0]
    
    def cleanup(path=first_file):
        os.remove(path)

    return first_file, cleanup

def test_queue():
    if not queue_empty():
        raise Exception("can't test with a live queue!")
    enqueue(["a", "fred"])
    path, cleaner = dequeue()
    assert map(string.rstrip, file(path).readlines()) == ["a", "fred"]
    cleaner()

    enqueue(["b", "fred"])
    enqueue(["c", "fred"])
    # raw_input("foo")
    path, cleaner = dequeue()
    assert map(string.rstrip, file(path).readlines()) == ["b", "fred"]
    cleaner()
    path, cleaner = dequeue()
    assert map(string.rstrip, file(path).readlines()) == ["c", "fred"]
    cleaner()

def paint_tags(lines, tags, groups):
    """paint tags on lines in braindump syntax"""
    tags = (tags or []) + map(lambda g: "group:" + g, groups or [])
    if not tags:
        return lines
    # tagstring = "[" + " ".join(tags) + "] "
    # allow more advanced strings in tags...
    tagstring = " ".join("[%s]" % tag for tag in tags)
    return [tagstring + " " + line for line in lines]

def try_to_braindump(lines, tags, groups, force_queue=False):
    """try to braindump the whole queue"""
    if lines:
        enqueue(paint_tags(lines, tags, groups))
    results = []
    if we_have_net() and not force_queue:
        while not queue_empty():
            path, cleaner = dequeue()
            lines = map(string.rstrip,
                        file(path).readlines())
            try:
                result = braindump(lines)
                results.append(result)
                cleaner()
            except Exception, exc:
                print "braindump failed", exc, "queuing"
                break
            
    else:
        if lines:
            # a lie, we already did, but it fits the user model better
            print "queuing"
    return results

if __name__ == "__main__":
    parser = optparse.OptionParser(usage=__doc__, version="%%prog %s" % __version__)
    parser.allow_interspersed_args = False # works better for commandline items
    # parser.add_option("--emacs")
    parser.add_option("--tag", action="append", help="add this tag (for easier aliases)")
    parser.add_option("--group", action="append", help="add this group (for easier aliases)")
    parser.add_option("--test-queue", action="store_true")
    parser.add_option("--force-queue", action="store_true")
    parser.add_option("--flush-queue", action="store_true")
    options, args = parser.parse_args()
    
    if options.test_queue:
        sys.exit(test_queue())

    if options.force_queue and options.flush_queue:
        sys.exit("don't try to force and flush at the same time...")

    if args == ["-"]:
        if os.isatty(1):
            print "Enter content, c-d or a blank line to finish"
        lines = []
        while True:
            try:
                line = raw_input()
            except EOFError:
                break
            if not line:
                break
            lines.append(line)
        if os.isatty(1):
            print "posting...",
            sys.stdout.flush()
        results = try_to_braindump(lines, tags=options.tag, groups=options.group, force_queue=options.force_queue)
    elif args:
        results = try_to_braindump([" ".join(args)], tags=options.tag, groups=options.group, force_queue=options.force_queue)
    elif options.flush_queue:
        if options.tag:
            print "warning, ignoring tag, flushing everything"
        results = try_to_braindump([], [], [], force_queue=options.force_queue)
    else:
        parser.print_help()
        sys.exit(1)
    for result in results[:-1]:
        print "[from queue]", result
    for result in results[-1:]:
        print result
