#!/usr/bin/python

# tagged_tracks_cache.py
#       --copyright--                   Copyright 2007 (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--
#       January 1, 2008         bar     spun from trod_track
#       May 12, 2008            bar     use tzlib.same_object()
#       May 17, 2008            bar     email adr
#       September 4, 2008       bar     name our thread
#       November 15, 2008       bar     egad! I've been making default params as [] and {}
#       November 12, 2011       bar     correct url
#       November 29, 2011       bar     pyflake cleanup
#       May 27, 2012            bar     doxygen namespace
#       May 28, 2014            bar     put thread id in threads
#       --eodstamps--
##      \file
#       \namespace              tzpython.tagged_tracks_cache
#
#       Define a class that caches GPS tracks.
#       The tracks are tagged for lookup.
#       The tracks are computed using given routines.
#       The implementation of the tags is through routines, but the default is for a simple numeric tag.
#
#

import  threading

import  tz_gps
import  tzlib



class   a_tag_rtns(object) :
    """ Store the routines needed by the tag cache to operate on tags. """

    def _lower_numeric_tag(me, tag) :
        """
            Typical neighbor routine.
            Returns a new tag that is nearby the given tag, but far enough away that the given tag would just barely cover it in the direction this routine is concerned with.
        """

        return(tag * 0.75)

    def _higher_numeric_tag(me, tag) :
        return(tag * 1.5)


    def cmp_tags(me, one_tag, other_tag) :
        """ Override this routine for tags that are not numeric, greater than 0.0 """

        if  (one_tag <= 0.0) or (other_tag <= 0.0) :
            raise ValueError("Not strictly positive tag value t=%s ot=%s" % ( str(one_tag), str(other_tag) ) )

        m                           = False
        if  one_tag < other_tag :
            ( one_tag, other_tag )  = ( other_tag, one_tag )
            m                       = True

        d                           = 1.0 - (float(other_tag) / one_tag)
        if  m :
            d                       = -d

        return(d)


    def is_close_enough(me, t1, t2) :
        """ Are the two tags close enough to be ok to use by whatever the owner wants these tagged tracks to be used for? """

        v   = abs(me.cmp_tags(t1, t2))
        # print "v", v, (v <= 1.0/3.0), t1, t2

        return(v <= 1.0/3.0)



    def __init__(me, neighbor_rtns = None) :
        neighbor_rtns       = neighbor_rtns or []
        # array of neighbor routines - they return a tag that's "near" a given tag in some logical or physical "direction".
        me.neighbor_rtns    = neighbor_rtns or [ me.__class__._lower_numeric_tag, me.__class__._higher_numeric_tag ]


    pass                    # a_tag_rtns




class   a_tracks_cache(threading.Thread) :
    """
        Keep a cache of tagged tracks, all computed somehow from a parent 'tracks'.

        You tell this cache to get computed tracks for a 'tag'.
            It calls back to you through an overridden routine, "do_tracks()" to compute the tracks.
        You ask this cache to give you cached, computed 'tracks' closest to a tag.

        The "tag" means something to a compare function (and functions to get neighboring tags) that you provide in a_tag_rtns.

        This cache can also drive you to create tracks for some 'tag' that this cache thinks may be needed sooner or later.
    """



    class   a_cache_entry(object) :
        def __init__(me, tag, tracks = None, computed_when = None) :
            me.tag                  = tag
            me.request()
            me.add_tracks(tracks, computed_when)


        def add_tracks(me,   tracks = None, computed_when = None, new_tag = None) :
            if  new_tag            != None :
                me.tag              = new_tag
            me.tracks               = tracks
            if  me.tracks          == None :
                me.computed_when    = 0.0
            else :
                me.computed_when    = computed_when or tzlib.elapsed_time()
            pass


        def no_computed_tracks(me, computed_when = None) :
            me.tracks               = None
            me.computed_when        = computed_when or tzlib.elapsed_time()


        def request(me) :
            me.requested_when       = tzlib.elapsed_time()


        pass




    @staticmethod
    def _do_numeric_tracks(owner, tag, tracks) :
        """ Default callback to owner asking him to compute the tracks, given the 'tag'. """

        tracks      = tz_gps.make_big_points(tracks, wanted_count = tag)            # note that we can return a copy of the input tracks (unless make_big_points is fixed to not do so)!
        if  tracks != None :
            tag     = tz_gps.count_points_in_tracks(tracks)

        return( ( tracks, tag ) )


    @staticmethod
    def _new_tagged_tracks_are_computed(owner, me) :
        """ Dummy callback to owner telling him that new tracks have been computed. """
        return(False)
    pass



    def __init__(me, tracks = None, tag_rtns = None, owner = None, do_tracks_rtn = None, cache_change_rtn = None, *args, **kwargs) :

        kwargs['name']  = kwargs.get('name', "tagged_tracks_cache.a_tracks_cache")
        super(a_tracks_cache, me).__init__(args = args, kwargs = kwargs)

        me._stop        = False
        me.notify       = False
        me.tid          = None

        me.owner        = owner             or me
        me.do_tracks    = do_tracks_rtn     or me.__class__._do_numeric_tracks
        me.chng_rtn     = cache_change_rtn  or me.__class__._new_tagged_tracks_are_computed

        me.tag_rtns     = tag_rtns          or a_tag_rtns()
        me.cache        = []                                                # array of a_cache_entry's
        me._flush()

        me.e            = threading.Event()

        me.do_ahead     = False

        me.tracks       = tracks or []

        me.setDaemon(True)                                                  # so that we can kick out of the program while the thread is running


    def run(me) :
        me.tid  = tzlib.get_tid()
        idle    = False
        while not me._stop  :
            if  idle and not me.do_ahead :
                # print "waiting"
                me.e.wait()
                me.e.clear()
                # print "waitdone"
            if  me._stop    :  break

            idle    = True

            ce      = me._find_next_to_compute()
            if  ce  :
                idle            = False
                # print "computing", ce.tag
                ct              = tzlib.elapsed_time()
                ( tracks, tag)  = me.do_tracks(me.owner, ce.tag, me.tracks) # compute some tracks, returning their proper, exact tag
                # print "compute done", ce.tag, tag
                if  tracks != None :
                    # print "got tracks", tz_gps.count_points_in_tracks(tracks), tag
                    if  me.tag_rtns.is_close_enough(ce.tag, tag) :
                        ce.add_tracks(tracks, ct, tag)                                      # tell us about the newly computed tracks
                    else :
                        # print "nocomp", ce.tag, tag
                        ce.no_computed_tracks()                                             # mark the entry as computed, but with no data (so it won't get returned to a caller, nor will we recompute it until after a flush)
                        me.cache.append(me.__class__.a_cache_entry(tag, tracks, ct))        # learn a new cache entry that we've inadvertantly computed
                    me.trigger()

                if  me.notify :
                    me.notify   = False
                    me.chng_rtn(me.owner, me)                                               # tell the owner that the cache has changed
                pass
            elif me.do_ahead :
                # !!!! we've done all the tags we know, now it's time to pre-compute some tracks on the off chance that they may be needed in the future
                pass
            pass
        pass


    def _flush(me) :
        me.when     = tzlib.elapsed_time()                                  # cached tracks before this time are always further away from sought tags than ones cached after this time


    def _find_next_to_compute(me) :
        ft  = None
        fw  = 0
        for t in me.cache :
            # print "ww", t.requested_when, t.computed_when, me.when, len(me.cache), t.tag
            if  t.computed_when <= me.when :
                if  me.do_ahead  or (t.requested_when >= me.when) :
                    if  (not ft) or (t.requested_when > fw) :
                        ft  = t
                        fw  = t.requested_when
                    pass
                pass
            pass

        # print "fnxt", ft

        return(ft)



    def set_tracks(me, tracks) :
        """ Tell us that the source tracks may have changed. Anyway, keep us up to date about 'em. """

        if  not tzlib.same_object(me.tracks, tracks) :
            me.tracks   = tracks
            me.do_ahead = False                                     # don't do any precomputed tracks until we're told to
            me._flush()
            me.trigger()
        pass



    def stop(me) :
        """ Stop the music. """

        me.stop     = True
        me.trigger()



    def trigger(me) :
        me.notify   = True
        me.e.set()



    def _find_nearest_cache_entry(me, tag) :
        """ Find the nearest tracks to the given 'tag'. """

        ft  = None
        for t in me.cache :
            if  not ft :
                fd  = abs(me.tag_rtns.cmp_tags(tag, t.tag))
                ft  = t
            else :
                d   = abs(me.tag_rtns.cmp_tags(tag, t.tag))
                if  d   < fd :
                    fd  = d
                    ft  = t
                pass
            pass

        return(ft)


    def request_tagged_tracks(me, tag) :
        """
            Ask us to drive computation of the given tag's tracks.

            This tag will be the next to have tracks computed for (unless there is another call to this routine).
        """

        ce  = me._find_nearest_cache_entry(tag)
        if  not ce :
            me.cache.append(me.__class__.a_cache_entry(tag))
        elif me.tag_rtns.is_close_enough(ce.tag, tag) :
            ce.request()                                        # update when we last requested this thing
        else :
            me.cache.append(me.__class__.a_cache_entry(tag))

        me.e.set()



    def find_nearest_computed_cache_entry(me, tag) :
        """
            Find the nearest tracks to the given 'tag'.
            Don't find requested but uncomputed tracks.
            All cached tracks stored since the last flush() take precedence over those stored before.

            Returns, as the 1st item, None or the cache entry, 'tracks' in which have the computed tracks.
            The 2nd item is a bool telling whether the found entry is "close enough" to the asked-for tag.
        """

        ft  = None
        af  = None
        for t in me.cache :
            if  t.tracks != None :
                taf     = (t.computed_when >= me.when)
                if  (not ft) or (taf and not af) :
                    fd  = abs(me.tag_rtns.cmp_tags(tag, t.tag))
                    af  = taf
                    ft  = t
                    # print "1st", fd, tag, t.tag
                elif taf or not af :
                    d       = abs(me.tag_rtns.cmp_tags(tag, t.tag))
                    # print "next", d, fd, tag, t.tag
                    if  d   < fd :
                        fd  = d
                        af  = taf
                        ft  = t
                    pass
                pass
            pass

        if  not ft :
            return( ( ft, False ) )

        return( ( ft, me.tag_rtns.is_close_enough(tag, ft.tag) ) )



    pass            # a_tracks_cache




if  __name__ == '__main__' :
    t   = a_tracks_cache()


#
#
#
# eof
