#!/usr/bin/python
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from urllib import quote, unquote_plus
import sqlite3, os, sys, re, time

PORT = 8000
DBNAME = 'default.db'


# connect to DB, init if necessary
def loadDB():
    global DB
    print >> sys.stderr, 'Loading DB:', DBNAME
    needsInit = not os.path.exists(DBNAME)
    DB = sqlite3.connect(DBNAME)
    if needsInit:
        print >> sys.stderr, 'Initializing DB:', DBNAME
        cur = DB.cursor()
        cur.execute('create table node (title varchar(255) unique, body varchar(65535), time integer)')
        cur.execute('create table softlink (title1 varchar(255), title2 varchar(255), rank integer)')
        cur.execute('create index idx_softlink on softlink (title1, title2)')
        cur.close()
        postNode('home', 0, DEFAULT_HOME_NODE)


DEFAULT_HOME_NODE = r'''
<html>
Welcome to your Personal Everything!
<noscript><br><br><span style="color: red">Javascript is off! You'll need it to edit nodes.</span></noscript>
<p>
Paragraphs/sections are individually editable and are separated by <p> on a line by itself. Once they are saved, you won't be able to see the <p> any more, but it's there. <b>To create new paragraphs</b>, just add one or more <p>s where desired, followed by text. When you save the paragraph, you will see the new paragraphs separately. <b>To delete a paragraph</b>, just remove all of its text and save it. Empty paragraphs are automatically removed.
<p>
Use brackets to create [links] (you enter: \[links]) and [this is a pipelink|pipelinks] (you enter: \[this is a pipelink|pipelinks]). To enter actual brackets, you can either use the HTML entities &amp;#91; and &amp;#93; or put a \ before any left brackets: \\[like this]

Links to non-existent nodes will be displayed in [There is probably no node with this title.|red]. Links beginning with http:// will be identified as external links, like so: [http://www.google.com]. (The \[^] marks them as being external, even when [http://www.google.com|pipelinked].)
<p>
Allowed HTML tags: b, i, u, s, sup, sub, big, small, tt.

For more HTML, put <html> at the very beginning of a paragraph. This will make the whole paragraph raw HTML (and hence disable linking and automatic line-breaking). Be careful with this, as it is probably possible to screw up a node accidentally with bad HTML.
'''


def htmlSafe(s):
    return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')
def unhtmlSafe(s):
    return s.replace('&gt;','>').replace('&lt;','<').replace('&amp;','&')


def decodeQuery(s):
    d = {}
    for kv in s.split('&'):
        if '=' not in kv: continue
        k, v = kv.split('=', 1)
        d[k] = unquote_plus(v)
    return d


def linker(m, softlink = None):
    style = ''
    if type(m) == str or type(m) == unicode: dest = text = str(m)
    else: dest = text = m.group(1)
    if '|' in text: dest, text = text.split('|', 1)
    if dest[:7].lower() == 'http://': link = dest; text += '[^]'
    else:
        link = '/node/' + dest
        if softlink and dest != softlink:
            link += '?softlink=' + quote(softlink)
        cur = DB.cursor()
        cur.execute('select time from node where title=?', (dest,))
        if not cur.fetchone(): style = 'style="color: red" '
        cur.close()
    return '<a %stitle="%s" href="%s">%s</a>' % (style, dest, link, text)


RE_LINKS = re.compile(r'(?<!\\)\[(.{,512}?)\]')
RE_ALLOWED_TAGS = re.compile(r'&lt;([bius]|sup|sub|big|small|tt)&gt;(.*?)&lt;/\1&gt;', re.I | re.DOTALL)
RE_ALLOWED_TAGS_FUNC = lambda m: '<%s>%s</%s>' % (m.group(1), m.group(2), m.group(1))
RE_ALLOWED_ENTITIES = re.compile(r'&amp;(#\d+|[a-zA-Z0-9]+);')

def getNode(title):
    body = getNodeBody(title)
    exists = (body != None)
    if not exists: body = []
    display = '''\
<style>
form { margin-bottom: 0px; }
.linktable { width: 100%; border: 1px solid #ddddff; border-spacing: 0; }
.linktable td { text-align: center; padding: 5px; background-color: #fafaff; border: 1px solid #eeeeff; }
</style>
<script>
var editingPara = -1;
var editTemp;
function mouseOver(eid) {
    var el = document.getElementById('para' + eid);
    el.style.backgroundColor = 'lightblue';
}
function mouseOut(eid) {
    var el = document.getElementById('para' + eid);
    el.style.backgroundColor = 'transparent';
}
function mouseClick(eid) {
    if (editingPara != -1) return;
    editingPara = eid;
    var el = document.getElementById('para' + eid);
    var cel = document.getElementById('editpara' + eid);
    editTemp = el.innerHTML;
    el.innerHTML = '<form method="POST"><input type="hidden" name="para" value="' + eid + '"><textarea name="text" style="width: 100%; height: 200px">' + cel.innerHTML + '</textarea><br><table style="width: 100%"><tr><td align=left><input name="save" type="submit" value="save"></td><td align=right><input onclick="cancelEdit(' + eid + ')" type="button" value="cancel"></td></tr></table></form>';
}
function cancelEdit(eid) {
    if (editingPara == -1) return;
    var el = document.getElementById('para' + eid);
    var cel = document.getElementById('editpara' + eid);
    el.innerHTML = editTemp;
    el.style.backgroundColor = 'transparent';
    editingPara = -1;
}
</script>'''
    pi = 0
    for p in body:
        clean = p
        # html mode
        if p[:12] == '&lt;html&gt;':
            p = unhtmlSafe(p[12:])
        else:
            # links and brackets
            p = RE_LINKS.sub(lambda x: linker(x, title), p)
            p = p.replace(r'\[', '[')
            # html tags
            oldp = None
            while oldp != p:
                oldp = p
                p = RE_ALLOWED_TAGS.sub(RE_ALLOWED_TAGS_FUNC, p)
            # html entities
            p = RE_ALLOWED_ENTITIES.sub(lambda m: '&%s;' % m.group(1), p)
            # automatic linebreaks
            p = re.sub(r'\r?\n', '<br>', p)
        # paragraph magic
        display += ('<div id="para%i" style="margin: 4px; padding: 4px' + ('; padding-top: 8px' if pi else '') + '"><span style="float: right" onmouseover="mouseOver(%i)" onmouseout="mouseOut(%i)"><input type="button" value="edit" onclick="mouseClick(%i)"></span>').replace('%i', str(pi)) + p + '</div><div id="editpara%i" style="display: none">' % pi + clean + '</div>'
        pi += 1
    if not len(body):
        display += '<div id="para0"></div><div id="editpara0" style="display: none"></div>'
        display += '<script> mouseClick(0); editingPara = -1; </script>'
    # get softlinks
    cur = DB.cursor()
    cur.execute('select title1, title2 from softlink where title1=? or title2=? order by rank desc limit 30', (title, title))
    display += niceLinkTable(title, [t[0] if t[1] == title else t[1] for t in cur.fetchall()])
    # get latest changed nodes
    cur.execute('select title from node order by time desc limit 30')
    latest = niceLinkTable(title, [x[0] for x in cur.fetchall()], 'Recent Nodes')
    # lay out the page (maybe eventually generate this with some pretty templating...)
    cur.close()
    return '''\
<table style="width: 100%; border-bottom: 1px dotted black"><tr>
    <td><big><big><b>''' + title + '''</b></big></big></td>
    <td align=center>''' + ('<a href="?op=delete">delete</a>' if exists else '') + '''</td>
    <td align=center><small>database: ''' + DBNAME + '''<br><a href="/setdb">change</a></small></td>
    <td align=right><form method="GET" action="/search">
        <input name="q"><br>
        <input type="hidden" name="softlink" value="''' + title + '''">
        <input type="checkbox" name="ignoreexact"><small> ignore exact</small>
    </form></td>
</tr></table>
<div style="background-color: #EEEEEE; margin: 8px; padding: 1px">''' + display + '''</div>
''' + latest


def getNodeBody(title):
    cur = DB.cursor()
    cur.execute('select body from node where title=?', (title,))
    body = cur.fetchone()
    cur.close()
    if not body: return None
    lst = [s.strip() for s in body[0].split('<p>')]
    while '' in lst: lst.remove('')
    return lst


def postNode(title, pi, text):
    body = getNodeBody(title)
    if body == None: body = []
    body[pi:pi+1] = map(htmlSafe, re.split(r'\r?\n<p>\r?\n', text))
    body = '<p>'.join(body)
    if not len(body): return deleteNode(title)
    cur = DB.cursor()
    cur.execute('insert or replace into node (title, body, time) values (?,?,?)', (title, body, time.time()))
    cur.close()
    DB.commit()


def softlinkNode(title1, title2):
    if title1 == title2: return
    if title1 > title2: title1, title2 = title2, title1
    cur = DB.cursor()
    cur.execute('update softlink set rank=rank+1 where title1=? and title2=?', (title1, title2))
    if cur.rowcount == 0:  # create softlink
        cur.execute('insert into softlink values (?,?,?)', (title1, title2, 1))
    cur.close()
    DB.commit()


def deleteNode(title):
    cur = DB.cursor()
    cur.execute('delete from node where title=?', (title,))
    cur.close()
    DB.commit()


def doSearch(curnode, query):
    cur = DB.cursor()
    results = {}
    for term in query.split():
        cur.execute('select title from node where title like ?', ('%%%s%%' % term,))
        for tup in cur.fetchall():
            if tup[0] not in results: results[tup[0]] = 1
            else: results[tup[0]] += 1
    cur.close()
    if not len(results): return  # this will redirect to the nodeshell
    html = '<b>Search results for</b> <a href="/node/%s">%s</a><br><br>' % (query, query)
    results = [(a, b) for b, a in results.items()]
    results.sort(); results.reverse()
    for rank, title in results:
        html += linker(title, curnode)
    return html


NICEBOX = '<div align=center style="border: 1px solid black; padding: 8px; margin: 8px">%s</div>'

def niceLinkTable(curnode, links, title = None):
    if not len(links): return ''
    html = '<table class="linktable"><tr>'; i = 0; multirow = False
    if title: html += '<td colspan=5>%s</td></tr><tr>' % title
    for ln in links:
        html += '<td>' + linker(ln, curnode) + '</td>'; i += 1
        if i == 5: html += '</tr><tr>'; i = 0; multirow = True
    if multirow:
        while i and i < 5: html += '<td>&zwnj;</td>'; i += 1
    return html + '</tr></table>'


class PetHandler(BaseHTTPRequestHandler):
    def do_GET(my): my.doAny()
    def do_POST(my): my.doAny()
    def doAny(my):
        # variables used to write the response at the end of this func
        code = 200
        ctype = 'text/html'
        headers = ''
        data = ''
        
        # parse query string
        if '?' in my.path:
            my.path, q = my.path.split('?', 1)
            q = decodeQuery(q)
        else: q = None
        
        # get POST data if any
        if my.command == 'POST':
            clen = int(my.headers.get('Content-Length'))
            post = decodeQuery(my.rfile.read(clen))
        else: post = None
        
        # action based on path
        
        if my.path[:6] == '/node/':
            title = unquote_plus(my.path[6:])
            # replace or insert para
            if post:
                code = 303
                headers += 'Location: /node/' + title
                postNode(title, int(post['para']), post['text'])
            # node deletion
            elif q and 'op' in q and q['op'] == 'delete':
                if 'confirm' in q and q['confirm'] == '1':
                    code = 303
                    headers += 'Location: /node/' + title
                    deleteNode(title)
                else:
                    data = '<b>Delete node: %s</b><br><br>' % title
                    data += '<a href="/node/%s">no, do nothing</a><br><br>' % title
                    data += '<a href="?op=delete&confirm=1">yes, delete</a><br><br>'
                    data = NICEBOX % data
            # softlinks
            elif q and 'softlink' in q:
                code = 303
                headers += 'Location: /node/' + title
                softlinkNode(q['softlink'], title)
            # node retrieval
            else: data = getNode(title)
        
        elif my.path == '/search':
            curnode = q['softlink']
            if 'ignoreexact' not in q:
                cur = DB.cursor()
                cur.execute('select time from node where title=?', (q['q'],))
                if cur.fetchone():
                    code = 303
                    headers += 'Location: /node/' + q['q'] + '?softlink=' + curnode
                cur.close()
            if code != 303:
                data = doSearch(curnode, q['q'])
                if not data:
                    data = ''
                    code = 303
                    # node probably doesn't exist
                    headers += 'Location: /node/' + q['q']
                else: data = NICEBOX % data
        
        elif my.path == '/setdb':
            if q and 'db' in q:
                global DBNAME
                if q['db'][-3:] != '.db': q['db'] += '.db'
                DBNAME = q['db']
                loadDB()
                code = 303
                headers += 'Location: /node/home'
            else:
                data = '<b>Select database:</b><br><br>'
                for fn in os.listdir('.'):
                    if fn[-3:] == '.db':
                        data += '<a href="?db=%s">%s</a><br>' % (fn, fn)
                data += '<br><form method="GET"><input name="db"><input type="submit" value="create"></form>'
                data = NICEBOX % data
        
        else:  # redirect to home node
            code = 303
            headers += 'Location: /node/home'
        
        # write the response to the client
        my.wfile.write('HTTP/1.1 %i \r\n' % code)
        my.wfile.write('Content-Type: %s\r\n' % ctype)
        my.wfile.write('Content-Length: %i\r\n' % len(data))
        my.wfile.write(headers + '\r\n')
        my.wfile.write(data)


if __name__ == '__main__':
    loadDB()
    HOST = 'localhost'
    print >> sys.stderr, 'Starting server on http://%s:%i' % (HOST, PORT)
    HTTPServer((HOST, PORT), PetHandler).serve_forever()

