#!/usr/bin/python

# poll_server_linear.py
#       --copyright--                   Copyright 2010 (C) Tranzoa, Co. All rights reserved.    Warranty: You're free and on your own here. This code is not necessarily up-to-date or of public quality.
#       --url--                         http://www.tranzoa.net/tzpython/
#       --email--                       pycode is the name to send to. tranzoa.com is the place to send to.
#       --bodstamps--
#       December 25, 2010       bar
#       December 26, 2010       bar
#       December 27, 2010       bar     client name, not
#       December 28, 2010       bar
#       March 23, 2011          bar     a_logger is spun off to tz_server_logger
#       November 7, 2011        bar     close log in main
#       November 29, 2011       bar     pyflake cleanup
#       May 27, 2012            bar     doxygen namespace
#       August 19, 2015         bar     pyflakes
#       November 7, 2017        bar     maxint->maxsize
#       --eodstamps--
##      \file
#       \namespace              tzpython.poll_server_linear
#
#
#       This is a web server for polling people about some linearly related items.
#       That is, the items can be rated against each other on a linear, 1-dimension scale. Better, worse or the same.
#
#

import  random
import  re
import  sys
import  threading

import  poll_server
import  strip_files
import  tz_chooser
import  tz_server_logger
import  tzlib



LOG_FILE_NAME   = "poll_server.log"
STATE_FILE_NAME = "poll_server.pickle"


#
#           template for the form contents with choices and buttons
#
A_FORM          = """
<table>
<tr>
    <td style="text-align:center;"><input type="submit" name="west"  value="%s"></td>
    <td style="text-align:center;"><input type="submit" name="equal" value="%s"></td>
    <td style="text-align:center;"><input type="submit" name="east"  value="%s"></td>
</tr>
<tr>
    <td style="text-align:center;"><button type="submit" name="west" >%s</button></td>
    <td style="text-align:center;">%s</td>
    <td style="text-align:center;"><button type="submit" name="east" >%s</button></td>
</tr>
</table>
"""


KEYBOARD_JS = """
<script language="javascript">
<!--
""" + strip_files.strip_curly_string("""
function    cancel_event(e)
{
    e   = e ? e : (window.event ? window.event : null);
    if (e)
    {
        e.cancelBubble  = true;
        if (e.stopPropagation)
        {
            e.stopPropagation(true);
        }

        e.returnValue   = false;
        if (e.preventDefault)
        {
            e.preventDefault();
        }
    }

    return(false);
};


function    key_pressed(e)
{
    var nm  = null;

    e   = e ? e : (window.event ? window.event : null);

    if  (e.keyCode == 37)                   /* left arrow */
    {
        nm  = 'west';
    }
    else if (e.keyCode == 39)               /* rite arrow */
    {
        nm  = 'east';
    }                                       /*  up                   down */
    else if ((e.keyCode == 32) || (e.keyCode == 38) || (e.keyCode == 40))
    {
        nm  = 'equal';
    }
    else
    {
        return(true);
    }

    if  (nm)
    {
        var el  = document.getElementById('key_submit');
        if (el)
        {
            el.name = nm;
            el      = document.forms['poll_server_form'].submit();
            cancel_event(e);
        }
    }

    return(false);
};


function    set_keyboard()
{
    var els  = window.document.getElementsByTagName("body");

    if (els.length)
    {
        els[0].onKeyPress   = "return(key_pressed(event));"
        els[0].setAttribute("onkeydown", "key_pressed(event);")
    }
};
set_keyboard()

""")[0] + """
-->
</script>
"""



VIEW_EM_PAGE    = """
<html>
<head>
    <title>%s</title>
</head>
<body>
<h1>%s</h1>
<p><hr><p>
%s
<p><hr><hr><p>
</body>
</html>
"""




class   a_query(object) :
    def __init__(me, qn, west, east) :
        me.qn   = qn
        me.west = west
        me.east = east
    #   a_query


class   a_choice(object) :
    def __init__(me, choice, htm, btn) :
        me.choice   = choice
        me.htm      = htm
        me.btn      = btn
    #   a_choice


class   a_pair(object) :

    def __init__(me, west, equal, east) :
        me.west             = west
        me.equal            = equal
        me.east             = east

        me.qn               = str(random.randint(1, sys.maxsize))
        me.qn_htm           = poll_server.A_FORM_QN % me.qn

        me.pre_form_htm     = ""
        me.post_form_htm    = ""

    #   a_pair







class   a_poll_server(poll_server.a_poll_server) :

    view_em_re      = re.compile(r"^/?(top|bottom)_*(\d+)$", re.DOTALL)

    def __init__(me, chooser = None, *kargs, **kwargs) :
        super(a_poll_server, me).__init__(*kargs, **kwargs)
        me.chooser  = chooser
        me.queries  = {}
        me.hit_cnt  = 0

        me.lock     = threading.RLock()


    def generic_hit(me, hit) :
        if  not super(a_poll_server, me).generic_hit(hit) :
            fn      = me.possible_file(hit)

            fd      = me.load_file(fn, ".png")
            if  fd  :
                if  hit.handler.send_http_headers(hit.is_head, content_type = "image/png") :
                    hit.handler.write_data(fd)
                return(True)

            fd      = me.load_file(fn, ".jpg")
            if  not fd  :
                fd  = me.load_file(fn, ".jpeg")
            if  fd  :
                if  hit.handler.send_http_headers(hit.is_head, content_type = "image/jpeg") :
                    hit.handler.write_data(fd)
                return(True)

            if  me.chooser :
                if  hit.handler.file_only_lc   == "relearn" :
                    me.chooser.relearn_order()
                    hit.handler.file_only_lc    = "top10"
                g   = me.view_em_re.search(hit.handler.file_only_lc)
                if  g :
                    me.lock.acquire()

                    me.chooser.sort_as_best_we_can()

                    if  g.group(1) == 'bottom' :
                        ca  = me.chooser[:int(g.group(2))]
                        tt  = "Bottom %d" % len(ca)

                    if  g.group(1) == 'top' :
                        ca  = me.chooser[max(0, len(me.chooser) - int(g.group(2))):]
                        ca.reverse()
                        tt  = "Top %d" % len(ca)

                    me.lock.release()

                    if  len(ca) :
                        if  hit.server.send_http_headers(hit.handler, is_head = hit.is_head) :
                            lst = "\n<p><hr>\n" + "\n<p>\n".join([ c.data for c in ca ])
                            pg  = VIEW_EM_PAGE % ( tt, tt, lst )
                            hit.handler.wfile.write(pg)

                        return(True)

                    pass

                pass

            pass

        return(False)


    def pre_main_page(me, hit) :
        return("Pick one or the other or call it a toss-up.")


    def poll_form(me, hit) :
        me.handle_hit(hit)

        p       = me.get_pair(hit)
        if  p  != None :
            frm = A_FORM                    % ( p.west.btn, p.equal.btn, p.east.btn, p.west.htm, p.equal.htm, p.east.htm, )
        else    :
            frm = A_FORM                    % ( "Oooo", "I'm", "lost!", "This", "is", "bad.", )
        if  False :
            if  not hit.cn :
                sf  = poll_server.A_FORM % ( "%s", "%s",  poll_server.YOUR_NAME                 + "%s" )
            else    :
                sf  = poll_server.A_FORM % ( "%s", "%s", (poll_server.YOUR_NAME_KNOWN % hit.cn) + "%s" )
            pass
        else    :
                sf  = poll_server.A_FORM
        frm     = sf % ( hit.ci, hit.fn,                (p.qn_htm        if p else "") + frm )
        frm     = (p.pre_form_htm if p else "") + frm + (p.post_form_htm if p else "") + KEYBOARD_JS
        return(frm)


    def handle_hit(me, hit) :
        if  not hit.qn :
            return(False)

        me.lock.acquire()
        if  hit.qn in me.queries :
            p   = me.queries[hit.qn]
            del(me.queries[hit.qn])
            r   = None
            if  'equal' in hit.args :
                r   =  0
            elif 'west' in hit.args :
                r   =  1
            elif 'east' in hit.args :
                r   = -1
            if  (r  != None) and me.chooser :
                me.chooser.learn(   p.west.choice, p.east.choice, r)
                me.chooser.make_old(p.west.choice, p.east.choice)
                me.hit_cnt += 1
                me.lock.release()
                me.server.log('Choice  %s#%s %d "%s" "%s"' % ( hit.handler.client_address[0], hit.ci, r, p.west.choice.name, p.east.choice.name, ) )

                return(True)

            pass
        me.lock.release()

        return(False)


    def get_pair(me, hit) :
        p       = None
        me.lock.acquire()

        if  me.chooser  :
            ( r, a, b )     = me.chooser.find_pair()
            if  random.random() >= 0.5 :
                ( b, a )    = ( a, b )

            w   = a_choice(a,    (a.data if a and a.data else "West choice"), "This one")
            eq  = a_choice(None, "",                                          "Toss up")
            e   = a_choice(b,    (b.data if b and b.data else "East choice"), "This one")
            p   = a_pair(w, eq, e)
            me.queries[p.qn]    = p

            me.hit_cnt += 1

        me.lock.release()

        return(p)


    log_file_choice_re  = re.compile(r'^Choice\s+\S+\s*(-?\d+)\s+"([^"]+)"\s+"([^"]+)"\s*$', re.MULTILINE)

    def learn_log(me, fn) :
        try     :
            if  me.chooser  :
                fd  = tzlib.read_whole_text_file(fn)
                lns = me.log_file_choice_re.findall(fd)

                me.lock.acquire()

                cd  = {}
                for c in me.chooser :
                    cd[c.name]  = c

                da  = []
                li  = 0
                for r, a, b in lns :
                    li += 1
                    if  (a in cd) and (b in cd) :
                        # print "@@@@", a, b, r, li, len(lns)
                        if  me.chooser.learn(cd[a], cd[b], int(r), extend = False) :
                            me.chooser.make_old(cd[a], cd[b])
                            da.append((int(r), cd[a], cd[b]))
                        pass
                    pass

                if  tz_chooser.EXTEND_LEARNING :
                    for r, a, b in da :
                        a.extend_learning(b,  r)
                        b.extend_learning(a, -r)
                    pass

                me.lock.release()

                return(len(da))

            pass
        except ( IOError, OSError ) :
            pass
        return(-1)

    #   a_poll_server




if __name__ == '__main__' :
    import  os
    import  time

    import  TZKeyReady
    import  TZCommandLineAtFile
    import  tz_os_priority


    program_name    = os.path.basename(sys.argv.pop(0))
    TZCommandLineAtFile.expand_at_sign_command_line_files(sys.argv)


    http_port   = poll_server.HTTP_PORT
    log_file    = None
    chooser     = tz_chooser.a_chooser()
    query_htm   = "Which picture is prettier?"
    root_dir    = None
    state_file  = None
    learn_logs  = []


    help_str    = tzlib.multiline_flush_left("""
                        %s (options) (root_dir|state_file)

                        I am a web server that asks people to compare images.

                    Options:

                        --http_port     port_number     Set the HTTP port number            (default: %u)
                        --log_file      file_name       Set the log file name.              (default: none)
                        --learn_log     file_name       Learn the choices in the given log file.
                        --query         html            Set the query html.
                        --choice        name    html    Add a choice to the possibilities.
                        --root          directory       Set the server root file directory. (default: ".")
                        --state_file    file_name       Set the state file name.            (default: none)


                  """) % ( program_name, http_port, )


    oi  = tzlib.array_find(sys.argv, [ "--help", "-?", "?", "-h", "/h", "/?", "?" ] )
    if  oi >= 0 :
        print help_str
        sys.exit(254)


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--http_port", "--http-port", "--httpport", "--port", "-p", ] )
        if  oi < 0  : break
        del sys.argv[oi]
        http_port   = int(sys.argv.pop(oi))


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--log_file", "--log-file", "--logfile", "-l", ] )
        if  oi < 0  : break
        del sys.argv[oi]
        log_file    = sys.argv.pop(oi)


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--state_file", "--state-file", "--statefile", "-f", "-s", ] )
        if  oi < 0  : break
        del sys.argv[oi]
        state_file  = sys.argv.pop(oi)


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--root", "--root_dir", "--root-dir", "--rootdir", "--root_directory", "--root-directory", "--rootdirectory", "-r", ] )
        if  oi < 0  : break
        del sys.argv[oi]
        root_dir            = sys.argv.pop(oi)


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--query", "-q" ] )
        if  oi < 0  : break
        del sys.argv[oi]
        query_htm   = sys.argv.pop(oi)


    while True :
        oi  = tzlib.array_find(sys.argv, [ "--choice", "-c" ] )
        if  oi < 0  : break
        del sys.argv[oi]
        name        = sys.argv.pop(oi)
        htm         = sys.argv.pop(oi)
        chooser.append(tz_chooser.a_choice(name, htm))

    while True :
        oi  = tzlib.array_find(sys.argv, [ "--learn_log", "--learn-log", "--learnlog", ] )
        if  oi < 0  : break
        del sys.argv[oi]
        learn_logs.append(sys.argv.pop(oi))


    fn      = sys.argv.pop(0) if len(sys.argv) else None
    if  fn  :
        if  os.path.isdir(fn) :
            root_dir        = fn
            if  not log_file :
                log_file    = os.path.join(root_dir, LOG_FILE_NAME)
            if  not state_file :
                state_file  = os.path.join(root_dir, STATE_FILE_NAME)
            state_file      = sys.argv.pop(0) if len(sys.argv) else state_file
        else                :
            state_file      = fn
        pass

    if  state_file  :
        ch          = tzlib.unpickle_file(state_file)

        if  ch      :
            try     :
                ch.find_pair()
            except  :
                ch  = None
            pass
        if  ch      :
            chd     = ch.make_choice_dict()
            for c in chooser :
                if  c.name not in chd :
                    ch.append(c)
                elif chd[c.name].data  != c.data :
                    chd[c.name].data    = c.data        # update the html from the cmd line in case the config file changed
                pass
            if  len(chooser) :
                chd     = chooser.make_choice_dict()
                for ci in xrange(len(ch) - 1, -1, -1) :
                    cc  = ch[ci]
                    if  cc.name not in chd :
                        del(ch[ci])                     # whack any choices that have gone away from the cmd line file
                    pass
                pass
            chooser.choices     = ch.choices
            chooser.learn_cnt   = ch.learn_cnt
            chooser.found_cnt   = ch.found_cnt
            chooser.hurry_up    = 0
        pass

    chooser.set_chunk_size(sys.maxsize)

    cw      = max([ len(c.name) for c in chooser ]) + 3


    class   a_test_server(a_poll_server) :
        def pre_main_page(me, hit) :
            return(query_htm)

        #   a_test_server


    server  = a_test_server(root_dir = root_dir, chooser = chooser)
    logger  = tz_server_logger.a_logger(file_name = log_file, http_port = http_port)

    me      = poll_server.a_server(http_port = http_port, server = server, logger = logger, show_exceptions = True)

    cnt     = 0
    for fn in learn_logs :
        cc  = server.learn_log(fn)
        if  cc <= 0 :
            print "Learned nothing from %s!" % cc
        else    :
            cnt += cc
        pass
    if  cnt :
        print "Learned %d choices." % cnt


    me.start()

    print "%d choices" % len(chooser)
    sys.setrecursionlimit(len(chooser) * 100)

    if  tz_chooser.EXTEND_COMPARE or (not tz_chooser.EXTEND_LEARNING) :
        tz_os_priority.set_proc_to_idle_priority(0)

    work_queue  =   []
    while True :
        k   = TZKeyReady.key_ready()
        if  k :
            print "key", k
            if  k == '?' :
                server.lock.acquire()
                chooser.sort_as_best_we_can()
                print "results", chooser.all_done, chooser.learn_cnt, chooser.age(), "\n  " + "\n  ".join([ "%-*s %7s" % ( cw, ccc.name, str(round(ccc.order(), 1)) ) for ccc in chooser ]), "\n", chooser.age()
                server.lock.release()
            elif k.lower() == 'r' :
                server.lock.acquire()
                chooser.relearn_order()
                print "relearn", chooser.all_done, chooser.learn_cnt, chooser.age(), "\n  " + "\n  ".join([ "%-*s %7s" % ( cw, ccc.name, str(round(ccc.order(), 1)) ) for ccc in chooser ]), "\n", chooser.age()
                server.lock.release()
            elif k.lower() == 'a' :
                server.lock.acquire()
                pa  = chooser.age()
                ap  = chooser.age(tz_chooser.AGE)
                chooser.relearn_order()
                print "age    ", chooser.all_done, chooser.learn_cnt, pa, ap, pa - ap, "\n  " + "\n  ".join([ "%-*s %7s" % ( cw, ccc.name, str(round(ccc.order(), 1)) ) for ccc in chooser ]), "\n", pa, ap
                server.lock.release()
            elif k.lower() == 'q' :
                break
            elif k == chr(27) :
                break
            logger.flush()

        if  len(work_queue) :
            ( cnm, onm, r ) = work_queue[0]
            hc              = server.hit_cnt
            ca              = chooser.find_tweens(cnm, onm, r)
            server.lock.acquire()
            if  server.hit_cnt == hc :
                work_queue.pop(0)
                # print "@@@@ ca", cnm, onm, r, [ ccc.name for ccc in ca ]
                chooser.put_choice_to_extend_learning(ca)
            server.lock.release()
        elif tz_chooser.EXTEND_COMPARE :
            while chooser.get_choice_to_extend_learning()   : pass
            hc          = server.hit_cnt
            ( r, a, b ) = chooser.find_distant_pair()           # doing this outside the lock is asking for trouble, but it takes many seconds to run, at the least. Very slow.
            if  r != None :
                server.lock.acquire()
                if  (server.hit_cnt == hc) and (not a.knows(b)) :
                    a.learn(    b,  r, extend = False)
                    if  r :
                        b.learn(a, -r, extend = False)
                    print "@@@@ extended", a.name, b.name, r, chooser.age()
                server.lock.release()
            pass
        elif not tz_chooser.EXTEND_LEARNING    :
            hc          = server.hit_cnt
            server.lock.acquire()
            c           = chooser.get_choice_to_extend_learning()
            server.lock.release()
            if  c       :
                ( m, z, p ) = c.find_branches()

                server.lock.acquire()
                if  server.hit_cnt == hc :
                    ( m, z, p ) = chooser.learn_only_if_new(c, m, z, p)
                else    :
                    chooser.put_choice_to_extend_learning(c)
                    m           = {}
                    z           = {}
                    p           = {}

                server.lock.release()
                if  m : print "@@@@ xl", c.name, -1, m
                if  z : print "@@@@ xl", c.name,  0, z
                if  p : print "@@@@ xl", c.name,  1, p
                for n in m :    work_queue.append(( c.name, n, -1 ))
                for n in z :    work_queue.append(( c.name, n,  0 ))
                for n in p :    work_queue.append(( c.name, n,  1 ))
            pass
        else    :
            while chooser.get_choice_to_extend_learning()   : pass
            time.sleep(0.1)
        pass

    chooser.set_hurry_up(sys.maxsize / 2)

    me.stop()
    logger.close()


    if  state_file :
        server.lock.acquire()
        tzlib.pickle_file(state_file, chooser)
        server.lock.release()

    pass


#
#
#
# eof
