#
# pubsub.py
#
# This source file is part of the FoundationDB open source project
#
# Copyright 2013-2018 Apple Inc. and the FoundationDB project authors
# 
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 
#     http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""FoundationDB PubSub Layer.

Provides the PubSub class for message passing according to the publish-subscribe
pattern. PubSub methods supports operations on:

     - Feeds, which publish messages
     - Inboxes, which subscribe to feeds and receive messages from them
     - Messages

The layer allows management of feeds and inboxes as well as message delivery. An
inbox can subscribe to any number of feeds. The inbox will then receive all
messages posted by each feed to which it subscribes. Subscriptions are
retroactive, so messages posted prior to subscription will become available once
a subscription is established.

"""

import simpledoc

feed_messages = simpledoc.OrderedIndex("messages.?", "fromfeed")

feeds = simpledoc.root.feeds
inboxes = simpledoc.root.inboxes
messages = simpledoc.root.messages


#####################################
# Transactions for SimpleDoc Access #
#####################################

@simpledoc.transactional
def _create_feed(name):
    feed = feeds[name]
    feed.set_value(name)
    return feed


@simpledoc.transactional
def _create_inbox(name):
    inbox = inboxes[name]
    inbox.set_value(name)
    return inbox


@simpledoc.transactional
def _create_feed_and_inbox(name):
    feed = _create_feed(name)
    inbox = _create_inbox(name)
    return feed, inbox


# When creating a subscription, initialize the state of the feed as dirty in
# relation to the inbox as if there have been prior posts by the feed. There may
# in fact have been such posts, but this is the correct initial state regardless.
@simpledoc.transactional
def _create_subscription(inbox, feed):
    inbox.subs[feed.get_name()] = ""
    inbox.dirtyfeeds[feed.get_name()] = "1"
    return True


@simpledoc.transactional
def _post_message(feed, contents):
    message = messages.prepend()
    message.set_value(contents)
    message.fromfeed = feed.get_name()
    # Mark the feed as dirty in relation to each watching inbox, setting the
    # inbox to copy the feed the next time it gets messages
    for inbox in feed.watchinginboxes.get_children():
        inboxes[inbox.get_name()].dirtyfeeds[feed.get_name()] = "1"
    # Clear all watching inboxes so the feed will not need to touch a subscribed
    # inbox upon subsequent posts until the inbox has gotten its messages
    feed.watchinginboxes.clear_all()


# Print without other side-effects.
@simpledoc.transactional
def _list_messages(inbox):
    print "Messages in {}'s inbox:".format(inbox.get_value())
    for feed in inbox.subs.get_children():
        print " from {}:".format(feeds[feed.get_name()].get_value())
        for message in feed_messages.find_all(feed.get_name()):
            print "   ", message.get_value()


# Read-only operation without side-effects.
@simpledoc.transactional
def _get_feed_messages(feed, limit):
    message_list = []
    counter = 0
    for message in feed_messages.find_all(feed.get_name()):
        if counter == limit:
            break
        message_list.append(message.get_value())
        counter += 1
    return message_list


# Read-only operation without side-effects.
@simpledoc.transactional
def _get_inbox_subscriptions(inbox, limit):
    subscriptions = []
    for message in inbox.subs.get_children():
        subscriptions.append(message.get_name())
    return subscriptions


# For each of an inbox's dirty feeds, copy the feed's new messages to the inbox
# and mark the inbox as watching the feed. Then unmark the feeds as dirty.
@simpledoc.transactional
def _copy_dirty_feeds(inbox):
    changed = False
    latest_id = inbox.latest_message.get_value()
    for feed in inbox.dirtyfeeds.get_children():
        for message in feed_messages.find_all(feed.get_name()):
            if latest_id != None and message.get_name() >= latest_id:
                break
            changed = True
            inbox.messages[message.get_name()] = feed.get_name()
        feeds[feed.get_name()].watchinginboxes[inbox.get_name()] = "1"
    inbox.dirtyfeeds.clear_all()
    return changed


# Copy messages from an inbox's dirty feeds and update state accordingly. Return
# the most recent messages up to limit.
@simpledoc.transactional
def _get_inbox_messages(inbox, limit):
    inbox_changed = _copy_dirty_feeds(inbox)
    message_ids = []
    for message in inbox.messages.get_children():
        if len(message_ids) >= limit:
            break
        message_ids.append(message.get_name())
    if inbox_changed and len(message_ids) > 0:
        inbox.latest_message = message_ids[0]
    return [messages[mid].get_value() for mid in message_ids]


@simpledoc.transactional
def _clear_all_messages():
    simpledoc.root.clear_all()


@simpledoc.transactional
def _print_feed_stats(feed):
    count = len(list(feed_messages.find_all(feed.get_name())))
    print "{} messages in feed {}".format(count, feed.get_name())


# Pretty print the entire PubSub database from SimpleDoc
@simpledoc.transactional
def _print_pubsub():
    print simpledoc.root.get_json(pretty=True)


################
# PubSub Class #
################

class PubSub(object):
    def __init__(self, db):
        self.db = db

    def create_feed(self, name):
        return _create_feed(self.db, name)

    def create_inbox(self, name):
        return _create_inbox(self.db, name)

    def create_inbox_and_feed(self, name):
        return _create_feed_and_inbox(self.db, name)

    def get_feed_by_name(self, name):
        return feeds[name]

    def get_inbox_by_name(self, name):
        return inboxes[name]

    def create_subscription(self, feed, inbox):
        return _create_subscription(self.db, feed, inbox)

    def post_message(self, feed, contents):
        return _post_message(self.db, feed, contents)

    def list_inbox_messages(self, inbox):
        return _list_messages(self.db, inbox)

    def get_feed_messages(self, feed, limit=10):
        return _get_feed_messages(self.db, feed, limit)

    def get_inbox_subscriptions(self, inbox, limit=10):
        return _get_inbox_subscriptions(self.db, inbox, limit)

    def get_inbox_messages(self, inbox, limit=10):
        return _get_inbox_messages(self.db, inbox, limit)

    def clear_all_messages(self):
        _clear_all_messages(self.db)

    def print_feed_stats(self, feed):
        _print_feed_stats(self.db, feed)

    def print_pubsub(self):
        _print_pubsub(self.db)


###################
# PubSub Example  #
###################

# This example generates a simple topology with specified numbers of feeds and
# inboxes. Inboxes are randomly subscribed to feeds. Each feed and inbox is then
# run in its own thread. Feeds post a specified number of messages, waiting a
# random interval between messages. Each inbox is polled for messages received,
# terminating when no messages are received for a wait limit.


# Create the specified numbers of feeds and inboxes. Subscribe each inbox to a
# randomly selected subset of feeds.
def setup_topology(feeds, inboxes):
    feed_map = {f: ps.create_feed('Alice '+str(f)) for f in range(feeds)}
    inbox_map = {}
    for i in range(inboxes):
        inbox_map[i] = ps.create_inbox('Bob '+str(i))
        for f in random.sample(xrange(feeds), random.randint(1, feeds)):
            ps.create_subscription(inbox_map[i], feed_map[f])
    return feed_map, inbox_map


# Post a fixed number of messages, waiting a random interval under 1 sec
# between each message
def feed_driver(feed, messages):
    for i in range(messages):
        ps.post_message(feed, 'Message {} from {}'.format(i, feed.get_name()))
        time.sleep(random.random())


def get_and_print_inbox_messages(inbox, limit=10):
    print "\nMessages to {}:".format(inbox.get_name())
    for m in ps.get_inbox_messages(inbox, limit):
        print " ->", m


# Poll the inbox every 0.1 sec, getting and printing messages received,
# until no messages have been received for 1.1 sec
def inbox_driver(inbox):
    wait_limit = 1.1
    wait_inc = 0.1
    waited = 0.0
    changed = False
    latest = None
    while True:
        get_and_print_inbox_messages(inbox)
        changed = (latest != inbox.latest_message)
        latest = inbox.latest_message
        if not changed and waited > wait_limit: break
        waited += wait_inc
        time.sleep(wait_inc)


# Generate and run a thread for each feed and each inbox.
def run_threads(feed_map, inbox_map, messages):
    feed_threads = [threading.Thread(target=feed_driver, args=(feed_map[id], messages))
                    for id in feed_map]
    inbox_threads = [threading.Thread(target=inbox_driver, args=(inbox_map[id],))
                     for id in inbox_map]
    for f in feed_threads: f.start()
    for i in inbox_threads: i.start()
    for f in feed_threads: f.join()
    for i in inbox_threads: i.join()


def sample_pubsub(feeds, inboxes, messages):
    feed_map, inbox_map = setup_topology(feeds, inboxes)
    run_threads(feed_map, inbox_map, messages)

if __name__ == "__main__":
    import random
    import threading
    import time
    import fdb

    fdb.api_version(22)
    db = fdb.open()

    ps = PubSub(db)
    ps.clear_all_messages()
    sample_pubsub(3, 3, 3)
